From f4199ade274d43b2627d17d9087f42d0f8175369 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 30 Mar 2022 15:09:00 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rails.gitlab-ci.yml | 3 + GITLAB_SHELL_VERSION | 2 +- app/assets/javascripts/api.js | 21 +- .../lib/utils/datetime/timeago_utility.js | 46 ++++ .../components/list/harbor_list.vue | 42 ++++ .../components/list/harbor_list_header.vue | 67 ++++++ .../components/list/harbor_list_row.vue | 84 ++++++++ .../harbor_registry/constants/common.js | 29 +++ .../harbor_registry/constants/details.js | 39 ++++ .../harbor_registry/constants/index.js | 3 + .../harbor_registry/constants/list.js | 33 +++ .../harbor_registry/index.js | 78 +++++++ .../harbor_registry/mock_api.js | 200 ++++++++++++++++++ .../harbor_registry/pages/details.vue | 0 .../harbor_registry/pages/index.vue | 5 + .../harbor_registry/pages/list.vue | 177 ++++++++++++++++ .../harbor_registry/router.js | 35 +++ .../shared/components/registry_list.vue | 3 +- .../pages/groups/harbor/repositories/index.js | 8 + .../pages/projects/commit/show/index.js | 27 +++ .../projects/harbor/repositories/index.js | 8 + .../ui/pipeline_editor_empty_state.vue | 2 +- .../javascripts/search/store/actions.js | 34 +-- app/helpers/diff_helper.rb | 12 ++ app/models/application_setting.rb | 2 + .../third_party/delete_tags_service.rb | 2 +- .../quick_actions/interpret_service.rb | 4 +- .../harbor/repositories/index.html.haml | 6 +- app/views/projects/diffs/_file.html.haml | 1 + .../harbor/repositories/index.html.haml | 6 +- app/views/shared/groups/_dropdown.html.haml | 27 +-- app/workers/all_queues.yml | 2 +- app/workers/project_export_worker.rb | 2 +- ...er_releases_url_to_application_settings.rb | 10 + ...unner_releases_url_application_settings.rb | 13 ++ db/schema_migrations/20220324171254 | 1 + db/schema_migrations/20220324173554 | 1 + db/structure.sql | 2 + doc/ci/variables/predefined_variables.md | 1 + doc/user/group/iterations/index.md | 21 +- .../img/attention_filter_v14_10.png | Bin 16362 -> 0 bytes doc/user/search/index.md | 10 +- lib/gitlab/ci/runner_releases.rb | 65 ++++++ lib/gitlab/lazy.rb | 4 +- .../quick_actions/merge_request_actions.rb | 2 +- .../menus/packages_registries_menu.rb | 4 +- locale/gitlab.pot | 115 +++++++++- spec/deprecation_toolkit_env.rb | 3 - spec/features/commit_spec.rb | 4 + .../projects/user_sorts_projects_spec.rb | 28 ++- .../__helpers__/vuex_action_helper.js | 1 + spec/frontend/api_spec.js | 30 +++ .../utils/datetime/timeago_utility_spec.js | 50 ++++- .../list/harbor_list_header_spec.js | 88 ++++++++ .../components/list/harbor_list_row_spec.js | 99 +++++++++ .../components/list/harbor_list_spec.js | 39 ++++ .../harbor_registry/mock_data.js | 175 +++++++++++++++ .../harbor_registry/pages/index_spec.js | 24 +++ .../harbor_registry/pages/list_spec.js | 140 ++++++++++++ spec/frontend/search/store/actions_spec.js | 20 +- spec/lib/gitlab/ci/runner_releases_spec.rb | 114 ++++++++++ .../third_party/delete_tags_service_spec.rb | 14 +- .../quick_actions/interpret_service_spec.rb | 11 + spec/support/database_cleaner.rb | 3 + spec/support/matchers/graphql_matchers.rb | 10 +- .../shared/groups/_dropdown.html.haml_spec.rb | 27 +++ 66 files changed, 2037 insertions(+), 102 deletions(-) create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list.vue create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/constants/index.js create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/index.js create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/pages/index.vue create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue create mode 100644 app/assets/javascripts/packages_and_registries/harbor_registry/router.js create mode 100644 app/assets/javascripts/pages/groups/harbor/repositories/index.js create mode 100644 app/assets/javascripts/pages/projects/harbor/repositories/index.js create mode 100644 db/migrate/20220324171254_add_public_git_lab_runner_releases_url_to_application_settings.rb create mode 100644 db/migrate/20220324173554_add_text_limit_to_public_git_lab_runner_releases_url_application_settings.rb create mode 100644 db/schema_migrations/20220324171254 create mode 100644 db/schema_migrations/20220324173554 delete mode 100644 doc/user/project/merge_requests/img/attention_filter_v14_10.png create mode 100644 lib/gitlab/ci/runner_releases.rb create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/mock_data.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js create mode 100644 spec/lib/gitlab/ci/runner_releases_spec.rb create mode 100644 spec/views/shared/groups/_dropdown.html.haml_spec.rb diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index fbf335ac777..3908de6b5e1 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -187,6 +187,9 @@ setup-test-env: - .setup-test-env-cache - .rails:rules:setup-test-env stage: prepare + needs: + - job: "update-gitaly-binaries-cache" + optional: true variables: SETUP_DB: "false" script: diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 227f42dd16b..af3deaab686 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -13.24.1 +13.24.2 diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 35fc64d43e5..33d7da8fd53 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -234,7 +234,7 @@ const Api = { return axios .get(url, { - params: Object.assign(defaults, options), + params: { ...defaults, ...options }, }) .then(({ data, headers }) => { callback(data); @@ -445,7 +445,7 @@ const Api = { }, // Return group projects list. Filtered by query - groupProjects(groupId, query, options, callback) { + groupProjects(groupId, query, options, callback = () => {}, useCustomErrorHandler = false) { const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId); const defaults = { search: query, @@ -455,14 +455,21 @@ const Api = { .get(url, { params: { ...defaults, ...options }, }) - .then(({ data }) => (callback ? callback(data) : data)) - .catch(() => { + .then(({ data, headers }) => { + callback(data); + + return { data, headers }; + }) + .catch((error) => { + if (useCustomErrorHandler) { + throw error; + } + createFlash({ message: __('Something went wrong while fetching projects'), }); - if (callback) { - callback(); - } + + callback(); }); }, diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js index d68682ebed1..2b898d8b6a5 100644 --- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js @@ -70,8 +70,41 @@ const memoizedLocale = () => { }; }; +/** + * Registers timeago time duration + */ +const memoizedLocaleDuration = () => { + const cache = []; + + const durations = [ + () => [s__('Duration|%s seconds')], + () => [s__('Duration|%s seconds')], + () => [s__('Duration|1 minute')], + () => [s__('Duration|%s minutes')], + () => [s__('Duration|1 hour')], + () => [s__('Duration|%s hours')], + () => [s__('Duration|1 day')], + () => [s__('Duration|%s days')], + () => [s__('Duration|1 week')], + () => [s__('Duration|%s weeks')], + () => [s__('Duration|1 month')], + () => [s__('Duration|%s months')], + () => [s__('Duration|1 year')], + () => [s__('Duration|%s years')], + ]; + + return (_, index) => { + if (cache[index]) { + return cache[index]; + } + cache[index] = durations[index] && durations[index](); + return cache[index]; + }; +}; + timeago.register(timeagoLanguageCode, memoizedLocale()); timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining()); +timeago.register(`${timeagoLanguageCode}-duration`, memoizedLocaleDuration()); let memoizedFormatter = null; @@ -133,3 +166,16 @@ export const timeFor = (time, expiredLabel) => { } return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim(); }; + +/** + * Returns a duration of time given an amount. + * + * @param {number} milliseconds - Duration in milliseconds. + * @returns {string} A formatted duration, e.g. "10 minutes". + */ +export const duration = (milliseconds) => { + const now = new Date(); + return timeago + .format(now.getTime() - Math.abs(milliseconds), `${timeagoLanguageCode}-duration`) + .trim(); +}; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list.vue new file mode 100644 index 00000000000..c1b5367c96a --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list.vue @@ -0,0 +1,42 @@ + + + diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue new file mode 100644 index 00000000000..086b9c73d75 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue new file mode 100644 index 00000000000..258472fe16e --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue @@ -0,0 +1,84 @@ + + + diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js new file mode 100644 index 00000000000..a7891821755 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js @@ -0,0 +1,29 @@ +import { s__, __ } from '~/locale'; + +export const ROOT_IMAGE_TEXT = s__('HarborRegistry|Root image'); +export const NAME_SORT_FIELD = { orderBy: 'NAME', label: __('Name') }; + +export const ASCENDING_ORDER = 'asc'; +export const DESCENDING_ORDER = 'desc'; + +export const NAME_SORT_FIELD_KEY = 'name'; +export const UPDATED_SORT_FIELD_KEY = 'update_time'; +export const CREATED_SORT_FIELD_KEY = 'creation_time'; + +export const SORT_FIELD_MAPPING = { + NAME: NAME_SORT_FIELD_KEY, + UPDATED: UPDATED_SORT_FIELD_KEY, + CREATED: CREATED_SORT_FIELD_KEY, +}; + +/* eslint-disable @gitlab/require-i18n-strings */ +export const dockerBuildCommand = (repositoryUrl) => { + return `docker build -t ${repositoryUrl} .`; +}; +export const dockerPushCommand = (repositoryUrl) => { + return `docker push ${repositoryUrl}`; +}; +export const dockerLoginCommand = (registryHostUrlWithPort) => { + return `docker login ${registryHostUrlWithPort}`; +}; +/* eslint-enable @gitlab/require-i18n-strings */ diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js new file mode 100644 index 00000000000..2519f6b74a2 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js @@ -0,0 +1,39 @@ +import { s__, __ } from '~/locale'; + +export const UPDATED_AT = s__('HarborRegistry|Last updated %{time}'); + +export const MISSING_OR_DELETED_IMAGE_TITLE = s__( + 'HarborRegistry|The image repository could not be found.', +); + +export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__( + 'HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.', +); + +export const NO_TAGS_TITLE = s__('HarborRegistry|This image has no active tags'); + +export const NO_TAGS_MESSAGE = s__( + `HarborRegistry|The last tag related to this image was recently removed. +This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. +If you have any questions, contact your administrator.`, +); + +export const NO_TAGS_MATCHING_FILTERS_TITLE = s__('HarborRegistry|The filter returned no results'); + +export const NO_TAGS_MATCHING_FILTERS_DESCRIPTION = s__( + 'HarborRegistry|Please try different search criteria', +); + +export const DIGEST_LABEL = s__('HarborRegistry|Digest: %{imageId}'); +export const CREATED_AT_LABEL = s__('HarborRegistry|Published %{timeInfo}'); +export const PUBLISHED_DETAILS_ROW_TEXT = s__( + 'HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}', +); +export const MANIFEST_DETAILS_ROW_TEST = s__('HarborRegistry|Manifest digest: %{digest}'); +export const CONFIGURATION_DETAILS_ROW_TEST = s__('HarborRegistry|Configuration digest: %{digest}'); +export const MISSING_MANIFEST_WARNING_TOOLTIP = s__( + 'HarborRegistry|Invalid tag: missing manifest digest', +); + +export const NOT_AVAILABLE_TEXT = __('N/A'); +export const NOT_AVAILABLE_SIZE = __('0 bytes'); diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/index.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/index.js new file mode 100644 index 00000000000..22f462e0b97 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/index.js @@ -0,0 +1,3 @@ +export * from './common'; +export * from './list'; +export * from './details'; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js new file mode 100644 index 00000000000..a6cd59918ff --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js @@ -0,0 +1,33 @@ +import { s__, __, n__ } from '~/locale'; +import { NAME_SORT_FIELD } from './common'; + +// Translations strings + +export const HARBOR_REGISTRY_TITLE = s__('HarborRegistry|Harbor Registry'); + +export const CONNECTION_ERROR_TITLE = s__('HarborRegistry|Harbor connection error'); +export const CONNECTION_ERROR_MESSAGE = s__( + `HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}.`, +); +export const LIST_INTRO_TEXT = s__( + `HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`, +); + +export const imagesCountInfoText = (count) => { + return n__( + 'HarborRegistry|%{count} Image repository', + 'HarborRegistry|%{count} Image repositories', + count, + ); +}; + +export const EMPTY_RESULT_TITLE = s__('HarborRegistry|Sorry, your filter produced no results.'); +export const EMPTY_RESULT_MESSAGE = s__( + 'HarborRegistry|To widen your search, change or remove the filters above.', +); + +export const SORT_FIELDS = [ + { orderBy: 'UPDATED', label: __('Updated') }, + { orderBy: 'CREATED', label: __('Created') }, + NAME_SORT_FIELD, +]; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/index.js b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js new file mode 100644 index 00000000000..ecfefead61a --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js @@ -0,0 +1,78 @@ +import { GlToast } from '@gitlab/ui'; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import PerformancePlugin from '~/performance/vue_performance_plugin'; +import Translate from '~/vue_shared/translate'; +import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue'; +import { renderBreadcrumb } from '~/packages_and_registries/shared/utils'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { + dockerBuildCommand, + dockerPushCommand, + dockerLoginCommand, +} from '~/packages_and_registries/harbor_registry/constants'; +import createRouter from './router'; +import HarborRegistryExplorer from './pages/index.vue'; + +Vue.use(Translate); +Vue.use(GlToast); + +Vue.use(PerformancePlugin, { + components: [ + 'RegistryListPage', + 'ListHeader', + 'ImageListRow', + 'RegistryDetailsPage', + 'DetailsHeader', + 'TagsList', + ], +}); + +export default (id) => { + const el = document.getElementById(id); + + if (!el) { + return null; + } + + const { endpoint, connectionError, invalidPathError, isGroupPage, ...config } = el.dataset; + + const breadCrumbState = Vue.observable({ + name: '', + updateName(value) { + this.name = value; + }, + }); + + const router = createRouter(endpoint, breadCrumbState); + + const attachMainComponent = () => { + return new Vue({ + el, + router, + provide() { + return { + breadCrumbState, + config: { + ...config, + connectionError: parseBoolean(connectionError), + invalidPathError: parseBoolean(invalidPathError), + isGroupPage: parseBoolean(isGroupPage), + helpPagePath: helpPagePath('user/packages/container_registry/index'), + }, + dockerBuildCommand: dockerBuildCommand(config.repositoryUrl), + dockerPushCommand: dockerPushCommand(config.repositoryUrl), + dockerLoginCommand: dockerLoginCommand(config.registryHostUrlWithPort), + }; + }, + render(createElement) { + return createElement(HarborRegistryExplorer); + }, + }); + }; + + return { + attachBreadcrumb: renderBreadcrumb(router, null, RegistryBreadcrumb), + attachMainComponent, + }; +}; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js b/app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js new file mode 100644 index 00000000000..50c7df1483c --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js @@ -0,0 +1,200 @@ +const mockRequestFn = (mockData) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(mockData); + }, 2000); + }); +}; +export const harborListResponse = () => { + const harborListResponseData = { + repositories: [ + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 25, + name: 'shao/flinkx', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 26, + name: 'shao/flinkx1', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 27, + name: 'shao/flinkx2', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + ], + totalCount: 3, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, + }; + + return mockRequestFn(harborListResponseData); +}; + +export const getHarborRegistryImageDetail = () => { + const harborRegistryImageDetailData = { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 25, + name: 'shao/flinkx', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + tagsCount: 10, + }; + + return mockRequestFn(harborRegistryImageDetailData); +}; + +export const harborTagsResponse = () => { + const harborTagsResponseData = { + tags: [ + { + digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255', + shortRevision: 'f53bde3d4', + createdAt: '2022-03-02T23:59:05+00:00', + totalSize: '6623124', + }, + { + digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e', + shortRevision: 'e1fe52d8b', + createdAt: '2022-02-10T01:09:56+00:00', + totalSize: '920760', + }, + { + digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f', + shortRevision: 'c72770c6e', + createdAt: '2021-12-22T04:48:48+00:00', + totalSize: '48609053', + }, + { + digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a', + shortRevision: '1ac2a4319', + createdAt: '2022-03-09T11:02:27+00:00', + totalSize: '35141894', + }, + { + digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c', + shortRevision: 'cf8fee086', + createdAt: '2022-01-21T11:31:43+00:00', + totalSize: '48716070', + }, + { + digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15', + shortRevision: '1a4b48198', + createdAt: '2022-01-21T11:31:51+00:00', + totalSize: '6623127', + }, + { + digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61', + shortRevision: '03e2e2777', + createdAt: '2022-03-02T23:58:20+00:00', + totalSize: '911377', + }, + { + digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012', + shortRevision: '350e78d60', + createdAt: '2022-01-19T13:49:14+00:00', + totalSize: '48710241', + }, + { + digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18', + shortRevision: '76038370b', + createdAt: '2022-01-24T12:56:22+00:00', + totalSize: '280065', + }, + { + digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f', + shortRevision: '3d4b49a7b', + createdAt: '2022-02-17T17:37:52+00:00', + totalSize: '48655767', + }, + ], + totalCount: 10, + pageInfo: { + hasNextPage: false, + hasPreviousPage: true, + }, + }; + + return mockRequestFn(harborTagsResponseData); +}; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/index.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/index.vue new file mode 100644 index 00000000000..dca63e1a569 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/index.vue @@ -0,0 +1,5 @@ + diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue new file mode 100644 index 00000000000..7aaef2ed57a --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue @@ -0,0 +1,177 @@ + + + diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/router.js b/app/assets/javascripts/packages_and_registries/harbor_registry/router.js new file mode 100644 index 00000000000..572dd382be3 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/router.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import { HARBOR_REGISTRY_TITLE } from './constants/index'; +import List from './pages/list.vue'; +import Details from './pages/details.vue'; + +Vue.use(VueRouter); + +export default function createRouter(base, breadCrumbState) { + const router = new VueRouter({ + base, + mode: 'history', + routes: [ + { + name: 'list', + path: '/', + component: List, + meta: { + nameGenerator: () => HARBOR_REGISTRY_TITLE, + root: true, + }, + }, + { + name: 'details', + path: '/:id', + component: Details, + meta: { + nameGenerator: () => breadCrumbState.name, + }, + }, + ], + }); + + return router; +} diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue index 79381f82009..cc345fda7e8 100644 --- a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue +++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue @@ -13,7 +13,8 @@ export default { props: { title: { type: String, - required: true, + default: '', + required: false, }, isLoading: { type: Boolean, diff --git a/app/assets/javascripts/pages/groups/harbor/repositories/index.js b/app/assets/javascripts/pages/groups/harbor/repositories/index.js new file mode 100644 index 00000000000..0ecce44be54 --- /dev/null +++ b/app/assets/javascripts/pages/groups/harbor/repositories/index.js @@ -0,0 +1,8 @@ +import HarborRegistryExplorer from '~/packages_and_registries/harbor_registry/index'; + +const explorer = HarborRegistryExplorer('js-harbor-registry-list-group'); + +if (explorer) { + explorer.attachBreadcrumb(); + explorer.attachMainComponent(); +} diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index c6a76df7bde..bbd33ac2f07 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ import $ from 'jquery'; +import Vue from 'vue'; import loadAwardsHandler from '~/awards_handler'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import Diff from '~/diff'; @@ -14,6 +15,7 @@ import { initCommitBoxInfo } from '~/projects/commit_box/info'; import syntaxHighlight from '~/syntax_highlight'; import ZenMode from '~/zen_mode'; import '~/sourcegraph/load'; +import DiffStats from '~/diffs/components/diff_stats.vue'; const hasPerfBar = document.querySelector('.with-performance-bar'); const performanceHeight = hasPerfBar ? 35 : 0; @@ -26,6 +28,7 @@ initCommitBoxInfo(); initDeprecatedNotes(); const filesContainer = $('.js-diffs-batch'); +const diffStatsElements = document.querySelectorAll('#js-diff-stats'); if (filesContainer.length) { const batchPath = filesContainer.data('diffFilesPath'); @@ -44,5 +47,29 @@ if (filesContainer.length) { } else { new Diff(); } + +if (diffStatsElements.length) { + diffStatsElements.forEach((diffStatsEl) => { + const { addedLines, removedLines, oldSize, newSize, viewerName } = diffStatsEl.dataset; + + new Vue({ + el: diffStatsEl, + render(createElement) { + return createElement(DiffStats, { + props: { + diffFile: { + old_size: oldSize, + new_size: newSize, + viewer: { name: viewerName }, + }, + addedLines: Number(addedLines), + removedLines: Number(removedLines), + }, + }); + }, + }); + }); +} + loadAwardsHandler(); initCommitActions(); diff --git a/app/assets/javascripts/pages/projects/harbor/repositories/index.js b/app/assets/javascripts/pages/projects/harbor/repositories/index.js new file mode 100644 index 00000000000..efbe24ac346 --- /dev/null +++ b/app/assets/javascripts/pages/projects/harbor/repositories/index.js @@ -0,0 +1,8 @@ +import HarborRegistryExplorer from '~/packages_and_registries/harbor_registry/index'; + +const explorer = HarborRegistryExplorer('js-harbor-registry-list-project'); + +if (explorer) { + explorer.attachBreadcrumb(); + explorer.attachMainComponent(); +} diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue index aee71999373..3e87088e77e 100644 --- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue +++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue @@ -14,7 +14,7 @@ export default { body: __( 'Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started.', ), - btnText: __('Create new CI/CD pipeline'), + btnText: __('Configure pipeline'), }, inject: { emptyStateIllustrationPath: { diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index a6af5644681..40513a7f363 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -18,31 +18,35 @@ export const fetchGroups = ({ commit }, search) => { }); }; -export const fetchProjects = ({ commit, state }, search) => { +export const fetchProjects = ({ commit, state }, search, emptyCallback = () => {}) => { commit(types.REQUEST_PROJECTS); const groupId = state.query?.group_id; - const callback = (data) => { - if (data) { - commit(types.RECEIVE_PROJECTS_SUCCESS, data); - } else { - createFlash({ message: __('There was an error fetching projects') }); - commit(types.RECEIVE_PROJECTS_ERROR); - } + + const handleCatch = () => { + createFlash({ message: __('There was an error fetching projects') }); + commit(types.RECEIVE_PROJECTS_ERROR); + }; + const handleSuccess = ({ data }) => { + commit(types.RECEIVE_PROJECTS_SUCCESS, data); }; if (groupId) { - // TODO (https://gitlab.com/gitlab-org/gitlab/-/issues/323331): For errors `createFlash` is called twice; in `callback` and in `Api.groupProjects` Api.groupProjects( groupId, search, - { order_by: 'similarity', with_shared: false, include_subgroups: true }, - callback, - ); + { + order_by: 'similarity', + with_shared: false, + include_subgroups: true, + }, + emptyCallback, + true, + ) + .then(handleSuccess) + .catch(handleCatch); } else { // The .catch() is due to the API method not handling a rejection properly - Api.projects(search, { order_by: 'similarity' }, callback).catch(() => { - callback(); - }); + Api.projects(search, { order_by: 'similarity' }).then(handleSuccess).catch(handleCatch); } }; diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 121bd260928..2aabf0febdf 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -179,6 +179,18 @@ module DiffHelper } end + def diff_file_stats_data(diff_file) + old_blob = diff_file.old_blob + new_blob = diff_file.new_blob + { + old_size: old_blob&.size, + new_size: new_blob&.size, + added_lines: diff_file.added_lines, + removed_lines: diff_file.removed_lines, + viewer_name: diff_file.viewer.partial_name + } + end + def editable_diff?(diff_file) !diff_file.deleted_file? && @merge_request && @merge_request.source_project end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2d512990822..f0888009f7f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -576,6 +576,8 @@ class ApplicationSetting < ApplicationRecord length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, allow_nil: false + validates :public_runner_releases_url, addressable_url: true, presence: true + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, diff --git a/app/services/projects/container_repository/third_party/delete_tags_service.rb b/app/services/projects/container_repository/third_party/delete_tags_service.rb index 4184c676fc3..942df177bea 100644 --- a/app/services/projects/container_repository/third_party/delete_tags_service.rb +++ b/app/services/projects/container_repository/third_party/delete_tags_service.rb @@ -33,7 +33,7 @@ module Projects if deleted_tags.any? && @container_repository.delete_tag_by_digest(deleted_tags.each_value.first) success(deleted: deleted_tags.keys) else - error('could not delete tags') + error("could not delete tags: #{@tag_names.join(', ')}".truncate(1000)) end end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 1baa4ddf0eb..9184c99c039 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -168,7 +168,7 @@ module QuickActions next unless definition definition.execute(self, arg) - usage_ping_tracking(name, arg) + usage_ping_tracking(definition.name, arg) end end @@ -186,7 +186,7 @@ module QuickActions def usage_ping_tracking(quick_action_name, arg) Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter.track_unique_action( - quick_action_name, + quick_action_name.to_s, args: arg&.strip, user: current_user ) diff --git a/app/views/groups/harbor/repositories/index.html.haml b/app/views/groups/harbor/repositories/index.html.haml index 1ee15557e21..6a1e66520b5 100644 --- a/app/views/groups/harbor/repositories/index.html.haml +++ b/app/views/groups/harbor/repositories/index.html.haml @@ -4,6 +4,8 @@ #js-harbor-registry-list-group{ data: { endpoint: group_harbor_registries_path(@group), "no_containers_image" => image_path('illustrations/docker-empty-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'), - "help_page_path" => help_page_path('user/packages/container_registry/index'), + "repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + "registry_host_url_with_port" => 'demo.harbor.com', connection_error: (!!@connection_error).to_s, - invalid_path_error: (!!@invalid_path_error).to_s, } } + invalid_path_error: (!!@invalid_path_error).to_s, + is_group_page: true.to_s } } diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 0638481d968..64bd1bf32f0 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -15,6 +15,7 @@ - unless diff_file.submodule? .file-actions.gl-display-none.gl-sm-display-flex + #js-diff-stats{ data: diff_file_stats_data(diff_file) } - if diff_file.blob&.readable_text? %span.has-tooltip{ title: _("Toggle comments for this file") } = link_to '#', class: 'js-toggle-diff-comments btn gl-button btn-default btn-icon selected', disabled: @diff_notes_disabled do diff --git a/app/views/projects/harbor/repositories/index.html.haml b/app/views/projects/harbor/repositories/index.html.haml index b3f5b91596d..270cbf3facd 100644 --- a/app/views/projects/harbor/repositories/index.html.haml +++ b/app/views/projects/harbor/repositories/index.html.haml @@ -4,6 +4,8 @@ #js-harbor-registry-list-project{ data: { endpoint: project_harbor_registry_index_path(@project), "no_containers_image" => image_path('illustrations/docker-empty-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'), - "help_page_path" => help_page_path('user/packages/container_registry/index'), + "repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + "registry_host_url_with_port" => 'demo.harbor.com', connection_error: (!!@connection_error).to_s, - invalid_path_error: (!!@invalid_path_error).to_s, } } + invalid_path_error: (!!@invalid_path_error).to_s, + is_group_page: false.to_s, } } diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml index 75c34102935..80edce8e7c4 100644 --- a/app/views/shared/groups/_dropdown.html.haml +++ b/app/views/shared/groups/_dropdown.html.haml @@ -1,26 +1,5 @@ - options_hash = local_assigns.fetch(:options_hash, groups_sort_options_hash) -- show_archive_options = local_assigns.fetch(:show_archive_options, false) +- groups_sort_options = options_hash.map { |value, title| { value: value, text: title, href: filter_groups_path(sort: value) } } -.dropdown.inline.js-group-filter-dropdown-wrap.gl-mr-3 - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.dropdown-label - = options_hash[project_list_sort_by] - = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3') - %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable - %li.dropdown-header - = _("Sort by") - - options_hash.each do |value, title| - %li.js-filter-sort-order - = link_to filter_groups_path(sort: value), class: ("is-active" if project_list_sort_by == value) do - = title - - if show_archive_options - %li.divider - %li.js-filter-archived-projects - = link_to filter_groups_path(archived: nil), class: ("is-active" unless params[:archived].present?) do - = _("Hide archived projects") - %li.js-filter-archived-projects - = link_to filter_groups_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do - = _("Show archived projects") - %li.js-filter-archived-projects - = link_to filter_groups_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do - = _("Show archived projects only") +%div{ data: { testid: 'group_sort_by_dropdown' } } + = gl_redirect_listbox_tag groups_sort_options, project_list_sort_by, data: { right: true } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 2f450998455..86d36fd7f79 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -2771,7 +2771,7 @@ :worker_name: ProjectExportWorker :feature_category: :importers :has_external_dependencies: - :urgency: :throttled + :urgency: :low :resource_boundary: :memory :weight: 1 :idempotent: diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb index e3f8c4bcd9d..8b87f362285 100644 --- a/app/workers/project_export_worker.rb +++ b/app/workers/project_export_worker.rb @@ -8,7 +8,7 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker feature_category :importers worker_resource_boundary :memory - urgency :throttled + urgency :low loggable_arguments 2, 3 sidekiq_options retry: false, dead: false sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION diff --git a/db/migrate/20220324171254_add_public_git_lab_runner_releases_url_to_application_settings.rb b/db/migrate/20220324171254_add_public_git_lab_runner_releases_url_to_application_settings.rb new file mode 100644 index 00000000000..af963fa5454 --- /dev/null +++ b/db/migrate/20220324171254_add_public_git_lab_runner_releases_url_to_application_settings.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddPublicGitLabRunnerReleasesUrlToApplicationSettings < Gitlab::Database::Migration[1.0] + # rubocop:disable Migration/AddLimitToTextColumns + # limit is added in 20220324173554_add_text_limit_to_public_git_lab_runner_releases_url_application_settings + def change + add_column :application_settings, :public_runner_releases_url, :text, null: false, default: 'https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab-runner/releases' + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20220324173554_add_text_limit_to_public_git_lab_runner_releases_url_application_settings.rb b/db/migrate/20220324173554_add_text_limit_to_public_git_lab_runner_releases_url_application_settings.rb new file mode 100644 index 00000000000..26f7447021b --- /dev/null +++ b/db/migrate/20220324173554_add_text_limit_to_public_git_lab_runner_releases_url_application_settings.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddTextLimitToPublicGitLabRunnerReleasesUrlApplicationSettings < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + add_text_limit :application_settings, :public_runner_releases_url, 255 + end + + def down + remove_text_limit :application_settings, :public_runner_releases_url + end +end diff --git a/db/schema_migrations/20220324171254 b/db/schema_migrations/20220324171254 new file mode 100644 index 00000000000..df966fd5300 --- /dev/null +++ b/db/schema_migrations/20220324171254 @@ -0,0 +1 @@ +610c5ded785f174d195a660062bb74e718bfd5a38b13773215e20e8f95c59da4 \ No newline at end of file diff --git a/db/schema_migrations/20220324173554 b/db/schema_migrations/20220324173554 new file mode 100644 index 00000000000..8292460972f --- /dev/null +++ b/db/schema_migrations/20220324173554 @@ -0,0 +1 @@ +9f597a462768531b0c6ad23e6e1a52edb765724518e1cebc0684160b030d6225 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d2af03d1096..1fc4733c6fd 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11257,6 +11257,7 @@ CREATE TABLE application_settings ( encrypted_database_grafana_api_key_iv bytea, database_grafana_api_url text, database_grafana_tag text, + public_runner_releases_url text DEFAULT 'https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab-runner/releases'::text NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), @@ -11279,6 +11280,7 @@ CREATE TABLE application_settings ( CONSTRAINT check_718b4458ae CHECK ((char_length(personal_access_token_prefix) <= 20)), CONSTRAINT check_7227fad848 CHECK ((char_length(rate_limiting_response_text) <= 255)), CONSTRAINT check_85a39b68ff CHECK ((char_length(encrypted_ci_jwt_signing_key_iv) <= 255)), + CONSTRAINT check_8dca35398a CHECK ((char_length(public_runner_releases_url) <= 255)), CONSTRAINT check_9a719834eb CHECK ((char_length(secret_detection_token_revocation_url) <= 255)), CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)), CONSTRAINT check_a5704163cc CHECK ((char_length(secret_detection_revocation_token_types_url) <= 255)), diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index b3a0cae7810..44a8e631660 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -113,6 +113,7 @@ There are also a number of [variables you can use to configure runner behavior]( | `CI_SERVER_PORT` | 12.8 | all | The port of the GitLab instance URL, without host or protocol. For example `8080`. | | `CI_SERVER_PROTOCOL` | 12.8 | all | The protocol of the GitLab instance URL, without host or port. For example `https`. | | `CI_SERVER_REVISION` | all | all | GitLab revision that schedules jobs. | +| `CI_SERVER_TLS_CA_FILE` | all | all | File containing the CA certificate to verify the GitLab server. | | `CI_SERVER_URL` | 12.7 | all | The base URL of the GitLab instance, including protocol and port. For example `https://gitlab.example.com:8080`. | | `CI_SERVER_VERSION_MAJOR` | 11.4 | all | The major version of the GitLab instance. For example, if the GitLab version is `13.6.1`, the `CI_SERVER_VERSION_MAJOR` is `13`. | | `CI_SERVER_VERSION_MINOR` | 11.4 | all | The minor version of the GitLab instance. For example, if the GitLab version is `13.6.1`, the `CI_SERVER_VERSION_MINOR` is `6`. | diff --git a/doc/user/group/iterations/index.md b/doc/user/group/iterations/index.md index b5912a0b40e..5beef7cb1ba 100644 --- a/doc/user/group/iterations/index.md +++ b/doc/user/group/iterations/index.md @@ -84,6 +84,12 @@ From there you can create a new iteration or select an iteration to get a more d ## Create an iteration +> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 14.10. + +WARNING: +Manual iteration management is in its end-of-life process. Creating an iteration is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) +for use in GitLab 14.10, and is planned for removal in GitLab 15.6. + Prerequisites: - You must have at least the Developer role for a group. @@ -100,7 +106,13 @@ To create an iteration: ## Edit an iteration -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218277) in GitLab 13.2. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218277) in GitLab 13.2. +> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 14.10. + +WARNING: +Editing all attributes, with the exception of `description` is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) +for use in GitLab 14.10, and is planned for removal in GitLab 15.6. +In the future only editing an iteration's `description` will be allowed. Prerequisites: @@ -110,7 +122,12 @@ To edit an iteration, select the three-dot menu (**{ellipsis_v}**) > **Edit**. ## Delete an iteration -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292268) in GitLab 14.3. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292268) in GitLab 14.3. +> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 14.10. + +WARNING: +Manual iteration management is in its end-of-life process. Deleting an iteration is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) +for use in GitLab 14.10, and is planned for removal in GitLab 15.6. Prerequisites: diff --git a/doc/user/project/merge_requests/img/attention_filter_v14_10.png b/doc/user/project/merge_requests/img/attention_filter_v14_10.png deleted file mode 100644 index e959a42765a1244070be36bf2adfaed024fe5d8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16362 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 - Filtering by iterations was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6. > - Filtering by iterations was moved from GitLab Ultimate to GitLab Premium in 13.9. > - Filtering by type was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322755) in GitLab 13.10 [with a flag](../../administration/feature_flags.md) named `vue_issues_list`. Disabled by default. +> - Filtering by attention request was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/343528) in GitLab 14.10 [with a flag](../../administration/feature_flags.md) named `mr_attention_requests`. Disabled by default. Follow these steps to filter the **Issues** and **Merge requests** list pages in projects and groups: @@ -44,6 +49,7 @@ groups: 1. Select **Search or filter results...**. 1. In the dropdown list that appears, select the attribute you wish to filter by: - Assignee + - [Attention requests](../project/merge_requests/index.md#request-attention-to-a-merge-request) - Author - Confidential - [Epic and child Epic](../group/epics/index.md) (available only for the group the Epic was created, not for [higher group levels](https://gitlab.com/gitlab-org/gitlab/-/issues/233729)). diff --git a/lib/gitlab/ci/runner_releases.rb b/lib/gitlab/ci/runner_releases.rb new file mode 100644 index 00000000000..944c24ca128 --- /dev/null +++ b/lib/gitlab/ci/runner_releases.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class RunnerReleases + include Singleton + + RELEASES_VALIDITY_PERIOD = 1.day + RELEASES_VALIDITY_AFTER_ERROR_PERIOD = 5.seconds + + INITIAL_BACKOFF = 5.seconds + MAX_BACKOFF = 1.hour + BACKOFF_GROWTH_FACTOR = 2.0 + + def initialize + reset! + end + + # Returns a sorted list of the publicly available GitLab Runner releases + # + def releases + return @releases unless Time.now.utc >= @expire_time + + @releases = fetch_new_releases + end + + def reset! + @expire_time = Time.now.utc + @releases = nil + @backoff_count = 0 + end + + public_class_method :instance + + private + + def fetch_new_releases + response = Gitlab::HTTP.try_get(::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url) + + releases = response.success? ? extract_releases(response) : nil + ensure + @expire_time = (releases ? RELEASES_VALIDITY_PERIOD : next_backoff).from_now + end + + def extract_releases(response) + response.parsed_response.map { |release| parse_runner_release(release) }.sort! + end + + def parse_runner_release(release) + ::Gitlab::VersionInfo.parse(release['name'].delete_prefix('v')) + end + + def next_backoff + return MAX_BACKOFF if @backoff_count >= 11 # optimization to prevent expensive exponentiation and possible overflows + + backoff = (INITIAL_BACKOFF * (BACKOFF_GROWTH_FACTOR**@backoff_count)) + .clamp(INITIAL_BACKOFF, MAX_BACKOFF) + .seconds + @backoff_count += 1 + + backoff + end + end + end +end diff --git a/lib/gitlab/lazy.rb b/lib/gitlab/lazy.rb index d7a22aa339e..c589d613efc 100644 --- a/lib/gitlab/lazy.rb +++ b/lib/gitlab/lazy.rb @@ -15,10 +15,10 @@ module Gitlab @block = block end - def method_missing(name, *args, &block) + def method_missing(...) __evaluate__ - @result.__send__(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend + @result.__send__(...) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(name, include_private = false) diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb index 1e0f9872152..4efa29337d1 100644 --- a/lib/gitlab/quick_actions/merge_request_actions.rb +++ b/lib/gitlab/quick_actions/merge_request_actions.rb @@ -291,7 +291,7 @@ module Gitlab parse_params do |attention_param| extract_users(attention_param) end - command :attention do |users| + command :attention, :attn do |users| next if users.empty? users.each do |user| diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb index 77f09986b19..d82a02a342f 100644 --- a/lib/sidebars/projects/menus/packages_registries_menu.rb +++ b/lib/sidebars/projects/menus/packages_registries_menu.rb @@ -47,7 +47,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Container Registry'), link: project_container_registry_index_path(context.project), - active_routes: { controller: :repositories }, + active_routes: { controller: 'projects/registry/repositories' }, item_id: :container_registry ) end @@ -71,7 +71,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Harbor Registry'), link: project_harbor_registry_index_path(context.project), - active_routes: { controller: :harbor_registry }, + active_routes: { controller: 'projects/harbor/repositories' }, item_id: :harbor_registry ) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e1de407de9c..9d00d5e42a0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9413,6 +9413,9 @@ msgstr "" msgid "Configure existing installation" msgstr "" +msgid "Configure pipeline" +msgstr "" + msgid "Configure pipelines to deploy web apps, backend services, APIs and static resources to Google Cloud" msgstr "" @@ -10511,9 +10514,6 @@ msgstr "" msgid "Create new %{name} by email" msgstr "" -msgid "Create new CI/CD pipeline" -msgstr "" - msgid "Create new Value Stream" msgstr "" @@ -13409,6 +13409,45 @@ msgstr "" msgid "Duration" msgstr "" +msgid "Duration|%s days" +msgstr "" + +msgid "Duration|%s hours" +msgstr "" + +msgid "Duration|%s minutes" +msgstr "" + +msgid "Duration|%s months" +msgstr "" + +msgid "Duration|%s seconds" +msgstr "" + +msgid "Duration|%s weeks" +msgstr "" + +msgid "Duration|%s years" +msgstr "" + +msgid "Duration|1 day" +msgstr "" + +msgid "Duration|1 hour" +msgstr "" + +msgid "Duration|1 minute" +msgstr "" + +msgid "Duration|1 month" +msgstr "" + +msgid "Duration|1 week" +msgstr "" + +msgid "Duration|1 year" +msgstr "" + msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below." msgstr "" @@ -18262,6 +18301,76 @@ msgstr "" msgid "HarborIntegration|Use Harbor as this project's container registry." msgstr "" +msgid "HarborRegistry|%{count} Image repository" +msgid_plural "HarborRegistry|%{count} Image repositories" +msgstr[0] "" +msgstr[1] "" + +msgid "HarborRegistry|%{count} Tag" +msgid_plural "HarborRegistry|%{count} Tags" +msgstr[0] "" +msgstr[1] "" + +msgid "HarborRegistry|Configuration digest: %{digest}" +msgstr "" + +msgid "HarborRegistry|Digest: %{imageId}" +msgstr "" + +msgid "HarborRegistry|Harbor Registry" +msgstr "" + +msgid "HarborRegistry|Harbor connection error" +msgstr "" + +msgid "HarborRegistry|Invalid tag: missing manifest digest" +msgstr "" + +msgid "HarborRegistry|Last updated %{time}" +msgstr "" + +msgid "HarborRegistry|Manifest digest: %{digest}" +msgstr "" + +msgid "HarborRegistry|Please try different search criteria" +msgstr "" + +msgid "HarborRegistry|Published %{timeInfo}" +msgstr "" + +msgid "HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}" +msgstr "" + +msgid "HarborRegistry|Root image" +msgstr "" + +msgid "HarborRegistry|Sorry, your filter produced no results." +msgstr "" + +msgid "HarborRegistry|The filter returned no results" +msgstr "" + +msgid "HarborRegistry|The image repository could not be found." +msgstr "" + +msgid "HarborRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator." +msgstr "" + +msgid "HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page." +msgstr "" + +msgid "HarborRegistry|This image has no active tags" +msgstr "" + +msgid "HarborRegistry|To widen your search, change or remove the filters above." +msgstr "" + +msgid "HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}." +msgstr "" + +msgid "HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}" +msgstr "" + msgid "Hashed Storage must be enabled to use Geo" msgstr "" diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb index ad861a7d58e..fa4fdf805ec 100644 --- a/spec/deprecation_toolkit_env.rb +++ b/spec/deprecation_toolkit_env.rb @@ -56,11 +56,8 @@ module DeprecationToolkitEnv # In this case, we recommend to add a silence together with an issue to patch or update # the dependency causing the problem. # See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736 - # - # - lib/gitlab/lazy.rb: https://gitlab.com/gitlab-org/gitlab/-/issues/356367 def self.allowed_kwarg_warning_paths %w[ - lib/gitlab/lazy.rb ] end diff --git a/spec/features/commit_spec.rb b/spec/features/commit_spec.rb index 3fd613ce393..c9fa10d58e6 100644 --- a/spec/features/commit_spec.rb +++ b/spec/features/commit_spec.rb @@ -33,6 +33,10 @@ RSpec.describe 'Commit' do it "reports the correct number of total changes" do expect(page).to have_content("Changes #{commit.diffs.size}") end + + it 'renders diff stats', :js do + expect(page).to have_selector(".diff-stats") + end end describe "pagination" do diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb index 71e43467a39..7c970f7ee3d 100644 --- a/spec/features/projects/user_sorts_projects_spec.rb +++ b/spec/features/projects/user_sorts_projects_spec.rb @@ -14,25 +14,29 @@ RSpec.describe 'User sorts projects and order persists' do it "is set on the dashboard_projects_path" do visit(dashboard_projects_path) - expect(find('.dropdown-menu a.is-active', text: project_paths_label)).to have_content(project_paths_label) + expect(find('#sort-projects-dropdown')).to have_content(project_paths_label) end it "is set on the explore_projects_path" do visit(explore_projects_path) - expect(find('.dropdown-menu a.is-active', text: project_paths_label)).to have_content(project_paths_label) + expect(find('#sort-projects-dropdown')).to have_content(project_paths_label) end it "is set on the group_canonical_path" do visit(group_canonical_path(group)) - expect(find('.dropdown-menu a.is-active', text: group_paths_label)).to have_content(group_paths_label) + within '[data-testid=group_sort_by_dropdown]' do + expect(find('.gl-dropdown-toggle')).to have_content(group_paths_label) + end end it "is set on the details_group_path" do visit(details_group_path(group)) - expect(find('.dropdown-menu a.is-active', text: group_paths_label)).to have_content(group_paths_label) + within '[data-testid=group_sort_by_dropdown]' do + expect(find('.gl-dropdown-toggle')).to have_content(group_paths_label) + end end end @@ -58,23 +62,27 @@ RSpec.describe 'User sorts projects and order persists' do it_behaves_like "sort order persists across all views", "Name", "Name" end - context 'from group homepage' do + context 'from group homepage', :js do before do sign_in(user) visit(group_canonical_path(group)) - find('button.dropdown-menu-toggle').click - first(:link, 'Last created').click + within '[data-testid=group_sort_by_dropdown]' do + find('button.gl-dropdown-toggle').click + first(:button, 'Last created').click + end end it_behaves_like "sort order persists across all views", "Created date", "Last created" end - context 'from group details' do + context 'from group details', :js do before do sign_in(user) visit(details_group_path(group)) - find('button.dropdown-menu-toggle').click - first(:link, 'Most stars').click + within '[data-testid=group_sort_by_dropdown]' do + find('button.gl-dropdown-toggle').click + first(:button, 'Most stars').click + end end it_behaves_like "sort order persists across all views", "Stars", "Most stars" diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js index 68203b544ef..95a811d0385 100644 --- a/spec/frontend/__helpers__/vuex_action_helper.js +++ b/spec/frontend/__helpers__/vuex_action_helper.js @@ -49,6 +49,7 @@ const noop = () => {}; * expectedActions: [], * }) */ + export default ( actionArg, payloadArg, diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index bc3e12d3fc4..91de109441a 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -2,6 +2,9 @@ import MockAdapter from 'axios-mock-adapter'; import Api, { DEFAULT_PER_PAGE } from '~/api'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; +import createFlash from '~/flash'; + +jest.mock('~/flash'); describe('Api', () => { const dummyApiVersion = 'v3000'; @@ -675,6 +678,33 @@ describe('Api', () => { done(); }); }); + + it('uses flesh on error by default', async () => { + const groupId = '123456'; + const query = 'dummy query'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; + const flashCallback = (callCount) => { + expect(createFlash).toHaveBeenCalledTimes(callCount); + createFlash.mockClear(); + }; + + mock.onGet(expectedUrl).reply(500, null); + + const response = await Api.groupProjects(groupId, query, {}, () => {}).then(() => { + flashCallback(1); + }); + expect(response).toBeUndefined(); + }); + + it('NOT uses flesh on error with param useCustomErrorHandler', async () => { + const groupId = '123456'; + const query = 'dummy query'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; + + mock.onGet(expectedUrl).reply(500, null); + const apiCall = Api.groupProjects(groupId, query, {}, () => {}, true); + await expect(apiCall).rejects.toThrow(); + }); }); describe('groupShareWithGroup', () => { diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js index 2314ec678d3..1ef7047d959 100644 --- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js +++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js @@ -1,4 +1,4 @@ -import { getTimeago, localTimeAgo, timeFor } from '~/lib/utils/datetime/timeago_utility'; +import { getTimeago, localTimeAgo, timeFor, duration } from '~/lib/utils/datetime/timeago_utility'; import { s__ } from '~/locale'; import '~/commons/bootstrap'; @@ -66,6 +66,54 @@ describe('TimeAgo utils', () => { }); }); + describe('duration', () => { + const ONE_DAY = 24 * 60 * 60; + + it.each` + secs | formatted + ${0} | ${'0 seconds'} + ${30} | ${'30 seconds'} + ${59} | ${'59 seconds'} + ${60} | ${'1 minute'} + ${-60} | ${'1 minute'} + ${2 * 60} | ${'2 minutes'} + ${60 * 60} | ${'1 hour'} + ${2 * 60 * 60} | ${'2 hours'} + ${ONE_DAY} | ${'1 day'} + ${2 * ONE_DAY} | ${'2 days'} + ${7 * ONE_DAY} | ${'1 week'} + ${14 * ONE_DAY} | ${'2 weeks'} + ${31 * ONE_DAY} | ${'1 month'} + ${61 * ONE_DAY} | ${'2 months'} + ${365 * ONE_DAY} | ${'1 year'} + ${365 * 2 * ONE_DAY} | ${'2 years'} + `('formats $secs as "$formatted"', ({ secs, formatted }) => { + const ms = secs * 1000; + + expect(duration(ms)).toBe(formatted); + }); + + // `duration` can be used to format Rails month durations. + // Ensure formatting for quantities such as `2.months.to_i` + // based on ActiveSupport::Duration::SECONDS_PER_MONTH. + // See: https://api.rubyonrails.org/classes/ActiveSupport/Duration.html + const SECONDS_PER_MONTH = 2629746; // 1.month.to_i + + it.each` + duration | secs | formatted + ${'1.month'} | ${SECONDS_PER_MONTH} | ${'1 month'} + ${'2.months'} | ${SECONDS_PER_MONTH * 2} | ${'2 months'} + ${'3.months'} | ${SECONDS_PER_MONTH * 3} | ${'3 months'} + `( + 'formats ActiveSupport::Duration of `$duration` ($secs) as "$formatted"', + ({ secs, formatted }) => { + const ms = secs * 1000; + + expect(duration(ms)).toBe(formatted); + }, + ); + }); + describe('localTimeAgo', () => { beforeEach(() => { document.body.innerHTML = diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js new file mode 100644 index 00000000000..636f3eeb04a --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js @@ -0,0 +1,88 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlSprintf } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue'; +import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; +import { + HARBOR_REGISTRY_TITLE, + LIST_INTRO_TEXT, +} from '~/packages_and_registries/harbor_registry/constants/index'; + +describe('harbor_list_header', () => { + let wrapper; + + const findTitleArea = () => wrapper.find(TitleArea); + const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]'); + const findImagesMetaDataItem = () => wrapper.find(MetadataItem); + + const mountComponent = async (propsData, slots) => { + wrapper = shallowMount(HarborListHeader, { + stubs: { + GlSprintf, + TitleArea, + }, + propsData, + slots, + }); + await nextTick(); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('header', () => { + it('has a title', () => { + mountComponent({ metadataLoading: true }); + + expect(findTitleArea().props()).toMatchObject({ + title: HARBOR_REGISTRY_TITLE, + metadataLoading: true, + }); + }); + + it('has a commands slot', () => { + mountComponent(null, { commands: '
baz
' }); + + expect(findCommandsSlot().text()).toBe('baz'); + }); + + describe('sub header parts', () => { + describe('images count', () => { + it('exists', async () => { + await mountComponent({ imagesCount: 1 }); + + expect(findImagesMetaDataItem().exists()).toBe(true); + }); + + it('when there is one image', async () => { + await mountComponent({ imagesCount: 1 }); + + expect(findImagesMetaDataItem().props()).toMatchObject({ + text: '1 Image repository', + icon: 'container-image', + }); + }); + + it('when there is more than one image', async () => { + await mountComponent({ imagesCount: 3 }); + + expect(findImagesMetaDataItem().props('text')).toBe('3 Image repositories'); + }); + }); + }); + }); + + describe('info messages', () => { + describe('default message', () => { + it('is correctly bound to title_area props', () => { + mountComponent({ helpPagePath: 'foo' }); + + expect(findTitleArea().props('infoMessages')).toEqual([ + { text: LIST_INTRO_TEXT, link: 'foo' }, + ]); + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js new file mode 100644 index 00000000000..8560c4f78f7 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js @@ -0,0 +1,99 @@ +import { shallowMount, RouterLinkStub as RouterLink } from '@vue/test-utils'; +import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; + +import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { harborListResponse } from '../../mock_data'; + +describe('Harbor List Row', () => { + let wrapper; + const [item] = harborListResponse.repositories; + + const findDetailsLink = () => wrapper.find(RouterLink); + const findClipboardButton = () => wrapper.findComponent(ClipboardButton); + const findTagsCount = () => wrapper.find('[data-testid="tags-count"]'); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + + const mountComponent = (props) => { + wrapper = shallowMount(HarborListRow, { + stubs: { + RouterLink, + GlSprintf, + ListItem, + }, + propsData: { + item, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('image title and path', () => { + it('contains a link to the details page', () => { + mountComponent(); + + const link = findDetailsLink(); + expect(link.text()).toBe(item.name); + expect(findDetailsLink().props('to')).toMatchObject({ + name: 'details', + params: { + id: item.id, + }, + }); + }); + + it('contains a clipboard button', () => { + mountComponent(); + const button = findClipboardButton(); + expect(button.exists()).toBe(true); + expect(button.props('text')).toBe(item.location); + expect(button.props('title')).toBe(item.location); + }); + }); + + describe('tags count', () => { + it('exists', () => { + mountComponent(); + expect(findTagsCount().exists()).toBe(true); + }); + + it('contains a tag icon', () => { + mountComponent(); + const icon = findTagsCount().find(GlIcon); + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('tag'); + }); + + describe('loading state', () => { + it('shows a loader when metadataLoading is true', () => { + mountComponent({ metadataLoading: true }); + + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('hides the tags count while loading', () => { + mountComponent({ metadataLoading: true }); + + expect(findTagsCount().exists()).toBe(false); + }); + }); + + describe('tags count text', () => { + it('with one tag in the image', () => { + mountComponent({ item: { ...item, artifactCount: 1 } }); + + expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag'); + }); + it('with more than one tag in the image', () => { + mountComponent({ item: { ...item, artifactCount: 3 } }); + + expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags'); + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js new file mode 100644 index 00000000000..f018eff58c9 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js @@ -0,0 +1,39 @@ +import { shallowMount } from '@vue/test-utils'; +import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue'; +import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue'; +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import { harborListResponse } from '../../mock_data'; + +describe('Harbor List', () => { + let wrapper; + + const findHarborListRow = () => wrapper.findAll(HarborListRow); + + const mountComponent = (props) => { + wrapper = shallowMount(HarborList, { + stubs: { RegistryList }, + propsData: { + images: harborListResponse.repositories, + pageInfo: harborListResponse.pageInfo, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('list', () => { + it('contains one list element for each image', () => { + mountComponent(); + + expect(findHarborListRow().length).toBe(harborListResponse.repositories.length); + }); + + it('passes down the metadataLoading prop', () => { + mountComponent({ metadataLoading: true }); + expect(findHarborListRow().at(0).props('metadataLoading')).toBe(true); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/mock_data.js b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js new file mode 100644 index 00000000000..85399c22e79 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js @@ -0,0 +1,175 @@ +export const harborListResponse = { + repositories: [ + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 25, + name: 'shao/flinkx', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 26, + name: 'shao/flinkx1', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 27, + name: 'shao/flinkx2', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + ], + totalCount: 3, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, +}; + +export const harborTagsResponse = { + tags: [ + { + digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255', + shortRevision: 'f53bde3d4', + createdAt: '2022-03-02T23:59:05+00:00', + totalSize: '6623124', + }, + { + digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e', + shortRevision: 'e1fe52d8b', + createdAt: '2022-02-10T01:09:56+00:00', + totalSize: '920760', + }, + { + digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f', + shortRevision: 'c72770c6e', + createdAt: '2021-12-22T04:48:48+00:00', + totalSize: '48609053', + }, + { + digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a', + shortRevision: '1ac2a4319', + createdAt: '2022-03-09T11:02:27+00:00', + totalSize: '35141894', + }, + { + digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c', + shortRevision: 'cf8fee086', + createdAt: '2022-01-21T11:31:43+00:00', + totalSize: '48716070', + }, + { + digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15', + shortRevision: '1a4b48198', + createdAt: '2022-01-21T11:31:51+00:00', + totalSize: '6623127', + }, + { + digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61', + shortRevision: '03e2e2777', + createdAt: '2022-03-02T23:58:20+00:00', + totalSize: '911377', + }, + { + digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012', + shortRevision: '350e78d60', + createdAt: '2022-01-19T13:49:14+00:00', + totalSize: '48710241', + }, + { + digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18', + shortRevision: '76038370b', + createdAt: '2022-01-24T12:56:22+00:00', + totalSize: '280065', + }, + { + digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f', + shortRevision: '3d4b49a7b', + createdAt: '2022-02-17T17:37:52+00:00', + totalSize: '48655767', + }, + ], + totalCount: 100, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, +}; + +export const dockerCommands = { + dockerBuildCommand: 'foofoo', + dockerPushCommand: 'barbar', + dockerLoginCommand: 'bazbaz', +}; diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js new file mode 100644 index 00000000000..55fc8066f65 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js @@ -0,0 +1,24 @@ +import { shallowMount } from '@vue/test-utils'; +import component from '~/packages_and_registries/harbor_registry/pages/index.vue'; + +describe('List Page', () => { + let wrapper; + + const findRouterView = () => wrapper.find({ ref: 'router-view' }); + + const mountComponent = () => { + wrapper = shallowMount(component, { + stubs: { + RouterView: true, + }, + }); + }; + + beforeEach(() => { + mountComponent(); + }); + + it('has a router view', () => { + expect(findRouterView().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js new file mode 100644 index 00000000000..61ee36a2794 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js @@ -0,0 +1,140 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue'; +import HarborRegistryList from '~/packages_and_registries/harbor_registry/pages/list.vue'; +import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +// import { harborListResponse } from '~/packages_and_registries/harbor_registry/mock_api.js'; +import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue'; +import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue'; +import { SORT_FIELDS } from '~/packages_and_registries/harbor_registry/constants/index'; +import { harborListResponse, dockerCommands } from '../mock_data'; + +let mockHarborListResponse; +jest.mock('~/packages_and_registries/harbor_registry/mock_api.js', () => ({ + harborListResponse: () => mockHarborListResponse, +})); + +describe('Harbor List Page', () => { + let wrapper; + + const waitForHarborPageRequest = async () => { + await waitForPromises(); + await nextTick(); + }; + + beforeEach(() => { + mockHarborListResponse = Promise.resolve(harborListResponse); + }); + + const findHarborListHeader = () => wrapper.findComponent(HarborListHeader); + const findPersistedSearch = () => wrapper.findComponent(PersistedSearch); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findHarborList = () => wrapper.findComponent(HarborList); + const findCliCommands = () => wrapper.findComponent(CliCommands); + + const fireFirstSortUpdate = () => { + findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] }); + }; + + const mountComponent = ({ config = { isGroupPage: false } } = {}) => { + wrapper = shallowMount(HarborRegistryList, { + stubs: { + HarborListHeader, + }, + provide() { + return { + config, + ...dockerCommands, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('contains harbor registry header', async () => { + mountComponent(); + fireFirstSortUpdate(); + await waitForHarborPageRequest(); + await nextTick(); + + expect(findHarborListHeader().exists()).toBe(true); + expect(findHarborListHeader().props()).toMatchObject({ + imagesCount: 3, + metadataLoading: false, + }); + }); + + describe('isLoading is true', () => { + it('shows the skeleton loader', async () => { + mountComponent(); + fireFirstSortUpdate(); + + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('harborList is not visible', () => { + mountComponent(); + + expect(findHarborList().exists()).toBe(false); + }); + + it('cli commands is not visible', () => { + mountComponent(); + + expect(findCliCommands().exists()).toBe(false); + }); + + it('title has the metadataLoading props set to true', async () => { + mountComponent(); + fireFirstSortUpdate(); + + expect(findHarborListHeader().props('metadataLoading')).toBe(true); + }); + }); + + describe('list is not empty', () => { + describe('unfiltered state', () => { + it('quick start is visible', async () => { + mountComponent(); + fireFirstSortUpdate(); + + await waitForHarborPageRequest(); + await nextTick(); + + expect(findCliCommands().exists()).toBe(true); + }); + + it('list component is visible', async () => { + mountComponent(); + fireFirstSortUpdate(); + + await waitForHarborPageRequest(); + await nextTick(); + + expect(findHarborList().exists()).toBe(true); + }); + }); + + describe('search and sorting', () => { + it('has a persisted search box element', async () => { + mountComponent(); + fireFirstSortUpdate(); + await waitForHarborPageRequest(); + await nextTick(); + + const harborRegistrySearch = findPersistedSearch(); + expect(harborRegistrySearch.exists()).toBe(true); + expect(harborRegistrySearch.props()).toMatchObject({ + defaultOrder: 'UPDATED', + defaultSort: 'desc', + sortableFields: SORT_FIELDS, + }); + }); + }); + }); +}); diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js index 5f8cee8160f..67bd3194f20 100644 --- a/spec/frontend/search/store/actions_spec.js +++ b/spec/frontend/search/store/actions_spec.js @@ -56,7 +56,7 @@ describe('Global Search Store Actions', () => { ${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0} ${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1} ${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0} - ${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${2} + ${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1} `(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => { describe(action.name, () => { describe(`on ${type}`, () => { @@ -121,8 +121,8 @@ describe('Global Search Store Actions', () => { describe('when groupId is set', () => { it('calls Api.groupProjects with expected parameters', () => { - actions.fetchProjects({ commit: mockCommit, state }); - + const callbackTest = jest.fn(); + actions.fetchProjects({ commit: mockCommit, state }, undefined, callbackTest); expect(Api.groupProjects).toHaveBeenCalledWith( state.query.group_id, state.query.search, @@ -131,7 +131,8 @@ describe('Global Search Store Actions', () => { include_subgroups: true, with_shared: false, }, - expect.any(Function), + callbackTest, + true, ); expect(Api.projects).not.toHaveBeenCalled(); }); @@ -144,15 +145,10 @@ describe('Global Search Store Actions', () => { it('calls Api.projects', () => { actions.fetchProjects({ commit: mockCommit, state }); - expect(Api.groupProjects).not.toHaveBeenCalled(); - expect(Api.projects).toHaveBeenCalledWith( - state.query.search, - { - order_by: 'similarity', - }, - expect.any(Function), - ); + expect(Api.projects).toHaveBeenCalledWith(state.query.search, { + order_by: 'similarity', + }); }); }); }); diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb new file mode 100644 index 00000000000..9e4a8739c0f --- /dev/null +++ b/spec/lib/gitlab/ci/runner_releases_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::RunnerReleases do + subject { described_class.instance } + + describe '#releases' do + before do + subject.reset! + + stub_application_setting(public_runner_releases_url: 'the release API URL') + allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response(response) } + end + + def releases + subject.releases + end + + shared_examples 'requests that follow cache status' do |validity_period| + context "almost #{validity_period.inspect} later" do + let(:followup_request_interval) { validity_period - 0.001.seconds } + + it 'returns cached releases' do + releases + + travel followup_request_interval do + expect(Gitlab::HTTP).not_to receive(:try_get) + + expect(releases).to eq(expected_result) + end + end + end + + context "after #{validity_period.inspect}" do + let(:followup_request_interval) { validity_period + 1.second } + let(:followup_response) { (response || []) + [{ 'name' => 'v14.9.2' }] } + + it 'checks new releases' do + releases + + travel followup_request_interval do + expect(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response(followup_response) } + + expect(releases).to eq((expected_result || []) + [Gitlab::VersionInfo.new(14, 9, 2)]) + end + end + end + end + + context 'when response is nil' do + let(:response) { nil } + let(:expected_result) { nil } + + it 'returns nil' do + expect(releases).to be_nil + end + + it_behaves_like 'requests that follow cache status', 5.seconds + + it 'performs exponential backoff on requests', :aggregate_failures do + start_time = Time.now.utc.change(usec: 0) + + http_call_timestamp_offsets = [] + allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL') do + http_call_timestamp_offsets << Time.now.utc - start_time + mock_http_response(response) + end + + # An initial HTTP request fails + travel_to(start_time) + subject.reset! + expect(releases).to be_nil + + # Successive failed requests result in HTTP requests only after specific backoff periods + backoff_periods = [5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 3600].map(&:seconds) + backoff_periods.each do |period| + travel(period - 1.second) + expect(releases).to be_nil + + travel 1.second + expect(releases).to be_nil + end + + expect(http_call_timestamp_offsets).to eq([0, 5, 15, 35, 75, 155, 315, 635, 1275, 2555, 5115, 8715]) + + # Finally a successful HTTP request results in releases being returned + allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response([{ 'name' => 'v14.9.1' }]) } + travel 1.hour + expect(releases).not_to be_nil + end + end + + context 'when response is not nil' do + let(:response) { [{ 'name' => 'v14.9.1' }, { 'name' => 'v14.9.0' }] } + let(:expected_result) { [Gitlab::VersionInfo.new(14, 9, 0), Gitlab::VersionInfo.new(14, 9, 1)] } + + it 'returns parsed and sorted Gitlab::VersionInfo objects' do + expect(releases).to eq(expected_result) + end + + it_behaves_like 'requests that follow cache status', 1.day + end + + def mock_http_response(response) + http_response = instance_double(HTTParty::Response) + + allow(http_response).to receive(:success?).and_return(response.present?) + allow(http_response).to receive(:parsed_response).and_return(response) + + http_response + end + end +end diff --git a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb index 7fc963949eb..22cada7816b 100644 --- a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb +++ b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb @@ -58,7 +58,19 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService do stub_put_manifest_request('Ba', 500, {}) end - it { is_expected.to eq(status: :error, message: 'could not delete tags') } + it { is_expected.to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}")} + + context 'when a large list of tag updates fails' do + let(:tags) { Array.new(1000) { |i| "tag_#{i}" } } + + before do + expect(service).to receive(:replace_tag_manifests).and_return({}) + end + + it 'truncates the log message' do + expect(subject).to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}".truncate(1000)) + end + end end context 'a single tag update fails' do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index b00ee61004e..f7daa1d62ac 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -726,6 +726,17 @@ RSpec.describe QuickActions::InterpretService do expect(reviewer).to be_attention_requested end + + it 'supports attn alias' do + attn_cmd = content.gsub(/attention/, 'attn') + _, _, message = service.execute(attn_cmd, issuable) + + expect(message).to eq("Requested attention from #{developer.to_reference}.") + + reviewer.reload + + expect(reviewer).to be_attention_requested + end end shared_examples 'remove attention command' do diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb index 8f706fdebc9..f8ddf3e66a5 100644 --- a/spec/support/database_cleaner.rb +++ b/spec/support/database_cleaner.rb @@ -20,6 +20,9 @@ RSpec.configure do |config| # We drop and recreate the database if any table has more than 1200 columns, just to be safe. if any_connection_class_with_more_than_allowed_columns? recreate_all_databases! + + # Seed required data as recreating DBs will delete it + TestEnv.seed_db end end diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb index dcaec176687..3ba88c3ae71 100644 --- a/spec/support/matchers/graphql_matchers.rb +++ b/spec/support/matchers/graphql_matchers.rb @@ -7,14 +7,14 @@ RSpec::Matchers.define :require_graphql_authorizations do |*expected| if klass.respond_to?(:required_permissions) klass.required_permissions else - [klass.to_graphql.metadata[:authorize]] + Array.wrap(klass.authorize) end end match do |klass| actual = permissions_for(klass) - expect(actual).to match_array(expected) + expect(actual).to match_array(expected.compact) end failure_message do |klass| @@ -213,16 +213,16 @@ RSpec::Matchers.define :have_graphql_resolver do |expected| match do |field| case expected when Method - expect(field.to_graphql.metadata[:type_class].resolve_proc).to eq(expected) + expect(field.type_class.resolve_proc).to eq(expected) else - expect(field.to_graphql.metadata[:type_class].resolver).to eq(expected) + expect(field.type_class.resolver).to eq(expected) end end end RSpec::Matchers.define :have_graphql_extension do |expected| match do |field| - expect(field.to_graphql.metadata[:type_class].extensions).to include(expected) + expect(field.type_class.extensions).to include(expected) end end diff --git a/spec/views/shared/groups/_dropdown.html.haml_spec.rb b/spec/views/shared/groups/_dropdown.html.haml_spec.rb new file mode 100644 index 00000000000..71fa3a30711 --- /dev/null +++ b/spec/views/shared/groups/_dropdown.html.haml_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'shared/groups/_dropdown.html.haml' do + describe 'render' do + describe 'when a sort option is not selected' do + it 'renders a default sort option' do + render 'shared/groups/dropdown' + + expect(rendered).to have_content 'Last created' + end + end + + describe 'when a sort option is selected' do + before do + assign(:sort, 'name_desc') + + render 'shared/groups/dropdown' + end + + it 'renders the selected sort option' do + expect(rendered).to have_content 'Name, descending' + end + end + end +end