Commit b9a8fc58 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-12-10

parents 310d0c31 cd82c389
### Problem to solve ### Problem to solve
<!--- What problem do we solve? -->
### Target audience
<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/#/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst" -->
### Further details ### Further details
(Include use cases, benefits, and/or goals) <!--- Include use cases, benefits, and/or goals (contributes to our vision?) -->
### Proposal ### Proposal
<!--- How are we going to solve the problem? -->
### What does success look like, and how can we measure that? ### What does success look like, and how can we measure that?
(If no way to measure success, link to an issue that will implement a way to measure this) <!--- If no way to measure success, link to an issue that will implement a way to measure this -->
### Links / references ### Links / references
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import Flash from '../../flash';
import store from '../stores'; import store from '../stores';
import collapsibleContainer from './collapsible_container.vue'; import CollapsibleContainer from './collapsible_container.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
export default { export default {
name: 'RegistryListApp', name: 'RegistryListApp',
components: { components: {
collapsibleContainer, CollapsibleContainer,
GlLoadingIcon, GlLoadingIcon,
}, },
props: { props: {
...@@ -26,7 +24,7 @@ export default { ...@@ -26,7 +24,7 @@ export default {
this.setMainEndpoint(this.endpoint); this.setMainEndpoint(this.endpoint);
}, },
mounted() { mounted() {
this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); this.fetchRepos();
}, },
methods: { methods: {
...mapActions(['setMainEndpoint', 'fetchRepos']), ...mapActions(['setMainEndpoint', 'fetchRepos']),
...@@ -38,9 +36,9 @@ export default { ...@@ -38,9 +36,9 @@ export default {
<gl-loading-icon v-if="isLoading" :size="3" /> <gl-loading-icon v-if="isLoading" :size="3" />
<collapsible-container <collapsible-container
v-for="(item, index) in repos" v-for="item in repos"
v-else-if="!isLoading && repos.length" v-else-if="!isLoading && repos.length"
:key="index" :key="item.id"
:repo="item" :repo="item"
/> />
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import Flash from '../../flash'; import createFlash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import Icon from '../../vue_shared/components/icon.vue';
import tableRegistry from './table_registry.vue'; import TableRegistry from './table_registry.vue';
import { errorMessages, errorMessagesTypes } from '../constants'; import { errorMessages, errorMessagesTypes } from '../constants';
import { __ } from '../../locale'; import { __ } from '../../locale';
export default { export default {
name: 'CollapsibeContainerRegisty', name: 'CollapsibeContainerRegisty',
components: { components: {
clipboardButton, ClipboardButton,
tableRegistry, TableRegistry,
GlLoadingIcon, GlLoadingIcon,
GlButton,
Icon,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
repo: { repo: {
...@@ -29,30 +31,30 @@ export default { ...@@ -29,30 +31,30 @@ export default {
isOpen: false, isOpen: false,
}; };
}, },
computed: {
iconName() {
return this.isOpen ? 'angle-up' : 'angle-right';
},
},
methods: { methods: {
...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']), ...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']),
toggleRepo() { toggleRepo() {
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
if (this.isOpen) { if (this.isOpen) {
this.fetchList({ repo: this.repo }).catch(() => this.fetchList({ repo: this.repo });
this.showError(errorMessagesTypes.FETCH_REGISTRY),
);
} }
}, },
handleDeleteRepository() { handleDeleteRepository() {
this.deleteRepo(this.repo) this.deleteRepo(this.repo)
.then(() => { .then(() => {
Flash(__('This container registry has been scheduled for deletion.'), 'notice'); createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
this.fetchRepos(); this.fetchRepos();
}) })
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
}, },
showError(message) { showError(message) {
Flash(errorMessages[message]); createFlash(errorMessages[message]);
}, },
}, },
}; };
...@@ -61,18 +63,9 @@ export default { ...@@ -61,18 +63,9 @@ export default {
<template> <template>
<div class="container-image"> <div class="container-image">
<div class="container-image-head"> <div class="container-image-head">
<button type="button" class="js-toggle-repo btn-link" @click="toggleRepo"> <gl-button class="js-toggle-repo btn-link align-baseline" @click="toggleRepo">
<i <icon :name="iconName" /> {{ repo.name }}
:class="{ </gl-button>
'fa-chevron-right': !isOpen,
'fa-chevron-up': isOpen,
}"
class="fa"
aria-hidden="true"
>
</i>
{{ repo.name }}
</button>
<clipboard-button <clipboard-button
v-if="repo.location" v-if="repo.location"
...@@ -82,17 +75,17 @@ export default { ...@@ -82,17 +75,17 @@ export default {
/> />
<div class="controls d-none d-sm-block float-right"> <div class="controls d-none d-sm-block float-right">
<button <gl-button
v-if="repo.canDelete" v-if="repo.canDelete"
v-tooltip v-gl-tooltip
:title="s__('ContainerRegistry|Remove repository')" :title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')"
type="button" class="js-remove-repo"
class="js-remove-repo btn btn-danger" variant="danger"
@click="handleDeleteRepository" @click="handleDeleteRepository"
> >
<i class="fa fa-trash" aria-hidden="true"> </i> <icon name="remove" />
</button> </gl-button>
</div> </div>
</div> </div>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { n__ } from '../../locale'; import { n__ } from '../../locale';
import Flash from '../../flash'; import createFlash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import TablePagination from '../../vue_shared/components/table_pagination.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import Icon from '../../vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants'; import { errorMessages, errorMessagesTypes } from '../constants';
import { numberToHumanSize } from '../../lib/utils/number_utils'; import { numberToHumanSize } from '../../lib/utils/number_utils';
export default { export default {
components: { components: {
clipboardButton, ClipboardButton,
tablePagination, TablePagination,
GlButton,
Icon,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
...@@ -31,29 +34,24 @@ export default { ...@@ -31,29 +34,24 @@ export default {
}, },
methods: { methods: {
...mapActions(['fetchList', 'deleteRegistry']), ...mapActions(['fetchList', 'deleteRegistry']),
layers(item) { layers(item) {
return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
}, },
formatSize(size) { formatSize(size) {
return numberToHumanSize(size); return numberToHumanSize(size);
}, },
handleDeleteRegistry(registry) { handleDeleteRegistry(registry) {
this.deleteRegistry(registry) this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo })) .then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
}, },
onPageChange(pageNumber) { onPageChange(pageNumber) {
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() => this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
this.showError(errorMessagesTypes.FETCH_REGISTRY), this.showError(errorMessagesTypes.FETCH_REGISTRY),
); );
}, },
showError(message) { showError(message) {
Flash(errorMessages[message]); createFlash(errorMessages[message]);
}, },
}, },
}; };
...@@ -71,10 +69,9 @@ export default { ...@@ -71,10 +69,9 @@ export default {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(item, i) in repo.list" :key="i"> <tr v-for="item in repo.list" :key="item.tag">
<td> <td>
{{ item.tag }} {{ item.tag }}
<clipboard-button <clipboard-button
v-if="item.location" v-if="item.location"
:title="item.location" :title="item.location"
...@@ -83,37 +80,34 @@ export default { ...@@ -83,37 +80,34 @@ export default {
/> />
</td> </td>
<td> <td>
<span v-tooltip :title="item.revision" data-placement="bottom"> <span v-gl-tooltip.bottom :title="item.revision">{{ item.shortRevision }}</span>
{{ item.shortRevision }}
</span>
</td> </td>
<td> <td>
{{ formatSize(item.size) }} {{ formatSize(item.size) }}
<template v-if="item.size && item.layers"> <template v-if="item.size && item.layers"
&middot; >&middot;</template
</template> >
{{ layers(item) }} {{ layers(item) }}
</td> </td>
<td> <td>
<span v-tooltip :title="tooltipTitle(item.createdAt)" data-placement="bottom"> <span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{
{{ timeFormated(item.createdAt) }} timeFormated(item.createdAt)
</span> }}</span>
</td> </td>
<td class="content"> <td class="content">
<button <gl-button
v-if="item.canDelete" v-if="item.canDelete"
v-tooltip v-gl-tooltip
:title="s__('ContainerRegistry|Remove tag')" :title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')"
type="button" variant="danger"
class="js-delete-registry btn btn-danger d-none d-sm-block float-right" class="js-delete-registry d-none d-sm-block float-right"
data-container="body"
@click="handleDeleteRegistry(item);" @click="handleDeleteRegistry(item);"
> >
<i class="fa fa-trash" aria-hidden="true"> </i> <icon name="remove" />
</button> </gl-button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
......
import Vue from 'vue'; import axios from '~/lib/utils/axios_utils';
import VueResource from 'vue-resource'; import createFlash from '~/flash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { errorMessages, errorMessagesTypes } from '../constants';
Vue.use(VueResource);
export const fetchRepos = ({ commit, state }) => { export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING); commit(types.TOGGLE_MAIN_LOADING);
return Vue.http return axios
.get(state.endpoint) .get(state.endpoint)
.then(res => res.json()) .then(({ data }) => {
.then(response => { commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, data);
})
.catch(() => {
commit(types.TOGGLE_MAIN_LOADING); commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, response); createFlash(errorMessages[errorMessagesTypes.FETCH_REPOS]);
}); });
}; };
export const fetchList = ({ commit }, { repo, page }) => { export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => { return axios
const { headers } = response; .get(repo.tagsPath, { params: { page } })
.then(response => {
const { headers, data } = response;
return response.json().then(resp => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); commit(types.SET_REGISTRY_LIST, { repo, resp: data, headers });
}); })
.catch(() => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
createFlash(errorMessages[errorMessagesTypes.FETCH_REGISTRY]);
}); });
}; };
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath); export const deleteRepo = ({ commit }, repo) => axios.delete(repo.destroyPath);
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath); export const deleteRegistry = ({ commit }, image) => axios.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
......
...@@ -3,36 +3,12 @@ import Vuex from 'vuex'; ...@@ -3,36 +3,12 @@ import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import createState from './state';
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: createState(),
isLoading: false,
endpoint: '', // initial endpoint to fetch the repos list
/**
* Each object in `repos` has the following strucure:
* {
* name: String,
* isLoading: Boolean,
* tagsPath: String // endpoint to request the list
* destroyPath: String // endpoit to delete the repo
* list: Array // List of the registry images
* }
*
* Each registry image inside `list` has the following structure:
* {
* tag: String,
* revision: String
* shortRevision: String
* size: Number
* layers: Number
* createdAt: String
* destroyPath: String // endpoit to delete each image
* }
*/
repos: [],
},
actions, actions,
getters, getters,
mutations, mutations,
......
...@@ -48,6 +48,7 @@ export default { ...@@ -48,6 +48,7 @@ export default {
[types.TOGGLE_REGISTRY_LIST_LOADING](state, list) { [types.TOGGLE_REGISTRY_LIST_LOADING](state, list) {
const listToUpdate = state.repos.find(el => el.id === list.id); const listToUpdate = state.repos.find(el => el.id === list.id);
listToUpdate.isLoading = !listToUpdate.isLoading; listToUpdate.isLoading = !listToUpdate.isLoading;
}, },
}; };
export default () => ({
isLoading: false,
endpoint: '', // initial endpoint to fetch the repos list
/**
* Each object in `repos` has the following strucure:
* {
* name: String,
* isLoading: Boolean,
* tagsPath: String // endpoint to request the list
* destroyPath: String // endpoit to delete the repo
* list: Array // List of the registry images
* }
*
* Each registry image inside `list` has the following structure:
* {
* tag: String,
* revision: String
* shortRevision: String
* size: Number
* layers: Number
* createdAt: String
* destroyPath: String // endpoit to delete each image
* }
*/
repos: [],
});
...@@ -176,9 +176,9 @@ ...@@ -176,9 +176,9 @@
display: block; display: block;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
position: relative; position: relative;
padding: 8px 16px; padding: $dropdown-item-padding-y $dropdown-item-padding-x;
color: $gl-text-color; color: $gl-text-color;
line-height: normal; line-height: $gl-btn-line-height;
white-space: normal; white-space: normal;
overflow: hidden; overflow: hidden;
text-align: left; text-align: left;
...@@ -319,8 +319,8 @@ ...@@ -319,8 +319,8 @@
.dropdown-header { .dropdown-header {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: 13px; font-size: 13px;
line-height: 22px; line-height: $gl-line-height;
padding: 8px 16px; padding: $dropdown-item-padding-y $dropdown-item-padding-x;
} }
&.capitalize-header .dropdown-header { &.capitalize-header .dropdown-header {
...@@ -329,13 +329,8 @@ ...@@ -329,13 +329,8 @@
.dropdown-bold-header { .dropdown-bold-header {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
line-height: 22px; line-height: $gl-line-height;
padding: 0 16px; padding: $dropdown-item-padding-y $dropdown-item-padding-x;
}
.separator + .dropdown-header,
.separator + .dropdown-bold-header {
padding-top: 10px;
} }
.unclickable { .unclickable {
......
...@@ -127,12 +127,6 @@ ...@@ -127,12 +127,6 @@
} }
} }
li.dropdown-bold-header {
color: $gl-text-color-secondary;
font-size: 12px;
padding: 0 16px;
}
.navbar-collapse { .navbar-collapse {
flex: 0 0 auto; flex: 0 0 auto;
border-top: 0; border-top: 0;
...@@ -541,7 +535,7 @@ ...@@ -541,7 +535,7 @@
left: auto; left: auto;
li.current-user { li.current-user {
padding: 5px 18px; padding: $dropdown-item-padding-y $dropdown-item-padding-x;
.user-name { .user-name {
display: block; display: block;
......
...@@ -21,6 +21,8 @@ $danger: $red-500; ...@@ -21,6 +21,8 @@ $danger: $red-500;
$zindex-modal-backdrop: 1040; $zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2); $nav-divider-margin-y: ($grid-size / 2);
$dropdown-divider-bg: $theme-gray-200; $dropdown-divider-bg: $theme-gray-200;
$dropdown-item-padding-y: 8px;
$dropdown-item-padding-x: 12px;
$popover-max-width: 300px; $popover-max-width: 300px;
$popover-border-width: 1px; $popover-border-width: 1px;
$popover-border-color: $border-color; $popover-border-color: $border-color;
......
...@@ -101,8 +101,6 @@ input[type='checkbox']:hover { ...@@ -101,8 +101,6 @@ input[type='checkbox']:hover {
.dropdown-header { .dropdown-header {
// Necessary because glDropdown doesn't support a second style of headers // Necessary because glDropdown doesn't support a second style of headers
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
// .dropdown-menu li has 1px side padding
padding: $gl-padding-8 17px;
color: $gl-text-color; color: $gl-text-color;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 16px; line-height: 16px;
......
...@@ -181,15 +181,15 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -181,15 +181,15 @@ class Clusters::ClustersController < Clusters::BaseController
end end
def gcp_cluster def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| cluster = Clusters::BuildService.new(clusterable.subject).execute
cluster.build_provider_gcp cluster.build_provider_gcp
end.present(current_user: current_user) @gcp_cluster = cluster.present(current_user: current_user)
end end
def user_cluster def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster| cluster = Clusters::BuildService.new(clusterable.subject).execute
cluster.build_platform_kubernetes cluster.build_platform_kubernetes
end.present(current_user: current_user) @user_cluster = cluster.present(current_user: current_user)
end end
def validate_gcp_token def validate_gcp_token
......
...@@ -26,4 +26,10 @@ module RendersCommits ...@@ -26,4 +26,10 @@ module RendersCommits
commits commits
end end
def valid_ref?(ref_name)
return true unless ref_name.present?
Gitlab::GitRefValidator.validate(ref_name)
end
end end
...@@ -11,6 +11,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -11,6 +11,7 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :assign_ref_vars, except: :commits_root before_action :assign_ref_vars, except: :commits_root
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :validate_ref!, except: :commits_root
before_action :set_commits, except: :commits_root before_action :set_commits, except: :commits_root
def commits_root def commits_root
...@@ -54,6 +55,10 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -54,6 +55,10 @@ class Projects::CommitsController < Projects::ApplicationController
private private
def validate_ref!
render_404 unless valid_ref?(@ref)
end
def set_commits def set_commits
render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present? render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
......
...@@ -65,12 +65,6 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -65,12 +65,6 @@ class Projects::CompareController < Projects::ApplicationController
private private
def valid_ref?(ref_name)
return true unless ref_name.present?
Gitlab::GitRefValidator.validate(ref_name)
end
def validate_refs! def validate_refs!
valid = [head_ref, start_ref].map { |ref| valid_ref?(ref) } valid = [head_ref, start_ref].map { |ref| valid_ref?(ref) }
......
...@@ -38,6 +38,11 @@ module SelectsHelper ...@@ -38,6 +38,11 @@ module SelectsHelper
classes << 'ajax-groups-select' classes << 'ajax-groups-select'
classes << 'multiselect' if opts[:multiple] classes << 'multiselect' if opts[:multiple]
# EE requires this line to be present, but there is no easy way of injecting
# this into EE without causing merge conflicts. Given this line is very
# simple and not really EE specific on its own, we just include it in CE.
classes << 'multiselect' if opts[:multiple]
opts[:class] = classes.join(' ') opts[:class] = classes.join(' ')
select2_tag(id, opts) select2_tag(id, opts)
......
...@@ -168,7 +168,7 @@ class Notify < BaseMailer ...@@ -168,7 +168,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model) headers['In-Reply-To'] = message_id(model)
headers['References'] = [message_id(model)] headers['References'] = [message_id(model)]
headers[:subject]&.prepend('Re: ') headers[:subject] = "Re: #{headers[:subject]}" if headers[:subject]
mail_thread(model, headers) mail_thread(model, headers)
end end
......
# frozen_string_literal: true
module Clusters
class BuildService
def initialize(subject)
@subject = subject
end
def execute
::Clusters::Cluster.new.tap do |cluster|
case @subject
when ::Project
cluster.cluster_type = :project_type
when ::Group
cluster.cluster_type = :group_type
else
raise NotImplementedError
end
end
end
end
end
---
title: Fix a frozen string error in app/mailers/notify.rb
merge_request: 23683
author:
type: fixed
---
title: Only allow strings in URL::Sanitizer.valid?
merge_request: 23675
author:
type: fixed
---
title: Adjust dropdown item and header padding to comply with design specs
merge_request: 23552
author:
type: changed
...@@ -14,6 +14,7 @@ module Gitlab ...@@ -14,6 +14,7 @@ module Gitlab
def self.valid?(url) def self.valid?(url)
return false unless url.present? return false unless url.present?
return false unless url.is_a?(String)
uri = Addressable::URI.parse(url.strip) uri = Addressable::URI.parse(url.strip)
......
...@@ -71,6 +71,13 @@ module QA ...@@ -71,6 +71,13 @@ module QA
options.add_argument("disable-gpu") options.add_argument("disable-gpu")
end end
# Use the same profile on QA runs if CHROME_REUSE_PROFILE is true.
# Useful to speed up local QA.
if QA::Runtime::Env.reuse_chrome_profile?
qa_profile_dir = ::File.expand_path('../../tmp/qa-profile', __dir__)
options.add_argument("user-data-dir=#{qa_profile_dir}")
end
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci? options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci?
......
...@@ -32,6 +32,11 @@ module QA ...@@ -32,6 +32,11 @@ module QA
enabled?(ENV['CHROME_HEADLESS']) enabled?(ENV['CHROME_HEADLESS'])
end end
# set to 'true' to have Chrome use a fixed profile directory
def reuse_chrome_profile?
enabled?(ENV['CHROME_REUSE_PROFILE'], default: false)
end
def accept_insecure_certs? def accept_insecure_certs?
enabled?(ENV['ACCEPT_INSECURE_CERTS']) enabled?(ENV['ACCEPT_INSECURE_CERTS'])
end end
......
...@@ -53,6 +53,12 @@ describe Projects::CommitsController do ...@@ -53,6 +53,12 @@ describe Projects::CommitsController do
it { is_expected.to respond_with(:not_found) } it { is_expected.to respond_with(:not_found) }
end end
context "branch with invalid format, valid file" do
let(:id) { 'branch with space/README.md' }
it { is_expected.to respond_with(:not_found) }
end
end end
context "when the ref name ends in .atom" do context "when the ref name ends in .atom" do
...@@ -94,6 +100,30 @@ describe Projects::CommitsController do ...@@ -94,6 +100,30 @@ describe Projects::CommitsController do
end end
end end
end end
describe "GET /commits/:id/signatures" do
render_views
before do
get(:signatures,
namespace_id: project.namespace,
project_id: project,
id: id,
format: :json)
end
context "valid branch" do
let(:id) { 'master' }
it { is_expected.to respond_with(:success) }
end
context "invalid branch format" do
let(:id) { 'some branch' }
it { is_expected.to respond_with(:not_found) }
end
end
end end
context 'token authentication' do context 'token authentication' do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'User Cluster', :js do
include GoogleApi::CloudPlatformHelpers
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_maintainer(user)
gitlab_sign_in(user)
allow(Groups::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
end
context 'when user does not have a cluster and visits cluster index page' do
before do
visit group_clusters_path(group)
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
end
context 'when user filled form with valid parameters' do
shared_examples 'valid cluster user form' do
it 'user sees a cluster details page' do
subject
expect(page).to have_content('Kubernetes cluster integration')
expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
.to have_content('http://example.com')
expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
.to have_content('my-token')
end
end
before do
fill_in 'cluster_name', with: 'dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token'
end
subject { click_button 'Add Kubernetes cluster' }
it_behaves_like 'valid cluster user form'
context 'RBAC is enabled for the cluster' do
before do
check 'cluster_platform_kubernetes_attributes_authorization_type'
end
it_behaves_like 'valid cluster user form'
it 'user sees a cluster details page with RBAC enabled' do
subject
expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked
end
end
end
context 'when user filled form with invalid parameters' do
before do
click_button 'Add Kubernetes cluster'
end
it 'user sees a validation error' do
expect(page).to have_css('#error_explanation')
end
end
end
context 'when user does have a cluster and visits cluster page' do
let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) }
before do
visit group_cluster_path(group, cluster)
end
it 'user sees a cluster details page' do
expect(page).to have_button('Save changes')
end
context 'when user disables the cluster' do
before do
page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
page.within('#cluster-integration') { click_button 'Save changes' }
end
it 'user sees the successful message' do
expect(page).to have_content('Kubernetes cluster was successfully updated.')
end
end
context 'when user changes cluster parameters' do
before do
fill_in 'cluster_name', with: 'my-dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'new-token'
page.within('#js-cluster-details') { click_button 'Save changes' }
end
it 'user sees the successful message' do
expect(page).to have_content('Kubernetes cluster was successfully updated.')
expect(cluster.reload.name).to eq('my-dev-cluster')
expect(cluster.reload.platform_kubernetes.token).to eq('new-token')
end
end
context 'when user destroy the cluster' do
before do
page.accept_confirm do
click_link 'Remove integration'
end
end
it 'user sees creation form with the successful message' do
expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
expect(page).to have_link('Add Kubernetes cluster')
end
end
end
end
import _ from 'underscore'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Vue from 'vue'; import Vue from 'vue';
import registry from '~/registry/components/app.vue'; import registry from '~/registry/components/app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import { reposServerResponse } from '../mock_data'; import { reposServerResponse } from '../mock_data';
describe('Registry List', () => { describe('Registry List', () => {
const Component = Vue.extend(registry);
let vm; let vm;
let Component; let mock;
beforeEach(() => { beforeEach(() => {
Component = Vue.extend(registry); mock = new MockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
mock.restore();
vm.$destroy(); vm.$destroy();
}); });
describe('with data', () => { describe('with data', () => {
const interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}),
);
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(interceptor); mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse);
vm = mountComponent(Component, { endpoint: 'foo' });
});
afterEach(() => { vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
}); });
it('should render a list of repos', done => { it('should render a list of repos', done => {
...@@ -64,9 +57,9 @@ describe('Registry List', () => { ...@@ -64,9 +57,9 @@ describe('Registry List', () => {
Vue.nextTick(() => { Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click(); vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual( expect(
'fa fa-chevron-up', vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'),
); ).toContain('angle-up');
done(); done();
}); });
}); });
...@@ -76,21 +69,10 @@ describe('Registry List', () => { ...@@ -76,21 +69,10 @@ describe('Registry List', () => {
}); });
describe('without data', () => { describe('without data', () => {
const interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify([]), {
status: 200,
}),
);
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(interceptor); mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
vm = mountComponent(Component, { endpoint: 'foo' });
});
afterEach(() => { vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
}); });
it('should render empty message', done => { it('should render empty message', done => {
...@@ -109,21 +91,10 @@ describe('Registry List', () => { ...@@ -109,21 +91,10 @@ describe('Registry List', () => {
}); });
describe('while loading data', () => { describe('while loading data', () => {
const interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}),
);
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(interceptor); mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
vm = mountComponent(Component, { endpoint: 'foo' });
});
afterEach(() => { vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
}); });
it('should render a loading spinner', done => { it('should render a loading spinner', done => {
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Vue from 'vue'; import Vue from 'vue';
import collapsibleComponent from '~/registry/components/collapsible_container.vue'; import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import store from '~/registry/stores'; import store from '~/registry/stores';
import { repoPropsData } from '../mock_data'; import * as types from '~/registry/stores/mutation_types';
import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data';
describe('collapsible registry container', () => { describe('collapsible registry container', () => {
let vm; let vm;
let Component; let mock;
const Component = Vue.extend(collapsibleComponent);
beforeEach(() => { beforeEach(() => {
Component = Vue.extend(collapsibleComponent); mock = new MockAdapter(axios);
mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {});
store.commit(types.SET_REPOS_LIST, reposServerResponse);
vm = new Component({ vm = new Component({
store, store,
propsData: { propsData: {
...@@ -18,24 +28,23 @@ describe('collapsible registry container', () => { ...@@ -18,24 +28,23 @@ describe('collapsible registry container', () => {
}); });
afterEach(() => { afterEach(() => {
mock.restore();
vm.$destroy(); vm.$destroy();
}); });
describe('toggle', () => { describe('toggle', () => {
it('should be closed by default', () => { it('should be closed by default', () => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null); expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.$el.querySelector('.container-image-head i').className).toEqual( expect(vm.iconName).toEqual('angle-right');
'fa fa-chevron-right',
);
}); });
it('should be open when user clicks on closed repo', done => { it('should be open when user clicks on closed repo', done => {
vm.$el.querySelector('.js-toggle-repo').click(); vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBeDefined(); expect(vm.$el.querySelector('.container-image-tags')).not.toBeNull();
expect(vm.$el.querySelector('.container-image-head i').className).toEqual( expect(vm.iconName).toEqual('angle-up');
'fa fa-chevron-up',
);
done(); done();
}); });
}); });
...@@ -45,20 +54,20 @@ describe('collapsible registry container', () => { ...@@ -45,20 +54,20 @@ describe('collapsible registry container', () => {
Vue.nextTick(() => { Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click(); vm.$el.querySelector('.js-toggle-repo').click();
setTimeout(() => {
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null); expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.$el.querySelector('.container-image-head i').className).toEqual( expect(vm.iconName).toEqual('angle-right');
'fa fa-chevron-right',
);
done(); done();
}); });
}); });
}); });
}); });
});
describe('delete repo', () => { describe('delete repo', () => {
it('should be possible to delete a repo', () => { it('should be possible to delete a repo', () => {
expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined(); expect(vm.$el.querySelector('.js-remove-repo')).not.toBeNull();
}); });
}); });
}); });
import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter';
import VueResource from 'vue-resource'; import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
import * as actions from '~/registry/stores/actions'; import * as actions from '~/registry/stores/actions';
import * as types from '~/registry/stores/mutation_types'; import * as types from '~/registry/stores/mutation_types';
import state from '~/registry/stores/state';
import { TEST_HOST } from 'spec/test_constants';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
import { import {
defaultState,
reposServerResponse, reposServerResponse,
registryServerResponse, registryServerResponse,
parsedReposServerResponse, parsedReposServerResponse,
} from '../mock_data'; } from '../mock_data';
Vue.use(VueResource);
describe('Actions Registry Store', () => { describe('Actions Registry Store', () => {
let interceptor;
let mockedState; let mockedState;
let mock;
beforeEach(() => { beforeEach(() => {
mockedState = defaultState; mockedState = state();
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
}); });
describe('server requests', () => {
afterEach(() => { afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); mock.restore();
}); });
describe('server requests', () => {
describe('fetchRepos', () => { describe('fetchRepos', () => {
beforeEach(() => { beforeEach(() => {
interceptor = (request, next) => { mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
next(
request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}),
);
};
Vue.http.interceptors.push(interceptor);
}); });
it('should set receveived repos', done => { it('should set receveived repos', done => {
...@@ -56,23 +48,15 @@ describe('Actions Registry Store', () => { ...@@ -56,23 +48,15 @@ describe('Actions Registry Store', () => {
}); });
describe('fetchList', () => { describe('fetchList', () => {
let repo;
beforeEach(() => { beforeEach(() => {
interceptor = (request, next) => { mockedState.repos = parsedReposServerResponse;
next( [, repo] = mockedState.repos;
request.respondWith(JSON.stringify(registryServerResponse), {
status: 200,
}),
);
};
Vue.http.interceptors.push(interceptor); mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
}); });
it('should set received list', done => { it('should set received list', done => {
mockedState.repos = parsedReposServerResponse;
const repo = mockedState.repos[1];
testAction( testAction(
actions.fetchList, actions.fetchList,
{ repo }, { repo },
......
...@@ -41,6 +41,7 @@ describe Gitlab::UrlSanitizer do ...@@ -41,6 +41,7 @@ describe Gitlab::UrlSanitizer do
false | '123://invalid:url' false | '123://invalid:url'
false | 'valid@project:url.git' false | 'valid@project:url.git'
false | 'valid:pass@project:url.git' false | 'valid:pass@project:url.git'
false | %w(test array)
true | 'ssh://example.com' true | 'ssh://example.com'
true | 'ssh://:@example.com' true | 'ssh://:@example.com'
true | 'ssh://foo@example.com' true | 'ssh://foo@example.com'
......
...@@ -28,8 +28,8 @@ describe Notify do ...@@ -28,8 +28,8 @@ describe Notify do
end end
def have_referable_subject(referable, reply: false) def have_referable_subject(referable, reply: false)
prefix = referable.project ? "#{referable.project.name} | " : '' prefix = (referable.project ? "#{referable.project.name} | " : '').freeze
prefix.prepend('Re: ') if reply prefix = "Re: #{prefix}" if reply
suffix = "#{referable.title} (#{referable.to_reference})" suffix = "#{referable.title} (#{referable.to_reference})"
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::BuildService do
describe '#execute' do
subject { described_class.new(cluster_subject).execute }
describe 'when cluster subject is a project' do
let(:cluster_subject) { build(:project) }
it 'sets the cluster_type to project_type' do
is_expected.to be_project_type
end
end
describe 'when cluster subject is a group' do
let(:cluster_subject) { build(:group) }
it 'sets the cluster_type to group_type' do
is_expected.to be_group_type
end
end
end
end
...@@ -6,6 +6,13 @@ module JavaScriptFixturesHelpers ...@@ -6,6 +6,13 @@ module JavaScriptFixturesHelpers
FIXTURE_PATHS = %w[spec/javascripts/fixtures ee/spec/javascripts/fixtures].freeze FIXTURE_PATHS = %w[spec/javascripts/fixtures ee/spec/javascripts/fixtures].freeze
def self.included(base)
base.around do |example|
# pick an arbitrary date from the past, so tests are not time dependent
Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run }
end
end
# Public: Removes all fixture files from given directory # Public: Removes all fixture files from given directory
# #
# directory_name - directory of the fixtures (relative to FIXTURE_PATHS) # directory_name - directory of the fixtures (relative to FIXTURE_PATHS)
......
require 'spec_helper' require 'spec_helper'
IDENTIFIER = %r{\h+/\S+}
describe NamespaceFileUploader do describe NamespaceFileUploader do
let(:group) { build_stubbed(:group) } let(:group) { build_stubbed(:group) }
let(:uploader) { described_class.new(group) } let(:uploader) { described_class.new(group) }
let(:upload) { create(:upload, :namespace_upload, model: group) } let(:upload) { create(:upload, :namespace_upload, model: group) }
let(:identifier) { %r{\h+/\S+} }
subject { uploader } subject { uploader }
it_behaves_like 'builds correct paths', it_behaves_like 'builds correct paths' do
let(:patterns) do
{
store_dir: %r[uploads/-/system/namespace/\d+], store_dir: %r[uploads/-/system/namespace/\d+],
upload_path: IDENTIFIER, upload_path: identifier,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}] absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{identifier}]
}
end
end
context "object_store is REMOTE" do context "object_store is REMOTE" do
before do before do
...@@ -21,9 +25,14 @@ describe NamespaceFileUploader do ...@@ -21,9 +25,14 @@ describe NamespaceFileUploader do
include_context 'with storage', described_class::Store::REMOTE include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths', it_behaves_like 'builds correct paths' do
let(:patterns) do
{
store_dir: %r[namespace/\d+/\h+], store_dir: %r[namespace/\d+/\h+],
upload_path: IDENTIFIER upload_path: identifier
}
end
end
end end
context '.base_dir' do context '.base_dir' do
......
require 'spec_helper' require 'spec_helper'
IDENTIFIER = %r{\h+/\S+}
describe PersonalFileUploader do describe PersonalFileUploader do
let(:model) { create(:personal_snippet) } let(:model) { create(:personal_snippet) }
let(:uploader) { described_class.new(model) } let(:uploader) { described_class.new(model) }
let(:upload) { create(:upload, :personal_snippet_upload) } let(:upload) { create(:upload, :personal_snippet_upload) }
let(:identifier) { %r{\h+/\S+} }
subject { uploader } subject { uploader }
it_behaves_like 'builds correct paths', it_behaves_like 'builds correct paths' do
let(:patterns) do
{
store_dir: %r[uploads/-/system/personal_snippet/\d+], store_dir: %r[uploads/-/system/personal_snippet/\d+],
upload_path: IDENTIFIER, upload_path: identifier,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}] absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{identifier}]
}
end
end
context "object_store is REMOTE" do context "object_store is REMOTE" do
before do before do
...@@ -21,9 +25,14 @@ describe PersonalFileUploader do ...@@ -21,9 +25,14 @@ describe PersonalFileUploader do
include_context 'with storage', described_class::Store::REMOTE include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths', it_behaves_like 'builds correct paths' do
let(:patterns) do
{
store_dir: %r[\d+/\h+], store_dir: %r[\d+/\h+],
upload_path: IDENTIFIER upload_path: identifier
}
end
end
end end
describe '#to_h' do describe '#to_h' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment