Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-08 18:12:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-08 18:12:51 +0300
commitf2ac9ee99ac6b1afc0edbc8621a05176dddd6a14 (patch)
tree6245fd9661e7c5d85fbe78676dd212734713b1f4 /app
parentaf97e4dd4beb0ba1aa0cb3c31df413333cbce77d (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/analytics/shared/utils.js43
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js14
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_operator.js2
-rw-r--r--app/assets/javascripts/filtered_search/droplab/constants.js (renamed from app/assets/javascripts/droplab/constants.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/drop_down.js (renamed from app/assets/javascripts/droplab/drop_down.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/drop_lab_deprecated.js (renamed from app/assets/javascripts/droplab/drop_lab.js)10
-rw-r--r--app/assets/javascripts/filtered_search/droplab/hook.js (renamed from app/assets/javascripts/droplab/hook.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/hook_button.js (renamed from app/assets/javascripts/droplab/hook_button.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/hook_input.js (renamed from app/assets/javascripts/droplab/hook_input.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/keyboard.js (renamed from app/assets/javascripts/droplab/keyboard.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/ajax.js (renamed from app/assets/javascripts/droplab/plugins/ajax.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js (renamed from app/assets/javascripts/droplab/plugins/ajax_filter.js)3
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/filter.js (renamed from app/assets/javascripts/droplab/plugins/filter.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js (renamed from app/assets/javascripts/droplab/plugins/input_setter.js)0
-rw-r--r--app/assets/javascripts/filtered_search/droplab/utils.js (renamed from app/assets/javascripts/droplab/utils.js)0
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/namespace_select.js58
-rw-r--r--app/assets/javascripts/pages/admin/projects/components/namespace_select.vue143
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js38
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue163
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues.js7
-rw-r--r--app/views/admin/application_settings/_performance.html.haml17
-rw-r--r--app/views/admin/projects/index.html.haml20
-rw-r--r--app/views/admin/projects/show.html.haml11
-rw-r--r--app/views/clusters/clusters/_namespace.html.haml3
-rw-r--r--app/views/clusters/clusters/show.html.haml2
34 files changed, 430 insertions, 159 deletions
diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js
index 52901d4c5bb..c83000e0be7 100644
--- a/app/assets/javascripts/analytics/shared/utils.js
+++ b/app/assets/javascripts/analytics/shared/utils.js
@@ -1,4 +1,5 @@
import dateFormat from 'dateformat';
+import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { dateFormats } from './constants';
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
@@ -7,3 +8,45 @@ export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'na
};
export const toYmd = (date) => dateFormat(date, dateFormats.isoDate);
+
+/**
+ * Takes a url and extracts query parameters used for the shared
+ * filter bar
+ *
+ * @param {string} url The URL to extract query parameters from
+ * @returns {Object}
+ */
+export const extractFilterQueryParameters = (url = '') => {
+ const {
+ source_branch_name = null,
+ target_branch_name = null,
+ author_username = null,
+ milestone_title = null,
+ assignee_username = [],
+ label_name = [],
+ } = urlQueryToFilter(url);
+
+ return {
+ selectedSourceBranch: source_branch_name,
+ selectedTargetBranch: target_branch_name,
+ selectedAuthor: author_username,
+ selectedMilestone: milestone_title,
+ selectedAssigneeList: assignee_username,
+ selectedLabelList: label_name,
+ };
+};
+
+/**
+ * Takes a url and extracts sorting and pagination query parameters into an object
+ *
+ * @param {string} url The URL to extract query parameters from
+ * @returns {Object}
+ */
+export const extractPaginationQueryParameters = (url = '') => {
+ const { sort, direction, page } = urlQueryToFilter(url);
+ return {
+ sort: sort?.value || null,
+ direction: direction?.value || null,
+ page: page?.value || null,
+ };
+};
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index b1227fb3533..59905035257 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -38,23 +38,9 @@ $.fn.requiresInput = function requiresInput() {
$form.on('change input', fieldSelector, requireInput);
};
-// Hide or Show the help block when creating a new project
-// based on the option selected
-function hideOrShowHelpBlock(form) {
- const selected = $('.js-select-namespace option:selected');
- if (selected.length && selected.data('optionsParent') === 'groups') {
- form.find('.form-text.text-muted').hide();
- } else if (selected.length) {
- form.find('.form-text.text-muted').show();
- }
-}
-
$(() => {
$('form.js-requires-input').each((i, el) => {
const $form = $(el);
-
$form.requiresInput();
- hideOrShowHelpBlock($form);
- $('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form));
});
});
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 1c0dab11392..f4a27dc7d1f 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -5,8 +5,8 @@ import {
canCreateConfidentialMergeRequest,
} from './confidential_merge_request';
import confidentialMergeRequestState from './confidential_merge_request/state';
-import DropLab from './droplab/drop_lab';
-import ISetter from './droplab/plugins/input_setter';
+import DropLab from './filtered_search/droplab/drop_lab_deprecated';
+import ISetter from './filtered_search/droplab/plugins/input_setter';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { __, sprintf } from './locale';
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
index 545719ee681..9726b2164b7 100644
--- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
@@ -1,6 +1,6 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
-import AjaxFilter from '../droplab/plugins/ajax_filter';
+import AjaxFilter from './droplab/plugins/ajax_filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index a7648a3c463..5adc074b3ce 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,7 +1,7 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
-import Ajax from '../droplab/plugins/ajax';
-import Filter from '../droplab/plugins/filter';
+import Ajax from './droplab/plugins/ajax';
+import Filter from './droplab/plugins/filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 47f350dc6a2..9d29782c9a7 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -1,5 +1,5 @@
-import Filter from '~/droplab/plugins/filter';
import { __ } from '~/locale';
+import Filter from './droplab/plugins/filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index f78644a3893..ddc3c06a9d1 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,7 +1,7 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
-import Ajax from '../droplab/plugins/ajax';
-import Filter from '../droplab/plugins/filter';
+import Ajax from './droplab/plugins/ajax';
+import Filter from './droplab/plugins/filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js
index f933338514a..fb9f25a8c45 100644
--- a/app/assets/javascripts/filtered_search/dropdown_operator.js
+++ b/app/assets/javascripts/filtered_search/dropdown_operator.js
@@ -1,5 +1,5 @@
-import Filter from '~/droplab/plugins/filter';
import { __ } from '~/locale';
+import Filter from './droplab/plugins/filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/filtered_search/droplab/constants.js
index 6451af49d36..6451af49d36 100644
--- a/app/assets/javascripts/droplab/constants.js
+++ b/app/assets/javascripts/filtered_search/droplab/constants.js
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/filtered_search/droplab/drop_down.js
index 05b741af191..05b741af191 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/filtered_search/droplab/drop_down.js
diff --git a/app/assets/javascripts/droplab/drop_lab.js b/app/assets/javascripts/filtered_search/droplab/drop_lab_deprecated.js
index 6f068aaa800..15c4a4b7c6b 100644
--- a/app/assets/javascripts/droplab/drop_lab.js
+++ b/app/assets/javascripts/filtered_search/droplab/drop_lab_deprecated.js
@@ -1,3 +1,13 @@
+/**
+ * This library is deprecated and scheduled to be removed once the
+ * filtered_search component is replaced with GitLab's new Pajamas
+ * filter vue component.
+ *
+ * The documentation has been removed from the gitlab codebase but
+ * can still be found in the commit history here:
+ * https://gitlab.com/gitlab-org/gitlab/-/blob/28f20e28/doc/development/fe_guide/droplab/droplab.md
+ */
+
import { DATA_TRIGGER } from './constants';
import HookButton from './hook_button';
import HookInput from './hook_input';
diff --git a/app/assets/javascripts/droplab/hook.js b/app/assets/javascripts/filtered_search/droplab/hook.js
index 8a8dcde9f88..8a8dcde9f88 100644
--- a/app/assets/javascripts/droplab/hook.js
+++ b/app/assets/javascripts/filtered_search/droplab/hook.js
diff --git a/app/assets/javascripts/droplab/hook_button.js b/app/assets/javascripts/filtered_search/droplab/hook_button.js
index c51d6167fa3..c51d6167fa3 100644
--- a/app/assets/javascripts/droplab/hook_button.js
+++ b/app/assets/javascripts/filtered_search/droplab/hook_button.js
diff --git a/app/assets/javascripts/droplab/hook_input.js b/app/assets/javascripts/filtered_search/droplab/hook_input.js
index c523dae347f..c523dae347f 100644
--- a/app/assets/javascripts/droplab/hook_input.js
+++ b/app/assets/javascripts/filtered_search/droplab/hook_input.js
diff --git a/app/assets/javascripts/droplab/keyboard.js b/app/assets/javascripts/filtered_search/droplab/keyboard.js
index fe1ea2fa6b0..fe1ea2fa6b0 100644
--- a/app/assets/javascripts/droplab/keyboard.js
+++ b/app/assets/javascripts/filtered_search/droplab/keyboard.js
diff --git a/app/assets/javascripts/droplab/plugins/ajax.js b/app/assets/javascripts/filtered_search/droplab/plugins/ajax.js
index 77d60454d1a..77d60454d1a 100644
--- a/app/assets/javascripts/droplab/plugins/ajax.js
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/ajax.js
diff --git a/app/assets/javascripts/droplab/plugins/ajax_filter.js b/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js
index ac4d44adc17..d0f2d205bb6 100644
--- a/app/assets/javascripts/droplab/plugins/ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js
@@ -1,5 +1,6 @@
/* eslint-disable */
-import AjaxCache from '../../lib/utils/ajax_cache';
+
+import AjaxCache from '~/lib/utils/ajax_cache';
const AjaxFilter = {
init: function (hook) {
diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/filtered_search/droplab/plugins/filter.js
index 06391668928..06391668928 100644
--- a/app/assets/javascripts/droplab/plugins/filter.js
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/filter.js
diff --git a/app/assets/javascripts/droplab/plugins/input_setter.js b/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js
index 148d9a35b81..148d9a35b81 100644
--- a/app/assets/javascripts/droplab/plugins/input_setter.js
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js
diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/filtered_search/droplab/utils.js
index d7f49bf19d8..d7f49bf19d8 100644
--- a/app/assets/javascripts/droplab/utils.js
+++ b/app/assets/javascripts/filtered_search/droplab/utils.js
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index ebaa3ef98b1..e467e97dda9 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,6 +1,6 @@
import { last } from 'lodash';
import AvailableDropdownMappings from 'ee_else_ce/filtered_search/available_dropdown_mappings';
-import DropLab from '~/droplab/drop_lab';
+import DropLab from './droplab/drop_lab_deprecated';
import { DROPDOWN_TYPE } from './constants';
import FilteredSearchContainer from './container';
import DropdownUtils from './dropdown_utils';
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
deleted file mode 100644
index af7a600d1ad..00000000000
--- a/app/assets/javascripts/namespace_select.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import Api from './api';
-import { mergeUrlParams } from './lib/utils/url_utility';
-import { __ } from './locale';
-
-export default class NamespaceSelect {
- constructor(opts) {
- const isFilter = parseBoolean(opts.dropdown.dataset.isFilter);
- const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
-
- initDeprecatedJQueryDropdown($(opts.dropdown), {
- filterable: true,
- selectable: true,
- filterRemote: true,
- search: {
- fields: ['path'],
- },
- fieldName,
- toggleLabel(selected) {
- if (selected.id == null) {
- return selected.text;
- }
- return `${selected.kind}: ${selected.full_path}`;
- },
- data(term, dataCallback) {
- return Api.namespaces(term, (namespaces) => {
- if (isFilter) {
- const anyNamespace = {
- text: __('Any namespace'),
- id: null,
- };
- namespaces.unshift(anyNamespace);
- namespaces.splice(1, 0, { type: 'divider' });
- }
- return dataCallback(namespaces);
- });
- },
- text(namespace) {
- if (namespace.id == null) {
- return namespace.text;
- }
- return `${namespace.kind}: ${namespace.full_path}`;
- },
- renderRow: this.renderRow,
- clicked(options) {
- if (!isFilter) {
- const { e } = options;
- e.preventDefault();
- }
- },
- url(namespace) {
- return mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
- },
- });
- }
-}
diff --git a/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue b/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue
new file mode 100644
index 00000000000..c75c031b0b1
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue
@@ -0,0 +1,143 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import Api from '~/api';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ dropdownHeader: __('Namespaces'),
+ searchPlaceholder: __('Search for Namespace'),
+ anyNamespace: __('Any namespace'),
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ },
+ props: {
+ showAny: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ placeholder: {
+ type: String,
+ required: false,
+ default: __('Namespace'),
+ },
+ fieldName: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ namespaceOptions: [],
+ selectedNamespaceId: null,
+ selectedNamespace: null,
+ searchTerm: '',
+ isLoading: false,
+ };
+ },
+ computed: {
+ selectedNamespaceName() {
+ if (this.selectedNamespaceId === null) {
+ return this.placeholder;
+ }
+ return this.selectedNamespace;
+ },
+ },
+ watch: {
+ searchTerm() {
+ this.fetchNamespaces(this.searchTerm);
+ },
+ },
+ mounted() {
+ this.fetchNamespaces();
+ },
+ methods: {
+ fetchNamespaces(filter) {
+ this.isLoading = true;
+ this.namespaceOptions = [];
+ return Api.namespaces(filter, (namespaces) => {
+ this.namespaceOptions = namespaces;
+ this.isLoading = false;
+ });
+ },
+ selectNamespace(key) {
+ this.selectedNamespaceId = this.namespaceOptions[key].id;
+ this.selectedNamespace = this.getNamespaceString(this.namespaceOptions[key]);
+ this.$emit('setNamespace', this.selectedNamespaceId);
+ },
+ selectAnyNamespace() {
+ this.selectedNamespaceId = null;
+ this.selectedNamespace = null;
+ this.$emit('setNamespace', null);
+ },
+ getNamespaceString(namespace) {
+ return `${namespace.kind}: ${namespace.full_path}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex">
+ <input
+ v-if="fieldName"
+ :name="fieldName"
+ :value="selectedNamespaceId"
+ type="hidden"
+ data-testid="hidden-input"
+ />
+ <gl-dropdown
+ :text="selectedNamespaceName"
+ :header-text="$options.i18n.dropdownHeader"
+ toggle-class="dropdown-menu-toggle large"
+ data-testid="namespace-dropdown"
+ :right="true"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ class="namespace-search-box"
+ debounce="250"
+ :placeholder="$options.i18n.searchPlaceholder"
+ />
+ </template>
+
+ <template v-if="showAny">
+ <gl-dropdown-item @click="selectAnyNamespace">
+ {{ $options.i18n.anyNamespace }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ </template>
+
+ <gl-loading-icon v-if="isLoading" />
+
+ <gl-dropdown-item
+ v-for="(namespace, key) in namespaceOptions"
+ :key="namespace.id"
+ @click="selectNamespace(key)"
+ >
+ {{ getNamespaceString(namespace) }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
+
+<style scoped>
+/* workaround position: relative imposed by .top-area .nav-controls */
+.namespace-search-box >>> input {
+ position: static;
+}
+</style>
diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js
index b07ca815f13..3098d06510b 100644
--- a/app/assets/javascripts/pages/admin/projects/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index.js
@@ -1,8 +1,38 @@
-import NamespaceSelect from '~/namespace_select';
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import ProjectsList from '~/projects_list';
+import NamespaceSelect from './components/namespace_select.vue';
new ProjectsList(); // eslint-disable-line no-new
-document
- .querySelectorAll('.js-namespace-select')
- .forEach((dropdown) => new NamespaceSelect({ dropdown }));
+function mountNamespaceSelect() {
+ const el = document.querySelector('.js-namespace-select');
+ if (!el) {
+ return false;
+ }
+
+ const { showAny, fieldName, placeholder, updateLocation } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(NamespaceSelect, {
+ props: {
+ showAny: parseBoolean(showAny),
+ fieldName,
+ placeholder,
+ },
+ on: {
+ setNamespace(newNamespace) {
+ if (fieldName && updateLocation) {
+ window.location = mergeUrlParams({ [fieldName]: newNamespace }, window.location.href);
+ }
+ },
+ },
+ });
+ },
+ });
+}
+
+mountNamespaceSelect();
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index 35b001f8ef9..9823b0229a0 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -5,11 +5,10 @@ import {
GlDropdownSectionHeader,
GlDropdownDivider,
GlSearchBoxByType,
- GlLoadingIcon,
GlAvatar,
GlSprintf,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
import createFlash from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
@@ -32,7 +31,6 @@ export default {
GlDropdownSectionHeader,
GlDropdownDivider,
GlSearchBoxByType,
- GlLoadingIcon,
GlAvatar,
GlSprintf,
},
@@ -50,10 +48,26 @@ export default {
type: Boolean,
default: true,
},
+ label: {
+ type: String,
+ required: false,
+ default: i18n.selectUsers,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ preselectedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
loading: false,
+ initialLoading: false,
query: '',
users: [],
groups: [],
@@ -68,6 +82,9 @@ export default {
};
},
computed: {
+ preselected() {
+ return groupBy(this.preselectedItems, 'type');
+ },
showDeployKeys() {
return this.accessLevel === ACCESS_LEVELS.PUSH && this.deployKeys.length;
},
@@ -105,10 +122,18 @@ export default {
labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
}
- return labelPieces.join(', ') || i18n.selectUsers;
+ return labelPieces.join(', ') || this.label;
},
toggleClass() {
- return this.toggleLabel === i18n.selectUsers ? 'gl-text-gray-500!' : '';
+ return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
+ },
+ selection() {
+ return [
+ ...this.getDataForSave(LEVEL_TYPES.ROLE, 'access_level'),
+ ...this.getDataForSave(LEVEL_TYPES.GROUP, 'group_id'),
+ ...this.getDataForSave(LEVEL_TYPES.USER, 'user_id'),
+ ...this.getDataForSave(LEVEL_TYPES.DEPLOY_KEY, 'deploy_key_id'),
+ ];
},
},
watch: {
@@ -117,14 +142,14 @@ export default {
}, 500),
},
created() {
- this.getData();
+ this.getData({ initial: true });
},
-
methods: {
focusInput() {
this.$refs.search.focusInput();
},
- getData() {
+ getData({ initial = false } = {}) {
+ this.initialLoading = initial;
this.loading = true;
if (this.hasLicense) {
@@ -133,20 +158,26 @@ export default {
getUsers(this.query),
this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
])
- .then(([deployKeysResponse, usersResponse, groupsResponse]) =>
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data),
- )
+ .then(([deployKeysResponse, usersResponse, groupsResponse]) => {
+ this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
+ this.setSelected({ initial });
+ })
.catch(() =>
createFlash({ message: __('Failed to load groups, users and deploy keys.') }),
)
.finally(() => {
+ this.initialLoading = false;
this.loading = false;
});
} else {
getDeployKeys(this.query)
- .then((deployKeysResponse) => this.consolidateData(deployKeysResponse.data))
+ .then((deployKeysResponse) => {
+ this.consolidateData(deployKeysResponse.data);
+ this.setSelected({ initial });
+ })
.catch(() => createFlash({ message: __('Failed to load deploy keys.') }))
.finally(() => {
+ this.initialLoading = false;
this.loading = false;
});
}
@@ -159,7 +190,13 @@ export default {
if (this.hasLicense) {
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
- this.users = usersResponse.map((user) => ({ ...user, type: LEVEL_TYPES.USER }));
+ this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
+ id,
+ name,
+ username,
+ avatar_url,
+ type: LEVEL_TYPES.USER,
+ }));
}
this.deployKeys = deployKeysResponse.map((response) => {
@@ -182,12 +219,84 @@ export default {
};
});
},
+ setSelected({ initial } = {}) {
+ if (initial) {
+ // as all available groups && roles are always visible in the dropdown, we set local selected by looking
+ // for intersection in all roles/groups and initial selected (returned from BE).
+ // It is different for the users - not all the users will be returned on the first data load (another set
+ // will be returned on search, only first 20 are displayed initially).
+ // That is why we set ALL initial selected users (returned from BE) as local selected (not looking
+ // for the intersection with all users data) and later if the selected happens to be in the users list
+ // we filter it out from the list so that not to have duplicates
+ // TODO: we'll need to get back to how to handle deploy keys here but they are out of scope
+ // and will be checked when migrating protected branches access dropdown to the current component
+ // related issue - https://gitlab.com/gitlab-org/gitlab/-/issues/284784
+ const selectedRoles = intersectionWith(
+ this.roles,
+ this.preselectedItems,
+ (role, selected) => {
+ return selected.type === LEVEL_TYPES.ROLE && role.id === selected.access_level;
+ },
+ );
+ this.selected[LEVEL_TYPES.ROLE] = selectedRoles;
+
+ const selectedGroups = intersectionWith(
+ this.groups,
+ this.preselectedItems,
+ (group, selected) => {
+ return selected.type === LEVEL_TYPES.GROUP && group.id === selected.group_id;
+ },
+ );
+ this.selected[LEVEL_TYPES.GROUP] = selectedGroups;
+
+ const selectedDeployKeys = intersectionWith(
+ this.deployKeys,
+ this.preselectedItems,
+ (key, selected) => {
+ return selected.type === LEVEL_TYPES.DEPLOY_KEY && key.id === selected.deploy_key_id;
+ },
+ );
+ this.selected[LEVEL_TYPES.DEPLOY_KEY] = selectedDeployKeys;
+
+ const selectedUsers = this.preselectedItems
+ .filter(({ type }) => type === LEVEL_TYPES.USER)
+ .map(({ user_id, name, username, avatar_url, type }) => ({
+ id: user_id,
+ name,
+ username,
+ avatar_url,
+ type,
+ }));
+
+ this.selected[LEVEL_TYPES.USER] = selectedUsers;
+ }
+
+ this.users = this.users.filter(
+ (user) => !this.selected[LEVEL_TYPES.USER].some((selected) => selected.id === user.id),
+ );
+ this.users.unshift(...this.selected[LEVEL_TYPES.USER]);
+ },
+ getDataForSave(accessType, key) {
+ const selected = this.selected[accessType].map(({ id }) => ({ [key]: id }));
+ const preselected = this.preselected[accessType];
+ const added = differenceBy(selected, preselected, key);
+ const preserved = intersectionBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({
+ id,
+ [key]: keyId,
+ }));
+ const removed = differenceBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({
+ id,
+ [key]: keyId,
+ _destroy: true,
+ }));
+ return [...added, ...removed, ...preserved];
+ },
onItemClick(item) {
this.toggleSelection(this.selected[item.type], item);
this.emitUpdate();
},
toggleSelection(arr, item) {
- const itemIndex = arr.indexOf(item);
+ const itemIndex = arr.findIndex(({ id }) => id === item.id);
if (itemIndex > -1) {
arr.splice(itemIndex, 1);
} else arr.push(item);
@@ -196,8 +305,10 @@ export default {
return this.selected[item.type].some((selected) => selected.id === item.id);
},
emitUpdate() {
- const selected = Object.values(this.selected).flat();
- this.$emit('select', selected);
+ this.$emit('select', this.selection);
+ },
+ onHide() {
+ this.$emit('hidden', this.selection);
},
},
};
@@ -205,15 +316,16 @@ export default {
<template>
<gl-dropdown
+ :disabled="disabled || initialLoading"
:text="toggleLabel"
- class="gl-display-block"
+ class="gl-min-w-20"
:toggle-class="toggleClass"
aria-labelledby="allowed-users-label"
@shown="focusInput"
+ @hidden="onHide"
>
<template #header>
- <gl-search-box-by-type ref="search" v-model.trim="query" />
- <gl-loading-icon v-if="loading" size="sm" />
+ <gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
</template>
<template v-if="roles.length">
<gl-dropdown-section-header>{{
@@ -221,7 +333,8 @@ export default {
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="role in roles"
- :key="role.id"
+ :key="`${role.id}${role.text}`"
+ data-testid="role-dropdown-item"
is-check-item
:is-checked="isSelected(role)"
@click.native.capture.stop="onItemClick(role)"
@@ -237,7 +350,9 @@ export default {
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group in groups"
- :key="group.id"
+ :key="`${group.id}${group.name}`"
+ fingerprint
+ data-testid="group-dropdown-item"
:avatar-url="group.avatar_url"
is-check-item
:is-checked="isSelected(group)"
@@ -254,7 +369,8 @@ export default {
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="user in users"
- :key="user.id"
+ :key="`${user.id}${user.username}`"
+ data-testid="user-dropdown-item"
:avatar-url="user.avatar_url"
:secondary-text="user.username"
is-check-item
@@ -272,7 +388,8 @@ export default {
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="key in deployKeys"
- :key="key.id"
+ :key="`${key.id}${key.fingerprint}`"
+ data-testid="deploy_key-dropdown-item"
is-check-item
:is-checked="isSelected(key)"
class="gl-text-truncate"
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 9944b05d206..11272652b63 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/browser';
import Vue from 'vue';
import AccessDropdown from './components/access_dropdown.vue';
@@ -7,6 +8,13 @@ export const initAccessDropdown = (el, options) => {
}
const { accessLevelsData, accessLevel } = options;
+ const { label, disabled, preselectedItems } = el.dataset;
+ let preselected = [];
+ try {
+ preselected = JSON.parse(preselectedItems);
+ } catch (e) {
+ Sentry.captureException(e);
+ }
return new Vue({
el,
@@ -16,6 +24,9 @@ export const initAccessDropdown = (el, options) => {
props: {
accessLevel,
accessLevelsData: accessLevelsData.roles,
+ preselectedItems: preselected,
+ label,
+ disabled,
},
on: {
select(selected) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index c4e5fa06cd6..0da622448f9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -7,7 +7,7 @@ import {
GlSafeHtmlDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import { sprintf, s__ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import { EXTENSION_ICON_CLASS } from '../../constants';
import StatusIcon from './status_icon.vue';
@@ -42,6 +42,12 @@ export default {
};
},
computed: {
+ widgetLabel() {
+ return this.$options.i18n?.label || this.$options.name;
+ },
+ widgetLoadingText() {
+ return this.$options.i18n?.loading || __('Loading...');
+ },
isLoadingSummary() {
return this.loadingState === LOADING_STATES.collapsedLoading;
},
@@ -60,7 +66,7 @@ export default {
this.isCollapsed
? s__('mrWidget|Show %{widget} details')
: s__('mrWidget|Hide %{widget} details'),
- { widget: this.$options.label || this.$options.name },
+ { widget: this.widgetLabel },
);
},
statusIconName() {
@@ -119,24 +125,15 @@ export default {
<template>
<section class="media-section" data-testid="widget-extension">
<div class="media gl-p-5">
- <status-icon
- :name="$options.label || $options.name"
- :is-loading="isLoadingSummary"
- :icon-name="statusIconName"
- />
+ <status-icon :name="widgetLabel" :is-loading="isLoadingSummary" :icon-name="statusIconName" />
<div
class="media-body gl-display-flex gl-align-self-center gl-align-items-center gl-flex-direction-row!"
>
<div class="gl-flex-grow-1">
- <template v-if="isLoadingSummary">
- {{ __('Loading...') }}
- </template>
+ <template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
<div v-else v-safe-html="summary(collapsedData)"></div>
</div>
- <actions
- :widget="$options.label || $options.name"
- :tertiary-buttons="tertiaryActionsButtons"
- />
+ <actions :widget="widgetLabel" :tertiary-buttons="tertiaryActionsButtons" />
<div
class="gl-float-right gl-align-self-center gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index bb40e22fe3f..4ca0b660696 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -11,6 +11,7 @@ export const registerExtension = (extension) => {
extends: ExtensionBase,
name: extension.name,
props: extension.props,
+ i18n: extension.i18n,
computed: {
...Object.keys(extension.computed).reduce(
(acc, computedKey) => ({
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
index 21e0b95431b..08c9172cc5b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
@@ -6,8 +6,11 @@ import issuesQuery from './issues.query.graphql';
export default {
// Give the extension a name
// Make it easier to track in Vue dev tools
- name: 'Issues',
- label: 'Issues',
+ name: 'WidgetIssues',
+ i18n: {
+ label: 'Issues',
+ loading: 'Loading issues...',
+ },
// Add an array of props
// These then get mapped to values stored in the MR Widget store
props: ['targetProjectFullPath', 'conflictsDocsPath'],
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index 50fc11ec7f3..82e56cf8b81 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -6,29 +6,24 @@
.form-check
= f.check_box :authorized_keys_enabled, class: 'form-check-input'
= f.label :authorized_keys_enabled, class: 'form-check-label' do
- = _('Write to "authorized_keys" file')
+ = _('Use authorized_keys file to authenticate SSH keys')
.form-text.text-muted
- By default, we write to the "authorized_keys" file to support Git
- over SSH without additional configuration. GitLab can be optimized
- to authenticate SSH keys via the database file. Only uncheck this
- if you have configured your OpenSSH server to use the
- AuthorizedKeysCommand. Click on the help icon for more details.
- = link_to sprite_icon('question-o'), help_page_path('administration/operations/fast_ssh_key_lookup')
-
+ = _('Authenticate user SSH keys without requiring additional configuration. Performance of GitLab can be improved by using the GitLab database instead.')
+ = link_to _('How do I configure authentication using the GitLab database?'), help_page_path('administration/operations/fast_ssh_key_lookup'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.label :raw_blob_request_limit, _('Raw blob request rate limit per minute'), class: 'label-bold'
= f.number_field :raw_blob_request_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0.')
+ = _('Maximum number of requests per minute for each raw path (default is 300). Set to 0 to disable throttling.')
.form-group
= f.label :push_event_hooks_limit, class: 'label-bold'
= f.number_field :push_event_hooks_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _("Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value.")
+ = _('Maximum number of changes (branches or tags) in a single push for which webhooks and services trigger (default is 3).')
.form-group
= f.label :push_event_activities_limit, class: 'label-bold'
= f.number_field :push_event_activities_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.')
+ = _('Threshold number of changes (branches or tags) in a single push above which a bulk push event is created (default is 3).')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index f30e8eb0d54..f947e174990 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -11,18 +11,14 @@
.nav-controls
.search-holder
= render 'shared/projects/search_form', autofocus: true, admin_view: true
- .dropdown
- - toggle_text = _('Namespace')
- - if params[:namespace_id].present?
- = hidden_field_tag :namespace_id, params[:namespace_id]
- - namespace = Namespace.find(params[:namespace_id])
- - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
- = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
- .dropdown-menu.dropdown-select.dropdown-menu-right
- = dropdown_title(_('Namespaces'))
- = dropdown_filter(_("Search for Namespace"))
- = dropdown_content
- = dropdown_loading
+ - current_namespace = _('Namespace')
+ - if params[:namespace_id].present?
+ - namespace = Namespace.find(params[:namespace_id])
+ - current_namespace = "#{namespace.kind}: #{namespace.full_path}"
+ %button.dropdown-menu-toggle.btn.btn-default.btn-md.gl-button.js-namespace-select{ data: { show_any: 'true', field_name: 'namespace_id', placeholder: current_namespace, update_location: 'true' }, type: 'button' }
+ %span.gl-new-dropdown-button-text
+ = current_namespace
+
= render 'shared/projects/dropdown'
= link_to new_project_path, class: 'gl-button btn btn-confirm' do
= _('New Project')
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 1a87b21351c..3069aab2710 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -143,13 +143,10 @@
.col-sm-3.col-form-label
= f.label :new_namespace_id, _("Namespace")
.col-sm-9
- .dropdown
- = dropdown_toggle(_('Search for Namespace'), { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
- .dropdown-menu.dropdown-select
- = dropdown_title(_('Namespaces'))
- = dropdown_filter(_('Search for Namespace'))
- = dropdown_content
- = dropdown_loading
+ - placeholder = _('Search for Namespace')
+ %button.dropdown-menu-toggle.btn.btn-default.btn-md.gl-button.js-namespace-select{ data: { field_name: 'new_namespace_id', placeholder: placeholder }, type: 'button' }
+ %span.gl-new-dropdown-button-text
+ = placeholder
.form-group.row
.offset-sm-3.col-sm-9
diff --git a/app/views/clusters/clusters/_namespace.html.haml b/app/views/clusters/clusters/_namespace.html.haml
index 9b728e7a89b..6412972e195 100644
--- a/app/views/clusters/clusters/_namespace.html.haml
+++ b/app/views/clusters/clusters/_namespace.html.haml
@@ -1,7 +1,6 @@
- managed_namespace_help_text = s_('ClusterIntegration|Set a prefix for your namespaces. If not set, defaults to your project path. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- non_managed_namespace_help_text = s_('ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, logs, and Web terminals.')
-- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
- anchor: 'gitlab-managed-clusters'), target: '_blank'
+- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank'
.js-namespace-prefixed
= platform_field.text_field :namespace,
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index cfeceb4f463..4b8a71a8e29 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -15,7 +15,7 @@
provider_type: @cluster.provider_type,
help_path: help_page_path('user/project/clusters/index.md'),
environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
- clusters_help_path: help_page_path('user/project/clusters/index.md', anchor: 'deploying-to-a-kubernetes-cluster'),
+ clusters_help_path: help_page_path('user/project/clusters/deploy_to_cluster.md'),
deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
cluster_id: @cluster.id } }