Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7bd8f9822b
commit
8a70817cd9
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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">
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import packageApp from '~/packages_and_registries/package_registry/index';
|
||||
|
||||
const app = packageApp();
|
||||
|
||||
if (app) {
|
||||
app.attachBreadcrumb();
|
||||
app.attachMainComponent();
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import packageApp from '~/packages_and_registries/package_registry/index';
|
||||
|
||||
packageApp();
|
|
@ -0,0 +1,8 @@
|
|||
import packageApp from '~/packages_and_registries/package_registry/index';
|
||||
|
||||
const app = packageApp();
|
||||
|
||||
if (app) {
|
||||
app.attachBreadcrumb();
|
||||
app.attachMainComponent();
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import packageApp from '~/packages_and_registries/package_registry/index';
|
||||
|
||||
packageApp();
|
|
@ -1,3 +0,0 @@
|
|||
import initPackageDetails from '~/packages_and_registries/package_registry/pages/details';
|
||||
|
||||
initPackageDetails();
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -13,7 +13,7 @@ query getGroupRunners(
|
|||
$sort: CiRunnerSort
|
||||
) {
|
||||
group(fullPath: $groupFullPath) {
|
||||
id
|
||||
id # Apollo required
|
||||
runners(
|
||||
membership: DESCENDANTS
|
||||
before: $before
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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) } }
|
||||
|
|
|
@ -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: '' } }
|
||||
|
|
|
@ -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) }
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -30,7 +30,7 @@
|
|||
},
|
||||
"status": {
|
||||
"type": ["string"],
|
||||
"enum": ["active", "deprecated", "removed", "broken"]
|
||||
"enum": ["active", "removed", "broken"]
|
||||
},
|
||||
"milestone": {
|
||||
"type": ["string"],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
649360f4069aac4784f4d039015f8dda3f4bae28e8132f841e25b48f034a392e
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
|
||||
expect(page).to have_content('laptop')
|
||||
end
|
||||
end
|
||||
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 '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).to have_content('new-title')
|
||||
end
|
||||
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
|
||||
|
||||
context 'when `admin_deploy_keys_vue` feature flag is enabled', :js do
|
||||
it_behaves_like 'renders deploy keys correctly'
|
||||
it 'shows all the projects the deploy key has write access' do
|
||||
write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key)
|
||||
|
||||
describe 'remove an existing deploy key' do
|
||||
before do
|
||||
visit admin_deploy_keys_path
|
||||
end
|
||||
visit admin_deploy_keys_path
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
|
||||
expect(page).to have_content(write_key.project.full_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `admin_deploy_keys_vue` feature flag is disabled' do
|
||||
describe 'create a new deploy key' do
|
||||
let(:new_ssh_key) { attributes_for(:key)[:key] }
|
||||
|
||||
before do
|
||||
stub_feature_flags(admin_deploy_keys_vue: false)
|
||||
visit admin_deploy_keys_path
|
||||
click_link 'New deploy key'
|
||||
end
|
||||
|
||||
it_behaves_like 'renders deploy keys correctly'
|
||||
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'
|
||||
|
||||
describe 'remove an existing deploy key' do
|
||||
before do
|
||||
visit 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('laptop')
|
||||
end
|
||||
end
|
||||
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 '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).to have_content('new-title')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remove an existing deploy key' do
|
||||
before do
|
||||
visit admin_deploy_keys_path
|
||||
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 'removes an existing deploy key' do
|
||||
page.within('tr', text: deploy_key.title) do
|
||||
click_link _('Remove deploy key')
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}")
|
||||
describe GraphQL::Query, type: :request do
|
||||
get_runners_query_name = 'get_runners.query.graphql'
|
||||
|
||||
let_it_be(:query) do
|
||||
get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_runners_query_name}.json" do
|
||||
post_graphql(query, current_user: admin, variables: {})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_runners_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
|
||||
let_it_be(:query) do
|
||||
get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_runners_count_query_name}.json" do
|
||||
post_graphql(query, current_user: admin, variables: {})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
|
||||
post_graphql(query, current_user: admin, variables: { first: 2 })
|
||||
describe GraphQL::Query, type: :request do
|
||||
get_runner_query_name = 'get_runner.query.graphql'
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
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
|
||||
})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe GraphQL::Query, type: :request do
|
||||
get_runners_count_query_name = 'get_runners_count.query.graphql'
|
||||
|
||||
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}")
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_runners_count_query_name}.json" do
|
||||
post_graphql(query, current_user: admin, variables: {})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe GraphQL::Query, type: :request do
|
||||
get_runner_query_name = 'get_runner.query.graphql'
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
enable_admin_mode!(admin)
|
||||
end
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
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}")
|
||||
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
|
||||
})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
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
|
||||
})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_group_runners_query_name}.json" do
|
||||
post_graphql(query, current_user: group_owner, variables: {
|
||||
groupFullPath: group.full_path
|
||||
})
|
||||
describe GraphQL::Query, type: :request do
|
||||
get_group_runners_count_query_name = 'get_group_runners_count.query.graphql'
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
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_query_name}.paginated.json" do
|
||||
post_graphql(query, current_user: group_owner, variables: {
|
||||
groupFullPath: group.full_path,
|
||||
first: 1
|
||||
})
|
||||
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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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: '' },
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
||||
<!---->
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) => {
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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`));
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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}`);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
example.run
|
||||
ensure
|
||||
clear_pool
|
||||
end
|
||||
|
||||
after do
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue