summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-10 09:09:01 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-10 09:09:01 +0000
commitcdd71cf36a45b72d8207fe4fcfc4e44a405d3607 (patch)
treef476a58abd1b6bbb44f9dbbc2fa6aa483f2c65a3
parentff1701e51d8eac96e371de168c582b964ea96a83 (diff)
downloadgitlab-ce-cdd71cf36a45b72d8207fe4fcfc4e44a405d3607.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/issuables_list/components/issuable.vue2
-rw-r--r--app/assets/javascripts/issuables_list/components/issuables_list_app.vue149
-rw-r--r--app/assets/javascripts/issuables_list/constants.js21
-rw-r--r--app/assets/javascripts/main.js4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js11
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue31
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js8
-rw-r--r--app/assets/javascripts/search_autocomplete.js (renamed from app/assets/javascripts/global_search_input.js)207
-rw-r--r--app/controllers/search_controller.rb15
-rw-r--r--app/helpers/export_helper.rb2
-rw-r--r--app/helpers/search_helper.rb107
-rw-r--r--app/services/jira/requests/issues/list_service.rb4
-rw-r--r--app/views/layouts/_search.html.haml5
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml2
-rw-r--r--changelogs/unreleased/212811-adding-new-task-always-shows-error-something-went-wrong-while-fetc.yml5
-rw-r--r--changelogs/unreleased/220342-remove-services-from-import-export.yml5
-rw-r--r--changelogs/unreleased/225187-replace-fa-comment-icons-with-gitlab-svg-comment-icon.yml5
-rw-r--r--changelogs/unreleased/227558-missing-digest-revision-and-short-revision-in-tags.yml5
-rw-r--r--changelogs/unreleased/feature-secure-eslint-to-core.yml5
-rw-r--r--changelogs/unreleased/revert-bc8546a9.yml5
-rw-r--r--config/routes.rb1
-rw-r--r--crowdin.yml4
-rw-r--r--doc/administration/gitaly/praefect.md32
-rw-r--r--doc/user/application_security/sast/index.md9
-rw-r--r--doc/user/project/settings/import_export.md2
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml1
-rw-r--r--lib/gitlab/import_export/project/import_export.yml9
-rw-r--r--lib/gitlab/import_export/project/relation_factory.rb11
-rw-r--r--locale/gitlab.pot47
-rw-r--r--spec/controllers/search_controller_spec.rb5
-rw-r--r--spec/features/issuables/issuable_list_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin3360 -> 3176 bytes
-rw-r--r--spec/features/signed_commits_spec.rb7
-rw-r--r--spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gzbin3973 -> 3846 bytes
-rw-r--r--spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gzbin3778 -> 3647 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json370
-rw-r--r--spec/fixtures/lib/gitlab/import_export/designs/project.json3
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/project.json42
-rw-r--r--spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json1
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js5
-rw-r--r--spec/frontend/environment.js7
-rw-r--r--spec/frontend/fixtures/static/search_autocomplete.html (renamed from spec/frontend/fixtures/static/global_search_input.html)0
-rw-r--r--spec/frontend/helpers/test_constants.js22
-rw-r--r--spec/frontend/issuables_list/components/issuables_list_app_spec.js54
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js3
-rw-r--r--spec/frontend/monitoring/utils_spec.js2
-rw-r--r--spec/frontend/notes/stores/actions_spec.js57
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js119
-rw-r--r--spec/frontend/repository/utils/dom_spec.js3
-rw-r--r--spec/frontend/search_autocomplete_spec.js (renamed from spec/frontend/global_search_input_spec.js)106
-rw-r--r--spec/frontend/self_monitor/components/self_monitor_form_spec.js3
-rw-r--r--spec/helpers/search_helper_spec.rb93
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb21
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb13
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml30
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/jira/requests/issues/list_service_spec.rb10
61 files changed, 990 insertions, 718 deletions
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue
index 9af1887ef12..04991a8d374 100644
--- a/app/assets/javascripts/issuables_list/components/issuable.vue
+++ b/app/assets/javascripts/issuables_list/components/issuable.vue
@@ -365,7 +365,7 @@ export default {
:title="__('Comments')"
:class="{ 'no-comments': hasNoComments }"
>
- <i class="fa fa-comments"></i>
+ <gl-icon name="comments" class="gl-vertical-align-text-bottom" />
{{ userNotesCount }}
</gl-link>
</div>
diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
index db18bcbce09..e1a40323f5d 100644
--- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
@@ -12,8 +12,10 @@ import {
import { __ } from '~/locale';
import initManualOrdering from '~/manual_ordering';
import Issuable from './issuable.vue';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
sortOrderMap,
+ availableSortOptionsJira,
RELATIVE_POSITION,
PAGE_SIZE,
PAGE_SIZE_MANUAL,
@@ -29,6 +31,7 @@ export default {
GlPagination,
GlSkeletonLoading,
Issuable,
+ FilteredSearchBar,
},
props: {
canBulkEdit: {
@@ -50,14 +53,25 @@ export default {
type: String,
required: true,
},
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
sortKey: {
type: String,
required: false,
default: '',
},
+ type: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
+ availableSortOptionsJira,
filters: {},
isBulkEditing: false,
issuables: [],
@@ -141,6 +155,22 @@ export default {
nextPage: this.paginationNext,
};
},
+ isJira() {
+ return this.type === 'jira';
+ },
+ initialFilterValue() {
+ const value = [];
+ const { search } = this.getQueryObject();
+
+ if (search) {
+ value.push(search);
+ }
+ return value;
+ },
+ initialSortBy() {
+ const { sort } = this.getQueryObject();
+ return sort || 'created_desc';
+ },
},
watch: {
selection() {
@@ -262,51 +292,92 @@ export default {
this.filters = filters;
},
+ refetchIssuables() {
+ const ignored = ['utf8', 'state'];
+ const params = omit(this.filters, ignored);
+
+ historyPushState(setUrlParams(params, window.location.href, true));
+ this.fetchIssuables();
+ },
+ handleFilter(filters) {
+ let search = null;
+
+ filters.forEach(filter => {
+ if (typeof filter === 'string') {
+ search = filter;
+ }
+ });
+
+ this.filters.search = search;
+ this.page = 1;
+
+ this.refetchIssuables();
+ },
+ handleSort(sort) {
+ this.filters.sort = sort;
+ this.page = 1;
+
+ this.refetchIssuables();
+ },
},
};
</script>
<template>
- <ul v-if="loading" class="content-list">
- <li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue gl-px-5! gl-py-5!">
- <gl-skeleton-loading />
- </li>
- </ul>
- <div v-else-if="issuables.length">
- <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light">
- <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" />
- <strong>{{ __('Select all') }}</strong>
- </div>
- <ul
- class="content-list issuable-list issues-list"
- :class="{ 'manual-ordering': isManualOrdering }"
- >
- <issuable
- v-for="issuable in issuables"
- :key="issuable.id"
- class="pr-3"
- :class="{ 'user-can-drag': isManualOrdering }"
- :issuable="issuable"
- :is-bulk-editing="isBulkEditing"
- :selected="isSelected(issuable.id)"
- :base-url="baseUrl"
- @select="onSelectIssuable"
- />
+ <div>
+ <filtered-search-bar
+ v-if="isJira"
+ :namespace="projectPath"
+ :search-input-placeholder="__('Search Jira issues')"
+ :tokens="[]"
+ :sort-options="availableSortOptionsJira"
+ :initial-filter-value="initialFilterValue"
+ :initial-sort-by="initialSortBy"
+ class="row-content-block"
+ @onFilter="handleFilter"
+ @onSort="handleSort"
+ />
+ <ul v-if="loading" class="content-list">
+ <li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue gl-px-5! gl-py-5!">
+ <gl-skeleton-loading />
+ </li>
</ul>
- <div class="mt-3">
- <gl-pagination
- v-bind="paginationProps"
- class="gl-justify-content-center"
- @input="onPaginate"
- />
+ <div v-else-if="issuables.length">
+ <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light">
+ <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" />
+ <strong>{{ __('Select all') }}</strong>
+ </div>
+ <ul
+ class="content-list issuable-list issues-list"
+ :class="{ 'manual-ordering': isManualOrdering }"
+ >
+ <issuable
+ v-for="issuable in issuables"
+ :key="issuable.id"
+ class="pr-3"
+ :class="{ 'user-can-drag': isManualOrdering }"
+ :issuable="issuable"
+ :is-bulk-editing="isBulkEditing"
+ :selected="isSelected(issuable.id)"
+ :base-url="baseUrl"
+ @select="onSelectIssuable"
+ />
+ </ul>
+ <div class="mt-3">
+ <gl-pagination
+ v-bind="paginationProps"
+ class="gl-justify-content-center"
+ @input="onPaginate"
+ />
+ </div>
</div>
+ <gl-empty-state
+ v-else
+ :title="emptyState.title"
+ :description="emptyState.description"
+ :svg-path="emptySvgPath"
+ :primary-button-link="emptyState.primaryLink"
+ :primary-button-text="emptyState.primaryText"
+ />
</div>
- <gl-empty-state
- v-else
- :title="emptyState.title"
- :description="emptyState.description"
- :svg-path="emptySvgPath"
- :primary-button-link="emptyState.primaryLink"
- :primary-button-text="emptyState.primaryText"
- />
</template>
diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issuables_list/constants.js
index 71b9c52c703..e240efd2804 100644
--- a/app/assets/javascripts/issuables_list/constants.js
+++ b/app/assets/javascripts/issuables_list/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
// Maps sort order as it appears in the URL query to API `order_by` and `sort` params.
const PRIORITY = 'priority';
const ASC = 'asc';
@@ -31,3 +33,22 @@ export const sortOrderMap = {
weight_desc: { order_by: WEIGHT, sort: DESC },
weight: { order_by: WEIGHT, sort: ASC },
};
+
+export const availableSortOptionsJira = [
+ {
+ id: 1,
+ title: __('Created date'),
+ sortDirection: {
+ descending: 'created_desc',
+ ascending: 'created_asc',
+ },
+ },
+ {
+ id: 2,
+ title: __('Last updated'),
+ sortDirection: {
+ descending: 'updated_desc',
+ ascending: 'updated_asc',
+ },
+ },
+];
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index b742fe42024..3f85295a5ed 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -33,7 +33,7 @@ import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent';
import initPerformanceBar from './performance_bar';
-import initGlobalSearchInput from './global_search_input';
+import initSearchAutocomplete from './search_autocomplete';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
@@ -113,7 +113,7 @@ function deferredInitialisation() {
initFrequentItemDropdowns();
initPersistentUserCallouts();
- if (document.querySelector('.search')) initGlobalSearchInput();
+ if (document.querySelector('.search')) initSearchAutocomplete();
addSelectOnFocusBehaviour('.js-select-on-focus');
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 32af62fe6f1..8a7734f4d31 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -402,9 +402,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
};
const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
- if (resp.notes && resp.notes.length) {
- updateOrCreateNotes({ commit, state, getters, dispatch }, resp.notes);
-
+ if (resp.notes?.length) {
+ dispatch('updateOrCreateNotes', resp.notes);
dispatch('startTaskList');
}
@@ -424,12 +423,12 @@ const getFetchDataParams = state => {
return { endpoint, options };
};
-export const fetchData = ({ commit, state, getters }) => {
+export const fetchData = ({ commit, state, getters, dispatch }) => {
const { endpoint, options } = getFetchDataParams(state);
axios
.get(endpoint, options)
- .then(({ data }) => pollSuccessCallBack(data, commit, state, getters))
+ .then(({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch))
.catch(() => Flash(__('Something went wrong while fetching latest comments.')));
};
@@ -449,7 +448,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
} else {
- fetchData({ commit, state, getters });
+ dispatch('fetchData');
}
Visibility.change(() => {
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index d9816dc5102..51ba2337db6 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -1,5 +1,5 @@
<script>
-import { GlFormCheckbox, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
+import { GlFormCheckbox, GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -16,12 +16,16 @@ import {
PUBLISHED_DETAILS_ROW_TEXT,
MANIFEST_DETAILS_ROW_TEST,
CONFIGURATION_DETAILS_ROW_TEST,
+ MISSING_MANIFEST_WARNING_TOOLTIP,
+ NOT_AVAILABLE_TEXT,
+ NOT_AVAILABLE_SIZE,
} from '../../constants/index';
export default {
components: {
GlSprintf,
GlFormCheckbox,
+ GlIcon,
DeleteButton,
ListItem,
ClipboardButton,
@@ -55,10 +59,11 @@ export default {
PUBLISHED_DETAILS_ROW_TEXT,
MANIFEST_DETAILS_ROW_TEST,
CONFIGURATION_DETAILS_ROW_TEST,
+ MISSING_MANIFEST_WARNING_TOOLTIP,
},
computed: {
formattedSize() {
- return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : '';
+ return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : NOT_AVAILABLE_SIZE;
},
layers() {
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
@@ -68,7 +73,7 @@ export default {
},
shortDigest() {
// remove sha256: from the string, and show only the first 7 char
- return this.tag.digest?.substring(7, 14);
+ return this.tag.digest?.substring(7, 14) ?? NOT_AVAILABLE_TEXT;
},
publishedDate() {
return formatDate(this.tag.created_at, 'isoDate');
@@ -85,6 +90,9 @@ export default {
tagLocation() {
return this.tag.path?.replace(`:${this.tag.name}`, '');
},
+ invalidTag() {
+ return !this.tag.digest;
+ },
},
};
</script>
@@ -94,6 +102,7 @@ export default {
<template #left-action>
<gl-form-checkbox
v-if="Boolean(tag.destroy_path)"
+ :disabled="invalidTag"
class="gl-m-0"
:checked="selected"
@change="$emit('select')"
@@ -116,6 +125,13 @@ export default {
:text="tag.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
+
+ <gl-icon
+ v-if="invalidTag"
+ v-gl-tooltip="{ title: $options.i18n.MISSING_MANIFEST_WARNING_TOOLTIP }"
+ name="warning"
+ class="gl-text-orange-500 gl-mb-2 gl-ml-2"
+ />
</div>
</template>
@@ -146,7 +162,7 @@ export default {
</template>
<template #right-action>
<delete-button
- :disabled="!tag.destroy_path"
+ :disabled="!tag.destroy_path || invalidTag"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP"
:tooltip-disabled="Boolean(tag.destroy_path)"
@@ -154,7 +170,8 @@ export default {
@delete="$emit('delete')"
/>
</template>
- <template #details_published>
+
+ <template v-if="!invalidTag" #details_published>
<details-row icon="clock" data-testid="published-date-detail">
<gl-sprintf :message="$options.i18n.PUBLISHED_DETAILS_ROW_TEXT">
<template #repositoryPath>
@@ -169,7 +186,7 @@ export default {
</gl-sprintf>
</details-row>
</template>
- <template #details_manifest_digest>
+ <template v-if="!invalidTag" #details_manifest_digest>
<details-row icon="log" data-testid="manifest-detail">
<gl-sprintf :message="$options.i18n.MANIFEST_DETAILS_ROW_TEST">
<template #digest>
@@ -184,7 +201,7 @@ export default {
/>
</details-row>
</template>
- <template #details_configuration_digest>
+ <template v-if="!invalidTag" #details_configuration_digest>
<details-row icon="cloud-gear" data-testid="configuration-detail">
<gl-sprintf :message="$options.i18n.CONFIGURATION_DETAILS_ROW_TEST">
<template #digest>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
index e62c0b78efb..2874d89d913 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -88,7 +88,7 @@ export default {
v-if="item.failedDelete"
v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
name="warning"
- class="text-warning"
+ class="gl-text-orange-500"
/>
</template>
<template #left-secondary>
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index c6b611114e6..1dc5882d415 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
// Translations strings
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
@@ -48,6 +48,12 @@ export const REMOVE_TAG_BUTTON_DISABLE_TOOLTIP = s__(
'ContainerRegistry|Deletion disabled due to missing or insufficient permissions.',
);
+export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
+ 'ContainerRegistry|Invalid tag: missing manifest digest',
+);
+
+export const NOT_AVAILABLE_TEXT = __('N/A');
+export const NOT_AVAILABLE_SIZE = __('0 bytes');
// Parameters
export const DEFAULT_PAGE = 1;
diff --git a/app/assets/javascripts/global_search_input.js b/app/assets/javascripts/search_autocomplete.js
index a7c121259d4..05e0b9e7089 100644
--- a/app/assets/javascripts/global_search_input.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,8 +1,10 @@
/* eslint-disable no-return-assign, consistent-return, class-methods-use-this */
import $ from 'jquery';
-import { throttle } from 'lodash';
+import { escape, throttle } from 'lodash';
import { s__, __, sprintf } from '~/locale';
+import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
+import axios from './lib/utils/axios_utils';
import {
isInGroupsPage,
isInProjectPage,
@@ -65,11 +67,15 @@ function setSearchOptions() {
}
}
-export class GlobalSearchInput {
- constructor({ wrap } = {}) {
+export class SearchAutocomplete {
+ constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
setSearchOptions();
this.bindEventContext();
this.wrap = wrap || $('.search');
+ this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
+ this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath');
+ this.projectId = projectId || (this.optsEl.data('autocompleteProjectId') || '');
+ this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
this.dropdownMenu = this.dropdown.find('.dropdown-menu');
@@ -86,7 +92,7 @@ export class GlobalSearchInput {
// Only when user is logged in
if (gon.current_user_id) {
- this.createGlobalSearchInput();
+ this.createAutocomplete();
}
this.bindEvents();
@@ -111,7 +117,7 @@ export class GlobalSearchInput {
return (this.originalState = this.serializeState());
}
- createGlobalSearchInput() {
+ createAutocomplete() {
return this.searchInput.glDropdown({
filterInputBlur: false,
filterable: true,
@@ -143,17 +149,116 @@ export class GlobalSearchInput {
if (glDropdownInstance) {
glDropdownInstance.filter.options.callback(contents);
}
- this.enableDropdown();
+ this.enableAutocomplete();
}
return;
}
- const options = this.scopedSearchOptions(term);
+ // Prevent multiple ajax calls
+ if (this.loadingSuggestions) {
+ return;
+ }
- callback(options);
+ this.loadingSuggestions = true;
+
+ return axios
+ .get(this.autocompletePath, {
+ params: {
+ project_id: this.projectId,
+ project_ref: this.projectRef,
+ term,
+ },
+ })
+ .then(response => {
+ const options = this.scopedSearchOptions(term);
+
+ // List results
+ let lastCategory = null;
+ for (let i = 0, len = response.data.length; i < len; i += 1) {
+ const suggestion = response.data[i];
+ // Add group header before list each group
+ if (lastCategory !== suggestion.category) {
+ options.push({ type: 'separator' });
+ options.push({
+ type: 'header',
+ content: suggestion.category,
+ });
+ lastCategory = suggestion.category;
+ }
+
+ // Add the suggestion
+ options.push({
+ id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
+ icon: this.getAvatar(suggestion),
+ category: suggestion.category,
+ text: suggestion.label,
+ url: suggestion.url,
+ });
+ }
- this.highlightFirstRow();
- this.setScrollFade();
+ callback(options);
+
+ this.loadingSuggestions = false;
+ this.highlightFirstRow();
+ this.setScrollFade();
+ })
+ .catch(() => {
+ this.loadingSuggestions = false;
+ });
+ }
+
+ getCategoryContents() {
+ const userName = gon.current_username;
+ const { projectOptions, groupOptions, dashboardOptions } = gl;
+
+ // Get options
+ let options;
+ if (isInProjectPage() && projectOptions) {
+ options = projectOptions[getProjectSlug()];
+ } else if (isInGroupsPage() && groupOptions) {
+ options = groupOptions[getGroupSlug()];
+ } else if (dashboardOptions) {
+ options = dashboardOptions;
+ }
+
+ const { issuesPath, mrPath, name, issuesDisabled } = options;
+ const baseItems = [];
+
+ if (name) {
+ baseItems.push({
+ type: 'header',
+ content: `${name}`,
+ });
+ }
+
+ const issueItems = [
+ {
+ text: s__('SearchAutocomplete|Issues assigned to me'),
+ url: `${issuesPath}/?assignee_username=${userName}`,
+ },
+ {
+ text: s__("SearchAutocomplete|Issues I've created"),
+ url: `${issuesPath}/?author_username=${userName}`,
+ },
+ ];
+ const mergeRequestItems = [
+ {
+ text: s__('SearchAutocomplete|Merge requests assigned to me'),
+ url: `${mrPath}/?assignee_username=${userName}`,
+ },
+ {
+ text: s__("SearchAutocomplete|Merge requests I've created"),
+ url: `${mrPath}/?author_username=${userName}`,
+ },
+ ];
+
+ let items;
+ if (issuesDisabled) {
+ items = baseItems.concat(mergeRequestItems);
+ } else {
+ items = baseItems.concat(...issueItems, ...mergeRequestItems);
+ }
+ return items;
}
// Add option to proceed with the search for each
@@ -238,7 +343,7 @@ export class GlobalSearchInput {
});
}
- enableDropdown() {
+ enableAutocomplete() {
this.setScrollFade();
// No need to enable anything if user is not logged in
@@ -255,7 +360,7 @@ export class GlobalSearchInput {
}
onSearchInputChange() {
- this.enableDropdown();
+ this.enableAutocomplete();
}
onSearchInputKeyUp(e) {
@@ -264,7 +369,7 @@ export class GlobalSearchInput {
this.restoreOriginalState();
break;
case KEYCODE.ENTER:
- this.disableDropdown();
+ this.disableAutocomplete();
break;
default:
}
@@ -317,7 +422,7 @@ export class GlobalSearchInput {
return results;
}
- disableDropdown() {
+ disableAutocomplete() {
if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('js-autocomplete-disabled');
this.dropdownToggle.dropdown('toggle');
@@ -333,8 +438,16 @@ export class GlobalSearchInput {
onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
+ /* eslint-disable-next-line @gitlab/require-i18n-strings */
+ if (item.category === 'Projects') {
+ this.projectInputEl.val(item.id);
+ }
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ if (item.category === 'Groups') {
+ this.groupInputEl.val(item.id);
+ }
$el.removeClass('is-active');
- this.disableDropdown();
+ this.disableAutocomplete();
return this.searchInput.val('').focus();
}
}
@@ -343,58 +456,20 @@ export class GlobalSearchInput {
this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
}
- getCategoryContents() {
- const userName = gon.current_username;
- const { projectOptions, groupOptions, dashboardOptions } = gl;
-
- // Get options
- let options;
- if (isInProjectPage() && projectOptions) {
- options = projectOptions[getProjectSlug()];
- } else if (isInGroupsPage() && groupOptions) {
- options = groupOptions[getGroupSlug()];
- } else if (dashboardOptions) {
- options = dashboardOptions;
+ getAvatar(item) {
+ if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
+ return false;
}
- const { issuesPath, mrPath, name, issuesDisabled } = options;
- const baseItems = [];
-
- if (name) {
- baseItems.push({
- type: 'header',
- content: `${name}`,
- });
- }
+ const { label, id } = item;
+ const avatarUrl = item.avatar_url;
+ const avatar = avatarUrl
+ ? `<img class="search-item-avatar" src="${avatarUrl}" />`
+ : `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle(
+ escape(label),
+ )}</div>`;
- const issueItems = [
- {
- text: s__('SearchAutocomplete|Issues assigned to me'),
- url: `${issuesPath}/?assignee_username=${userName}`,
- },
- {
- text: s__("SearchAutocomplete|Issues I've created"),
- url: `${issuesPath}/?author_username=${userName}`,
- },
- ];
- const mergeRequestItems = [
- {
- text: s__('SearchAutocomplete|Merge requests assigned to me'),
- url: `${mrPath}/?assignee_username=${userName}`,
- },
- {
- text: s__("SearchAutocomplete|Merge requests I've created"),
- url: `${mrPath}/?author_username=${userName}`,
- },
- ];
-
- let items;
- if (issuesDisabled) {
- items = baseItems.concat(mergeRequestItems);
- } else {
- items = baseItems.concat(...issueItems, ...mergeRequestItems);
- }
- return items;
+ return avatar;
}
isScrolledUp() {
@@ -420,6 +495,6 @@ export class GlobalSearchInput {
}
}
-export default function initGlobalSearchInput(opts) {
- return new GlobalSearchInput(opts);
+export default function initSearchAutocomplete(opts) {
+ return new SearchAutocomplete(opts);
}
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 217f08dd648..ff6d9350a5c 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -51,6 +51,21 @@ class SearchController < ApplicationController
render json: { count: count }
end
+ # rubocop: disable CodeReuse/ActiveRecord
+ def autocomplete
+ term = params[:term]
+
+ if params[:project_id].present?
+ @project = Project.find_by(id: params[:project_id])
+ @project = nil unless can?(current_user, :read_project, @project)
+ end
+
+ @ref = params[:project_ref] if params[:project_ref].present?
+
+ render json: search_autocomplete_opts(term).to_json
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
private
def preload_method
diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb
index 483b350b99b..38a4f7f1b4b 100644
--- a/app/helpers/export_helper.rb
+++ b/app/helpers/export_helper.rb
@@ -6,7 +6,7 @@ module ExportHelper
[
_('Project and wiki repositories'),
_('Project uploads'),
- _('Project configuration, including services'),
+ _('Project configuration, excluding integrations'),
_('Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities'),
_('LFS objects'),
_('Issue Boards'),
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 58ce063dbd4..1b9876b9a6a 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -3,6 +3,28 @@
module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze
+ def search_autocomplete_opts(term)
+ return unless current_user
+
+ resources_results = [
+ groups_autocomplete(term),
+ projects_autocomplete(term)
+ ].flatten
+
+ search_pattern = Regexp.new(Regexp.escape(term), "i")
+
+ generic_results = project_autocomplete + default_autocomplete + help_autocomplete
+ generic_results.concat(default_autocomplete_admin) if current_user.admin?
+ generic_results.select! { |result| result[:label] =~ search_pattern }
+
+ [
+ resources_results,
+ generic_results
+ ].flatten.uniq do |item|
+ item[:label]
+ end
+ end
+
def search_entries_info(collection, scope, term)
return if collection.to_a.empty?
@@ -73,6 +95,91 @@ module SearchHelper
private
+ # Autocomplete results for various settings pages
+ def default_autocomplete
+ [
+ { category: "Settings", label: _("User settings"), url: profile_path },
+ { category: "Settings", label: _("SSH Keys"), url: profile_keys_path },
+ { category: "Settings", label: _("Dashboard"), url: root_path }
+ ]
+ end
+
+ # Autocomplete results for settings pages, for admins
+ def default_autocomplete_admin
+ [
+ { category: "Settings", label: _("Admin Section"), url: admin_root_path }
+ ]
+ end
+
+ # Autocomplete results for internal help pages
+ def help_autocomplete
+ [
+ { category: "Help", label: _("API Help"), url: help_page_path("api/README") },
+ { category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") },
+ { category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") },
+ { category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") },
+ { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") },
+ { category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") },
+ { category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") },
+ { category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") },
+ { category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") }
+ ]
+ end
+
+ # Autocomplete results for the current project, if it's defined
+ def project_autocomplete
+ if @project && @project.repository.root_ref
+ ref = @ref || @project.repository.root_ref
+
+ [
+ { category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) },
+ { category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) },
+ { category: "In this project", label: _("Network"), url: project_network_path(@project, ref) },
+ { category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) },
+ { category: "In this project", label: _("Issues"), url: project_issues_path(@project) },
+ { category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) },
+ { category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) },
+ { category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) },
+ { category: "In this project", label: _("Members"), url: project_project_members_path(@project) },
+ { category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) }
+ ]
+ else
+ []
+ end
+ end
+
+ # Autocomplete results for the current user's groups
+ # rubocop: disable CodeReuse/ActiveRecord
+ def groups_autocomplete(term, limit = 5)
+ current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group|
+ {
+ category: "Groups",
+ id: group.id,
+ label: "#{search_result_sanitize(group.full_name)}",
+ url: group_path(group),
+ avatar_url: group.avatar_url || ''
+ }
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # Autocomplete results for the current user's projects
+ # rubocop: disable CodeReuse/ActiveRecord
+ def projects_autocomplete(term, limit = 5)
+ current_user.authorized_projects.order_id_desc.search_by_title(term)
+ .sorted_by_stars_desc.non_archived.limit(limit).map do |p|
+ {
+ category: "Projects",
+ id: p.id,
+ value: "#{search_result_sanitize(p.name)}",
+ label: "#{search_result_sanitize(p.full_name)}",
+ url: project_path(p),
+ avatar_url: p.avatar_url || ''
+ }
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def search_result_sanitize(str)
Sanitize.clean(str)
end
diff --git a/app/services/jira/requests/issues/list_service.rb b/app/services/jira/requests/issues/list_service.rb
index 5752e77d16f..442bf6ba27a 100644
--- a/app/services/jira/requests/issues/list_service.rb
+++ b/app/services/jira/requests/issues/list_service.rb
@@ -12,8 +12,8 @@ module Jira
super(jira_service, params)
@jql = params[:jql].to_s
- @page = params[:page].to_i || 1
- @per_page = params[:per_page].to_i || PER_PAGE
+ @page = (params[:page] || 1).to_i
+ @per_page = (params[:per_page] || PER_PAGE).to_i
end
private
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 97d00bce11b..81fe0798bd1 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -2,7 +2,7 @@
= form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
.search-input-wrap
- .dropdown
+ .dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: _('Search or jump to…'),
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false,
@@ -37,3 +37,6 @@
-# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb
- if ENV['RAILS_ENV'] == 'test'
%noscript= button_tag 'Search'
+ .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path,
+ :'data-autocomplete-project-id' => search_context.project.try(:id),
+ :'data-autocomplete-project-ref' => search_context.ref }
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index 6717939d034..b67bc71941a 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -4,7 +4,7 @@
.search-result-row
%h5.note-search-caption.str-truncated
- %i.fa.fa-comment
+ = sprite_icon('comment', size: 16, css_class: 'gl-vertical-align-text-bottom')
= link_to_member(project, note.author, avatar: false)
- link_to_project = link_to(project.full_name, project)
= _("commented on %{link_to_project}").html_safe % { link_to_project: link_to_project }
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index dee6dc1e078..d704eae2090 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -21,5 +21,5 @@
%li.issuable-comments.d-none.d-sm-block
= link_to issuable_path, class: ['has-tooltip', ('no-comments' if note_count.zero?)], title: _('Comments') do
- = icon('comments')
+ = sprite_icon('comments', size: 16, css_class: 'gl-vertical-align-text-bottom')
= note_count
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 128ddbb8e8b..b2c9a74b177 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -11,7 +11,7 @@
%ul.controls
%li
= link_to gitlab_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count.zero?) do
- = icon('comments')
+ = sprite_icon('comments', size: 16, css_class: 'gl-vertical-align-text-bottom')
= notes_count
%li
%span.sr-only
diff --git a/changelogs/unreleased/212811-adding-new-task-always-shows-error-something-went-wrong-while-fetc.yml b/changelogs/unreleased/212811-adding-new-task-always-shows-error-something-went-wrong-while-fetc.yml
new file mode 100644
index 00000000000..fd33640f42f
--- /dev/null
+++ b/changelogs/unreleased/212811-adding-new-task-always-shows-error-something-went-wrong-while-fetc.yml
@@ -0,0 +1,5 @@
+---
+title: Fix comment loading error in issues and merge requests
+merge_request: 36043
+author:
+type: fixed
diff --git a/changelogs/unreleased/220342-remove-services-from-import-export.yml b/changelogs/unreleased/220342-remove-services-from-import-export.yml
new file mode 100644
index 00000000000..1e5af27198b
--- /dev/null
+++ b/changelogs/unreleased/220342-remove-services-from-import-export.yml
@@ -0,0 +1,5 @@
+---
+title: Exclude integrations (services) from Project Import/Export
+merge_request: 35249
+author:
+type: changed
diff --git a/changelogs/unreleased/225187-replace-fa-comment-icons-with-gitlab-svg-comment-icon.yml b/changelogs/unreleased/225187-replace-fa-comment-icons-with-gitlab-svg-comment-icon.yml
new file mode 100644
index 00000000000..0a21c5d6622
--- /dev/null
+++ b/changelogs/unreleased/225187-replace-fa-comment-icons-with-gitlab-svg-comment-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Replace fa-comment / fa-comments icons with GitLab SVG
+merge_request: 36206
+author:
+type: changed
diff --git a/changelogs/unreleased/227558-missing-digest-revision-and-short-revision-in-tags.yml b/changelogs/unreleased/227558-missing-digest-revision-and-short-revision-in-tags.yml
new file mode 100644
index 00000000000..370d0688734
--- /dev/null
+++ b/changelogs/unreleased/227558-missing-digest-revision-and-short-revision-in-tags.yml
@@ -0,0 +1,5 @@
+---
+title: Add broken tag state to tags list items
+merge_request: 36442
+author:
+type: changed
diff --git a/changelogs/unreleased/feature-secure-eslint-to-core.yml b/changelogs/unreleased/feature-secure-eslint-to-core.yml
new file mode 100644
index 00000000000..d93ef151a6a
--- /dev/null
+++ b/changelogs/unreleased/feature-secure-eslint-to-core.yml
@@ -0,0 +1,5 @@
+---
+title: Bring SAST to Core - eslint
+merge_request: 36392
+author:
+type: changed
diff --git a/changelogs/unreleased/revert-bc8546a9.yml b/changelogs/unreleased/revert-bc8546a9.yml
new file mode 100644
index 00000000000..45513d899a1
--- /dev/null
+++ b/changelogs/unreleased/revert-bc8546a9.yml
@@ -0,0 +1,5 @@
+---
+title: Restore the search autocomplete for groups/project/other
+merge_request: 35983
+author:
+type: other
diff --git a/config/routes.rb b/config/routes.rb
index 9739d8fe0ff..c823be6084d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -58,6 +58,7 @@ Rails.application.routes.draw do
# Search
get 'search' => 'search#show'
+ get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
get 'search/count' => 'search#count', as: :search_count
# JSON Web Token
diff --git a/crowdin.yml b/crowdin.yml
index 2861d34c941..27d9fba92b2 100644
--- a/crowdin.yml
+++ b/crowdin.yml
@@ -1,7 +1,9 @@
project_identifier: 'gitlab-ee'
api_key_env: CROWDIN_API_KEY
preserve_hierarchy: true
-commit_message: "[skip ci]"
+commit_message: |
+
+ [skip ci]
files:
- source: /locale/gitlab.pot
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 4d9a502fe74..88e59ba7ffc 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -35,8 +35,7 @@ The availability objectives for Gitaly clusters are:
Writes are replicated asynchronously. Any writes that have not been replicated
to the newly promoted primary are lost.
- [Strong Consistency](https://gitlab.com/groups/gitlab-org/-/epics/1189) is
- planned to improve this to "no loss".
+ [Strong consistency](#strong-consistency) can be used to improve this to "no loss".
- **Recovery Time Objective (RTO):** Less than 10 seconds.
@@ -877,6 +876,35 @@ Prometheus counter metric. It has two labels:
They reflect configuration defined for this instance of Praefect.
+## Strong consistency
+
+> Introduced in GitLab 13.1 in [alpha](https://about.gitlab.com/handbook/product/#alpha-beta-ga), disabled by default.
+
+Praefect guarantees eventual consistency by replicating all writes to secondary nodes
+after the write to the primary Gitaly node has happened.
+
+Praefect can instead provide strong consistency by creating a transaction and writing
+changes to all Gitaly nodes at once. Strong consistency is currently in
+[alpha](https://about.gitlab.com/handbook/product/#alpha-beta-ga) and not enabled by
+default. For more information, see the
+[strong consistency epic](https://gitlab.com/groups/gitlab-org/-/epics/1189).
+
+To enable strong consistency:
+
+- In GitLab 13.2 and later, enable the `:gitaly_reference_transactions` feature flag.
+- In GitLab 13.1, enable the `:gitaly_reference_transactions` and `:gitaly_hooks_rpc`
+ feature flags.
+
+Enabling feature flags requires [access to the Rails console](../feature_flags.md#start-the-gitlab-rails-console).
+In the Rails console, enable or disable the flags as required. For example:
+
+```ruby
+Feature.enable(:gitaly_reference_transactions)
+```
+
+To monitor strong consistency, use the `gitaly_praefect_transactions_total` and
+`gitaly_praefect_transactions_delay_seconds` Prometheus counter metrics.
+
## Automatic failover and leader election
Praefect regularly checks the health of each backend Gitaly node. This
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index e1600579621..1e67236b448 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -80,13 +80,13 @@ The following table shows which languages, package managers and frameworks are s
| Groovy ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.3 (Gradle) & 11.9 (Ant, Maven, SBT) |
| Helm Charts | [Kubesec](https://github.com/controlplaneio/kubesec) | 13.1 |
| Java ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 10.6 (Maven), 10.8 (Gradle) & 11.9 (Ant, SBT) |
-| JavaScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8 |
+| JavaScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8, moved to [GitLab Core](https://about.gitlab.com/pricing/) in 13.2 |
| Kubernetes manifests | [Kubesec](https://github.com/controlplaneio/kubesec) | 12.6 |
| Node.js | [NodeJsScan](https://github.com/ajinabraham/NodeJsScan) | 11.1 |
| PHP | [phpcs-security-audit](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | 10.8 |
| Python ([pip](https://pip.pypa.io/en/stable/)) | [bandit](https://github.com/PyCQA/bandit) | 10.3 |
| React | [ESLint react plugin](https://github.com/yannickcr/eslint-plugin-react) | 12.5 |
-| Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3, moved to Core in 13.1 |
+| Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3, moved to [GitLab Core](https://about.gitlab.com/pricing/) in 13.1 |
| Scala ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.0 (SBT) & 11.9 (Ant, Gradle, Maven) |
| TypeScript | [`tslint-config-security`](https://github.com/webschik/tslint-config-security/) | 11.9 |
@@ -97,10 +97,13 @@ The Java analyzers can also be used for variants like the
### Making SAST analyzers available to all GitLab tiers
-All open source (OSS) analyzers are in the process of being reviewed and potentially moved to GitLab Core tier. Progress can be
+All open source (OSS) analyzers are in the process of being reviewed and potentially moved to the GitLab Core tier. Progress can be
tracked in the corresponding
[epic](https://gitlab.com/groups/gitlab-org/-/epics/2098).
+Please note that support for [Docker-in-Docker](#enabling-docker-in-docker)
+will not be extended to the GitLab Core tier.
+
#### Summary of features per tier
Different features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index a614d0a61d4..cb9f0491b44 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -97,7 +97,7 @@ The following items will be exported:
- Project and wiki repositories
- Project uploads
-- Project configuration, including services
+- Project configuration, excluding integrations
- Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, time tracking,
and other project entities
- Design Management files and data
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 97ee0c358ab..096f6a786db 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -90,7 +90,6 @@ eslint-sast:
- if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
when: never
- if: $CI_COMMIT_BRANCH &&
- $GITLAB_FEATURES =~ /\bsast\b/ &&
$SAST_DEFAULT_ANALYZERS =~ /eslint/
exists:
- '**/*.html'
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 7d2a26b2d39..8bffb2aa192 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -89,7 +89,6 @@ tree:
- :triggers
- :pipeline_schedules
- :container_expiration_policy
- - :services
- protected_branches:
- :merge_access_levels
- :push_access_levels
@@ -261,12 +260,6 @@ excluded_attributes:
runners:
- :token
- :token_encrypted
- services:
- - :description
- - :inherit_from_id
- - :instance
- - :template
- - :title
error_tracking_setting:
- :encrypted_token
- :encrypted_token_iv
@@ -355,8 +348,6 @@ methods:
- :type
statuses:
- :type
- services:
- - :type
merge_request_diff_files:
- :utf8_diff
merge_requests:
diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb
index 3ab9f2c4bfa..ae92228276e 100644
--- a/lib/gitlab/import_export/project/relation_factory.rb
+++ b/lib/gitlab/import_export/project/relation_factory.rb
@@ -70,10 +70,8 @@ module Gitlab
private
def invalid_relation?
- # Do not create relation if it is:
- # - An unknown service
- # - A legacy trigger
- unknown_service? || legacy_trigger?
+ # Do not create relation if it is a legacy trigger
+ legacy_trigger?
end
def setup_models
@@ -137,11 +135,6 @@ module Gitlab
end
end
- def unknown_service?
- @relation_name == :services && parsed_relation_hash['type'] &&
- !Object.const_defined?(parsed_relation_hash['type'])
- end
-
def legacy_trigger?
@relation_name == :'Ci::Trigger' && @relation_hash['owner_id'].nil?
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 65e092a9f12..ddd3429c495 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -824,6 +824,9 @@ msgstr ""
msgid "- show less"
msgstr ""
+msgid "0 bytes"
+msgstr ""
+
msgid "0 for unlimited"
msgstr ""
@@ -1124,6 +1127,9 @@ msgstr ""
msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
msgstr ""
+msgid "API Help"
+msgstr ""
+
msgid "API Token"
msgstr ""
@@ -1594,6 +1600,9 @@ msgstr ""
msgid "Admin Overview"
msgstr ""
+msgid "Admin Section"
+msgstr ""
+
msgid "Admin mode already enabled"
msgstr ""
@@ -6300,6 +6309,9 @@ msgstr ""
msgid "ContainerRegistry|Image tags"
msgstr ""
+msgid "ContainerRegistry|Invalid tag: missing manifest digest"
+msgstr ""
+
msgid "ContainerRegistry|Login"
msgstr ""
@@ -14014,6 +14026,9 @@ msgstr ""
msgid "Markdown"
msgstr ""
+msgid "Markdown Help"
+msgstr ""
+
msgid "Markdown enabled"
msgstr ""
@@ -15101,6 +15116,9 @@ msgstr ""
msgid "My-Reaction"
msgstr ""
+msgid "N/A"
+msgstr ""
+
msgid "Name"
msgstr ""
@@ -16605,6 +16623,9 @@ msgstr ""
msgid "Permissions"
msgstr ""
+msgid "Permissions Help"
+msgstr ""
+
msgid "Permissions, LFS, 2FA"
msgstr ""
@@ -17748,7 +17769,7 @@ msgstr ""
msgid "Project clone URL"
msgstr ""
-msgid "Project configuration, including services"
+msgid "Project configuration, excluding integrations"
msgstr ""
msgid "Project description (optional)"
@@ -18777,6 +18798,9 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication."
msgstr ""
+msgid "Public Access Help"
+msgstr ""
+
msgid "Public deploy keys (%{deploy_keys_count})"
msgstr ""
@@ -18909,6 +18933,9 @@ msgstr ""
msgid "README"
msgstr ""
+msgid "Rake Tasks Help"
+msgstr ""
+
msgid "Raw blob request rate limit per minute"
msgstr ""
@@ -20016,6 +20043,9 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "SSH Keys Help"
+msgstr ""
+
msgid "SSH host key fingerprints"
msgstr ""
@@ -20145,6 +20175,9 @@ msgstr ""
msgid "Search Button"
msgstr ""
+msgid "Search Jira issues"
+msgstr ""
+
msgid "Search Milestones"
msgstr ""
@@ -22575,6 +22608,9 @@ msgstr ""
msgid "System Hooks"
msgstr ""
+msgid "System Hooks Help"
+msgstr ""
+
msgid "System Info"
msgstr ""
@@ -25334,6 +25370,9 @@ msgstr ""
msgid "User restrictions"
msgstr ""
+msgid "User settings"
+msgstr ""
+
msgid "User was successfully created."
msgstr ""
@@ -26043,6 +26082,9 @@ msgstr ""
msgid "Webhooks"
msgstr ""
+msgid "Webhooks Help"
+msgstr ""
+
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
@@ -26315,6 +26357,9 @@ msgstr ""
msgid "Work in progress Limit"
msgstr ""
+msgid "Workflow Help"
+msgstr ""
+
msgid "Write"
msgstr ""
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index bae6bd07b67..0849fb00e73 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -211,4 +211,9 @@ RSpec.describe SearchController do
end.to raise_error(ActionController::ParameterMissing)
end
end
+
+ describe 'GET #autocomplete' do
+ it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
+ it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
+ end
end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 8cf4dac9afe..259c09b9d11 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'issuable list', :js do
expect(first('.issuable-upvotes')).to have_content(1)
expect(first('.issuable-downvotes')).to have_content(1)
- expect(first('.fa-comments').find(:xpath, '..')).to have_content(2)
+ expect(first('.issuable-comments')).to have_content(2)
end
it 'sorts labels alphabetically' do
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 730e586b278..b93da033aea 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 04ca8a09ca8..d679e4dbb99 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -72,6 +72,7 @@ RSpec.describe 'GPG signed commits' do
it 'unverified signature' do
visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA)
+ wait_for_all_requests
page.find('.gpg-status-box', text: 'Unverified').click
@@ -85,6 +86,7 @@ RSpec.describe 'GPG signed commits' do
user_2_key
visit project_commit_path(project, GpgHelpers::DIFFERING_EMAIL_SHA)
+ wait_for_all_requests
page.find('.gpg-status-box', text: 'Unverified').click
@@ -100,6 +102,7 @@ RSpec.describe 'GPG signed commits' do
user_2_key
visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA)
+ wait_for_all_requests
page.find('.gpg-status-box', text: 'Unverified').click
@@ -115,6 +118,7 @@ RSpec.describe 'GPG signed commits' do
user_1_key
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)
+ wait_for_all_requests
page.find('.gpg-status-box', text: 'Verified').click
@@ -130,6 +134,7 @@ RSpec.describe 'GPG signed commits' do
user_1_key
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)
+ wait_for_all_requests
# wait for the signature to get generated
expect(page).to have_selector('.gpg-status-box', text: 'Verified')
@@ -137,6 +142,7 @@ RSpec.describe 'GPG signed commits' do
user_1.destroy!
refresh
+ wait_for_all_requests
page.find('.gpg-status-box', text: 'Verified').click
@@ -153,6 +159,7 @@ RSpec.describe 'GPG signed commits' do
shared_examples 'a commit with a signature' do
before do
visit project_tree_path(project, 'signed-commits')
+ wait_for_all_requests
end
it 'displays commit signature' do
diff --git a/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz
index e0830c290d1..e99136e96b7 100644
--- a/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz
+++ b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
index 0aa41734778..e3ec4f603b9 100644
--- a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
+++ b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index f6a6671b7f1..d88b2ebc83a 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -7007,376 +7007,6 @@
"enabled": false
},
"deploy_keys": [],
- "services": [
- {
- "id": 101,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.327Z",
- "updated_at": "2016-06-14T15:01:51.327Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "YoutrackService",
- "category": "issue_tracker",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 100,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.315Z",
- "updated_at": "2016-06-14T15:01:51.315Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "TeamcityService",
- "category": "ci",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 99,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.303Z",
- "updated_at": "2016-06-14T15:01:51.303Z",
- "active": false,
- "properties": {
- "notify_only_broken_pipelines": true
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "pipeline_events": true,
- "type": "SlackService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 98,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.289Z",
- "updated_at": "2016-06-14T15:01:51.289Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "RedmineService",
- "category": "issue_tracker",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 97,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.277Z",
- "updated_at": "2016-06-14T15:01:51.277Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "PushoverService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 96,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.267Z",
- "updated_at": "2016-06-14T15:01:51.267Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "PivotalTrackerService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 95,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.255Z",
- "updated_at": "2016-06-14T15:01:51.255Z",
- "active": false,
- "properties": {
- "api_url": "",
- "jira_issue_transition_id": "2"
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "JiraService",
- "category": "issue_tracker",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 94,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.232Z",
- "updated_at": "2016-06-14T15:01:51.232Z",
- "active": true,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "IrkerService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 93,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.219Z",
- "updated_at": "2016-06-14T15:01:51.219Z",
- "active": false,
- "properties": {
- "notify_only_broken_pipelines": true
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "pipeline_events": true,
- "type": "HipchatService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 91,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.182Z",
- "updated_at": "2016-06-14T15:01:51.182Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "FlowdockService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 90,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.166Z",
- "updated_at": "2016-06-14T15:01:51.166Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "ExternalWikiService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 89,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.153Z",
- "updated_at": "2016-06-14T15:01:51.153Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "EmailsOnPushService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 88,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.139Z",
- "updated_at": "2016-06-14T15:01:51.139Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "DroneCiService",
- "category": "ci",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 87,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.125Z",
- "updated_at": "2016-06-14T15:01:51.125Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "CustomIssueTrackerService",
- "category": "issue_tracker",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 86,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.113Z",
- "updated_at": "2016-06-14T15:01:51.113Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "CampfireService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 84,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.080Z",
- "updated_at": "2016-06-14T15:01:51.080Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "BuildkiteService",
- "category": "ci",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 83,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.067Z",
- "updated_at": "2016-06-14T15:01:51.067Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "BambooService",
- "category": "ci",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 82,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.047Z",
- "updated_at": "2016-06-14T15:01:51.047Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "AssemblaService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 81,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.031Z",
- "updated_at": "2016-06-14T15:01:51.031Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "AsanaService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- }
- ],
"hooks": [],
"protected_branches": [
{
diff --git a/spec/fixtures/lib/gitlab/import_export/designs/project.json b/spec/fixtures/lib/gitlab/import_export/designs/project.json
index 28eaa38d387..ebc08868d9e 100644
--- a/spec/fixtures/lib/gitlab/import_export/designs/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/designs/project.json
@@ -456,9 +456,6 @@
"pipeline_schedules":[
],
- "services":[
-
- ],
"protected_branches":[
],
diff --git a/spec/fixtures/lib/gitlab/import_export/light/project.json b/spec/fixtures/lib/gitlab/import_export/light/project.json
index cef78316361..963cdb342b5 100644
--- a/spec/fixtures/lib/gitlab/import_export/light/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/light/project.json
@@ -141,48 +141,6 @@
]
}
],
- "services": [
- {
- "id": 100,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.315Z",
- "updated_at": "2016-06-14T15:01:51.315Z",
- "active": false,
- "properties": {},
- "template": true,
- "instance": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "TeamcityService",
- "category": "ci",
- "default": false,
- "wiki_page_events": true
- },
- {
- "id": 101,
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.315Z",
- "updated_at": "2016-06-14T15:01:51.315Z",
- "active": false,
- "properties": {},
- "template": false,
- "instance": true,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "JiraService",
- "category": "ci",
- "default": false,
- "wiki_page_events": true
- }
- ],
"snippets": [],
"hooks": [],
"custom_attributes": [
diff --git a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json
index a6e6ba43bdc..b9e791ee85a 100644
--- a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json
@@ -32,7 +32,6 @@
],
"labels": [],
"issues": [],
- "services": [],
"snippets": [],
"hooks": []
}
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index 0247a12d8d3..529e7cc85f5 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -13,7 +13,6 @@ describe('Blob Header Default Actions', () => {
let wrapper;
let btnGroup;
let buttons;
- const hrefPrefix = 'http://localhost';
function createComponent(propsData = {}) {
wrapper = mount(BlobHeaderActions, {
@@ -47,11 +46,11 @@ describe('Blob Header Default Actions', () => {
});
it('correct href attribute on RAW button', () => {
- expect(buttons.at(1).vm.$el.href).toBe(`${hrefPrefix}${Blob.rawPath}`);
+ expect(buttons.at(1).attributes('href')).toBe(Blob.rawPath);
});
it('correct href attribute on Download button', () => {
- expect(buttons.at(2).vm.$el.href).toBe(`${hrefPrefix}${Blob.rawPath}?inline=false`);
+ expect(buttons.at(2).attributes('href')).toBe(`${Blob.rawPath}?inline=false`);
});
it('does not render "Copy file contents" button as disables if the viewer is Simple', () => {
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 08da34aa27a..c9d77a34595 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -3,12 +3,14 @@
const path = require('path');
const { ErrorWithStack } = require('jest-util');
const JSDOMEnvironment = require('jest-environment-jsdom-sixteen');
+const { TEST_HOST } = require('./helpers/test_constants');
const ROOT_PATH = path.resolve(__dirname, '../..');
class CustomEnvironment extends JSDOMEnvironment {
constructor(config, context) {
- super(config, context);
+ // Setup testURL so that window.location is setup properly
+ super({ ...config, testURL: TEST_HOST }, context);
Object.assign(context.console, {
error(...args) {
@@ -57,6 +59,9 @@ class CustomEnvironment extends JSDOMEnvironment {
ownerDocument: this.global.document,
},
});
+
+ // Expose the jsdom (created in super class) to the global so that we can call reconfigure({ url: '' }) to properly set `window.location`
+ this.global.dom = this.dom;
}
async teardown() {
diff --git a/spec/frontend/fixtures/static/global_search_input.html b/spec/frontend/fixtures/static/search_autocomplete.html
index 29db9020424..29db9020424 100644
--- a/spec/frontend/fixtures/static/global_search_input.html
+++ b/spec/frontend/fixtures/static/search_autocomplete.html
diff --git a/spec/frontend/helpers/test_constants.js b/spec/frontend/helpers/test_constants.js
index c97d47a6406..69b78f556aa 100644
--- a/spec/frontend/helpers/test_constants.js
+++ b/spec/frontend/helpers/test_constants.js
@@ -1,7 +1,19 @@
-export const FIXTURES_PATH = `/fixtures`;
-export const TEST_HOST = 'http://test.host';
+const FIXTURES_PATH = `/fixtures`;
+const TEST_HOST = 'http://test.host';
-export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
+const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
-export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`;
-export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`;
+const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`;
+const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`;
+
+// NOTE: module.exports is needed so that this file can be used
+// by environment.js
+//
+// eslint-disable-next-line import/no-commonjs
+module.exports = {
+ FIXTURES_PATH,
+ TEST_HOST,
+ DUMMY_IMAGE_URL,
+ GREEN_BOX_IMAGE_URL,
+ RED_BOX_IMAGE_URL,
+};
diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
index 9bd4a994f0a..b135de9e6b5 100644
--- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
@@ -7,6 +7,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import flash from '~/flash';
import IssuablesListApp from '~/issuables_list/components/issuables_list_app.vue';
import Issuable from '~/issuables_list/components/issuable.vue';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import issueablesEventBus from '~/issuables_list/eventhub';
import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issuables_list/constants';
@@ -59,6 +60,7 @@ describe('Issuables list component', () => {
const findLoading = () => wrapper.find(GlSkeletonLoading);
const findIssuables = () => wrapper.findAll(Issuable);
+ const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
const findFirstIssuable = () => findIssuables().wrappers[0];
const findEmptyState = () => wrapper.find(GlEmptyState);
@@ -75,6 +77,7 @@ describe('Issuables list component', () => {
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
mockAxios.restore();
window.location = oldLocation;
});
@@ -131,6 +134,7 @@ describe('Issuables list component', () => {
});
it('does not call API until mounted', () => {
+ factory();
expect(apiSpy).not.toHaveBeenCalled();
});
@@ -173,6 +177,12 @@ describe('Issuables list component', () => {
expect(wrapper.find(GlPagination).exists()).toBe(true);
});
});
+
+ it('does not render FilteredSearchBar', () => {
+ factory();
+
+ expect(findFilteredSearchBar().exists()).toBe(false);
+ });
});
describe('with bulk editing enabled', () => {
@@ -523,4 +533,48 @@ describe('Issuables list component', () => {
});
});
});
+
+ describe('when type is "jira"', () => {
+ it('renders FilteredSearchBar', () => {
+ factory({ type: 'jira' });
+
+ expect(findFilteredSearchBar().exists()).toBe(true);
+ });
+
+ describe('initialSortBy', () => {
+ const query = '?sort=updated_asc';
+
+ it('sets default value', () => {
+ factory({ type: 'jira' });
+
+ expect(findFilteredSearchBar().props('initialSortBy')).toBe('created_desc');
+ });
+
+ it('sets value according to query', () => {
+ setUrl(query);
+
+ factory({ type: 'jira' });
+
+ expect(findFilteredSearchBar().props('initialSortBy')).toBe('updated_asc');
+ });
+ });
+
+ describe('initialFilterValue', () => {
+ it('does not set value when no query', () => {
+ factory({ type: 'jira' });
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([]);
+ });
+
+ it('sets value according to query', () => {
+ const query = '?search=free+text';
+
+ setUrl(query);
+
+ factory({ type: 'jira' });
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toEqual(['free text']);
+ });
+ });
+ });
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index ba1118ef3b7..996e11b2442 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -36,6 +36,7 @@ import {
dashboardProps,
} from '../fixture_data';
import createFlash from '~/flash';
+import { TEST_HOST } from 'helpers/test_constants';
jest.mock('~/flash');
@@ -448,7 +449,7 @@ describe('Dashboard', () => {
path: 'dashboard&copy.yml',
});
expect(window.location.assign).toHaveBeenCalledWith(
- 'http://localhost/?dashboard=dashboard%2526copy.yml',
+ `${TEST_HOST}/?dashboard=dashboard%2526copy.yml`,
);
});
});
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 1b4df286868..452fd756050 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -460,7 +460,7 @@ describe('monitoring/utils', () => {
expect(urlUtils.updateHistory).toHaveBeenCalledTimes(1);
expect(urlUtils.updateHistory).toHaveBeenCalledWith({
- url: `http://localhost/${urlParams}`,
+ url: `${TEST_HOST}/${urlParams}`,
title: '',
});
},
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index ba084d01742..a4d8927e89b 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -274,9 +274,54 @@ describe('Actions Notes Store', () => {
});
});
+ describe('fetchData', () => {
+ describe('given there are no notes', () => {
+ const lastFetchedAt = '13579';
+
+ beforeEach(() => {
+ axiosMock
+ .onGet(notesDataMock.notesPath)
+ .replyOnce(200, { notes: [], last_fetched_at: lastFetchedAt });
+ });
+
+ it('should commit SET_LAST_FETCHED_AT', () =>
+ testAction(
+ actions.fetchData,
+ undefined,
+ { notesData: notesDataMock },
+ [{ type: 'SET_LAST_FETCHED_AT', payload: lastFetchedAt }],
+ [],
+ ));
+ });
+
+ describe('given there are notes', () => {
+ const lastFetchedAt = '12358';
+
+ beforeEach(() => {
+ axiosMock
+ .onGet(notesDataMock.notesPath)
+ .replyOnce(200, { notes: discussionMock.notes, last_fetched_at: lastFetchedAt });
+ });
+
+ it('should dispatch updateOrCreateNotes, startTaskList and commit SET_LAST_FETCHED_AT', () =>
+ testAction(
+ actions.fetchData,
+ undefined,
+ { notesData: notesDataMock },
+ [{ type: 'SET_LAST_FETCHED_AT', payload: lastFetchedAt }],
+ [
+ { type: 'updateOrCreateNotes', payload: discussionMock.notes },
+ { type: 'startTaskList' },
+ ],
+ ));
+ });
+ });
+
describe('poll', () => {
beforeEach(done => {
- jest.spyOn(axios, 'get');
+ axiosMock
+ .onGet(notesDataMock.notesPath)
+ .reply(200, { notes: [], last_fetched_at: '123456' }, { 'poll-interval': '1000' });
store
.dispatch('setNotesData', notesDataMock)
@@ -285,15 +330,10 @@ describe('Actions Notes Store', () => {
});
it('calls service with last fetched state', done => {
- axiosMock
- .onAny()
- .reply(200, { notes: [], last_fetched_at: '123456' }, { 'poll-interval': '1000' });
-
store
.dispatch('poll')
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
.then(() => {
- expect(axios.get).toHaveBeenCalled();
expect(store.state.lastFetchedAt).toBe('123456');
jest.advanceTimersByTime(1500);
@@ -305,8 +345,9 @@ describe('Actions Notes Store', () => {
}),
)
.then(() => {
- expect(axios.get.mock.calls.length).toBe(2);
- expect(axios.get.mock.calls[axios.get.mock.calls.length - 1][1].headers).toEqual({
+ const expectedGetRequests = 2;
+ expect(axiosMock.history.get.length).toBe(expectedGetRequests);
+ expect(axiosMock.history.get[expectedGetRequests - 1].headers).toMatchObject({
'X-Last-Fetched-At': '123456',
});
})
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
index 5d465217d78..9e876d6d8a3 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlFormCheckbox, GlSprintf } from '@gitlab/ui';
+import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -9,6 +9,9 @@ import DetailsRow from '~/registry/explorer/components/details_page/details_row.
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
+ MISSING_MANIFEST_WARNING_TOOLTIP,
+ NOT_AVAILABLE_TEXT,
+ NOT_AVAILABLE_SIZE,
} from '~/registry/explorer/constants/index';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -33,6 +36,7 @@ describe('tags list row', () => {
const findPublishedDateDetail = () => wrapper.find('[data-testid="published-date-detail"]');
const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]');
const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]');
+ const findWarningIcon = () => wrapper.find(GlIcon);
const mountComponent = (propsData = defaultProps) => {
wrapper = shallowMount(component, {
@@ -68,6 +72,11 @@ describe('tags list row', () => {
expect(findCheckbox().exists()).toBe(false);
});
+ it('is disabled when the digest is missing', () => {
+ mountComponent({ tag: { ...tag, digest: null } });
+ expect(findCheckbox().attributes('disabled')).toBe('true');
+ });
+
it('is wired to the selected prop', () => {
mountComponent({ ...defaultProps, selected: true });
@@ -134,6 +143,27 @@ describe('tags list row', () => {
});
});
+ describe('warning icon', () => {
+ it('is normally hidden', () => {
+ mountComponent();
+
+ expect(findWarningIcon().exists()).toBe(false);
+ });
+
+ it('is shown when the tag is broken', () => {
+ mountComponent({ tag: { ...tag, digest: null } });
+
+ expect(findWarningIcon().exists()).toBe(true);
+ });
+
+ it('has an appropriate tooltip', () => {
+ mountComponent({ tag: { ...tag, digest: null } });
+
+ const tooltip = getBinding(findWarningIcon().element, 'gl-tooltip');
+ expect(tooltip.value.title).toBe(MISSING_MANIFEST_WARNING_TOOLTIP);
+ });
+ });
+
describe('size', () => {
it('exists', () => {
mountComponent();
@@ -150,7 +180,7 @@ describe('tags list row', () => {
it('when total_size is missing', () => {
mountComponent();
- expect(findSize().text()).toMatchInterpolatedText('10 layers');
+ expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 10 layers`);
});
it('when layers are missing', () => {
@@ -162,7 +192,7 @@ describe('tags list row', () => {
it('when there is 1 layer', () => {
mountComponent({ ...defaultProps, tag: { ...tag, layers: 1 } });
- expect(findSize().text()).toMatchInterpolatedText('1 layer');
+ expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 1 layer`);
});
});
@@ -204,6 +234,12 @@ describe('tags list row', () => {
expect(findShortRevision().text()).toMatchInterpolatedText('Digest: 1ab51d5');
});
+
+ it(`displays ${NOT_AVAILABLE_TEXT} when digest is missing`, () => {
+ mountComponent({ tag: { ...tag, digest: null } });
+
+ expect(findShortRevision().text()).toMatchInterpolatedText(`Digest: ${NOT_AVAILABLE_TEXT}`);
+ });
});
describe('delete button', () => {
@@ -223,11 +259,19 @@ describe('tags list row', () => {
});
});
- it('is disabled when tag has no destroy path', () => {
- mountComponent({ ...defaultProps, tag: { ...tag, destroy_path: null } });
-
- expect(findDeleteButton().attributes('disabled')).toBe('true');
- });
+ it.each`
+ destroy_path | digest
+ ${'foo'} | ${null}
+ ${null} | ${'foo'}
+ ${null} | ${null}
+ `(
+ 'is disabled when destroy_path is $destroy_path and digest is $digest',
+ ({ destroy_path, digest }) => {
+ mountComponent({ ...defaultProps, tag: { ...tag, destroy_path, digest } });
+
+ expect(findDeleteButton().attributes('disabled')).toBe('true');
+ },
+ );
it('delete event emits delete', () => {
mountComponent();
@@ -239,36 +283,47 @@ describe('tags list row', () => {
});
describe('details rows', () => {
- beforeEach(() => {
- mountComponent();
+ describe('when the tag has a digest', () => {
+ beforeEach(() => {
+ mountComponent();
- return wrapper.vm.$nextTick();
- });
-
- it('has 3 details rows', () => {
- expect(findDetailsRows().length).toBe(3);
- });
+ return wrapper.vm.$nextTick();
+ });
- describe.each`
- name | finderFunction | text | icon | clipboard
- ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false}
- ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true}
- ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true}
- `('$name details row', ({ finderFunction, text, icon, clipboard }) => {
- it(`has ${text} as text`, () => {
- expect(finderFunction().text()).toMatchInterpolatedText(text);
+ it('has 3 details rows', () => {
+ expect(findDetailsRows().length).toBe(3);
});
- it(`has the ${icon} icon`, () => {
- expect(finderFunction().props('icon')).toBe(icon);
+ describe.each`
+ name | finderFunction | text | icon | clipboard
+ ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false}
+ ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true}
+ ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true}
+ `('$name details row', ({ finderFunction, text, icon, clipboard }) => {
+ it(`has ${text} as text`, () => {
+ expect(finderFunction().text()).toMatchInterpolatedText(text);
+ });
+
+ it(`has the ${icon} icon`, () => {
+ expect(finderFunction().props('icon')).toBe(icon);
+ });
+
+ it(`is ${clipboard} that clipboard button exist`, () => {
+ expect(
+ finderFunction()
+ .find(ClipboardButton)
+ .exists(),
+ ).toBe(clipboard);
+ });
});
+ });
+
+ describe('when the tag does not have a digest', () => {
+ it('hides the details rows', async () => {
+ mountComponent({ tag: { ...tag, digest: null } });
- it(`is ${clipboard} that clipboard button exist`, () => {
- expect(
- finderFunction()
- .find(ClipboardButton)
- .exists(),
- ).toBe(clipboard);
+ await wrapper.vm.$nextTick();
+ expect(findDetailsRows().length).toBe(0);
});
});
});
diff --git a/spec/frontend/repository/utils/dom_spec.js b/spec/frontend/repository/utils/dom_spec.js
index 0b61161c9d0..e8b0565868e 100644
--- a/spec/frontend/repository/utils/dom_spec.js
+++ b/spec/frontend/repository/utils/dom_spec.js
@@ -1,5 +1,6 @@
import { setHTMLFixture } from '../../helpers/fixtures';
import { updateElementsVisibility, updateFormAction } from '~/repository/utils/dom';
+import { TEST_HOST } from 'helpers/test_constants';
describe('updateElementsVisibility', () => {
it('adds hidden class', () => {
@@ -31,7 +32,7 @@ describe('updateFormAction', () => {
updateFormAction('.js-test', '/gitlab/create', path);
expect(document.querySelector('.js-test').action).toBe(
- `http://localhost/gitlab/create/${path.replace(/^\//, '')}`,
+ `${TEST_HOST}/gitlab/create/${path.replace(/^\//, '')}`,
);
});
});
diff --git a/spec/frontend/global_search_input_spec.js b/spec/frontend/search_autocomplete_spec.js
index 8c00ea5f193..1003a076f9e 100644
--- a/spec/frontend/global_search_input_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -2,30 +2,24 @@
import $ from 'jquery';
import '~/gl_dropdown';
-import initGlobalSearchInput from '~/global_search_input';
+import initSearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
+import axios from '~/lib/utils/axios_utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
-describe('Global search input dropdown', () => {
+describe('Search autocomplete dropdown', () => {
let widget = null;
const userName = 'root';
-
const userId = 1;
-
const dashboardIssuesPath = '/dashboard/issues';
-
const dashboardMRsPath = '/dashboard/merge_requests';
-
const projectIssuesPath = '/gitlab-org/gitlab-foss/issues';
-
const projectMRsPath = '/gitlab-org/gitlab-foss/-/merge_requests';
-
const groupIssuesPath = '/groups/gitlab-org/-/issues';
-
const groupMRsPath = '/groups/gitlab-org/-/merge_requests';
-
+ const autocompletePath = '/search/autocomplete';
const projectName = 'GitLab Community Edition';
-
const groupName = 'Gitlab Org';
const removeBodyAttributes = () => {
@@ -112,15 +106,15 @@ describe('Global search input dropdown', () => {
expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
};
- preloadFixtures('static/global_search_input.html');
+ preloadFixtures('static/search_autocomplete.html');
beforeEach(() => {
- loadFixtures('static/global_search_input.html');
+ loadFixtures('static/search_autocomplete.html');
window.gon = {};
window.gon.current_user_id = userId;
window.gon.current_username = userName;
- return (widget = initGlobalSearchInput());
+ return (widget = initSearchAutocomplete({ autocompletePath }));
});
afterEach(() => {
@@ -183,31 +177,105 @@ describe('Global search input dropdown', () => {
widget.wrap.trigger($.Event('keydown', { which: DOWN }));
const enterKeyEvent = $.Event('keydown', { which: ENTER });
widget.searchInput.trigger(enterKeyEvent);
+
// This does not currently catch failing behavior. For security reasons,
// browsers will not trigger default behavior (form submit, in this
// example) on JavaScript-created keypresses.
expect(submitSpy).not.toHaveBeenCalled();
});
- describe('disableDropdown', () => {
+ describe('show autocomplete results', () => {
+ beforeEach(() => {
+ widget.enableAutocomplete();
+
+ const axiosMock = new AxiosMockAdapter(axios);
+ const autocompleteUrl = new RegExp(autocompletePath);
+
+ axiosMock.onGet(autocompleteUrl).reply(200, [
+ {
+ category: 'Projects',
+ id: 1,
+ value: 'Gitlab Test',
+ label: 'Gitlab Org / Gitlab Test',
+ url: '/gitlab-org/gitlab-test',
+ avatar_url: '',
+ },
+ {
+ category: 'Groups',
+ id: 1,
+ value: 'Gitlab Org',
+ label: 'Gitlab Org',
+ url: '/gitlab-org',
+ avatar_url: '',
+ },
+ ]);
+ });
+
+ function triggerAutocomplete() {
+ return new Promise(resolve => {
+ const dropdown = widget.searchInput.data('glDropdown');
+ const filterCallback = dropdown.filter.options.callback;
+ dropdown.filter.options.callback = jest.fn(data => {
+ filterCallback(data);
+
+ resolve();
+ });
+
+ widget.searchInput.val('Gitlab');
+ widget.searchInput.triggerHandler('input');
+ });
+ }
+
+ it('suggest Projects', done => {
+ // eslint-disable-next-line promise/catch-or-return
+ triggerAutocomplete().finally(() => {
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
+ const link = "a[href$='/gitlab-org/gitlab-test']";
+
+ expect(list.find(link).length).toBe(1);
+
+ done();
+ });
+
+ // Make sure jest properly acknowledge the `done` invocation
+ jest.runOnlyPendingTimers();
+ });
+
+ it('suggest Groups', done => {
+ // eslint-disable-next-line promise/catch-or-return
+ triggerAutocomplete().finally(() => {
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
+ const link = "a[href$='/gitlab-org']";
+
+ expect(list.find(link).length).toBe(1);
+
+ done();
+ });
+
+ // Make sure jest properly acknowledge the `done` invocation
+ jest.runOnlyPendingTimers();
+ });
+ });
+
+ describe('disableAutocomplete', () => {
beforeEach(() => {
- widget.enableDropdown();
+ widget.enableAutocomplete();
});
it('should close the Dropdown', () => {
const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown');
widget.dropdown.addClass('show');
- widget.disableDropdown();
+ widget.disableAutocomplete();
expect(toggleSpy).toHaveBeenCalledWith('toggle');
});
});
- describe('enableDropdown', () => {
+ describe('enableAutocomplete', () => {
it('should open the Dropdown', () => {
const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown');
- widget.enableDropdown();
+ widget.enableAutocomplete();
expect(toggleSpy).toHaveBeenCalledWith('toggle');
});
diff --git a/spec/frontend/self_monitor/components/self_monitor_form_spec.js b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
index 0e6abba08f3..aa6f71b6412 100644
--- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js
+++ b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlDeprecatedButton } from '@gitlab/ui';
import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue';
import { createStore } from '~/self_monitor/store';
+import { TEST_HOST } from 'helpers/test_constants';
describe('self monitor component', () => {
let wrapper;
@@ -82,7 +83,7 @@ describe('self monitor component', () => {
.find({ ref: 'selfMonitoringFormText' })
.find('a')
.attributes('href'),
- ).toEqual('http://localhost/instance-administrators-random/gitlab-self-monitoring');
+ ).toEqual(`${TEST_HOST}/instance-administrators-random/gitlab-self-monitoring`);
});
});
});
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 1360a4264a9..699232e67b1 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -8,6 +8,99 @@ RSpec.describe SearchHelper do
str
end
+ describe 'search_autocomplete_opts' do
+ context "with no current user" do
+ before do
+ allow(self).to receive(:current_user).and_return(nil)
+ end
+
+ it "returns nil" do
+ expect(search_autocomplete_opts("q")).to be_nil
+ end
+ end
+
+ context "with a standard user" do
+ let(:user) { create(:user) }
+
+ before do
+ allow(self).to receive(:current_user).and_return(user)
+ end
+
+ it "includes Help sections" do
+ expect(search_autocomplete_opts("hel").size).to eq(9)
+ end
+
+ it "includes default sections" do
+ expect(search_autocomplete_opts("dash").size).to eq(1)
+ end
+
+ it "does not include admin sections" do
+ expect(search_autocomplete_opts("admin").size).to eq(0)
+ end
+
+ it "does not allow regular expression in search term" do
+ expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0)
+ end
+
+ it "includes the user's groups" do
+ create(:group).add_owner(user)
+ expect(search_autocomplete_opts("gro").size).to eq(1)
+ end
+
+ it "includes nested group" do
+ create(:group, :nested, name: 'foo').add_owner(user)
+ expect(search_autocomplete_opts('foo').size).to eq(1)
+ end
+
+ it "includes the user's projects" do
+ project = create(:project, namespace: create(:namespace, owner: user))
+ expect(search_autocomplete_opts(project.name).size).to eq(1)
+ end
+
+ it "includes the required project attrs" do
+ project = create(:project, namespace: create(:namespace, owner: user))
+ result = search_autocomplete_opts(project.name).first
+
+ expect(result.keys).to match_array(%i[category id value label url avatar_url])
+ end
+
+ it "includes the required group attrs" do
+ create(:group).add_owner(user)
+ result = search_autocomplete_opts("gro").first
+
+ expect(result.keys).to match_array(%i[category id label url avatar_url])
+ end
+
+ it "does not include the public group" do
+ group = create(:group)
+ expect(search_autocomplete_opts(group.name).size).to eq(0)
+ end
+
+ context "with a current project" do
+ before do
+ @project = create(:project, :repository)
+ end
+
+ it "includes project-specific sections" do
+ expect(search_autocomplete_opts("Files").size).to eq(1)
+ expect(search_autocomplete_opts("Commits").size).to eq(1)
+ end
+ end
+ end
+
+ context 'with an admin user' do
+ let(:admin) { create(:admin) }
+
+ before do
+ allow(self).to receive(:current_user).and_return(admin)
+ end
+
+ it "includes admin sections" do
+ expect(search_autocomplete_opts("admin").size).to eq(1)
+ end
+ end
+ end
+
describe 'search_entries_info' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index 8e048d877a1..5b6be0b3198 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -175,14 +175,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
expect(subject['merge_requests'].first['resource_label_events']).not_to be_empty
end
- it 'saves the correct service type' do
- expect(subject['services'].first['type']).to eq('CustomIssueTrackerService')
- end
-
- it 'saves the properties for a service' do
- expect(subject['services'].first['properties']).to eq('one' => 'value')
- end
-
it 'has project feature' do
project_feature = subject['project_feature']
expect(project_feature).not_to be_empty
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index e43686e632a..6d5604dc40f 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -291,10 +291,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
expect(@project.auto_devops.deploy_strategy).to eq('continuous')
end
- it 'restores the correct service' do
- expect(CustomIssueTrackerService.first).not_to be_nil
- end
-
it 'restores zoom meetings' do
meetings = @project.issues.first.zoom_meetings
@@ -553,8 +549,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
labels: 2,
label_with_priorities: 'A project label',
milestones: 1,
- first_issue_labels: 1,
- services: 1
+ first_issue_labels: 1
end
context 'when there is an existing build with build token' do
@@ -637,7 +632,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
label_with_priorities: 'A project label',
milestones: 1,
first_issue_labels: 1,
- services: 1,
import_failures: 1
it 'records the failures in the database' do
@@ -757,18 +751,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
setup_reader(reader)
end
- it 'does not import any templated services' do
- expect(restored_project_json).to eq(true)
-
- expect(project.services.where(template: true).count).to eq(0)
- end
-
- it 'does not import any instance services' do
- expect(restored_project_json).to eq(true)
-
- expect(project.services.where(instance: true).count).to eq(0)
- end
-
it 'imports labels' do
create(:group_label, name: 'Another label', group: project.group)
@@ -972,7 +954,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
label_with_priorities: nil,
milestones: 1,
first_issue_labels: 0,
- services: 0,
import_failures: 1
it 'records the failures in the database' do
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index 4f6bc19ebd1..40c103eeda6 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -223,18 +223,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
it { is_expected.not_to be_empty }
end
- context 'with services' do
- let(:relation_name) { :services }
-
- it 'saves the correct service type' do
- expect(subject.first['type']).to eq('CustomIssueTrackerService')
- end
-
- it 'saves the properties for a service' do
- expect(subject.first['properties']).to eq('one' => 'value')
- end
- end
-
context 'with project_feature' do
let(:relation_name) { :project_feature }
@@ -453,7 +441,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
create(:resource_label_event, label: group_label, merge_request: merge_request)
create(:event, :created, target: milestone, project: project, author: user)
- create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' })
create(:project_custom_attribute, project: project)
create(:project_custom_attribute, project: project)
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 395eda1754f..9707c0a4ff4 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -461,36 +461,6 @@ DeployKey:
- public
- can_push
- last_used_at
-Service:
-- id
-- type
-- title
-- project_id
-- created_at
-- updated_at
-- active
-- properties
-- template
-- instance
-- alert_events
-- push_events
-- issues_events
-- commit_events
-- merge_requests_events
-- tag_push_events
-- note_events
-- pipeline_events
-- job_events
-- comment_on_event_enabled
-- comment_detail
-- category
-- default
-- wiki_page_events
-- confidential_issues_events
-- confidential_note_events
-- deployment_events
-- description
-- inherit_from_id
ProjectHook:
- id
- url
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 53ef1db6a3e..b8d39f49224 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -512,7 +512,7 @@ RSpec.describe Ci::CreatePipelineService do
it 'pull it from Auto-DevOps' do
pipeline = execute_service
expect(pipeline).to be_auto_devops_source
- expect(pipeline.builds.map(&:name)).to match_array(%w[test code_quality build])
+ expect(pipeline.builds.map(&:name)).to match_array(%w[build code_quality eslint-sast test])
end
end
diff --git a/spec/services/jira/requests/issues/list_service_spec.rb b/spec/services/jira/requests/issues/list_service_spec.rb
index adfc9771cc7..289dd444225 100644
--- a/spec/services/jira/requests/issues/list_service_spec.rb
+++ b/spec/services/jira/requests/issues/list_service_spec.rb
@@ -93,6 +93,16 @@ RSpec.describe Jira::Requests::Issues::ListService do
subject
end
end
+
+ context 'without pagination parameters' do
+ let(:params) { {} }
+
+ it 'uses the default options' do
+ expect(client).to receive(:get).with(include('startAt=0&maxResults=100'))
+
+ subject
+ end
+ end
end
end
end