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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/database/multiple_databases.yml11
-rw-r--r--app/assets/javascripts/google_cloud/components/deployments_service_table.vue61
-rw-r--r--app/assets/javascripts/google_cloud/components/home.vue17
-rw-r--r--app/assets/javascripts/group.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js37
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue20
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/index.js58
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.js27
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue (renamed from app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue)12
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/router.js20
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue11
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/utils.js35
-rw-r--r--app/assets/javascripts/pages/groups/packages/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/packages/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/show/index.js3
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue41
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js8
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue30
-rw-r--r--app/assets/javascripts/runner/components/stat/runner_online_stat.vue17
-rw-r--r--app/assets/javascripts/runner/components/stat/runner_stats.vue49
-rw-r--r--app/assets/javascripts/runner/components/stat/runner_status_stat.vue65
-rw-r--r--app/assets/javascripts/runner/graphql/get_group_runners.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql20
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue52
-rw-r--r--app/controllers/groups/packages_controller.rb5
-rw-r--r--app/controllers/projects/packages/packages_controller.rb3
-rw-r--r--app/graphql/mutations/issues/set_escalation_status.rb46
-rw-r--r--app/graphql/resolvers/base_issues_resolver.rb3
-rw-r--r--app/graphql/types/incident_management/escalation_status_enum.rb14
-rw-r--r--app/graphql/types/issue_type.rb9
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/helpers/ci/runners_helper.rb5
-rw-r--r--app/models/alert_management/alert.rb13
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/runner.rb11
-rw-r--r--app/models/concerns/incident_management/escalatable.rb11
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/user.rb6
-rw-r--r--app/services/ci/create_pipeline_service.rb16
-rw-r--r--app/services/ci/update_build_queue_service.rb14
-rw-r--r--app/services/incident_management/issuable_escalation_statuses/after_update_service.rb42
-rw-r--r--app/services/issues/update_service.rb7
-rw-r--r--app/views/admin/deploy_keys/index.html.haml43
-rw-r--r--app/views/groups/packages/index.html.haml5
-rw-r--r--app/views/projects/packages/packages/index.html.haml5
-rw-r--r--app/views/projects/packages/packages/show.html.haml9
-rw-r--r--app/views/shared/empty_states/_deploy_keys.html.haml9
-rw-r--r--config/feature_flags/development/admin_deploy_keys_vue.yml8
-rw-r--r--config/feature_flags/development/ci_decompose_belonging_to_parent_group_of_project_query.yml8
-rw-r--r--config/feature_flags/development/ci_publish_pipeline_events.yml8
-rw-r--r--config/feature_flags/development/use_primary_and_secondary_stores_for_sessions.yml8
-rw-r--r--config/feature_flags/development/use_primary_store_as_default_for_sessions.yml8
-rw-r--r--config/metrics/schema.json2
-rw-r--r--config/routes/group.rb2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/post_migrate/20211217174331_mark_recalculate_finding_signatures_as_completed.rb20
-rw-r--r--db/schema_migrations/202112171743311
-rw-r--r--doc/api/graphql/reference/index.md34
-rw-r--r--doc/user/admin_area/license.md2
-rw-r--r--lib/gitlab/redis/multi_store.rb232
-rw-r--r--lib/gitlab/redis/sessions.rb36
-rw-r--r--locale/gitlab.pot32
-rw-r--r--qa/qa/page/project/packages/show.rb2
-rw-r--r--spec/controllers/groups/packages_controller_spec.rb27
-rw-r--r--spec/controllers/projects/packages/packages_controller_spec.rb28
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb125
-rw-r--r--spec/features/admin/admin_runners_spec.rb18
-rw-r--r--spec/features/groups/packages_spec.rb3
-rw-r--r--spec/features/projects/packages_spec.rb3
-rw-r--r--spec/frontend/fixtures/runner.rb130
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js2
-rw-r--r--spec/frontend/google_cloud/components/deployments_service_table_spec.js40
-rw-r--r--spec/frontend/google_cloud/components/home_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js (renamed from spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js)30
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap22
-rw-r--r--spec/frontend/pipeline_editor/components/header/validation_segment_spec.js31
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js40
-rw-r--r--spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js34
-rw-r--r--spec/frontend/runner/components/stat/runner_online_stat_spec.js34
-rw-r--r--spec/frontend/runner/components/stat/runner_stats_spec.js46
-rw-r--r--spec/frontend/runner/components/stat/runner_status_stat_spec.js67
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js46
-rw-r--r--spec/frontend/runner/mock_data.js2
-rw-r--r--spec/frontend/security_configuration/mock_data.js10
-rw-r--r--spec/graphql/mutations/issues/set_escalation_status_spec.rb66
-rw-r--r--spec/graphql/types/incident_management/escalation_status_enum_spec.rb25
-rw-r--r--spec/graphql/types/issue_type_spec.rb47
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb3
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb716
-rw-r--r--spec/lib/gitlab/redis/sessions_spec.rb73
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb4
-rw-r--r--spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb64
-rw-r--r--spec/models/alert_management/alert_spec.rb33
-rw-r--r--spec/models/ci/runner_spec.rb42
-rw-r--r--spec/models/user_spec.rb21
-rw-r--r--spec/models/users_statistics_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb82
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb37
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb24
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb58
-rw-r--r--spec/services/issues/update_service_spec.rb9
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb33
113 files changed, 1706 insertions, 1796 deletions
diff --git a/.rubocop_todo/database/multiple_databases.yml b/.rubocop_todo/database/multiple_databases.yml
index e555c2f912b..f2768276060 100644
--- a/.rubocop_todo/database/multiple_databases.yml
+++ b/.rubocop_todo/database/multiple_databases.yml
@@ -1,14 +1,12 @@
---
Database/MultipleDatabases:
Exclude:
- - ee/lib/ee/gitlab/database.rb
- ee/lib/gitlab/geo/database_tasks.rb
- ee/lib/gitlab/geo/geo_tasks.rb
- ee/lib/gitlab/geo/health_check.rb
- ee/lib/gitlab/geo/log_cursor/daemon.rb
- ee/lib/pseudonymizer/dumper.rb
- ee/lib/pseudonymizer/pager.rb
- - ee/lib/system_check/geo/geo_database_configured_check.rb
- ee/spec/lib/pseudonymizer/dumper_spec.rb
- ee/spec/services/ee/merge_requests/update_service_spec.rb
- lib/backup/database.rb
@@ -21,14 +19,12 @@ Database/MultipleDatabases:
- lib/gitlab/database/migrations/observers/query_log.rb
- lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
- lib/gitlab/database.rb
- - lib/gitlab/database/schema_cache_with_renamed_table.rb
- lib/gitlab/database/with_lock_retries.rb
- lib/gitlab/gitlab_import/importer.rb
- lib/gitlab/health_checks/db_check.rb
- lib/gitlab/import_export/base/relation_factory.rb
- lib/gitlab/import_export/group/relation_tree_restorer.rb
- lib/gitlab/legacy_github_import/importer.rb
- - lib/gitlab/metrics/samplers/database_sampler.rb
- lib/gitlab/seeder.rb
- lib/gitlab/sherlock/query.rb
- lib/system_check/orphans/repository_check.rb
@@ -39,15 +35,8 @@ Database/MultipleDatabases:
- spec/lib/gitlab/database_spec.rb
- spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
- spec/lib/gitlab/profiler_spec.rb
- - spec/lib/gitlab/usage_data_metrics_spec.rb
- - spec/lib/gitlab/usage_data_queries_spec.rb
- spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/constraints_spec.rb
- spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb
- - spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
- - spec/lib/gitlab/utils/usage_data_spec.rb
- - spec/models/project_feature_usage_spec.rb
- - spec/models/users_statistics_spec.rb
- - spec/services/users/activity_service_spec.rb
- spec/support/caching.rb
- spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
- spec/support/helpers/database_connection_helpers.rb
diff --git a/app/assets/javascripts/google_cloud/components/deployments_service_table.vue b/app/assets/javascripts/google_cloud/components/deployments_service_table.vue
new file mode 100644
index 00000000000..7d27d7cf6b2
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/deployments_service_table.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlButton, GlTable } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+const i18n = {
+ cloudRun: __('Cloud Run'),
+ cloudRunDescription: __('Deploy container based web apps on Google managed clusters'),
+ cloudStorage: __('Cloud Storage'),
+ cloudStorageDescription: __('Deploy static assets and resources to Google managed CDN'),
+ deployments: __('Deployments'),
+ deploymentsDescription: __(
+ 'Configure pipelines to deploy web apps, backend services, APIs and static resources to Google Cloud',
+ ),
+ configureViaMergeRequest: __('Configure via Merge Request'),
+ service: __('Service'),
+ description: __('Description'),
+};
+
+export default {
+ components: { GlButton, GlTable },
+ props: {
+ cloudRunUrl: {
+ type: String,
+ required: true,
+ },
+ cloudStorageUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ fields: [
+ { key: 'title', label: i18n.service },
+ { key: 'description', label: i18n.description },
+ { key: 'action', label: '' },
+ ],
+ items: [
+ {
+ title: i18n.cloudRun,
+ description: i18n.cloudRunDescription,
+ action: { title: i18n.configureViaMergeRequest, disabled: true },
+ },
+ {
+ title: i18n.cloudStorage,
+ description: i18n.cloudStorageDescription,
+ action: { title: i18n.configureViaMergeRequest, disabled: true },
+ },
+ ],
+ i18n,
+};
+</script>
+<template>
+ <div class="gl-mx-3">
+ <h2 class="gl-font-size-h2">{{ $options.i18n.deployments }}</h2>
+ <p>{{ $options.i18n.deploymentsDescription }}</p>
+ <gl-table :fields="$options.fields" :items="$options.items">
+ <template #cell(action)="{ value }">
+ <gl-button :disabled="value.disabled">{{ value.title }}</gl-button>
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/home.vue b/app/assets/javascripts/google_cloud/components/home.vue
index 05f39de66ee..8ef110dcf22 100644
--- a/app/assets/javascripts/google_cloud/components/home.vue
+++ b/app/assets/javascripts/google_cloud/components/home.vue
@@ -1,11 +1,13 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
+import DeploymentsServiceTable from './deployments_service_table.vue';
import ServiceAccountsList from './service_accounts_list.vue';
export default {
components: {
GlTabs,
GlTab,
+ DeploymentsServiceTable,
ServiceAccountsList,
},
props: {
@@ -21,6 +23,14 @@ export default {
type: String,
required: true,
},
+ deploymentsCloudRunUrl: {
+ type: String,
+ required: true,
+ },
+ deploymentsCloudStorageUrl: {
+ type: String,
+ required: true,
+ },
},
};
</script>
@@ -35,7 +45,12 @@ export default {
:empty-illustration-url="emptyIllustrationUrl"
/>
</gl-tab>
- <gl-tab :title="__('Deployments')" disabled />
+ <gl-tab :title="__('Deployments')">
+ <deployments-service-table
+ :cloud-run-url="deploymentsCloudRunUrl"
+ :cloud-storage-url="deploymentsCloudStorageUrl"
+ />
+ </gl-tab>
<gl-tab :title="__('Services')" disabled />
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index f255f8a084c..b6a6720e7a1 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -13,11 +13,8 @@ export default class Group {
this.updateGroupPathSlugHandler = this.updateGroupPathSlug.bind(this);
this.groupNames.forEach((groupName) => {
- if (groupName.value === '') {
- groupName.addEventListener('keyup', this.updateHandler);
-
- groupName.addEventListener('keyup', this.updateGroupPathSlugHandler);
- }
+ groupName.addEventListener('keyup', this.updateHandler);
+ groupName.addEventListener('keyup', this.updateGroupPathSlugHandler);
});
this.groupPaths.forEach((groupPath) => {
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
index e6c197a30dd..ca5bd8d6964 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
@@ -4,6 +4,7 @@ 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 { apolloProvider } from './graphql/index';
import RegistryExplorer from './pages/index.vue';
import createRouter from './router';
@@ -84,38 +85,8 @@ export default () => {
},
});
- const attachBreadcrumb = () => {
- const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
- const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
- const crumbs = [breadCrumbEl.querySelector('h2')];
- const nestedBreadcrumbEl = document.createElement('div');
- breadCrumbEl.replaceChild(nestedBreadcrumbEl, breadCrumbEl.querySelector('h2'));
- return new Vue({
- el: nestedBreadcrumbEl,
- router,
- apolloProvider,
- components: {
- RegistryBreadcrumb,
- },
- render(createElement) {
- // FIXME(@tnir): this is a workaround until the MR gets merged:
- // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115
- const parentEl = breadCrumbEl.parentElement.parentElement;
- if (parentEl) {
- parentEl.classList.remove('breadcrumbs-container');
- parentEl.classList.add('gl-display-flex');
- parentEl.classList.add('w-100');
- }
- // End of FIXME(@tnir)
- return createElement('registry-breadcrumb', {
- class: breadCrumbEl.className,
- props: {
- crumbs,
- },
- });
- },
- });
+ return {
+ attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb),
+ attachMainComponent,
};
-
- return { attachBreadcrumb, attachMainComponent };
};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
index 95b09b25678..7479f748a56 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
@@ -26,7 +26,7 @@ export default {
GlSprintf,
GlFormRadioGroup,
},
- inject: ['npmPath'],
+ inject: ['npmInstanceUrl'],
props: {
packageEntity: {
type: Object,
@@ -66,7 +66,9 @@ export default {
npmSetupCommand(type, endpointType) {
const scope = this.packageEntity.name.substring(0, this.packageEntity.name.indexOf('/'));
const npmPathForEndpoint =
- endpointType === INSTANCE_PACKAGE_ENDPOINT_TYPE ? this.npmPath : this.packageEntity.npmUrl;
+ endpointType === INSTANCE_PACKAGE_ENDPOINT_TYPE
+ ? this.npmInstanceUrl
+ : this.packageEntity.npmUrl;
if (type === NPM_PACKAGE_MANAGER) {
return `echo ${scope}:registry=${npmPathForEndpoint}/ >> .npmrc`;
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index 6fd96c0654f..6222c2e73d7 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import {
@@ -18,7 +18,6 @@ export default {
name: 'PackageListRow',
components: {
GlButton,
- GlLink,
GlSprintf,
GlTruncate,
PackageTags,
@@ -42,9 +41,8 @@ export default {
packageType() {
return getPackageTypeLabel(this.packageEntity.packageType);
},
- packageLink() {
- const { project, id } = this.packageEntity;
- return `${project?.webUrl}/-/packages/${getIdFromGraphQLId(id)}`;
+ packageId() {
+ return getIdFromGraphQLId(this.packageEntity.id);
},
pipeline() {
return this.packageEntity?.pipelines?.nodes[0];
@@ -61,6 +59,9 @@ export default {
disabledRow() {
return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS;
},
+ routerLinkEvent() {
+ return this.disabledRow ? '' : 'click';
+ },
},
i18n: {
erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'),
@@ -73,14 +74,15 @@ export default {
<list-item data-qa-selector="package_row" :disabled="disabledRow">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
- <gl-link
- :href="packageLink"
+ <router-link
class="gl-text-body gl-min-w-0"
+ data-testid="details-link"
data-qa-selector="package_link"
- :disabled="disabledRow"
+ :event="routerLinkEvent"
+ :to="{ name: 'details', params: { id: packageId } }"
>
<gl-truncate :text="packageEntity.name" />
- </gl-link>
+ </router-link>
<gl-button
v-if="showWarningIcon"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index af4e586231c..c4d331fa384 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -74,6 +74,7 @@ export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
);
export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully');
+export const PACKAGE_REGISTRY_TITLE = __('Package Registry');
export const PACKAGE_ERROR_STATUS = 'ERROR';
export const PACKAGE_DEFAULT_STATUS = 'DEFAULT';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/index.js b/app/assets/javascripts/packages_and_registries/package_registry/index.js
index 7ec931ff9a0..6680e612985 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/index.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/index.js
@@ -2,29 +2,59 @@ import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import PackageRegistry from '~/packages_and_registries/package_registry/pages/index.vue';
+import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
+import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
import createRouter from './router';
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-vue-packages-list');
- const { endpoint, resourceId, fullPath, pageType, emptyListIllustration } = el.dataset;
- const router = createRouter(endpoint);
+ const {
+ endpoint,
+ resourceId,
+ fullPath,
+ pageType,
+ emptyListIllustration,
+ npmInstanceUrl,
+ projectListUrl,
+ groupListUrl,
+ } = el.dataset;
const isGroupPage = pageType === 'groups';
- return new Vue({
- el,
- router,
- apolloProvider,
- provide: {
- resourceId,
- fullPath,
- emptyListIllustration,
- isGroupPage,
- },
- render(createElement) {
- return createElement(PackageRegistry);
+ // This is a mini state to help the breadcrumb have the correct name in the details page
+ const breadCrumbState = Vue.observable({
+ name: '',
+ updateName(value) {
+ this.name = value;
},
});
+
+ const router = createRouter(endpoint, breadCrumbState);
+
+ const attachMainComponent = () =>
+ new Vue({
+ el,
+ router,
+ apolloProvider,
+ provide: {
+ resourceId,
+ fullPath,
+ emptyListIllustration,
+ isGroupPage,
+ npmInstanceUrl,
+ projectListUrl,
+ groupListUrl,
+ breadCrumbState,
+ },
+ render(createElement) {
+ return createElement(PackageRegistry);
+ },
+ });
+
+ return {
+ attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb),
+ attachMainComponent,
+ };
};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js
deleted file mode 100644
index d94bbd21035..00000000000
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
-import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
-import Translate from '~/vue_shared/translate';
-
-Vue.use(Translate);
-
-export default () => {
- const el = document.getElementById('js-vue-packages-detail-new');
- if (!el) {
- return null;
- }
-
- const { canDelete, ...datasetOptions } = el.dataset;
- return new Vue({
- el,
- apolloProvider,
- provide: {
- canDelete: parseBoolean(canDelete),
- ...datasetOptions,
- },
- render(createElement) {
- return createElement(PackagesApp);
- },
- });
-};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index 52fea4bebbe..162b420a784 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -68,7 +68,7 @@ export default {
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin()],
- inject: ['packageId', 'svgPath', 'projectListUrl', 'groupListUrl'],
+ inject: ['emptyListIllustration', 'projectListUrl', 'groupListUrl', 'breadCrumbState'],
trackingActions: {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
@@ -100,12 +100,20 @@ export default {
error,
});
},
+ result() {
+ this.breadCrumbState.updateName(
+ `${this.packageEntity?.name} v ${this.packageEntity?.version}`,
+ );
+ },
},
},
computed: {
projectName() {
return this.packageEntity.project?.name;
},
+ packageId() {
+ return this.$route.params.id;
+ },
queryVariables() {
return {
id: convertToGraphQLId('Packages::Package', this.packageId),
@@ -229,7 +237,7 @@ export default {
v-if="!isValidPackage"
:title="s__('PackageRegistry|Unable to load package')"
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
- :svg-path="svgPath"
+ :svg-path="emptyListIllustration"
/>
<div v-else-if="!isLoading" class="packages-app">
<package-title :package-entity="packageEntity">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/router.js b/app/assets/javascripts/packages_and_registries/package_registry/router.js
index ea5b740e879..c5ef4f70dd9 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/router.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/router.js
@@ -1,10 +1,12 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import List from '~/packages_and_registries/package_registry/pages/list.vue';
+import Details from '~/packages_and_registries/package_registry/pages/details.vue';
+import { PACKAGE_REGISTRY_TITLE } from '~/packages_and_registries/package_registry/constants';
Vue.use(VueRouter);
-export default function createRouter(base) {
+export default function createRouter(base, breadCrumbState) {
const router = new VueRouter({
base,
mode: 'history',
@@ -13,9 +15,25 @@ export default function createRouter(base) {
name: 'list',
path: '/',
component: List,
+ meta: {
+ nameGenerator: () => PACKAGE_REGISTRY_TITLE,
+ root: true,
+ },
+ },
+ {
+ name: 'details',
+ path: '/:id',
+ component: Details,
+ meta: {
+ nameGenerator: () => breadCrumbState.name,
+ },
},
],
});
+ router.afterEach(() => {
+ breadCrumbState.updateName('');
+ });
+
return router;
}
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
index e77eda31596..a1e3c06812c 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
@@ -20,8 +20,11 @@ export default {
isRootRoute() {
return this.$route.name === this.rootRoute.name;
},
+ detailsRouteName() {
+ return this.detailsRoute.meta.nameGenerator();
+ },
isLoaded() {
- return this.isRootRoute || this.$store?.state.imageDetails?.name;
+ return this.isRootRoute || this.detailsRouteName;
},
allCrumbs() {
const crumbs = [
@@ -32,7 +35,7 @@ export default {
];
if (!this.isRootRoute) {
crumbs.push({
- text: this.detailsRoute.meta.nameGenerator(),
+ text: this.detailsRouteName,
href: this.detailsRoute.meta.path,
});
}
@@ -45,7 +48,9 @@ export default {
<template>
<gl-breadcrumb :key="isLoaded" :items="allCrumbs">
<template #separator>
- <gl-icon name="angle-right" :size="8" />
+ <span class="gl-mx-n5">
+ <gl-icon name="angle-right" :size="8" />
+ </span>
</template>
</gl-breadcrumb>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js
index cf18f655e79..7e963cd0b08 100644
--- a/app/assets/javascripts/packages_and_registries/shared/utils.js
+++ b/app/assets/javascripts/packages_and_registries/shared/utils.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
import { queryToObject } from '~/lib/utils/url_utility';
import { FILTERED_SEARCH_TERM } from './constants';
@@ -38,3 +39,37 @@ export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGr
return `../commit/${pipeline.sha}`;
};
+
+export const renderBreadcrumb = (router, apolloProvider, RegistryBreadcrumb) => () => {
+ const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
+ const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
+ const lastCrumb = breadCrumbEl.children[0];
+ const crumbs = [lastCrumb];
+ const nestedBreadcrumbEl = document.createElement('div');
+ breadCrumbEl.replaceChild(nestedBreadcrumbEl, lastCrumb);
+ return new Vue({
+ el: nestedBreadcrumbEl,
+ router,
+ apolloProvider,
+ components: {
+ RegistryBreadcrumb,
+ },
+ render(createElement) {
+ // FIXME(@tnir): this is a workaround until the MR gets merged:
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115
+ const parentEl = breadCrumbEl.parentElement.parentElement;
+ if (parentEl) {
+ parentEl.classList.remove('breadcrumbs-container');
+ parentEl.classList.add('gl-display-flex');
+ parentEl.classList.add('w-100');
+ }
+ // End of FIXME(@tnir)
+ return createElement('registry-breadcrumb', {
+ class: breadCrumbEl.className,
+ props: {
+ crumbs,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/groups/packages/index.js b/app/assets/javascripts/pages/groups/packages/index.js
new file mode 100644
index 00000000000..cbe08565cfa
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/packages/index.js
@@ -0,0 +1,8 @@
+import packageApp from '~/packages_and_registries/package_registry/index';
+
+const app = packageApp();
+
+if (app) {
+ app.attachBreadcrumb();
+ app.attachMainComponent();
+}
diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js
deleted file mode 100644
index 174973a9fad..00000000000
--- a/app/assets/javascripts/pages/groups/packages/index/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import packageApp from '~/packages_and_registries/package_registry/index';
-
-packageApp();
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index.js b/app/assets/javascripts/pages/projects/packages/packages/index.js
new file mode 100644
index 00000000000..cbe08565cfa
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/packages/packages/index.js
@@ -0,0 +1,8 @@
+import packageApp from '~/packages_and_registries/package_registry/index';
+
+const app = packageApp();
+
+if (app) {
+ app.attachBreadcrumb();
+ app.attachMainComponent();
+}
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index/index.js b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
deleted file mode 100644
index 174973a9fad..00000000000
--- a/app/assets/javascripts/pages/projects/packages/packages/index/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import packageApp from '~/packages_and_registries/package_registry/index';
-
-packageApp();
diff --git a/app/assets/javascripts/pages/projects/packages/packages/show/index.js b/app/assets/javascripts/pages/projects/packages/packages/show/index.js
deleted file mode 100644
index 2dee87985cb..00000000000
--- a/app/assets/javascripts/pages/projects/packages/packages/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initPackageDetails from '~/packages_and_registries/package_registry/pages/details';
-
-initPackageDetails();
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index 3366942f6ad..bb2bac531a7 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
-import RunnerOnlineStat from '../components/stat/runner_online_stat.vue';
+import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
@@ -20,6 +20,9 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
I18N_FETCH_ERROR,
} from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
@@ -51,7 +54,7 @@ export default {
RunnerFilteredSearchBar,
RunnerList,
RunnerName,
- RunnerOnlineStat,
+ RunnerStats,
RunnerPagination,
RunnerTypeTabs,
},
@@ -60,10 +63,6 @@ export default {
type: String,
required: true,
},
- activeRunnersCount: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -130,6 +129,30 @@ export default {
};
},
},
+ onlineRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ status: STATUS_ONLINE,
+ };
+ },
+ },
+ offlineRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ status: STATUS_OFFLINE,
+ };
+ },
+ },
+ staleRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ status: STATUS_STALE,
+ };
+ },
+ },
},
computed: {
variables() {
@@ -205,7 +228,11 @@ export default {
</script>
<template>
<div>
- <runner-online-stat class="gl-py-6 gl-px-5" :value="activeRunnersCount" />
+ <runner-stats
+ :online-runners-count="onlineRunnersTotal"
+ :offline-runners-count="offlineRunnersTotal"
+ :stale-runners-count="staleRunnersTotal"
+ />
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js
index 5ca87aa7a68..3b8a8fe9cd1 100644
--- a/app/assets/javascripts/runner/admin_runners/index.js
+++ b/app/assets/javascripts/runner/admin_runners/index.js
@@ -25,9 +25,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
return null;
}
- // TODO `activeRunnersCount` should be implemented using a GraphQL API
- // https://gitlab.com/gitlab-org/gitlab/-/issues/333806
- const { runnerInstallHelpPage, registrationToken, activeRunnersCount } = el.dataset;
+ const { runnerInstallHelpPage, registrationToken } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -43,10 +41,6 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
return h(AdminRunnersApp, {
props: {
registrationToken,
-
- // Runner counts are returned as formatted
- // strings, we do not use `parseInt`.
- activeRunnersCount,
},
});
},
diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
index edcbcb2bf69..0e259807f98 100644
--- a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
+++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -10,9 +10,17 @@ import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
export default {
name: 'RunnerRegistrationTokenReset',
+ i18n: {
+ modalTitle: __('Reset registration token'),
+ modalCopy: __('Are you sure you want to reset the registration token?'),
+ },
components: {
GlDropdownItem,
GlLoadingIcon,
+ GlModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
},
inject: {
groupId: {
@@ -22,6 +30,7 @@ export default {
default: null,
},
},
+ modalID: 'token-reset-modal',
props: {
type: {
type: String,
@@ -59,14 +68,10 @@ export default {
},
},
methods: {
+ handleModalPrimary() {
+ this.resetToken();
+ },
async resetToken() {
- // TODO Replace confirmation with gl-modal
- // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810
- // eslint-disable-next-line no-alert
- if (!window.confirm(__('Are you sure you want to reset the registration token?'))) {
- return;
- }
-
this.loading = true;
try {
const {
@@ -106,8 +111,15 @@ export default {
};
</script>
<template>
- <gl-dropdown-item @click.capture.native.stop="resetToken">
+ <gl-dropdown-item v-gl-modal="$options.modalID">
{{ __('Reset registration token') }}
+ <gl-modal
+ :modal-id="$options.modalID"
+ :title="$options.i18n.modalTitle"
+ @primary="handleModalPrimary"
+ >
+ <p>{{ $options.i18n.modalCopy }}</p>
+ </gl-modal>
<gl-loading-icon v-if="loading" inline />
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue b/app/assets/javascripts/runner/components/stat/runner_online_stat.vue
deleted file mode 100644
index b92b9badef0..00000000000
--- a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-<script>
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-
-export default {
- components: {
- GlSingleStat,
- },
-};
-</script>
-<template>
- <gl-single-stat
- v-bind="$attrs"
- variant="success"
- :title="s__('Runners|Online Runners')"
- :meta-text="s__('Runners|online')"
- />
-</template>
diff --git a/app/assets/javascripts/runner/components/stat/runner_stats.vue b/app/assets/javascripts/runner/components/stat/runner_stats.vue
new file mode 100644
index 00000000000..d3693ee593e
--- /dev/null
+++ b/app/assets/javascripts/runner/components/stat/runner_stats.vue
@@ -0,0 +1,49 @@
+<script>
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants';
+import RunnerStatusStat from './runner_status_stat.vue';
+
+export default {
+ components: {
+ RunnerStatusStat,
+ },
+ props: {
+ onlineRunnersCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ offlineRunnersCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ staleRunnersCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-py-6">
+ <runner-status-stat
+ class="gl-px-5"
+ :status="$options.STATUS_ONLINE"
+ :value="onlineRunnersCount"
+ />
+ <runner-status-stat
+ class="gl-px-5"
+ :status="$options.STATUS_OFFLINE"
+ :value="offlineRunnersCount"
+ />
+ <runner-status-stat
+ class="gl-px-5"
+ :status="$options.STATUS_STALE"
+ :value="staleRunnersCount"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/components/stat/runner_status_stat.vue b/app/assets/javascripts/runner/components/stat/runner_status_stat.vue
new file mode 100644
index 00000000000..b77bbe15541
--- /dev/null
+++ b/app/assets/javascripts/runner/components/stat/runner_status_stat.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { s__, formatNumber } from '~/locale';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants';
+
+export default {
+ components: {
+ GlSingleStat,
+ },
+ props: {
+ value: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ formattedValue() {
+ if (typeof this.value === 'number') {
+ return formatNumber(this.value);
+ }
+ return '-';
+ },
+ stat() {
+ switch (this.status) {
+ case STATUS_ONLINE:
+ return {
+ variant: 'success',
+ title: s__('Runners|Online runners'),
+ metaText: s__('Runners|online'),
+ };
+ case STATUS_OFFLINE:
+ return {
+ variant: 'muted',
+ title: s__('Runners|Offline runners'),
+ metaText: s__('Runners|offline'),
+ };
+ case STATUS_STALE:
+ return {
+ variant: 'warning',
+ title: s__('Runners|Stale runners'),
+ metaText: s__('Runners|stale'),
+ };
+ default:
+ return {
+ title: s__('Runners|Runners'),
+ };
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-single-stat
+ v-if="stat"
+ :value="formattedValue"
+ :variant="stat.variant"
+ :title="stat.title"
+ :meta-text="stat.metaText"
+ />
+</template>
diff --git a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
index 6da9e276f74..f7bcd683718 100644
--- a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
@@ -13,7 +13,7 @@ query getGroupRunners(
$sort: CiRunnerSort
) {
group(fullPath: $groupFullPath) {
- id
+ id # Apollo required
runners(
membership: DESCENDANTS
before: $before
diff --git a/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql
new file mode 100644
index 00000000000..554eb09e372
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql
@@ -0,0 +1,20 @@
+query getGroupRunnersCount(
+ $groupFullPath: ID!
+ $status: CiRunnerStatus
+ $type: CiRunnerType
+ $tagList: [String!]
+ $search: String
+) {
+ group(fullPath: $groupFullPath) {
+ id # Apollo required
+ runners(
+ membership: DESCENDANTS
+ status: $status
+ type: $type
+ tagList: $tagList
+ search: $search
+ ) {
+ count
+ }
+ }
+}
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index f33f28c11e3..3a7b58e3dc9 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
-import RunnerOnlineStat from '../components/stat/runner_online_stat.vue';
+import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
@@ -19,8 +19,12 @@ import {
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
GROUP_RUNNER_COUNT_LIMIT,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
} from '../constants';
import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql';
+import getGroupRunnersCountQuery from '../graphql/get_group_runners_count.query.graphql';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
@@ -28,6 +32,17 @@ import {
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
+const runnersCountSmartQuery = {
+ query: getGroupRunnersCountQuery,
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ update(data) {
+ return data?.group?.runners?.count;
+ },
+ error(error) {
+ this.reportToSentry(error);
+ },
+};
+
export default {
name: 'GroupRunnersApp',
components: {
@@ -36,7 +51,7 @@ export default {
RunnerFilteredSearchBar,
RunnerList,
RunnerName,
- RunnerOnlineStat,
+ RunnerStats,
RunnerPagination,
RunnerTypeTabs,
},
@@ -89,6 +104,33 @@ export default {
this.reportToSentry(error);
},
},
+ onlineRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ status: STATUS_ONLINE,
+ };
+ },
+ },
+ offlineRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ status: STATUS_OFFLINE,
+ };
+ },
+ },
+ staleRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ status: STATUS_STALE,
+ };
+ },
+ },
},
computed: {
variables() {
@@ -147,7 +189,11 @@ export default {
<template>
<div>
- <runner-online-stat class="gl-py-6 gl-px-5" :value="groupRunnersCount" />
+ <runner-stats
+ :online-runners-count="onlineRunnersTotal"
+ :offline-runners-count="offlineRunnersTotal"
+ :stale-runners-count="staleRunnersTotal"
+ />
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
diff --git a/app/controllers/groups/packages_controller.rb b/app/controllers/groups/packages_controller.rb
index 47f1816cc4c..1f3d80260ed 100644
--- a/app/controllers/groups/packages_controller.rb
+++ b/app/controllers/groups/packages_controller.rb
@@ -6,6 +6,11 @@ module Groups
feature_category :package_registry
+ # The show action renders index to allow frontend routing to work on page refresh
+ def show
+ render :index
+ end
+
private
def verify_packages_enabled!
diff --git a/app/controllers/projects/packages/packages_controller.rb b/app/controllers/projects/packages/packages_controller.rb
index 5de71466c10..969922266fa 100644
--- a/app/controllers/projects/packages/packages_controller.rb
+++ b/app/controllers/projects/packages/packages_controller.rb
@@ -7,8 +7,9 @@ module Projects
feature_category :package_registry
+ # The show action renders index to allow frontend routing to work on page refresh
def show
- @package = project.packages.find(params[:id])
+ render :index
end
end
end
diff --git a/app/graphql/mutations/issues/set_escalation_status.rb b/app/graphql/mutations/issues/set_escalation_status.rb
new file mode 100644
index 00000000000..6073b73277b
--- /dev/null
+++ b/app/graphql/mutations/issues/set_escalation_status.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Issues
+ class SetEscalationStatus < Base
+ graphql_name 'IssueSetEscalationStatus'
+
+ argument :status, Types::IncidentManagement::EscalationStatusEnum,
+ required: true,
+ description: 'Set the escalation status.'
+
+ def resolve(project_path:, iid:, status:)
+ issue = authorized_find!(project_path: project_path, iid: iid)
+ project = issue.project
+
+ authorize_escalation_status!(project)
+ check_feature_availability!(project, issue)
+
+ ::Issues::UpdateService.new(
+ project: project,
+ current_user: current_user,
+ params: { escalation_status: { status: status } }
+ ).execute(issue)
+
+ {
+ issue: issue,
+ errors: errors_on_object(issue)
+ }
+ end
+
+ private
+
+ def authorize_escalation_status!(project)
+ return if Ability.allowed?(current_user, :update_escalation_status, project)
+
+ raise_resource_not_available_error!
+ end
+
+ def check_feature_availability!(project, issue)
+ return if Feature.enabled?(:incident_escalations, project) && issue.supports_escalation?
+
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue'
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/base_issues_resolver.rb b/app/graphql/resolvers/base_issues_resolver.rb
index 3983a3697cb..3e7509b4068 100644
--- a/app/graphql/resolvers/base_issues_resolver.rb
+++ b/app/graphql/resolvers/base_issues_resolver.rb
@@ -48,7 +48,8 @@ module Resolvers
labels: [:labels],
assignees: [:assignees],
timelogs: [:timelogs],
- customer_relations_contacts: { customer_relations_contacts: [:group] }
+ customer_relations_contacts: { customer_relations_contacts: [:group] },
+ escalation_status: [:incident_management_issuable_escalation_status]
}
end
diff --git a/app/graphql/types/incident_management/escalation_status_enum.rb b/app/graphql/types/incident_management/escalation_status_enum.rb
new file mode 100644
index 00000000000..bc462f03148
--- /dev/null
+++ b/app/graphql/types/incident_management/escalation_status_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module IncidentManagement
+ class EscalationStatusEnum < BaseEnum
+ graphql_name 'IssueEscalationStatus'
+ description 'Issue escalation status values'
+
+ ::IncidentManagement::IssuableEscalationStatus.status_names.each do |status|
+ value status.to_s.upcase, value: status, description: "#{::IncidentManagement::IssuableEscalationStatus::STATUS_DESCRIPTIONS[status]}."
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 498569f11ca..46fe91feae4 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -140,6 +140,9 @@ module Types
field :customer_relations_contacts, Types::CustomerRelations::ContactType.connection_type, null: true,
description: 'Customer relations contacts of the issue.'
+ field :escalation_status, Types::IncidentManagement::EscalationStatusEnum, null: true,
+ description: 'Escalation status of the issue.'
+
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
@@ -167,6 +170,12 @@ module Types
def hidden?
object.hidden? if Feature.enabled?(:ban_user_feature_flag)
end
+
+ def escalation_status
+ return unless Feature.enabled?(:incident_escalations, object.project) && object.supports_escalation?
+
+ object.escalation_status&.status_name
+ end
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index e01bb0b4e70..c350f4dd922 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -55,6 +55,7 @@ module Types
mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::SetSeverity
mount_mutation Mutations::Issues::SetSubscription
+ mount_mutation Mutations::Issues::SetEscalationStatus
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::Issues::Move
mount_mutation Mutations::Labels::Create
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index fb75a3e15d6..f84b42209da 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -65,10 +65,7 @@ module Ci
# Runner install help page is external, located at
# https://gitlab.com/gitlab-org/gitlab-runner
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
- registration_token: Gitlab::CurrentSettings.runners_registration_token,
-
- # Runner counts are returned as formatted strings
- active_runners_count: Ci::Runner.online.count.to_s
+ registration_token: Gitlab::CurrentSettings.runners_registration_token
}
end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index f40d0cd2fa4..a53fa39c58f 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -78,7 +78,6 @@ module AlertManagement
scope :for_environment, -> (environment) { where(environment: environment) }
scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
- scope :open, -> { with_status(open_statuses) }
scope :not_resolved, -> { without_status(:resolved) }
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
scope :with_threat_monitoring_alerts, -> { where(domain: :threat_monitoring ) }
@@ -143,18 +142,6 @@ module AlertManagement
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
- def self.open_statuses
- [:triggered, :acknowledged]
- end
-
- def self.open_status?(status)
- open_statuses.include?(status)
- end
-
- def open?
- self.class.open_status?(status_name)
- end
-
def prometheus?
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b3e23adb7d6..dbfe184c048 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -763,9 +763,7 @@ module Ci
def any_runners_available?
cache_for_available_runners do
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do
- project.active_runners.exists?
- end
+ project.active_runners.exists?
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 35616a542c1..1c469377231 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -141,16 +141,9 @@ module Ci
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
- if Feature.enabled?(:ci_decompose_belonging_to_parent_group_of_project_query, default_enabled: :yaml)
- belonging_to_group(project_groups.self_and_ancestors.pluck(:id))
- else
- joins(:groups)
- .where(namespaces: { id: project_groups.self_and_ancestors.as_ids })
- .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
- end
+ belonging_to_group(project_groups.self_and_ancestors.pluck(:id))
}
- # deprecated
scope :owned_or_instance_wide, -> (project_id) do
from_union(
[
@@ -159,7 +152,7 @@ module Ci
instance_type
],
remove_duplicates: false
- ).allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
+ )
end
scope :assignable_for, ->(project) do
diff --git a/app/models/concerns/incident_management/escalatable.rb b/app/models/concerns/incident_management/escalatable.rb
index 81eef50603a..a9e4a066e0e 100644
--- a/app/models/concerns/incident_management/escalatable.rb
+++ b/app/models/concerns/incident_management/escalatable.rb
@@ -27,6 +27,8 @@ module IncidentManagement
ignored: 'No action will be taken'
}.freeze
+ OPEN_STATUSES = [:triggered, :acknowledged].freeze
+
included do
validates :status, presence: true
@@ -34,6 +36,7 @@ module IncidentManagement
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
+ scope :open, -> { with_status(OPEN_STATUSES) }
state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered]
@@ -89,6 +92,10 @@ module IncidentManagement
@status_names ||= state_machine_statuses.keys
end
+ def open_status?(status)
+ OPEN_STATUSES.include?(status)
+ end
+
private
def state_machine_statuses
@@ -99,6 +106,10 @@ module IncidentManagement
def status_event_for(status)
self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
end
+
+ def open?
+ self.class.open_status?(status_name)
+ end
end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 81ee1c1fe55..a553246ed79 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1775,17 +1775,12 @@ class Project < ApplicationRecord
def all_runners
Ci::Runner.from_union([runners, group_runners, shared_runners])
- .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937')
end
def all_available_runners
Ci::Runner.from_union([runners, group_runners, available_shared_runners])
- .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937')
end
- # Once issue 339937 is fixed, please search for all mentioned of
- # https://gitlab.com/gitlab-org/gitlab/-/issues/339937,
- # and remove the allow_cross_joins_across_databases.
def active_runners
strong_memoize(:active_runners) do
all_available_runners.active
@@ -1793,9 +1788,7 @@ class Project < ApplicationRecord
end
def any_online_runners?(&block)
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do
- online_runners_with_tags.any?(&block)
- end
+ online_runners_with_tags.any?(&block)
end
def valid_runners_token?(token)
diff --git a/app/models/user.rb b/app/models/user.rb
index fec37172284..3e08ac7612d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -251,7 +251,7 @@ class User < ApplicationRecord
validate :notification_email_verified, if: :notification_email_changed?
validate :public_email_verified, if: :public_email_changed?
validate :commit_email_verified, if: :commit_email_changed?
- validate :signup_email_valid?, on: :create, if: ->(user) { !user.created_by_id }
+ validate :email_allowed_by_restrictions?, if: ->(user) { user.new_record? ? !user.created_by_id : user.email_changed? }
validate :check_username_format, if: :username_changed?
validates :theme_id, allow_nil: true, inclusion: { in: Gitlab::Themes.valid_ids,
@@ -2145,14 +2145,14 @@ class User < ApplicationRecord
end
end
- def signup_email_valid?
+ def email_allowed_by_restrictions?
error = validate_admin_signup_restrictions(email)
errors.add(:email, error) if error
end
def signup_email_invalid_message
- _('is not allowed for sign-up.')
+ self.new_record? ? _('is not allowed for sign-up.') : _('is not allowed.')
end
def check_username_format
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 5338f047051..d53e136effb 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -95,13 +95,9 @@ module Ci
.build!
if pipeline.persisted?
- if Feature.enabled?(:ci_publish_pipeline_events, pipeline.project, default_enabled: :yaml)
- Gitlab::EventStore.publish(
- Ci::PipelineCreatedEvent.new(data: { pipeline_id: pipeline.id })
- )
- else
- schedule_head_pipeline_update
- end
+ Gitlab::EventStore.publish(
+ Ci::PipelineCreatedEvent.new(data: { pipeline_id: pipeline.id })
+ )
create_namespace_onboarding_action
else
@@ -141,12 +137,6 @@ module Ci
commit.try(:id)
end
- def schedule_head_pipeline_update
- pipeline.all_merge_requests.opened.each do |merge_request|
- UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
- end
- end
-
def create_namespace_onboarding_action
Namespaces::OnboardingPipelineCreatedWorker.perform_async(project.namespace_id)
end
diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb
index 146239bb7e5..2e38969c7a9 100644
--- a/app/services/ci/update_build_queue_service.rb
+++ b/app/services/ci/update_build_queue_service.rb
@@ -99,17 +99,15 @@ module Ci
private
def tick_for(build, runners)
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339937') do
- runners = runners.with_recent_runner_queue
- runners = runners.with_tags if Feature.enabled?(:ci_preload_runner_tags, default_enabled: :yaml)
+ runners = runners.with_recent_runner_queue
+ runners = runners.with_tags if Feature.enabled?(:ci_preload_runner_tags, default_enabled: :yaml)
- metrics.observe_active_runners(-> { runners.to_a.size })
+ metrics.observe_active_runners(-> { runners.to_a.size })
- runners.each do |runner|
- metrics.increment_runner_tick(runner)
+ runners.each do |runner|
+ metrics.increment_runner_tick(runner)
- runner.pick_build!(build)
- end
+ runner.pick_build!(build)
end
end
diff --git a/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb b/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb
new file mode 100644
index 00000000000..49d7198d7b0
--- /dev/null
+++ b/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module IssuableEscalationStatuses
+ class AfterUpdateService < ::BaseProjectService
+ def initialize(issuable, current_user)
+ @issuable = issuable
+ @escalation_status = issuable.escalation_status
+ @alert = issuable.alert_management_alert
+
+ super(project: issuable.project, current_user: current_user)
+ end
+
+ def execute
+ after_update
+
+ ServiceResponse.success(payload: { escalation_status: escalation_status })
+ end
+
+ private
+
+ attr_reader :issuable, :escalation_status, :alert
+
+ def after_update
+ sync_to_alert
+ end
+
+ def sync_to_alert
+ return unless alert
+ return unless escalation_status.status_previously_changed?
+
+ ::AlertManagement::Alerts::UpdateService.new(
+ alert,
+ current_user,
+ status: escalation_status.status_name
+ ).execute
+ end
+ end
+ end
+end
+
+::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.prepend_mod
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 083e422f164..66a1f0c8756 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -213,13 +213,8 @@ module Issues
def handle_escalation_status_change(issue, old_escalation_status)
return unless old_escalation_status.present?
return if issue.escalation_status&.slice(:status, :policy_id) == old_escalation_status
- return unless issue.alert_management_alert
- ::AlertManagement::Alerts::UpdateService.new(
- issue.alert_management_alert,
- current_user,
- status: issue.escalation_status.status_name
- ).execute
+ ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user).execute
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index ba4abdc02e4..de2a737faa1 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -1,44 +1,3 @@
- page_title _('Deploy Keys')
-- if Feature.enabled?(:admin_deploy_keys_vue, default_enabled: :yaml)
- #js-admin-deploy-keys-table{ data: admin_deploy_keys_data }
-- else
- - if @deploy_keys.any?
- %h3.page-title.deploy-keys-title
- = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size }
- = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'float-right btn gl-button btn-confirm btn-md gl-button'
- %table.table.b-table.gl-table.b-table-stacked-lg{ data: { testid: 'deploy-keys-list' } }
- %thead
- %tr
- %th= _('Title')
- %th= _('Fingerprint')
- %th= _('Projects with write access')
- %th= _('Created')
- %th.gl-lg-w-1px.gl-white-space-nowrap
- %span.gl-sr-only
- = _('Actions')
- %tbody
- - @deploy_keys.each do |deploy_key|
- %tr
- %td{ data: { label: _('Title') } }
- %div
- = deploy_key.title
- %td{ data: { label: _('Fingerprint') } }
- %div
- %code= deploy_key.fingerprint
- %td{ data: { label: _('Projects with write access') } }
- %div
- - deploy_key.projects_with_write_access.each do |project|
- = link_to project.full_name, admin_project_path(project), class: 'gl-display-block'
- %td{ data: { label: _('Created') } }
- %div
- = time_ago_with_tooltip(deploy_key.created_at)
- %td.gl-lg-w-1px.gl-white-space-nowrap{ data: { label: _('Actions') } }
- %div
- = link_to edit_admin_deploy_key_path(deploy_key), class: 'btn btn-default btn-md gl-button btn-icon gl-mr-3', aria: { label: _('Edit deploy key') } do
- = sprite_icon('pencil', css_class: 'gl-button-icon')
- = link_to admin_deploy_key_path(deploy_key), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-danger btn-md gl-button btn-icon', aria: { label: _('Remove deploy key') } do
- = sprite_icon('remove', css_class: 'gl-button-icon')
-
- - else
- = render 'shared/empty_states/deploy_keys'
+#js-admin-deploy-keys-table{ data: admin_deploy_keys_data }
diff --git a/app/views/groups/packages/index.html.haml b/app/views/groups/packages/index.html.haml
index d56a806f082..1c0627779ec 100644
--- a/app/views/groups/packages/index.html.haml
+++ b/app/views/groups/packages/index.html.haml
@@ -7,4 +7,7 @@
full_path: @group.full_path,
endpoint: group_packages_path(@group),
page_type: 'groups',
- empty_list_illustration: image_path('illustrations/no-packages.svg'), } }
+ empty_list_illustration: image_path('illustrations/no-packages.svg'),
+ npm_instance_url: package_registry_instance_url(:npm),
+ project_list_url: '',
+ group_list_url: group_packages_path(@group) } }
diff --git a/app/views/projects/packages/packages/index.html.haml b/app/views/projects/packages/packages/index.html.haml
index c67b06218e2..4ab16f25dd2 100644
--- a/app/views/projects/packages/packages/index.html.haml
+++ b/app/views/projects/packages/packages/index.html.haml
@@ -7,4 +7,7 @@
full_path: @project.full_path,
endpoint: project_packages_path(@project),
page_type: 'projects',
- empty_list_illustration: image_path('illustrations/no-packages.svg'), } }
+ empty_list_illustration: image_path('illustrations/no-packages.svg'),
+ npm_instance_url: package_registry_instance_url(:npm),
+ project_list_url: project_packages_path(@project),
+ group_list_url: '' } }
diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml
deleted file mode 100644
index ebdc9e654f6..00000000000
--- a/app/views/projects/packages/packages/show.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- add_to_breadcrumbs _("Package Registry"), project_packages_path(@project)
-- add_to_breadcrumbs @package.name, project_packages_path(@project)
-- breadcrumb_title @package.version
-- page_title _("Package Registry")
-- @content_class = "limit-container-width" unless fluid_layout
-
-.row
- .col-12
- #js-vue-packages-detail-new{ data: package_details_data(@project, @package) }
diff --git a/app/views/shared/empty_states/_deploy_keys.html.haml b/app/views/shared/empty_states/_deploy_keys.html.haml
deleted file mode 100644
index 6c615de9c56..00000000000
--- a/app/views/shared/empty_states/_deploy_keys.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-.empty-state.gl-display-flex.gl-flex-direction-column.gl-flex-wrap.gl-text-center
- .gl-flex-grow-0.gl-flex-shrink-0
- .svg-250.svg-content
- = image_tag 'illustrations/empty-state/empty-deploy-keys-lg.svg'
- .gl-flex-grow-0.gl-flex-shrink-0
- .text-content.gl-mx-auto.gl-my-0.gl-p-5
- %h4.h4= _('Deploy Keys')
- %p= _('Deploy keys grant read/write access to all repositories in your instance')
- = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'gl-button btn btn-confirm btn-md'
diff --git a/config/feature_flags/development/admin_deploy_keys_vue.yml b/config/feature_flags/development/admin_deploy_keys_vue.yml
deleted file mode 100644
index 21e1b501d7a..00000000000
--- a/config/feature_flags/development/admin_deploy_keys_vue.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: admin_deploy_keys_vue
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73580
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344855
-milestone: '14.5'
-type: development
-group: group::access
-default_enabled: true
diff --git a/config/feature_flags/development/ci_decompose_belonging_to_parent_group_of_project_query.yml b/config/feature_flags/development/ci_decompose_belonging_to_parent_group_of_project_query.yml
deleted file mode 100644
index 98950c0ccb1..00000000000
--- a/config/feature_flags/development/ci_decompose_belonging_to_parent_group_of_project_query.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_decompose_belonging_to_parent_group_of_project_query
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76454
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348560
-milestone: '14.7'
-type: development
-group: group::pipeline execution
-default_enabled: false
diff --git a/config/feature_flags/development/ci_publish_pipeline_events.yml b/config/feature_flags/development/ci_publish_pipeline_events.yml
deleted file mode 100644
index 2d47084f499..00000000000
--- a/config/feature_flags/development/ci_publish_pipeline_events.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_publish_pipeline_events
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34042
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336752
-milestone: '14.3'
-type: development
-group: group::pipeline execution
-default_enabled: false
diff --git a/config/feature_flags/development/use_primary_and_secondary_stores_for_sessions.yml b/config/feature_flags/development/use_primary_and_secondary_stores_for_sessions.yml
deleted file mode 100644
index 2204472c0a6..00000000000
--- a/config/feature_flags/development/use_primary_and_secondary_stores_for_sessions.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: use_primary_and_secondary_stores_for_sessions
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73660
-rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1429
-milestone: '14.6'
-type: development
-group: group::memory
-default_enabled: false
diff --git a/config/feature_flags/development/use_primary_store_as_default_for_sessions.yml b/config/feature_flags/development/use_primary_store_as_default_for_sessions.yml
deleted file mode 100644
index ac130ab7761..00000000000
--- a/config/feature_flags/development/use_primary_store_as_default_for_sessions.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: use_primary_store_as_default_for_sessions
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75258
-rollout_issue_url:
-milestone: '14.6'
-type: development
-group: group::memory
-default_enabled: false
diff --git a/config/metrics/schema.json b/config/metrics/schema.json
index d416c7b6d6e..09376e32ef0 100644
--- a/config/metrics/schema.json
+++ b/config/metrics/schema.json
@@ -30,7 +30,7 @@
},
"status": {
"type": ["string"],
- "enum": ["active", "deprecated", "removed", "broken"]
+ "enum": ["active", "removed", "broken"]
},
"milestone": {
"type": ["string"],
diff --git a/config/routes/group.rb b/config/routes/group.rb
index da205163e6d..f7a8747d0cf 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -64,7 +64,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
post :toggle_subscription, on: :member
end
- resources :packages, only: [:index]
+ resources :packages, only: [:index, :show]
resources :milestones, constraints: { id: %r{[^/]+} } do
member do
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 710bde34fe7..80c49660bdb 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -211,6 +211,8 @@
- 1
- - incident_management_pending_escalations_issue_check
- 1
+- - incident_management_pending_escalations_issue_create
+ - 1
- - integrations_create_external_cross_reference
- 1
- - invalid_gpg_signature_update
diff --git a/db/post_migrate/20211217174331_mark_recalculate_finding_signatures_as_completed.rb b/db/post_migrate/20211217174331_mark_recalculate_finding_signatures_as_completed.rb
new file mode 100644
index 00000000000..316209ae1f4
--- /dev/null
+++ b/db/post_migrate/20211217174331_mark_recalculate_finding_signatures_as_completed.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class MarkRecalculateFindingSignaturesAsCompleted < Gitlab::Database::Migration[1.0]
+ MIGRATION = 'RecalculateVulnerabilitiesOccurrencesUuid'
+
+ def up
+ # Only run migration for Gitlab.com
+ return unless ::Gitlab.com?
+
+ # In previous migration marking jobs as successful was missed
+ Gitlab::Database::BackgroundMigrationJob
+ .for_migration_class(MIGRATION)
+ .pending
+ .update_all(status: :succeeded)
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20211217174331 b/db/schema_migrations/20211217174331
new file mode 100644
index 00000000000..32657e28f96
--- /dev/null
+++ b/db/schema_migrations/20211217174331
@@ -0,0 +1 @@
+649360f4069aac4784f4d039015f8dda3f4bae28e8132f841e25b48f034a392e \ No newline at end of file
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 645748d061e..80c9e94415a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2962,6 +2962,27 @@ Input type: `IssueSetEscalationPolicyInput`
| <a id="mutationissuesetescalationpolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationissuesetescalationpolicyissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
+### `Mutation.issueSetEscalationStatus`
+
+Input type: `IssueSetEscalationStatusInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationissuesetescalationstatusclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationissuesetescalationstatusiid"></a>`iid` | [`String!`](#string) | IID of the issue to mutate. |
+| <a id="mutationissuesetescalationstatusprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the issue to mutate is in. |
+| <a id="mutationissuesetescalationstatusstatus"></a>`status` | [`IssueEscalationStatus!`](#issueescalationstatus) | Set the escalation status. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationissuesetescalationstatusclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationissuesetescalationstatuserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationissuesetescalationstatusissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
+
### `Mutation.issueSetIteration`
Input type: `IssueSetIterationInput`
@@ -10317,6 +10338,7 @@ Relationship between an epic and an issue.
| <a id="epicissueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. |
| <a id="epicissueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
+| <a id="epicissueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
| <a id="epicissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. |
| <a id="epicissuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. |
| <a id="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
@@ -11500,6 +11522,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
| <a id="issueemailsdisabled"></a>`emailsDisabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `true` if email notifications are disabled. |
| <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="issueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
+| <a id="issueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
| <a id="issuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. |
| <a id="issuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. |
| <a id="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
@@ -16821,6 +16844,17 @@ Iteration ID wildcard values for issue creation.
| ----- | ----------- |
| <a id="issuecreationiterationwildcardidcurrent"></a>`CURRENT` | Current iteration. |
+### `IssueEscalationStatus`
+
+Issue escalation status values.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="issueescalationstatusacknowledged"></a>`ACKNOWLEDGED` | Someone is actively investigating the problem. |
+| <a id="issueescalationstatusignored"></a>`IGNORED` | No action will be taken. |
+| <a id="issueescalationstatusresolved"></a>`RESOLVED` | The problem has been addressed. |
+| <a id="issueescalationstatustriggered"></a>`TRIGGERED` | Investigation has not started. |
+
### `IssueSort`
Values for sorting issues.
diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md
index d5087c20e6f..c3f0c94db21 100644
--- a/doc/user/admin_area/license.md
+++ b/doc/user/admin_area/license.md
@@ -94,7 +94,7 @@ a license, upload the license in the **Admin Area** in the web user interface.
## What happens when your license expires
-One month before the license expires, a message with the upcoming expiration
+Fifteen days before the license expires, a notification banner with the upcoming expiration
date displays to GitLab administrators.
When your license expires, GitLab locks features, like Git pushes
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
deleted file mode 100644
index ff7758f3e53..00000000000
--- a/lib/gitlab/redis/multi_store.rb
+++ /dev/null
@@ -1,232 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Redis
- class MultiStore
- include Gitlab::Utils::StrongMemoize
-
- class ReadFromPrimaryError < StandardError
- def message
- 'Value not found on the redis primary store. Read from the redis secondary store successful.'
- end
- end
- class MethodMissingError < StandardError
- def message
- 'Method missing. Falling back to execute method on the redis secondary store.'
- end
- end
-
- attr_reader :primary_store, :secondary_store, :instance_name
-
- FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis primary_store.'
- FAILED_TO_WRITE_ERROR_MESSAGE = 'Failed to write to the redis primary_store.'
-
- SKIP_LOG_METHOD_MISSING_FOR_COMMANDS = %i(info).freeze
-
- READ_COMMANDS = %i(
- get
- mget
- smembers
- scard
- ).freeze
-
- WRITE_COMMANDS = %i(
- set
- setnx
- setex
- sadd
- srem
- del
- pipelined
- flushdb
- ).freeze
-
- def initialize(primary_store, secondary_store, instance_name)
- @primary_store = primary_store
- @secondary_store = secondary_store
- @instance_name = instance_name
-
- validate_stores!
- end
- # rubocop:disable GitlabSecurity/PublicSend
- READ_COMMANDS.each do |name|
- define_method(name) do |*args, &block|
- if use_primary_and_secondary_stores?
- read_command(name, *args, &block)
- else
- default_store.send(name, *args, &block)
- end
- end
- end
-
- WRITE_COMMANDS.each do |name|
- define_method(name) do |*args, &block|
- if use_primary_and_secondary_stores?
- write_command(name, *args, &block)
- else
- default_store.send(name, *args, &block)
- end
- end
- end
-
- def method_missing(...)
- return @instance.send(...) if @instance
-
- log_method_missing(...)
-
- default_store.send(...)
- end
- # rubocop:enable GitlabSecurity/PublicSend
-
- def respond_to_missing?(command_name, include_private = false)
- true
- end
-
- # This is needed because of Redis::Rack::Connection is requiring Redis::Store
- # https://github.com/redis-store/redis-rack/blob/a833086ba494083b6a384a1a4e58b36573a9165d/lib/redis/rack/connection.rb#L15
- # Done similarly in https://github.com/lsegal/yard/blob/main/lib/yard/templates/template.rb#L122
- def is_a?(klass)
- return true if klass == default_store.class
-
- super(klass)
- end
- alias_method :kind_of?, :is_a?
-
- def to_s
- use_primary_and_secondary_stores? ? primary_store.to_s : default_store.to_s
- end
-
- def use_primary_and_secondary_stores?
- feature_table_exists? && Feature.enabled?("use_primary_and_secondary_stores_for_#{instance_name.underscore}", default_enabled: :yaml) && !same_redis_store?
- end
-
- def use_primary_store_as_default?
- feature_table_exists? && Feature.enabled?("use_primary_store_as_default_for_#{instance_name.underscore}", default_enabled: :yaml) && !same_redis_store?
- end
-
- private
-
- # @return [Boolean]
- def feature_table_exists?
- Feature::FlipperFeature.table_exists?
- rescue StandardError
- false
- end
-
- def default_store
- use_primary_store_as_default? ? primary_store : secondary_store
- end
-
- def log_method_missing(command_name, *_args)
- return if SKIP_LOG_METHOD_MISSING_FOR_COMMANDS.include?(command_name)
-
- log_error(MethodMissingError.new, command_name)
- increment_method_missing_count(command_name)
- end
-
- def read_command(command_name, *args, &block)
- if @instance
- send_command(@instance, command_name, *args, &block)
- else
- read_one_with_fallback(command_name, *args, &block)
- end
- end
-
- def write_command(command_name, *args, &block)
- if @instance
- send_command(@instance, command_name, *args, &block)
- else
- write_both(command_name, *args, &block)
- end
- end
-
- def read_one_with_fallback(command_name, *args, &block)
- begin
- value = send_command(primary_store, command_name, *args, &block)
- rescue StandardError => e
- log_error(e, command_name,
- multi_store_error_message: FAILED_TO_READ_ERROR_MESSAGE)
- end
-
- value ||= fallback_read(command_name, *args, &block)
-
- value
- end
-
- def fallback_read(command_name, *args, &block)
- value = send_command(secondary_store, command_name, *args, &block)
-
- if value
- log_error(ReadFromPrimaryError.new, command_name)
- increment_read_fallback_count(command_name)
- end
-
- value
- end
-
- def write_both(command_name, *args, &block)
- begin
- send_command(primary_store, command_name, *args, &block)
- rescue StandardError => e
- log_error(e, command_name,
- multi_store_error_message: FAILED_TO_WRITE_ERROR_MESSAGE)
- end
-
- send_command(secondary_store, command_name, *args, &block)
- end
-
- def same_redis_store?
- strong_memoize(:same_redis_store) do
- # <Redis client v4.4.0 for redis:///path_to/redis/redis.socket/5>"
- primary_store.inspect == secondary_store.inspect
- end
- end
-
- # rubocop:disable GitlabSecurity/PublicSend
- def send_command(redis_instance, command_name, *args, &block)
- if block_given?
- # Make sure that block is wrapped and executed only on the redis instance that is executing the block
- redis_instance.send(command_name, *args) do |*params|
- with_instance(redis_instance, *params, &block)
- end
- else
- redis_instance.send(command_name, *args)
- end
- end
- # rubocop:enable GitlabSecurity/PublicSend
-
- def with_instance(instance, *params)
- @instance = instance
-
- yield(*params)
- ensure
- @instance = nil
- end
-
- def increment_read_fallback_count(command_name)
- @read_fallback_counter ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_read_fallback_total, 'Client side Redis MultiStore reading fallback')
- @read_fallback_counter.increment(command: command_name, instance_name: instance_name)
- end
-
- def increment_method_missing_count(command_name)
- @method_missing_counter ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_method_missing_total, 'Client side Redis MultiStore method missing')
- @method_missing_counter.increment(command: command_name, instance_name: instance_name)
- end
-
- def validate_stores!
- raise ArgumentError, 'primary_store is required' unless primary_store
- raise ArgumentError, 'secondary_store is required' unless secondary_store
- raise ArgumentError, 'instance_name is required' unless instance_name
- raise ArgumentError, 'invalid primary_store' unless primary_store.is_a?(::Redis)
- raise ArgumentError, 'invalid secondary_store' unless secondary_store.is_a?(::Redis)
- end
-
- def log_error(exception, command_name, extra = {})
- Gitlab::ErrorTracking.log_exception(
- exception,
- command_name: command_name,
- extra: extra.merge(instance_name: instance_name))
- end
- end
- end
-end
diff --git a/lib/gitlab/redis/sessions.rb b/lib/gitlab/redis/sessions.rb
index c547828d907..ddcfdf6e798 100644
--- a/lib/gitlab/redis/sessions.rb
+++ b/lib/gitlab/redis/sessions.rb
@@ -9,39 +9,9 @@ module Gitlab
IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab2'
OTP_SESSIONS_NAMESPACE = 'session:otp'
- class << self
- # The data we store on Sessions used to be stored on SharedState.
- def config_fallback
- SharedState
- end
-
- private
-
- def redis
- # Don't use multistore if redis.sessions configuration is not provided
- return super if config_fallback?
-
- primary_store = ::Redis.new(params)
- secondary_store = ::Redis.new(config_fallback.params)
-
- MultiStore.new(primary_store, secondary_store, store_name)
- end
- end
-
- def store(extras = {})
- # Don't use multistore if redis.sessions configuration is not provided
- return super if self.class.config_fallback?
-
- primary_store = create_redis_store(redis_store_options, extras)
- secondary_store = create_redis_store(self.class.config_fallback.params, extras)
-
- MultiStore.new(primary_store, secondary_store, self.class.store_name)
- end
-
- private
-
- def create_redis_store(options, extras)
- ::Redis::Store.new(options.merge(extras))
+ # The data we store on Sessions used to be stored on SharedState.
+ def self.config_fallback
+ SharedState
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index eef2ca6eaaf..adc6002547b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7485,6 +7485,12 @@ msgstr ""
msgid "Closes this %{quick_action_target}."
msgstr ""
+msgid "Cloud Run"
+msgstr ""
+
+msgid "Cloud Storage"
+msgstr ""
+
msgid "Cluster"
msgstr ""
@@ -9034,6 +9040,9 @@ msgstr ""
msgid "Configure existing installation"
msgstr ""
+msgid "Configure pipelines to deploy web apps, backend services, APIs and static resources to Google Cloud"
+msgstr ""
+
msgid "Configure repository mirroring."
msgstr ""
@@ -9061,6 +9070,9 @@ msgstr ""
msgid "Configure the way a user creates a new account."
msgstr ""
+msgid "Configure via Merge Request"
+msgstr ""
+
msgid "Configure which lists are shown for anyone who visits this board"
msgstr ""
@@ -11737,6 +11749,9 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
+msgid "Deploy container based web apps on Google managed clusters"
+msgstr ""
+
msgid "Deploy freezes"
msgstr ""
@@ -11752,6 +11767,9 @@ msgstr ""
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
+msgid "Deploy static assets and resources to Google managed CDN"
+msgstr ""
+
msgid "Deploy to..."
msgstr ""
@@ -28930,9 +28948,6 @@ msgstr ""
msgid "Public deploy keys"
msgstr ""
-msgid "Public deploy keys (%{deploy_keys_count})"
-msgstr ""
-
msgid "Public pipelines"
msgstr ""
@@ -30726,10 +30741,13 @@ msgstr ""
msgid "Runners|Offline"
msgstr ""
+msgid "Runners|Offline runners"
+msgstr ""
+
msgid "Runners|Online"
msgstr ""
-msgid "Runners|Online Runners"
+msgid "Runners|Online runners"
msgstr ""
msgid "Runners|Paused"
@@ -30825,6 +30843,9 @@ msgstr ""
msgid "Runners|Stale"
msgstr ""
+msgid "Runners|Stale runners"
+msgstr ""
+
msgid "Runners|Status"
msgstr ""
@@ -42268,6 +42289,9 @@ msgstr ""
msgid "is not allowed since the group is not top-level group."
msgstr ""
+msgid "is not allowed."
+msgstr ""
+
msgid "is not allowed. We do not currently support project-level iterations"
msgstr ""
diff --git a/qa/qa/page/project/packages/show.rb b/qa/qa/page/project/packages/show.rb
index 4872c0bc705..5ba9ad7df40 100644
--- a/qa/qa/page/project/packages/show.rb
+++ b/qa/qa/page/project/packages/show.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Packages
class Show < QA::Page::Base
- view 'app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue' do
+ view 'app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue' do
element :delete_button
element :delete_modal_button
element :package_information_content
diff --git a/spec/controllers/groups/packages_controller_spec.rb b/spec/controllers/groups/packages_controller_spec.rb
new file mode 100644
index 00000000000..fc9b79da47c
--- /dev/null
+++ b/spec/controllers/groups/packages_controller_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::PackagesController do
+ let_it_be(:group) { create(:group) }
+
+ let(:page) { :index }
+ let(:additional_parameters) { {} }
+
+ subject do
+ get page, params: additional_parameters.merge({
+ group_id: group
+ })
+ end
+
+ context 'GET #index' do
+ it_behaves_like 'returning response status', :ok
+ end
+
+ context 'GET #show' do
+ let(:page) { :show }
+ let(:additional_parameters) { { id: 1 } }
+
+ it_behaves_like 'returning response status', :ok
+ end
+end
diff --git a/spec/controllers/projects/packages/packages_controller_spec.rb b/spec/controllers/projects/packages/packages_controller_spec.rb
new file mode 100644
index 00000000000..da9cae47c62
--- /dev/null
+++ b/spec/controllers/projects/packages/packages_controller_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Packages::PackagesController do
+ let_it_be(:project) { create(:project, :public) }
+
+ let(:page) { :index }
+ let(:additional_parameters) { {} }
+
+ subject do
+ get page, params: additional_parameters.merge({
+ project_id: project,
+ namespace_id: project.namespace
+ })
+ end
+
+ context 'GET #index' do
+ it_behaves_like 'returning response status', :ok
+ end
+
+ context 'GET #show' do
+ let(:page) { :show }
+ let(:additional_parameters) { { id: 1 } }
+
+ it_behaves_like 'returning response status', :ok
+ end
+end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index 9b74aa2ac5a..88b8fcd8d5e 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin deploy keys' do
+RSpec.describe 'admin deploy keys', :js do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
@@ -15,112 +15,81 @@ RSpec.describe 'admin deploy keys' do
gitlab_enable_admin_mode_sign_in(admin)
end
- shared_examples 'renders deploy keys correctly' do
- it 'show all public deploy keys' do
- visit admin_deploy_keys_path
+ it 'show all public deploy keys' do
+ visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content(deploy_key.title)
- expect(page).to have_content(another_deploy_key.title)
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content(deploy_key.title)
+ expect(page).to have_content(another_deploy_key.title)
end
+ end
- it 'shows all the projects the deploy key has write access' do
- write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key)
+ it 'shows all the projects the deploy key has write access' do
+ write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key)
- visit admin_deploy_keys_path
+ visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content(write_key.project.full_name)
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content(write_key.project.full_name)
end
+ end
- describe 'create a new deploy key' do
- let(:new_ssh_key) { attributes_for(:key)[:key] }
-
- before do
- visit admin_deploy_keys_path
- click_link 'New deploy key'
- end
-
- it 'creates a new deploy key' do
- fill_in 'deploy_key_title', with: 'laptop'
- fill_in 'deploy_key_key', with: new_ssh_key
- click_button 'Create'
-
- expect(current_path).to eq admin_deploy_keys_path
+ describe 'create a new deploy key' do
+ let(:new_ssh_key) { attributes_for(:key)[:key] }
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content('laptop')
- end
- end
+ before do
+ visit admin_deploy_keys_path
+ click_link 'New deploy key'
end
- describe 'update an existing deploy key' do
- before do
- visit admin_deploy_keys_path
- page.within('tr', text: deploy_key.title) do
- click_link(_('Edit deploy key'))
- end
- end
+ it 'creates a new deploy key' do
+ fill_in 'deploy_key_title', with: 'laptop'
+ fill_in 'deploy_key_key', with: new_ssh_key
+ click_button 'Create'
- it 'updates an existing deploy key' do
- fill_in 'deploy_key_title', with: 'new-title'
- click_button 'Save changes'
+ expect(current_path).to eq admin_deploy_keys_path
- expect(current_path).to eq admin_deploy_keys_path
-
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content('new-title')
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content('laptop')
end
end
end
- context 'when `admin_deploy_keys_vue` feature flag is enabled', :js do
- it_behaves_like 'renders deploy keys correctly'
-
- describe 'remove an existing deploy key' do
- before do
- visit admin_deploy_keys_path
+ describe 'update an existing deploy key' do
+ before do
+ visit admin_deploy_keys_path
+ page.within('tr', text: deploy_key.title) do
+ click_link(_('Edit deploy key'))
end
+ end
- it 'removes an existing deploy key' do
- accept_gl_confirm('Are you sure you want to delete this deploy key?', button_text: 'Delete') do
- page.within('tr', text: deploy_key.title) do
- click_button _('Delete deploy key')
- end
- end
+ it 'updates an existing deploy key' do
+ fill_in 'deploy_key_title', with: 'new-title'
+ click_button 'Save changes'
- expect(current_path).to eq admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).not_to have_content(deploy_key.title)
- end
+ expect(current_path).to eq admin_deploy_keys_path
+
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content('new-title')
end
end
end
- context 'when `admin_deploy_keys_vue` feature flag is disabled' do
+ describe 'remove an existing deploy key' do
before do
- stub_feature_flags(admin_deploy_keys_vue: false)
+ visit admin_deploy_keys_path
end
- it_behaves_like 'renders deploy keys correctly'
-
- describe 'remove an existing deploy key' do
- before do
- visit admin_deploy_keys_path
- end
-
- it 'removes an existing deploy key' do
+ it 'removes an existing deploy key' do
+ accept_gl_confirm('Are you sure you want to delete this deploy key?', button_text: 'Delete') do
page.within('tr', text: deploy_key.title) do
- click_link _('Remove deploy key')
+ click_button _('Delete deploy key')
end
+ end
- expect(current_path).to eq admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).not_to have_content(deploy_key.title)
- end
+ expect(current_path).to eq admin_deploy_keys_path
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).not_to have_content(deploy_key.title)
end
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index c53948ee6f3..ceb91b86876 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -21,12 +21,16 @@ RSpec.describe "Admin Runners" do
context "when there are runners" do
it 'has all necessary texts' do
- create(:ci_runner, :instance, contacted_at: Time.now)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: Time.now)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.week.ago)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.year.ago)
visit admin_runners_path
expect(page).to have_text "Register an instance runner"
- expect(page).to have_text "Online Runners 1"
+ expect(page).to have_text "Online runners 1"
+ expect(page).to have_text "Offline runners 2"
+ expect(page).to have_text "Stale runners 1"
end
it 'with an instance runner shows an instance badge' do
@@ -387,7 +391,11 @@ RSpec.describe "Admin Runners" do
it 'has all necessary texts including no runner message' do
expect(page).to have_text "Register an instance runner"
- expect(page).to have_text "Online Runners 0"
+
+ expect(page).to have_text "Online runners 0"
+ expect(page).to have_text "Offline runners 0"
+ expect(page).to have_text "Stale runners 0"
+
expect(page).to have_text 'No runners found'
end
@@ -451,7 +459,9 @@ RSpec.describe "Admin Runners" do
before do
click_on 'Reset registration token'
- page.accept_alert
+ within_modal do
+ click_button('OK', match: :first)
+ end
wait_for_requests
end
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index 3c2ade6b274..26338b03349 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -42,6 +42,9 @@ RSpec.describe 'Group Packages' do
let_it_be(:maven_package) { create(:maven_package, project: second_project, name: 'aaa', created_at: 2.days.ago, version: '2.0.0') }
let_it_be(:packages) { [npm_package, maven_package] }
+ let(:package) { packages.first }
+ let(:package_details_path) { group_package_path(group, package) }
+
it_behaves_like 'packages list', check_project_name: true
it_behaves_like 'package details link'
diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb
index 7fcc8200b1c..8180f6b9aff 100644
--- a/spec/features/projects/packages_spec.rb
+++ b/spec/features/projects/packages_spec.rb
@@ -35,6 +35,9 @@ RSpec.describe 'Packages' do
let_it_be(:maven_package) { create(:maven_package, project: project, name: 'aaa', created_at: 2.days.ago, version: '2.0.0') }
let_it_be(:packages) { [npm_package, maven_package] }
+ let(:package) { packages.first }
+ let(:package_details_path) { project_package_path(project, package) }
+
it_behaves_like 'packages list'
it_behaves_like 'package details link'
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index b5dd3576e8b..36e6cf72750 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -24,99 +24,109 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project)
end
- describe GraphQL::Query, type: :request do
- get_runners_query_name = 'get_runners.query.graphql'
-
+ describe do
before do
sign_in(admin)
enable_admin_mode!(admin)
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_runners_query_name = 'get_runners.query.graphql'
- it "#{fixtures_path}#{get_runners_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {})
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
+ end
- expect_graphql_errors_to_be_empty
- end
+ it "#{fixtures_path}#{get_runners_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
- it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
- post_graphql(query, current_user: admin, variables: { first: 2 })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
- end
- end
-
- describe GraphQL::Query, type: :request do
- get_runners_count_query_name = 'get_runners_count.query.graphql'
+ it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: admin, variables: { first: 2 })
- before do
- sign_in(admin)
- enable_admin_mode!(admin)
- end
-
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_runners_count_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {})
+ describe GraphQL::Query, type: :request do
+ get_runners_count_query_name = 'get_runners_count.query.graphql'
- expect_graphql_errors_to_be_empty
- end
- end
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
+ end
- describe GraphQL::Query, type: :request do
- get_runner_query_name = 'get_runner.query.graphql'
+ it "#{fixtures_path}#{get_runners_count_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
- before do
- sign_in(admin)
- enable_admin_mode!(admin)
+ expect_graphql_errors_to_be_empty
+ end
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_runner_query_name = 'get_runner.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
+ end
- it "#{fixtures_path}#{get_runner_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {
- id: instance_runner.to_global_id.to_s
- })
+ it "#{fixtures_path}#{get_runner_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: instance_runner.to_global_id.to_s
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
end
end
- describe GraphQL::Query, type: :request do
- get_group_runners_query_name = 'get_group_runners.query.graphql'
-
+ describe do
let_it_be(:group_owner) { create(:user) }
before do
group.add_owner(group_owner)
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_group_runners_query_name = 'get_group_runners.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_group_runners_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
- it "#{fixtures_path}#{get_group_runners_query_name}.json" do
- post_graphql(query, current_user: group_owner, variables: {
- groupFullPath: group.full_path
- })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
+ it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path,
+ first: 1
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
- post_graphql(query, current_user: group_owner, variables: {
- groupFullPath: group.full_path,
- first: 1
- })
+ describe GraphQL::Query, type: :request do
+ get_group_runners_count_query_name = 'get_group_runners_count.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_count_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_group_runners_count_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
end
end
end
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
index 570ac1e6ed1..92bc7596f7d 100644
--- a/spec/frontend/google_cloud/components/app_spec.js
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -24,6 +24,8 @@ const HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
+ deploymentsCloudRunUrl: '#url-deployments-cloud-run',
+ deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
};
describe('google_cloud App component', () => {
diff --git a/spec/frontend/google_cloud/components/deployments_service_table_spec.js b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
new file mode 100644
index 00000000000..76c3bfd00a8
--- /dev/null
+++ b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
@@ -0,0 +1,40 @@
+import { mount } from '@vue/test-utils';
+import { GlButton, GlTable } from '@gitlab/ui';
+import DeploymentsServiceTable from '~/google_cloud/components/deployments_service_table.vue';
+
+describe('google_cloud DeploymentsServiceTable component', () => {
+ let wrapper;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findButtons = () => findTable().findAllComponents(GlButton);
+ const findCloudRunButton = () => findButtons().at(0);
+ const findCloudStorageButton = () => findButtons().at(1);
+
+ beforeEach(() => {
+ const propsData = {
+ cloudRunUrl: '#url-deployments-cloud-run',
+ cloudStorageUrl: '#url-deployments-cloud-storage',
+ };
+ wrapper = mount(DeploymentsServiceTable, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should contain a table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('should contain configure cloud run button', () => {
+ const cloudRunButton = findCloudRunButton();
+ expect(cloudRunButton.exists()).toBe(true);
+ expect(cloudRunButton.props().disabled).toBe(true);
+ });
+
+ it('should contain configure cloud storage button', () => {
+ const cloudStorageButton = findCloudStorageButton();
+ expect(cloudStorageButton.exists()).toBe(true);
+ expect(cloudStorageButton.props().disabled).toBe(true);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js
index 9b4c3a79f11..3a009fc88ce 100644
--- a/spec/frontend/google_cloud/components/home_spec.js
+++ b/spec/frontend/google_cloud/components/home_spec.js
@@ -20,6 +20,8 @@ describe('google_cloud Home component', () => {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
+ deploymentsCloudRunUrl: '#url-deployments-cloud-run',
+ deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
};
beforeEach(() => {
@@ -42,7 +44,7 @@ describe('google_cloud Home component', () => {
it('should contain three tab items', () => {
expect(findTabItemsModel()).toEqual([
{ title: 'Configuration', disabled: undefined },
- { title: 'Deployments', disabled: '' },
+ { title: 'Deployments', disabled: undefined },
{ title: 'Services', disabled: '' },
]);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
index 16b573bb4a0..4520ae9c328 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
@@ -32,7 +32,7 @@ exports[`NpmInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy npm setup command"
- instruction="echo @gitlab-org:registry=npmPath/ >> .npmrc"
+ instruction="echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc"
label=""
trackingaction="copy_npm_setup_command"
trackinglabel="code_instruction"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
index 4ac979448d8..8c0e2d948ca 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
@@ -35,7 +35,7 @@ describe('NpmInstallation', () => {
function createComponent({ data = {} } = {}) {
wrapper = shallowMountExtended(NpmInstallation, {
provide: {
- npmPath: 'npmPath',
+ npmInstanceUrl: 'npmInstanceUrl',
},
propsData: {
packageEntity,
@@ -117,7 +117,7 @@ describe('NpmInstallation', () => {
it('renders the correct setup command', () => {
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo @gitlab-org:registry=npmPath/ >> .npmrc',
+ instruction: 'echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -139,7 +139,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: `echo @gitlab-org:registry=npmPath/ >> .npmrc`,
+ instruction: `echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc`,
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -161,7 +161,7 @@ describe('NpmInstallation', () => {
it('renders the correct registry command', () => {
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
@@ -183,7 +183,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index 165ee962417..18a99f70756 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -22,16 +22,20 @@ exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
>
- <gl-link-stub
+ <router-link-stub
+ ariacurrentvalue="page"
class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
- href="http://gdk.test:3000/gitlab-org/gitlab-test/-/packages/111"
+ data-testid="details-link"
+ event="click"
+ tag="a"
+ to="[object Object]"
>
<gl-truncate-stub
position="end"
text="@gitlab-org/package-15"
/>
- </gl-link-stub>
+ </router-link-stub>
<!---->
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 292667ec47c..9467a613b2a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,7 +1,11 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlSprintf } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
@@ -13,6 +17,9 @@ import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data';
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+
describe('packages_list_row', () => {
let wrapper;
@@ -28,7 +35,7 @@ describe('packages_list_row', () => {
const findDeleteButton = () => wrapper.findByTestId('action-delete');
const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
const findListItem = () => wrapper.findComponent(ListItem);
- const findPackageLink = () => wrapper.findComponent(GlLink);
+ const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
@@ -40,6 +47,7 @@ describe('packages_list_row', () => {
provide = defaultProvide,
} = {}) => {
wrapper = shallowMountExtended(PackagesListRow, {
+ localVue,
provide,
stubs: {
ListItem,
@@ -63,6 +71,15 @@ describe('packages_list_row', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('has a link to navigate to the details page', () => {
+ mountComponent();
+
+ expect(findPackageLink().props()).toMatchObject({
+ event: 'click',
+ to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } },
+ });
+ });
+
describe('tags', () => {
it('renders package tags when a package has tags', () => {
mountComponent({ packageEntity: packageWithTags });
@@ -120,7 +137,7 @@ describe('packages_list_row', () => {
});
it('details link is disabled', () => {
- expect(findPackageLink().attributes('disabled')).toBe('true');
+ expect(findPackageLink().props('event')).toBe('');
});
it('has a warning icon', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 8b7aa943ace..637e2edf3be 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -9,7 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
-import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
+import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
@@ -36,7 +36,7 @@ import {
packageFiles,
packageDestroyFileMutation,
packageDestroyFileMutationError,
-} from '../../mock_data';
+} from '../mock_data';
jest.mock('~/flash');
useMockLocationHelper();
@@ -47,18 +47,22 @@ describe('PackagesApp', () => {
let wrapper;
let apolloProvider;
+ const breadCrumbState = {
+ updateName: jest.fn(),
+ };
+
const provide = {
packageId: '111',
- svgPath: 'svgPath',
- npmPath: 'npmPath',
- npmHelpPath: 'npmHelpPath',
+ emptyListIllustration: 'svgPath',
projectListUrl: 'projectListUrl',
groupListUrl: 'groupListUrl',
+ breadCrumbState,
};
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
+ routeId = '1',
} = {}) {
localVue.use(VueApollo);
@@ -84,6 +88,13 @@ describe('PackagesApp', () => {
GlTabs,
GlTab,
},
+ mocks: {
+ $route: {
+ params: {
+ id: routeId,
+ },
+ },
+ },
});
}
@@ -172,6 +183,15 @@ describe('PackagesApp', () => {
});
});
+ it('calls the appropriate function to set the breadcrumbState', async () => {
+ const { name, version } = packageData();
+ createComponent();
+
+ await waitForPromises();
+
+ expect(breadCrumbState.updateName).toHaveBeenCalledWith(`${name} v ${version}`);
+ });
+
describe('delete package', () => {
const originalReferrer = document.referrer;
const setReferrer = (value = packageDetailsQuery().data.package.project.name) => {
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index 7044c1285d8..fa02d60e440 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -24,16 +24,20 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class="gl-breadcrumb-separator"
data-testid="separator"
>
- <svg
- aria-hidden="true"
- class="gl-icon s8"
- data-testid="angle-right-icon"
- role="img"
+ <span
+ class="gl-mx-n5"
>
- <use
- href="#angle-right"
- />
- </svg>
+ <svg
+ aria-hidden="true"
+ class="gl-icon s8"
+ data-testid="angle-right-icon"
+ role="img"
+ >
+ <use
+ href="#angle-right"
+ />
+ </svg>
+ </span>
</span>
</a>
</li>
diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
index d97851a1d55..570323826d1 100644
--- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,11 +1,14 @@
+import VueApollo from 'vue-apollo';
import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { escape } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
import ValidationSegment, {
i18n,
} from '~/pipeline_editor/components/header/validation_segment.vue';
+import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import {
CI_CONFIG_STATUS_INVALID,
EDITOR_APP_STATUS_EMPTY,
@@ -21,12 +24,29 @@ import {
mockYmlHelpPagePath,
} from '../../mock_data';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
describe('Validation segment component', () => {
let wrapper;
- const createComponent = ({ props = {}, appStatus }) => {
+ const mockApollo = createMockApollo();
+
+ const createComponent = ({ props = {}, appStatus = EDITOR_APP_STATUS_INVALID }) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ app: {
+ __typename: 'PipelineEditorApp',
+ status: appStatus,
+ },
+ },
+ });
+
wrapper = extendedWrapper(
shallowMount(ValidationSegment, {
+ localVue,
+ apolloProvider: mockApollo,
provide: {
ymlHelpPagePath: mockYmlHelpPagePath,
lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath,
@@ -36,12 +56,6 @@ describe('Validation segment component', () => {
ciFileContent: mockCiYml,
...props,
},
- // Simulate graphQL client query result
- data() {
- return {
- appStatus,
- };
- },
}),
);
};
@@ -99,6 +113,7 @@ describe('Validation segment component', () => {
appStatus: EDITOR_APP_STATUS_INVALID,
});
});
+
it('has warning icon', () => {
expect(findIcon().props('name')).toBe('warning-solid');
});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index d7f5bb43e41..42be691ba4c 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -13,6 +13,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
@@ -37,7 +38,6 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockActiveRunnersCount = '2';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -54,6 +54,7 @@ describe('AdminRunnersApp', () => {
let mockRunnersQuery;
let mockRunnersCountQuery;
+ const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
@@ -70,15 +71,16 @@ describe('AdminRunnersApp', () => {
[getRunnersCountQuery, mockRunnersCountQuery],
];
- wrapper = mountFn(AdminRunnersApp, {
- localVue,
- apolloProvider: createMockApollo(handlers),
- propsData: {
- registrationToken: mockRegistrationToken,
- activeRunnersCount: mockActiveRunnersCount,
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ mountFn(AdminRunnersApp, {
+ localVue,
+ apolloProvider: createMockApollo(handlers),
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
+ }),
+ );
};
beforeEach(async () => {
@@ -95,6 +97,18 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
+ it('shows total runner counts', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ const stats = findRunnerStats().text();
+
+ expect(stats).toMatch('Online runners 4');
+ expect(stats).toMatch('Offline runners 4');
+ expect(stats).toMatch('Stale runners 4');
+ });
+
it('shows the runner tabs with a runner count for each type', async () => {
mockRunnersCountQuery.mockImplementation(({ type }) => {
let count;
@@ -198,12 +212,6 @@ describe('AdminRunnersApp', () => {
]);
});
- it('shows the active runner count', () => {
- createComponent({ mountFn: mount });
-
- expect(wrapper.text()).toMatch(new RegExp(`Online Runners ${mockActiveRunnersCount}`));
- });
-
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 4e93d6bcaa3..e75decddf70 100644
--- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -9,6 +9,7 @@ import RegistrationTokenResetDropdownItem from '~/runner/components/registration
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -18,14 +19,18 @@ localVue.use(VueApollo);
localVue.use(GlToast);
const mockNewToken = 'NEW_TOKEN';
+const modalID = 'token-reset-modal';
describe('RegistrationTokenResetDropdownItem', () => {
let wrapper;
let runnersRegistrationTokenResetMutationHandler;
let showToast;
+ const mockEvent = { preventDefault: jest.fn() };
const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const clickSubmit = () => findModal().vm.$emit('primary', mockEvent);
const createComponent = ({ props, provide = {} } = {}) => {
wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
@@ -38,6 +43,9 @@ describe('RegistrationTokenResetDropdownItem', () => {
apolloProvider: createMockApollo([
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
+ directives: {
+ GlModal: createMockDirective(),
+ },
});
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
@@ -54,8 +62,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
});
createComponent();
-
- jest.spyOn(window, 'confirm');
});
afterEach(() => {
@@ -66,6 +72,18 @@ describe('RegistrationTokenResetDropdownItem', () => {
expect(findDropdownItem().exists()).toBe(true);
});
+ describe('modal directive integration', () => {
+ it('has the correct ID on the dropdown', () => {
+ const binding = getBinding(findDropdownItem().element, 'gl-modal');
+
+ expect(binding.value).toBe(modalID);
+ });
+
+ it('has the correct ID on the modal', () => {
+ expect(findModal().props('modalId')).toBe(modalID);
+ });
+ });
+
describe('On click and confirmation', () => {
const mockGroupId = '11';
const mockProjectId = '22';
@@ -82,9 +100,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
props: { type },
});
- window.confirm.mockReturnValueOnce(true);
-
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
});
@@ -114,7 +131,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
describe('On click without confirmation', () => {
beforeEach(async () => {
- window.confirm.mockReturnValueOnce(false);
findDropdownItem().vm.$emit('click');
await waitForPromises();
});
@@ -142,8 +158,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
- window.confirm.mockReturnValueOnce(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
expect(createAlert).toHaveBeenLastCalledWith({
@@ -168,8 +184,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
},
});
- window.confirm.mockReturnValueOnce(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
expect(createAlert).toHaveBeenLastCalledWith({
@@ -184,8 +200,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
describe('Immediately after click', () => {
it('shows loading state', async () => {
- window.confirm.mockReturnValue(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
diff --git a/spec/frontend/runner/components/stat/runner_online_stat_spec.js b/spec/frontend/runner/components/stat/runner_online_stat_spec.js
deleted file mode 100644
index 18f865aa22c..00000000000
--- a/spec/frontend/runner/components/stat/runner_online_stat_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { shallowMount, mount } from '@vue/test-utils';
-import RunnerOnlineBadge from '~/runner/components/stat/runner_online_stat.vue';
-
-describe('RunnerOnlineBadge', () => {
- let wrapper;
-
- const findSingleStat = () => wrapper.findComponent(GlSingleStat);
-
- const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
- wrapper = mountFn(RunnerOnlineBadge, {
- propsData: {
- value: '99',
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Uses a success appearance', () => {
- createComponent({}, shallowMount);
-
- expect(findSingleStat().props('variant')).toBe('success');
- });
-
- it('Renders a value', () => {
- createComponent({}, mount);
-
- expect(wrapper.text()).toMatch(new RegExp(`Online Runners 99\\s+online`));
- });
-});
diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js
new file mode 100644
index 00000000000..68db8621ef0
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_stats_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
+import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+
+describe('RunnerStats', () => {
+ let wrapper;
+
+ const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(RunnerStats, {
+ propsData: {
+ onlineRunnersCount: 3,
+ offlineRunnersCount: 2,
+ staleRunnersCount: 1,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays all the stats', () => {
+ createComponent({ mountFn: mount });
+
+ const stats = wrapper.text();
+
+ expect(stats).toMatch('Online runners 3');
+ expect(stats).toMatch('Offline runners 2');
+ expect(stats).toMatch('Stale runners 1');
+ });
+
+ it.each`
+ i | status
+ ${0} | ${STATUS_ONLINE}
+ ${1} | ${STATUS_OFFLINE}
+ ${2} | ${STATUS_STALE}
+ `('Displays status types at index $i', ({ i, status }) => {
+ createComponent();
+
+ expect(findRunnerStatusStatAt(i).props('status')).toBe(status);
+ });
+});
diff --git a/spec/frontend/runner/components/stat/runner_status_stat_spec.js b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
new file mode 100644
index 00000000000..3218272eac7
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
@@ -0,0 +1,67 @@
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+
+describe('RunnerStatusStat', () => {
+ let wrapper;
+
+ const findSingleStat = () => wrapper.findComponent(GlSingleStat);
+
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(RunnerStatusStat, {
+ propsData: {
+ status: STATUS_ONLINE,
+ value: 99,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ status | variant | title | badge
+ ${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'}
+ ${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'}
+ ${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'}
+ `('Renders a stat for status "$status"', ({ status, variant, title, badge }) => {
+ beforeEach(() => {
+ createComponent({ props: { status } }, mount);
+ });
+
+ it('Renders text', () => {
+ expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`));
+ });
+
+ it(`Uses variant ${variant}`, () => {
+ expect(findSingleStat().props('variant')).toBe(variant);
+ });
+ });
+
+ it('Formats stat number', () => {
+ createComponent({ props: { value: 1000 } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners 1,000');
+ });
+
+ it('Shows a null result', () => {
+ createComponent({ props: { value: null } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners -');
+ });
+
+ it('Shows an undefined result', () => {
+ createComponent({ props: { value: undefined } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners -');
+ });
+
+ it('Shows result for an unknown status', () => {
+ createComponent({ props: { status: 'UNKNOWN' } }, mount);
+
+ expect(wrapper.text()).toMatch('Runners 99');
+ });
+});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 0ce6feceb5b..034b7848f35 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -12,6 +12,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
@@ -26,10 +27,11 @@ import {
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
+import getGroupRunnersCountQuery from '~/runner/graphql/get_group_runners_count.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data';
+import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -48,7 +50,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GroupRunnersApp', () => {
let wrapper;
let mockGroupRunnersQuery;
+ let mockGroupRunnersCountQuery;
+ const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
@@ -59,7 +63,10 @@ describe('GroupRunnersApp', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
- const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]];
+ const handlers = [
+ [getGroupRunnersQuery, mockGroupRunnersQuery],
+ [getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
+ ];
wrapper = mountFn(GroupRunnersApp, {
localVue,
@@ -77,11 +84,24 @@ describe('GroupRunnersApp', () => {
setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`);
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
+ mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData);
createComponent();
await waitForPromises();
});
+ it('shows total runner counts', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ const stats = findRunnerStats().text();
+
+ expect(stats).toMatch('Online runners 2');
+ expect(stats).toMatch('Offline runners 2');
+ expect(stats).toMatch('Stale runners 2');
+ });
+
it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
@@ -129,28 +149,6 @@ describe('GroupRunnersApp', () => {
);
});
- describe('shows the active runner count', () => {
- const expectedOnlineCount = (count) => new RegExp(`Online Runners ${count}`);
-
- it('with a regular value', () => {
- createComponent({ mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount(mockGroupRunnersLimitedCount));
- });
-
- it('at the limit', () => {
- createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount('1,000'));
- });
-
- it('over the limit', () => {
- createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount('1,000\\+'));
- });
- });
-
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index d4f265b2d7b..9c430e205ea 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -8,6 +8,7 @@ import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.js
// Group queries
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
+import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runners_count.query.graphql.json';
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json';
export {
@@ -16,5 +17,6 @@ export {
runnersDataPaginated,
runnersData,
groupRunnersData,
+ groupRunnersCountData,
groupRunnersDataPaginated,
};
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 32b1a0bff06..37ecce3886d 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -5,16 +5,16 @@ export const textProviderIds = [101, 102];
export const securityTrainingProviders = [
{
id: textProviderIds[0],
- name: 'Kontra',
- description: 'Interactive developer security education.',
- url: 'https://application.security/',
+ name: 'Vendor Name 1',
+ description: 'Interactive developer security education',
+ url: 'https://www.example.org/security/training',
isEnabled: false,
},
{
id: textProviderIds[1],
- name: 'SecureCodeWarrior',
+ name: 'Vendor Name 2',
description: 'Security training with guide and learning pathways.',
- url: 'https://www.securecodewarrior.com/',
+ url: 'https://www.vendornametwo.com/',
isEnabled: true,
},
];
diff --git a/spec/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/graphql/mutations/issues/set_escalation_status_spec.rb
new file mode 100644
index 00000000000..d41118b1812
--- /dev/null
+++ b/spec/graphql/mutations/issues/set_escalation_status_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Issues::SetEscalationStatus do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue, reload: true) { create(:incident, project: project) }
+ let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, issue: issue) }
+
+ let(:status) { :acknowledged }
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+
+ describe '#resolve' do
+ let(:args) { { status: status } }
+ let(:mutated_issue) { result[:issue] }
+
+ subject(:result) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, **args) }
+
+ it_behaves_like 'permission level for issue mutation is correctly verified', true
+
+ context 'when the user can update the issue' do
+ before_all do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'permission level for issue mutation is correctly verified', true
+
+ context 'when the user can update the escalation status' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it 'returns the issue with the escalation policy' do
+ expect(mutated_issue).to eq(issue)
+ expect(mutated_issue.escalation_status.status_name).to eq(status)
+ expect(result[:errors]).to be_empty
+ end
+
+ it 'returns errors when issue update fails' do
+ issue.update_column(:author_id, nil)
+
+ expect(result[:errors]).not_to be_empty
+ end
+
+ context 'with non-incident issue is provided' do
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
+ end
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/incident_management/escalation_status_enum_spec.rb b/spec/graphql/types/incident_management/escalation_status_enum_spec.rb
new file mode 100644
index 00000000000..b39d4d9324e
--- /dev/null
+++ b/spec/graphql/types/incident_management/escalation_status_enum_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['IssueEscalationStatus'] do
+ specify { expect(described_class.graphql_name).to eq('IssueEscalationStatus') }
+
+ describe 'statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status_name, :status_value) do
+ 'TRIGGERED' | :triggered
+ 'ACKNOWLEDGED' | :acknowledged
+ 'RESOLVED' | :resolved
+ 'IGNORED' | :ignored
+ 'INVALID' | nil
+ end
+
+ with_them do
+ it 'exposes a status with the correct value' do
+ expect(described_class.values[status_name]&.value).to eq(status_value)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 1b8bf007a73..1d4590cbb4e 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
confidential hidden discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
design_collection alert_management_alert severity current_user_todos moved moved_to
- create_note_email timelogs project_id customer_relations_contacts]
+ create_note_email timelogs project_id customer_relations_contacts escalation_status]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
@@ -257,4 +257,49 @@ RSpec.describe GitlabSchema.types['Issue'] do
end
end
end
+
+ describe 'escalation_status' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue, reload: true) { create(:issue, project: project) }
+
+ let(:execute) { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ issue(iid: "#{issue.iid}") {
+ escalationStatus
+ }
+ }
+ }
+ )
+ end
+
+ subject(:status) { execute.dig('data', 'project', 'issue', 'escalationStatus') }
+
+ it { is_expected.to be_nil }
+
+ context 'for an incident' do
+ before do
+ issue.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it { is_expected.to be_nil }
+
+ context 'with an escalation status record' do
+ let!(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
+
+ it { is_expected.to eq(escalation_status.status_name.to_s.upcase) }
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index 9694c7cb4b7..832b4da0e20 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -79,8 +79,7 @@ RSpec.describe Ci::RunnersHelper do
it 'returns the data in format' do
expect(helper.admin_runners_data_attributes).to eq({
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
- registration_token: Gitlab::CurrentSettings.runners_registration_token,
- active_runners_count: '0'
+ registration_token: Gitlab::CurrentSettings.runners_registration_token
})
end
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
deleted file mode 100644
index 663040e0ca7..00000000000
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ /dev/null
@@ -1,716 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Redis::MultiStore do
- using RSpec::Parameterized::TableSyntax
-
- let_it_be(:redis_store_class) do
- Class.new(Gitlab::Redis::Wrapper) do
- def config_file_name
- config_file_name = "spec/fixtures/config/redis_new_format_host.yml"
- Rails.root.join(config_file_name).to_s
- end
-
- def self.name
- 'Sessions'
- end
- end
- end
-
- let_it_be(:primary_db) { 1 }
- let_it_be(:secondary_db) { 2 }
- let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) }
- let_it_be(:instance_name) { 'TestStore' }
- let_it_be(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- subject { multi_store.send(name, *args) }
-
- before do
- skip_feature_flags_yaml_validation
- skip_default_enabled_yaml_check
- end
-
- after(:all) do
- primary_store.flushdb
- secondary_store.flushdb
- end
-
- context 'when primary_store is nil' do
- let(:multi_store) { described_class.new(nil, secondary_store, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /primary_store is required/)
- end
- end
-
- context 'when secondary_store is nil' do
- let(:multi_store) { described_class.new(primary_store, nil, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /secondary_store is required/)
- end
- end
-
- context 'when instance_name is nil' do
- let(:instance_name) { nil }
- let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /instance_name is required/)
- end
- end
-
- context 'when primary_store is not a ::Redis instance' do
- before do
- allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid primary_store/)
- end
- end
-
- context 'when secondary_store is not a ::Redis instance' do
- before do
- allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid secondary_store/)
- end
- end
-
- context 'with READ redis commands' do
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:value1) { "redis_value1"}
- let_it_be(:value2) { "redis_value2"}
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:keys) { [key1, key2] }
- let_it_be(:values) { [value1, value2] }
- let_it_be(:svalues) { [value2, value1] }
-
- where(:case_name, :name, :args, :value, :block) do
- 'execute :get command' | :get | ref(:key1) | ref(:value1) | nil
- 'execute :mget command' | :mget | ref(:keys) | ref(:values) | nil
- 'execute :mget with block' | :mget | ref(:keys) | ref(:values) | ->(value) { value }
- 'execute :smembers command' | :smembers | ref(:skey) | ref(:svalues) | nil
- 'execute :scard command' | :scard | ref(:skey) | 2 | nil
- end
-
- before(:all) do
- primary_store.multi do |multi|
- multi.set(key1, value1)
- multi.set(key2, value2)
- multi.sadd(skey, value1)
- multi.sadd(skey, value2)
- end
-
- secondary_store.multi do |multi|
- multi.set(key1, value1)
- multi.set(key2, value2)
- multi.sadd(skey, value1)
- multi.sadd(skey, value2)
- end
- end
-
- RSpec.shared_examples_for 'reads correct value' do
- it 'returns the correct value' do
- if value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values (unsorted set)
- is_expected.to match_array(value)
- else
- is_expected.to eq(value)
- end
- end
- end
-
- RSpec.shared_examples_for 'fallback read from the secondary store' do
- let(:counter) { Gitlab::Metrics::NullMetric.instance }
-
- before do
- allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
- end
-
- it 'fallback and execute on secondary instance' do
- expect(secondary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- it 'logs the ReadFromPrimaryError' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError),
- hash_including(command_name: name, extra: hash_including(instance_name: instance_name)))
-
- subject
- end
-
- it 'increment read fallback count metrics' do
- expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
-
- subject
- end
-
- include_examples 'reads correct value'
-
- context 'when fallback read from the secondary instance raises an exception' do
- before do
- allow(secondary_store).to receive(name).with(*args).and_raise(StandardError)
- end
-
- it 'fails with exception' do
- expect { subject }.to raise_error(StandardError)
- end
- end
- end
-
- RSpec.shared_examples_for 'secondary store' do
- it 'execute on the secondary instance' do
- expect(secondary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- include_examples 'reads correct value'
-
- it 'does not execute on the primary store' do
- expect(primary_store).not_to receive(name)
-
- subject
- end
- end
-
- with_them do
- describe "#{name}" do
- before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when reading from the primary is successful' do
- it 'returns the correct value' do
- expect(primary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
-
- subject
- end
-
- include_examples 'reads correct value'
- end
-
- context 'when reading from primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).with(*args).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
-
- it 'logs the exception' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(extra: hash_including(:multi_store_error_message, instance_name: instance_name),
- command_name: name))
-
- subject
- end
-
- include_examples 'fallback read from the secondary store'
- end
-
- context 'when reading from primary instance return no value' do
- before do
- allow(primary_store).to receive(name).and_return(nil)
- end
-
- include_examples 'fallback read from the secondary store'
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.send(name, *args)
- end
- end
-
- it 'is executed only 1 time on primary instance' do
- expect(primary_store).to receive(name).with(*args).once
-
- subject
- end
- end
-
- if params[:block]
- subject do
- multi_store.send(name, *args, &block)
- end
-
- context 'when block is provided' do
- it 'yields to the block' do
- expect(primary_store).to receive(name).and_yield(value)
-
- subject
- end
-
- include_examples 'reads correct value'
- end
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it_behaves_like 'secondary store'
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'execute on the primary instance' do
- expect(primary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- include_examples 'reads correct value'
-
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
-
- subject
- end
- end
- end
-
- context 'with both primary and secondary store using same redis instance' do
- let(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let(:secondary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- it_behaves_like 'secondary store'
- end
- end
- end
- end
-
- context 'with WRITE redis commands' do
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:value1) { "redis_value1"}
- let_it_be(:value2) { "redis_value2"}
- let_it_be(:key1_value1) { [key1, value1] }
- let_it_be(:key1_value2) { [key1, value2] }
- let_it_be(:ttl) { 10 }
- let_it_be(:key1_ttl_value1) { [key1, ttl, value1] }
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:svalues1) { [value2, value1] }
- let_it_be(:svalues2) { [value1] }
- let_it_be(:skey_value1) { [skey, value1] }
- let_it_be(:skey_value2) { [skey, value2] }
-
- where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do
- 'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1)
- 'execute :setnx command' | :setnx | ref(:key1_value2) | ref(:value1) | :get | ref(:key2)
- 'execute :setex command' | :setex | ref(:key1_ttl_value1) | ref(:ttl) | :ttl | ref(:key1)
- 'execute :sadd command' | :sadd | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
- 'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey)
- 'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2)
- 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
- end
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
-
- primary_store.multi do |multi|
- multi.set(key2, value1)
- multi.sadd(skey, value1)
- end
-
- secondary_store.multi do |multi|
- multi.set(key2, value1)
- multi.sadd(skey, value1)
- end
- end
-
- RSpec.shared_examples_for 'verify that store contains values' do |store|
- it "#{store} redis store contains correct values", :aggregate_errors do
- subject
-
- redis_store = multi_store.send(store)
-
- if expected_value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values
- expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
- else
- expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
- end
- end
- end
-
- with_them do
- describe "#{name}" do
- let(:expected_args) {args || no_args }
-
- before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when executing on primary instance is successful' do
- it 'executes on both primary and secondary redis store', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'when executing on the primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
-
- it 'logs the exception and execute on secondary instance', :aggregate_errors do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(extra: hash_including(:multi_store_error_message), command_name: name))
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.send(name, *args)
- end
- end
-
- it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args).once
- expect(secondary_store).to receive(name).with(*expected_args).once
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'executes only on the secondary redis store', :aggregate_errors do
- expect(secondary_store).to receive(name).with(*expected_args)
- expect(primary_store).not_to receive(name).with(*expected_args)
-
- subject
- end
-
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'executes only on the primary_redis redis store', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args)
- expect(secondary_store).not_to receive(name).with(*expected_args)
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- end
- end
- end
- end
- end
-
- context 'with unsupported command' do
- let(:counter) { Gitlab::Metrics::NullMetric.instance }
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
- allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
- end
-
- let_it_be(:key) { "redis:counter" }
-
- subject { multi_store.incr(key) }
-
- it 'executes method missing' do
- expect(multi_store).to receive(:method_missing)
-
- subject
- end
-
- context 'when command is not in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do
- it 'logs MethodMissingError' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError),
- hash_including(command_name: :incr, extra: hash_including(instance_name: instance_name)))
-
- subject
- end
-
- it 'increments method missing counter' do
- expect(counter).to receive(:increment).with(command: :incr, instance_name: instance_name)
-
- subject
- end
- end
-
- context 'when command is in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do
- subject { multi_store.info }
-
- it 'does not log MethodMissingError' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- subject
- end
-
- it 'does not increment method missing counter' do
- expect(counter).not_to receive(:increment)
-
- subject
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'fallback and executes only on the secondary store', :aggregate_errors do
- expect(primary_store).to receive(:incr).with(key).and_call_original
- expect(secondary_store).not_to receive(:incr)
-
- subject
- end
-
- it 'correct value is stored on the secondary store', :aggregate_errors do
- subject
-
- expect(secondary_store.get(key)).to be_nil
- expect(primary_store.get(key)).to eq('1')
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'fallback and executes only on the secondary store', :aggregate_errors do
- expect(secondary_store).to receive(:incr).with(key).and_call_original
- expect(primary_store).not_to receive(:incr)
-
- subject
- end
-
- it 'correct value is stored on the secondary store', :aggregate_errors do
- subject
-
- expect(primary_store.get(key)).to be_nil
- expect(secondary_store.get(key)).to eq('1')
- end
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.incr(key)
- end
- end
-
- it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(:incr).with(key).once
- expect(secondary_store).to receive(:incr).with(key).once
-
- subject
- end
-
- it "both redis stores are containing correct values", :aggregate_errors do
- subject
-
- expect(primary_store.get(key)).to eq('1')
- expect(secondary_store.get(key)).to eq('1')
- end
- end
- end
-
- describe '#to_s' do
- subject { multi_store.to_s }
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(primary_store.to_s)
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(primary_store.to_s)
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(secondary_store.to_s)
- end
- end
- end
- end
-
- describe '#is_a?' do
- it 'returns true for ::Redis::Store' do
- expect(multi_store.is_a?(::Redis::Store)).to be true
- end
- end
-
- describe '#use_primary_and_secondary_stores?' do
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be true
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context 'with empty DB' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context 'when FF table guard raises' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
- end
-
- describe '#use_primary_store_as_default?' do
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_store_as_default?).to be true
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_store_as_default?).to be false
- end
- end
-
- context 'with empty DB' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context 'when FF table guard raises' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
- end
-
- def create_redis_store(options, extras = {})
- ::Redis::Store.new(options.merge(extras))
- end
-end
diff --git a/spec/lib/gitlab/redis/sessions_spec.rb b/spec/lib/gitlab/redis/sessions_spec.rb
index 6ecbbf3294d..b02864cb73d 100644
--- a/spec/lib/gitlab/redis/sessions_spec.rb
+++ b/spec/lib/gitlab/redis/sessions_spec.rb
@@ -6,31 +6,16 @@ RSpec.describe Gitlab::Redis::Sessions do
it_behaves_like "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState
describe 'redis instance used in connection pool' do
- before do
+ around do |example|
clear_pool
- end
-
- after do
+ example.run
+ ensure
clear_pool
end
- context 'when redis.sessions configuration is not provided' do
- it 'uses ::Redis instance' do
- expect(described_class).to receive(:config_fallback?).and_return(true)
-
- described_class.pool.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Redis)
- end
- end
- end
-
- context 'when redis.sessions configuration is provided' do
- it 'instantiates an instance of MultiStore' do
- expect(described_class).to receive(:config_fallback?).and_return(false)
-
- described_class.pool.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
- end
+ it 'uses ::Redis instance' do
+ described_class.pool.with do |redis_instance|
+ expect(redis_instance).to be_instance_of(::Redis)
end
end
@@ -44,49 +29,9 @@ RSpec.describe Gitlab::Redis::Sessions do
describe '#store' do
subject(:store) { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
- context 'when redis.sessions configuration is NOT provided' do
- it 'instantiates ::Redis instance' do
- expect(described_class).to receive(:config_fallback?).and_return(true)
- expect(store).to be_instance_of(::Redis::Store)
- end
- end
-
- context 'when redis.sessions configuration is provided' do
- let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
-
- before do
- redis_clear_raw_config!(Gitlab::Redis::Sessions)
- redis_clear_raw_config!(Gitlab::Redis::SharedState)
- allow(described_class).to receive(:config_fallback?).and_return(false)
- end
-
- after do
- redis_clear_raw_config!(Gitlab::Redis::Sessions)
- redis_clear_raw_config!(Gitlab::Redis::SharedState)
- end
-
- # Check that Gitlab::Redis::Sessions is configured as MultiStore with proper attrs.
- it 'instantiates an instance of MultiStore', :aggregate_failures do
- expect(described_class).to receive(:config_file_name).and_return(config_new_format_host)
- expect(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
-
- expect(store).to be_instance_of(::Gitlab::Redis::MultiStore)
-
- expect(store.primary_store.to_s).to eq("Redis Client connected to test-host:6379 against DB 99 with namespace session:gitlab")
- expect(store.secondary_store.to_s).to eq("Redis Client connected to /path/to/redis.sock against DB 0 with namespace session:gitlab")
-
- expect(store.instance_name).to eq('Sessions')
- end
-
- context 'when MultiStore correctly configured' do
- before do
- allow(described_class).to receive(:config_file_name).and_return(config_new_format_host)
- allow(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
- end
-
- it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_sessions, :use_primary_store_as_default_for_sessions
- end
+ # Check that Gitlab::Redis::Sessions is configured as RedisStore.
+ it 'instantiates an instance of Redis::Store' do
+ expect(store).to be_instance_of(::Redis::Store)
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
index 3fca65cc5a4..4d84423cde4 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
let_it_be(:issues) { Issue.all }
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(Issue.connection).to receive(:transaction_open?).and_return(false)
end
it 'calculates a correct result' do
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
index 64eff76a9f2..a8cf87d9364 100644
--- a/spec/lib/gitlab/usage_data_queries_spec.rb
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -3,10 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::UsageDataQueries do
- before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
- end
-
describe '#add_metric' do
let(:metric) { 'CountBoardsMetric' }
diff --git a/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
new file mode 100644
index 00000000000..c5058f30d82
--- /dev/null
+++ b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_migration!
+
+def create_background_migration_jobs(ids, status, created_at)
+ proper_status = case status
+ when :pending
+ Gitlab::Database::BackgroundMigrationJob.statuses['pending']
+ when :succeeded
+ Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
+ else
+ raise ArgumentError
+ end
+
+ background_migration_jobs.create!(
+ class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ arguments: Array(ids),
+ status: proper_status,
+ created_at: created_at
+ )
+end
+
+RSpec.describe MarkRecalculateFindingSignaturesAsCompleted, :migration do
+ let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+
+ context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do
+ before do
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4))
+
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2))
+ create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4))
+ end
+
+ describe 'gitlab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'marks all jobs as succeeded' do
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+
+ migrate!
+
+ expect(background_migration_jobs.where(status: 1).count).to eq(5)
+ end
+ end
+
+ describe 'self managed' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not change job status' do
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+
+ migrate!
+
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 35398e29062..40bdfd4bc92 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -211,12 +211,6 @@ RSpec.describe AlertManagement::Alert do
end
end
- describe '.open' do
- subject { described_class.open }
-
- it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert) }
- end
-
describe '.not_resolved' do
subject { described_class.not_resolved }
@@ -324,33 +318,6 @@ RSpec.describe AlertManagement::Alert do
end
end
- describe '.open_status?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:status, :is_open_status) do
- :triggered | true
- :acknowledged | true
- :resolved | false
- :ignored | false
- nil | false
- end
-
- with_them do
- it 'returns true when the status is open status' do
- expect(described_class.open_status?(status)).to eq(is_open_status)
- end
- end
- end
-
- describe '#open?' do
- it 'returns true when the status is open status' do
- expect(triggered_alert.open?).to be true
- expect(acknowledged_alert.open?).to be true
- expect(resolved_alert.open?).to be false
- expect(ignored_alert.open?).to be false
- end
- end
-
describe '#to_reference' do
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index e3816c31f1c..8f66978c311 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -239,44 +239,20 @@ RSpec.describe Ci::Runner do
end
end
- context 'when ci_decompose_belonging_to_parent_group_of_project_query is enabled' do
- context 'when use_traversal_ids* are enabled' do
- it_behaves_like '.belonging_to_parent_group_of_project'
- end
-
- context 'when use_traversal_ids* are disabled' do
- before do
- stub_feature_flags(
- use_traversal_ids: false,
- use_traversal_ids_for_ancestors: false,
- use_traversal_ids_for_ancestor_scopes: false
- )
- end
-
- it_behaves_like '.belonging_to_parent_group_of_project'
- end
+ context 'when use_traversal_ids* are enabled' do
+ it_behaves_like '.belonging_to_parent_group_of_project'
end
- context 'when ci_decompose_belonging_to_parent_group_of_project_query is disabled' do
+ context 'when use_traversal_ids* are disabled' do
before do
- stub_feature_flags(ci_decompose_belonging_to_parent_group_of_project_query: false)
- end
-
- context 'when use_traversal_ids* are enabled' do
- it_behaves_like '.belonging_to_parent_group_of_project'
+ stub_feature_flags(
+ use_traversal_ids: false,
+ use_traversal_ids_for_ancestors: false,
+ use_traversal_ids_for_ancestor_scopes: false
+ )
end
- context 'when use_traversal_ids* are disabled' do
- before do
- stub_feature_flags(
- use_traversal_ids: false,
- use_traversal_ids_for_ancestors: false,
- use_traversal_ids_for_ancestor_scopes: false
- )
- end
-
- it_behaves_like '.belonging_to_parent_group_of_project'
- end
+ it_behaves_like '.belonging_to_parent_group_of_project'
end
describe '.owned_or_instance_wide' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b523f60d045..d4c105619cd 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -542,6 +542,13 @@ RSpec.describe User do
expect(user).to be_invalid
expect(user.errors.messages[:email].first).to eq(expected_error)
end
+
+ it 'does not allow user to update email to a non-allowlisted domain' do
+ user = create(:user, email: "info@test.example.com")
+
+ expect { user.update!(email: "test@notexample.com") }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
end
context 'when a signup domain is allowed and subdomains are not allowed' do
@@ -608,6 +615,13 @@ RSpec.describe User do
user = build(:user, email: 'info@example.com', created_by_id: 1)
expect(user).to be_valid
end
+
+ it 'does not allow user to update email to a denied domain' do
+ user = create(:user, email: 'info@test.com')
+
+ expect { user.update!(email: 'info@example.com') }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
end
context 'when a signup domain is denied but a wildcard subdomain is allowed' do
@@ -679,6 +693,13 @@ RSpec.describe User do
expect(user.errors.messages[:email].first).to eq(expected_error)
end
+ it 'does not allow user to update email to a restricted domain' do
+ user = create(:user, email: 'info@test.com')
+
+ expect { user.update!(email: 'info@gitlab.com') }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
+
it 'does accept a valid email address' do
user = build(:user, email: 'info@test.com')
diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb
index 8553d0bfdb0..add9bd18755 100644
--- a/spec/models/users_statistics_spec.rb
+++ b/spec/models/users_statistics_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe UsersStatistics do
create_list(:user, 2, :bot)
create_list(:user, 1, :blocked)
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(described_class.connection).to receive(:transaction_open?).and_return(false)
end
context 'when successful' do
diff --git a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
new file mode 100644
index 00000000000..0166871502b
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Setting the escalation status of an incident' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:incident, project: project) }
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
+ let_it_be(:user) { create(:user) }
+
+ let(:status) { 'ACKNOWLEDGED' }
+ let(:input) { { project_path: project.full_path, iid: issue.iid.to_s, status: status } }
+
+ let(:current_user) { user }
+ let(:mutation) do
+ graphql_mutation(:issue_set_escalation_status, input) do
+ <<~QL
+ clientMutationId
+ errors
+ issue {
+ iid
+ escalationStatus
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:issue_set_escalation_status) }
+
+ before_all do
+ project.add_developer(user)
+ end
+
+ context 'when user does not have permission to edit the escalation status' do
+ let(:current_user) { create(:user) }
+
+ before_all do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'with non-incident issue is provided' do
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
+ end
+
+ it 'sets given escalation_policy to the escalation status for the issue' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['issue']['escalationStatus']).to eq(status)
+ expect(escalation_status.reload.status_name).to eq(:acknowledged)
+ end
+
+ context 'when status argument is not given' do
+ let(:input) { {} }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { contain_exactly(include('status (Expected value to not be null)')) }
+ end
+ end
+
+ context 'when status argument is invalid' do
+ let(:status) { 'INVALID' }
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: :status
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index b3e91afb5b3..f358ec3e53f 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -539,6 +539,43 @@ RSpec.describe 'getting an issue list for a project' do
end
end
+ context 'when fetching escalation status' do
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) }
+
+ let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } }
+ let(:fields) do
+ <<~QUERY
+ edges {
+ node {
+ id
+ escalationStatus
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issue_a.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it 'returns the escalation status values' do
+ post_graphql(query, current_user: current_user)
+
+ statuses = issues_data.map { |issue| issue.dig('node', 'escalationStatus') }
+
+ expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ base_count = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) }
+
+ new_incident = create(:incident, project: project)
+ create(:incident_management_issuable_escalation_status, issue: new_incident)
+
+ expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(base_count)
+ end
+ end
+
describe 'N+1 query checks' do
let(:extra_iid_for_second_query) { issue_b.iid.to_s }
let(:search_params) { { iids: [issue_a.iid.to_s] } }
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 98ec02d59c6..9cc5a245333 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -161,30 +161,6 @@ RSpec.describe Ci::CreatePipelineService do
expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
end
-
- # TODO: remove after ci_publish_pipeline_events FF is removed
- # https://gitlab.com/gitlab-org/gitlab/-/issues/336752
- it 'does not schedule sync update for the head pipeline of the merge request' do
- expect(UpdateHeadPipelineForMergeRequestWorker)
- .not_to receive(:perform_async)
-
- execute_service(ref: 'feature', after: nil)
- end
- end
-
- context 'when feature flag ci_publish_pipeline_events is disabled' do
- before do
- stub_feature_flags(ci_publish_pipeline_events: false)
- end
-
- it 'schedules update for the head pipeline of the merge request' do
- expect(UpdateHeadPipelineForMergeRequestWorker)
- .to receive(:perform_async).with(merge_request_1.id)
- expect(UpdateHeadPipelineForMergeRequestWorker)
- .to receive(:perform_async).with(merge_request_2.id)
-
- execute_service(ref: 'feature', after: nil)
- end
end
end
diff --git a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
new file mode 100644
index 00000000000..78c93fd4591
--- /dev/null
+++ b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateService do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, :triggered) }
+ let_it_be(:issue, reload: true) { escalation_status.issue }
+ let_it_be(:project) { issue.project }
+ let_it_be(:alert) { create(:alert_management_alert, issue: issue, project: project) }
+
+ let(:status_event) { :acknowledge }
+ let(:update_params) { { incident_management_issuable_escalation_status_attributes: { status_event: status_event } } }
+ let(:service) { IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user) }
+
+ subject(:result) do
+ issue.update!(update_params)
+ service.execute
+ end
+
+ before do
+ issue.project.add_developer(current_user)
+ end
+
+ shared_examples 'does not attempt to update the alert' do
+ specify do
+ expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new)
+
+ expect(result).to be_success
+ end
+ end
+
+ context 'with status attributes' do
+ it 'updates an the associated alert with status changes' do
+ expect(::AlertManagement::Alerts::UpdateService)
+ .to receive(:new)
+ .with(alert, current_user, { status: :acknowledged })
+ .and_call_original
+
+ expect(result).to be_success
+ expect(alert.reload.status).to eq(escalation_status.reload.status)
+ end
+
+ context 'when incident is not associated with an alert' do
+ before do
+ alert.destroy!
+ end
+
+ it_behaves_like 'does not attempt to update the alert'
+ end
+
+ context 'when status was not changed' do
+ let(:status_event) { :trigger }
+
+ it_behaves_like 'does not attempt to update the alert'
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 98d2ab1341e..969d07ae4bd 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1166,9 +1166,15 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'updating escalation status' do
let(:opts) { { escalation_status: { status: 'acknowledged' } } }
+ let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService }
shared_examples 'updates the escalation status record' do |expected_status|
+ let(:service_double) { instance_double(escalation_update_class) }
+
it 'has correct value' do
+ expect(escalation_update_class).to receive(:new).with(issue, user).and_return(service_double)
+ expect(service_double).to receive(:execute)
+
update_issue(opts)
expect(issue.escalation_status.status_name).to eq(expected_status)
@@ -1185,7 +1191,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'does not trigger side-effects' do
- expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new)
+ expect(escalation_update_class).not_to receive(:new)
update_issue(opts)
end
@@ -1207,6 +1213,7 @@ RSpec.describe Issues::UpdateService, :mailer do
it 'syncs the update back to the alert' do
update_issue(opts)
+ expect(issue.escalation_status.status_name).to eq(:acknowledged)
expect(alert.reload.status_name).to eq(:acknowledged)
end
end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index d14b4638ca5..ded30f32314 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -19,14 +19,12 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
RSpec.shared_examples 'package details link' do |property|
- let(:package) { packages.first }
-
it 'navigates to the correct url' do
page.within(packages_table_selector) do
click_link package.name
end
- expect(page).to have_current_path(project_package_path(package.project, package))
+ expect(page).to have_current_path(package_details_path)
expect(page).to have_css('.packages-app h2[data-testid="title"]', text: package.name)
diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
deleted file mode 100644
index 046c70bf779..00000000000
--- a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default|
- context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do
- before do
- stub_feature_flags(use_primary_and_secondary_stores => true)
- end
-
- it 'multi store is enabled' do
- expect(subject.use_primary_and_secondary_stores?).to be true
- end
- end
-
- context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do
- before do
- stub_feature_flags(use_primary_and_secondary_stores => false)
- end
-
- it 'multi store is disabled' do
- expect(subject.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context "with feature flag :#{use_primary_store_as_default} is enabled" do
- before do
- stub_feature_flags(use_primary_store_as_default => true)
- end
-
- it 'primary store is enabled' do
- expect(subject.use_primary_store_as_default?).to be true
- end
- end
-
- context "with feature flag :#{use_primary_store_as_default} is disabled" do
- before do
- stub_feature_flags(use_primary_store_as_default => false)
- end
-
- it 'primary store is disabled' do
- expect(subject.use_primary_store_as_default?).to be false
- end
- end
-end
diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
index 7b33a95bfa1..8ee76efc896 100644
--- a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
@@ -95,6 +95,12 @@ RSpec.shared_examples 'a model including Escalatable' do
it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) }
end
end
+
+ describe '.open' do
+ subject { all_escalatables.open }
+
+ it { is_expected.to contain_exactly(acknowledged_escalatable, triggered_escalatable) }
+ end
end
describe '.status_value' do
@@ -133,6 +139,24 @@ RSpec.shared_examples 'a model including Escalatable' do
end
end
+ describe '.open_status?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :is_open_status) do
+ :triggered | true
+ :acknowledged | true
+ :resolved | false
+ :ignored | false
+ nil | false
+ end
+
+ with_them do
+ it 'returns true when the status is open status' do
+ expect(described_class.open_status?(status)).to eq(is_open_status)
+ end
+ end
+ end
+
describe '#trigger' do
subject { escalatable.trigger }
@@ -237,6 +261,15 @@ RSpec.shared_examples 'a model including Escalatable' do
end
end
+ describe '#open?' do
+ it 'returns true when the status is open status' do
+ expect(triggered_escalatable.open?).to be true
+ expect(acknowledged_escalatable.open?).to be true
+ expect(resolved_escalatable.open?).to be false
+ expect(ignored_escalatable.open?).to be false
+ end
+ end
+
private
def factory_from_class(klass)