Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7fcda12793
commit
9578c9f9e8
|
@ -24,7 +24,6 @@
|
|||
/doc/administration/troubleshooting @axil @marcia @mjang1
|
||||
/doc/ci/ @marcel.amirault @sselhorn
|
||||
/doc/ci/environments/ @axil
|
||||
/doc/ci/release/ @axil
|
||||
/doc/ci/services/ @sselhorn
|
||||
/doc/ci/test_cases/ @msedlakjakubowski
|
||||
/doc/development/ @marcia @mjang1
|
||||
|
@ -205,15 +204,16 @@ Dangerfile @gl-quality/eng-prod
|
|||
# Secure & Threat Management ownership delineation
|
||||
# https://about.gitlab.com/handbook/engineering/development/threat-management/delineate-secure-threat-management.html#technical-boundaries
|
||||
[Threat Insights]
|
||||
/app/models/vulnerability.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/finders/security/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/models/security/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/models/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/models/vulnerability.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/policies/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/lib/api/vulnerabilit*.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/spec/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/spec/policies/vulnerabilities/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/spec/policies/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
|
||||
[Secure]
|
||||
/ee/lib/gitlab/ci/parsers/license_compliance/ @gitlab-org/secure/composition-analysis-be
|
||||
/ee/lib/gitlab/ci/parsers/security/ @gitlab-org/secure/composition-analysis-be @gitlab-org/secure/dynamic-analysis-be @gitlab-org/secure/static-analysis-be @gitlab-org/secure/fuzzing-be
|
||||
|
@ -260,9 +260,7 @@ Dangerfile @gl-quality/eng-prod
|
|||
[Product Intelligence]
|
||||
/ee/lib/gitlab/usage_data_counters/ @gitlab-org/growth/product-intelligence/engineers
|
||||
/ee/lib/ee/gitlab/usage_data.rb @gitlab-org/growth/product-intelligence/engineers
|
||||
/lib/gitlab/grafana_embed_usage_data.rb @gitlab-org/growth/product-intelligence/engineers
|
||||
/lib/gitlab/usage_data.rb @gitlab-org/growth/product_intelligence/engineers
|
||||
/lib/gitlab/cycle_analytics/usage_data.rb @gitlab-org/growth/product-intelligence/engineers
|
||||
/lib/gitlab/usage_data_counters/ @gitlab-org/growth/product-intelligence/engineers
|
||||
|
||||
[Growth Experiments]
|
||||
|
|
|
@ -162,6 +162,7 @@ setup-test-env:
|
|||
- tmp/tests/gitaly/gitaly-lfs-smudge
|
||||
- tmp/tests/gitaly/gitaly-ssh
|
||||
- tmp/tests/gitaly/internal/
|
||||
- tmp/tests/gitaly/internal_gitaly2/
|
||||
- tmp/tests/gitaly/internal_sockets/
|
||||
- tmp/tests/gitaly/Makefile
|
||||
- tmp/tests/gitaly/praefect
|
||||
|
|
|
@ -73,21 +73,19 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center gl-h-full">
|
||||
<div>
|
||||
<gl-modal
|
||||
:modal-id="modalId"
|
||||
:title="$options.modal.title"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-cancel="$options.modal.actionCancel"
|
||||
@ok="$emit('deleteSelectedDesigns')"
|
||||
@ok="$emit('delete-selected-designs')"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'DesignManagement|Archived designs will still be available in previous versions of the design collection.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
{{
|
||||
s__(
|
||||
'DesignManagement|Archived designs will still be available in previous versions of the design collection.',
|
||||
)
|
||||
}}
|
||||
</gl-modal>
|
||||
<gl-button
|
||||
v-gl-modal-directive="modalId"
|
||||
|
|
|
@ -55,6 +55,7 @@ export default {
|
|||
iid,
|
||||
}"
|
||||
:update="updateStoreAfterDelete"
|
||||
:tag="null"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<slot v-bind="{ mutate, loading, error }"></slot>
|
||||
|
|
|
@ -130,7 +130,7 @@ export default {
|
|||
button-icon="archive"
|
||||
button-category="secondary"
|
||||
:title="s__('DesignManagement|Archive design')"
|
||||
@deleteSelectedDesigns="$emit('delete')"
|
||||
@delete-selected-designs="$emit('delete')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
|
|
@ -50,7 +50,7 @@ export default {
|
|||
type="file"
|
||||
name="design_file"
|
||||
:accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
|
||||
class="hide"
|
||||
class="gl-display-none"
|
||||
multiple
|
||||
@change="onFileUploadChange"
|
||||
/>
|
||||
|
|
|
@ -365,7 +365,8 @@ export default {
|
|||
v-if="isLatestVersion"
|
||||
variant="link"
|
||||
size="small"
|
||||
class="gl-mr-4 js-select-all"
|
||||
class="gl-mr-3"
|
||||
data-testid="select-all-designs-button"
|
||||
@click="toggleDesignsSelection"
|
||||
>{{ selectAllButtonText }}
|
||||
</gl-button>
|
||||
|
@ -385,7 +386,7 @@ export default {
|
|||
data-qa-selector="archive_button"
|
||||
:loading="loading"
|
||||
:has-selected-designs="hasSelectedDesigns"
|
||||
@deleteSelectedDesigns="mutate()"
|
||||
@delete-selected-designs="mutate()"
|
||||
>
|
||||
{{ s__('DesignManagement|Archive selected') }}
|
||||
</delete-button>
|
||||
|
|
|
@ -7,6 +7,7 @@ import PipelineGraph from './graph_component.vue';
|
|||
import {
|
||||
getQueryHeaders,
|
||||
reportToSentry,
|
||||
serializeGqlErr,
|
||||
toggleQueryPollingByVisibility,
|
||||
unwrapPipelineData,
|
||||
} from './utils';
|
||||
|
@ -60,8 +61,8 @@ export default {
|
|||
update(data) {
|
||||
return unwrapPipelineData(this.pipelineProjectPath, data);
|
||||
},
|
||||
error() {
|
||||
this.reportFailure(LOAD_FAILURE);
|
||||
error({ gqlError }) {
|
||||
this.reportFailure(LOAD_FAILURE, serializeGqlErr(gqlError));
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -112,10 +113,10 @@ export default {
|
|||
refreshPipelineGraph() {
|
||||
this.$apollo.queries.pipeline.refetch();
|
||||
},
|
||||
reportFailure(type) {
|
||||
reportFailure(type, err = '') {
|
||||
this.showAlert = true;
|
||||
this.alertType = type;
|
||||
reportToSentry(this.$options.name, this.alertType);
|
||||
reportToSentry(this.$options.name, `type: ${this.alertType}, info: ${err}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import LinkedPipeline from './linked_pipeline.vue';
|
|||
import {
|
||||
getQueryHeaders,
|
||||
reportToSentry,
|
||||
serializeGqlErr,
|
||||
toggleQueryPollingByVisibility,
|
||||
unwrapPipelineData,
|
||||
validateConfigPaths,
|
||||
|
@ -99,12 +100,14 @@ export default {
|
|||
this.loadingPipelineId = null;
|
||||
this.$emit('scrollContainer');
|
||||
},
|
||||
error(err, _vm, _key, type) {
|
||||
error({ gqlError }, _vm, _key, type) {
|
||||
this.$emit('error', LOAD_FAILURE);
|
||||
|
||||
reportToSentry(
|
||||
'linked_pipelines_column',
|
||||
`error type: ${LOAD_FAILURE}, error: ${err}, apollo error type: ${type}`,
|
||||
`error type: ${LOAD_FAILURE}, error: ${serializeGqlErr(
|
||||
gqlError,
|
||||
)}, apollo error type: ${type}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -23,7 +23,6 @@ const getQueryHeaders = (etagResource) => {
|
|||
},
|
||||
};
|
||||
};
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
|
||||
const reportToSentry = (component, failureType) => {
|
||||
Sentry.withScope((scope) => {
|
||||
|
@ -32,6 +31,25 @@ const reportToSentry = (component, failureType) => {
|
|||
});
|
||||
};
|
||||
|
||||
const serializeGqlErr = (gqlError) => {
|
||||
if (!gqlError) {
|
||||
return 'gqlError data not available.';
|
||||
}
|
||||
|
||||
const { locations, message, path } = gqlError;
|
||||
|
||||
return `
|
||||
${message}.
|
||||
Locations: ${locations
|
||||
.flatMap((loc) => Object.entries(loc))
|
||||
.flat(2)
|
||||
.join(' ')}.
|
||||
Path: ${path.join(', ')}.
|
||||
`;
|
||||
};
|
||||
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
|
||||
const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => {
|
||||
const stopStartQuery = (query) => {
|
||||
if (!Visibility.hidden()) {
|
||||
|
@ -82,6 +100,7 @@ const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
|
|||
export {
|
||||
getQueryHeaders,
|
||||
reportToSentry,
|
||||
serializeGqlErr,
|
||||
toggleQueryPollingByVisibility,
|
||||
unwrapPipelineData,
|
||||
validateConfigPaths,
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import eventHub from '../../event_hub';
|
||||
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
|
||||
import PipelinesManualActions from './pipelines_manual_actions.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
cancelTitle: __('Cancel'),
|
||||
redeployTitle: __('Retry'),
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlModalDirective,
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
PipelinesManualActions,
|
||||
PipelinesArtifactsComponent,
|
||||
},
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
cancelingPipeline: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isRetrying: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
displayPipelineActions() {
|
||||
return (
|
||||
this.pipeline.flags.retryable ||
|
||||
this.pipeline.flags.cancelable ||
|
||||
this.pipeline.details.manual_actions.length ||
|
||||
this.pipeline.details.artifacts.length
|
||||
);
|
||||
},
|
||||
actions() {
|
||||
if (!this.pipeline || !this.pipeline.details) {
|
||||
return [];
|
||||
}
|
||||
const { details } = this.pipeline;
|
||||
return [...(details.manual_actions || []), ...(details.scheduled_actions || [])];
|
||||
},
|
||||
isCancelling() {
|
||||
return this.cancelingPipeline === this.pipeline.id;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
pipeline() {
|
||||
this.isRetrying = false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleCancelClick() {
|
||||
eventHub.$emit('openConfirmationModal', {
|
||||
pipeline: this.pipeline,
|
||||
endpoint: this.pipeline.cancel_path,
|
||||
});
|
||||
},
|
||||
handleRetryClick() {
|
||||
this.isRetrying = true;
|
||||
eventHub.$emit('retryPipeline', this.pipeline.retry_path);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="displayPipelineActions" class="gl-text-right">
|
||||
<div class="btn-group">
|
||||
<pipelines-manual-actions v-if="actions.length > 0" :actions="actions" />
|
||||
|
||||
<pipelines-artifacts-component
|
||||
v-if="pipeline.details.artifacts.length"
|
||||
:artifacts="pipeline.details.artifacts"
|
||||
/>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.flags.retryable"
|
||||
v-gl-tooltip.hover
|
||||
:aria-label="$options.i18n.redeployTitle"
|
||||
:title="$options.i18n.redeployTitle"
|
||||
:disabled="isRetrying"
|
||||
:loading="isRetrying"
|
||||
class="js-pipelines-retry-button"
|
||||
data-qa-selector="pipeline_retry_button"
|
||||
icon="repeat"
|
||||
variant="default"
|
||||
category="secondary"
|
||||
@click="handleRetryClick"
|
||||
/>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.flags.cancelable"
|
||||
v-gl-tooltip.hover
|
||||
v-gl-modal-directive="'confirmation-modal'"
|
||||
:aria-label="$options.i18n.cancelTitle"
|
||||
:title="$options.i18n.cancelTitle"
|
||||
:loading="isCancelling"
|
||||
:disabled="isCancelling"
|
||||
icon="close"
|
||||
variant="danger"
|
||||
category="primary"
|
||||
class="js-pipelines-cancel-button"
|
||||
@click="handleCancelClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -29,7 +29,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div :class="classes" data-testid="pipeline-triggerer">
|
||||
<user-avatar-link
|
||||
v-if="user"
|
||||
:link-href="user.path"
|
||||
|
|
|
@ -61,7 +61,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div :class="classes" data-testid="pipeline-url-table-cell">
|
||||
<gl-link
|
||||
:href="pipeline.path"
|
||||
data-testid="pipeline-url-link"
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<script>
|
||||
import { CHILD_VIEW } from '~/pipelines/constants';
|
||||
import CommitComponent from '~/vue_shared/components/commit.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CommitComponent,
|
||||
},
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
viewType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
commitAuthor() {
|
||||
let commitAuthorInformation;
|
||||
|
||||
if (!this.pipeline || !this.pipeline.commit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. person who is an author of a commit might be a GitLab user
|
||||
if (this.pipeline.commit.author) {
|
||||
// 2. if person who is an author of a commit is a GitLab user
|
||||
// they can have a GitLab avatar
|
||||
if (this.pipeline.commit.author.avatar_url) {
|
||||
commitAuthorInformation = this.pipeline.commit.author;
|
||||
|
||||
// 3. If GitLab user does not have avatar, they might have a Gravatar
|
||||
} else if (this.pipeline.commit.author_gravatar_url) {
|
||||
commitAuthorInformation = {
|
||||
...this.pipeline.commit.author,
|
||||
avatar_url: this.pipeline.commit.author_gravatar_url,
|
||||
};
|
||||
}
|
||||
// 4. If committer is not a GitLab User, they can have a Gravatar
|
||||
} else {
|
||||
commitAuthorInformation = {
|
||||
avatar_url: this.pipeline.commit.author_gravatar_url,
|
||||
path: `mailto:${this.pipeline.commit.author_email}`,
|
||||
username: this.pipeline.commit.author_name,
|
||||
};
|
||||
}
|
||||
|
||||
return commitAuthorInformation;
|
||||
},
|
||||
commitTag() {
|
||||
return this.pipeline?.ref?.tag;
|
||||
},
|
||||
commitRef() {
|
||||
return this.pipeline?.ref;
|
||||
},
|
||||
commitUrl() {
|
||||
return this.pipeline?.commit?.commit_path;
|
||||
},
|
||||
commitShortSha() {
|
||||
return this.pipeline?.commit?.short_id;
|
||||
},
|
||||
commitTitle() {
|
||||
return this.pipeline?.commit?.title;
|
||||
},
|
||||
isChildView() {
|
||||
return this.viewType === CHILD_VIEW;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<commit-component
|
||||
:tag="commitTag"
|
||||
:commit-ref="commitRef"
|
||||
:commit-url="commitUrl"
|
||||
:merge-request-ref="pipeline.merge_request"
|
||||
:short-sha="commitShortSha"
|
||||
:title="commitTitle"
|
||||
:author="commitAuthor"
|
||||
:show-ref-info="!isChildView"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import { CHILD_VIEW } from '~/pipelines/constants';
|
||||
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CiBadge,
|
||||
},
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
viewType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pipelineStatus() {
|
||||
return this.pipeline?.details?.status ?? {};
|
||||
},
|
||||
isChildView() {
|
||||
return this.viewType === CHILD_VIEW;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ci-badge
|
||||
:status="pipelineStatus"
|
||||
:show-text="!isChildView"
|
||||
:icon-classes="'gl-vertical-align-middle!'"
|
||||
data-qa-selector="pipeline_commit_status"
|
||||
/>
|
||||
</template>
|
|
@ -1,15 +1,93 @@
|
|||
<script>
|
||||
import { GlTable, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import eventHub from '../../event_hub';
|
||||
import PipelineOperations from './pipeline_operations.vue';
|
||||
import PipelineStopModal from './pipeline_stop_modal.vue';
|
||||
import PipelineTriggerer from './pipeline_triggerer.vue';
|
||||
import PipelineUrl from './pipeline_url.vue';
|
||||
import PipelinesCommit from './pipelines_commit.vue';
|
||||
import PipelinesStatusBadge from './pipelines_status_badge.vue';
|
||||
import PipelinesTableRowComponent from './pipelines_table_row.vue';
|
||||
import PipelineStage from './stage.vue';
|
||||
import PipelinesTimeago from './time_ago.vue';
|
||||
|
||||
const DEFAULT_TD_CLASS = 'gl-p-5!';
|
||||
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
|
||||
const DEFAULT_TH_CLASSES =
|
||||
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'status',
|
||||
label: s__('Pipeline|Status'),
|
||||
thClass: DEFAULT_TH_CLASSES,
|
||||
columnClass: 'gl-w-10p',
|
||||
tdClass: DEFAULT_TD_CLASS,
|
||||
thAttr: { 'data-testid': 'status-th' },
|
||||
},
|
||||
{
|
||||
key: 'pipeline',
|
||||
label: s__('Pipeline|Pipeline'),
|
||||
thClass: DEFAULT_TH_CLASSES,
|
||||
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
|
||||
columnClass: 'gl-w-10p',
|
||||
thAttr: { 'data-testid': 'pipeline-th' },
|
||||
},
|
||||
{
|
||||
key: 'triggerer',
|
||||
label: s__('Pipeline|Triggerer'),
|
||||
thClass: DEFAULT_TH_CLASSES,
|
||||
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
|
||||
columnClass: 'gl-w-10p',
|
||||
thAttr: { 'data-testid': 'triggerer-th' },
|
||||
},
|
||||
{
|
||||
key: 'commit',
|
||||
label: s__('Pipeline|Commit'),
|
||||
thClass: DEFAULT_TH_CLASSES,
|
||||
tdClass: DEFAULT_TD_CLASS,
|
||||
columnClass: 'gl-w-20p',
|
||||
thAttr: { 'data-testid': 'commit-th' },
|
||||
},
|
||||
{
|
||||
key: 'stages',
|
||||
label: s__('Pipeline|Stages'),
|
||||
thClass: DEFAULT_TH_CLASSES,
|
||||
tdClass: DEFAULT_TD_CLASS,
|
||||
columnClass: 'gl-w-15p',
|
||||
thAttr: { 'data-testid': 'stages-th' },
|
||||
},
|
||||
{
|
||||
key: 'timeago',
|
||||
label: s__('Pipeline|Duration'),
|
||||
thClass: DEFAULT_TH_CLASSES,
|
||||
tdClass: DEFAULT_TD_CLASS,
|
||||
columnClass: 'gl-w-15p',
|
||||
thAttr: { 'data-testid': 'timeago-th' },
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
thClass: DEFAULT_TH_CLASSES,
|
||||
tdClass: DEFAULT_TD_CLASS,
|
||||
columnClass: 'gl-w-20p',
|
||||
thAttr: { 'data-testid': 'actions-th' },
|
||||
},
|
||||
],
|
||||
components: {
|
||||
GlTable,
|
||||
PipelinesTableRowComponent,
|
||||
PipelinesCommit,
|
||||
PipelineOperations,
|
||||
PipelineStage,
|
||||
PipelinesStatusBadge,
|
||||
PipelineStopModal,
|
||||
PipelinesTableRowComponent,
|
||||
PipelinesTimeago,
|
||||
PipelineTriggerer,
|
||||
PipelineUrl,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -43,11 +121,6 @@ export default {
|
|||
cancelingPipeline: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
legacyTableClass() {
|
||||
return !this.glFeatures.newPipelinesTable ? 'ci-table' : '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
pipelines() {
|
||||
this.cancelingPipeline = null;
|
||||
|
@ -73,8 +146,8 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :class="legacyTableClass">
|
||||
<div v-if="!glFeatures.newPipelinesTable" data-testid="ci-table">
|
||||
<div class="ci-table">
|
||||
<div v-if="!glFeatures.newPipelinesTable" data-testid="legacy-ci-table">
|
||||
<div class="gl-responsive-table-row table-row-header" role="row">
|
||||
<div class="table-section section-10 js-pipeline-status" role="rowheader">
|
||||
{{ s__('Pipeline|Status') }}
|
||||
|
@ -107,7 +180,71 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<gl-table v-else />
|
||||
<gl-table
|
||||
v-else
|
||||
:fields="$options.fields"
|
||||
:items="pipelines"
|
||||
tbody-tr-class="commit"
|
||||
:tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }"
|
||||
stacked="lg"
|
||||
fixed
|
||||
>
|
||||
<template #head(actions)>
|
||||
<slot name="table-header-actions"></slot>
|
||||
</template>
|
||||
|
||||
<template #table-colgroup="{ fields }">
|
||||
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />
|
||||
</template>
|
||||
|
||||
<template #cell(status)="{ item }">
|
||||
<pipelines-status-badge :pipeline="item" :view-type="viewType" />
|
||||
</template>
|
||||
|
||||
<template #cell(pipeline)="{ item }">
|
||||
<pipeline-url
|
||||
class="gl-text-truncate"
|
||||
:pipeline="item"
|
||||
:pipeline-schedule-url="pipelineScheduleUrl"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(triggerer)="{ item }">
|
||||
<pipeline-triggerer :pipeline="item" />
|
||||
</template>
|
||||
|
||||
<template #cell(commit)="{ item }">
|
||||
<pipelines-commit :pipeline="item" :view-type="viewType" />
|
||||
</template>
|
||||
|
||||
<template #cell(stages)="{ item }">
|
||||
<div class="stage-cell">
|
||||
<div></div>
|
||||
<template v-if="item.details.stages.length > 0">
|
||||
<div
|
||||
v-for="(stage, index) in item.details.stages"
|
||||
:key="index"
|
||||
class="stage-container dropdown"
|
||||
data-testid="widget-mini-pipeline-graph"
|
||||
>
|
||||
<pipeline-stage
|
||||
:type="$options.pipelinesTable"
|
||||
:stage="stage"
|
||||
:update-dropdown="updateGraphDropdown"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(timeago)="{ item }">
|
||||
<pipelines-timeago :pipeline="item" />
|
||||
</template>
|
||||
|
||||
<template #cell(actions)="{ item }">
|
||||
<pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
|
||||
</template>
|
||||
</gl-table>
|
||||
|
||||
<pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
|
||||
</div>
|
||||
|
|
|
@ -33,3 +33,5 @@ export const LOAD_FAILURE = 'load_failure';
|
|||
export const PARSE_FAILURE = 'parse_failure';
|
||||
export const POST_FAILURE = 'post_failure';
|
||||
export const UNSUPPORTED_DATA = 'unsupported_data';
|
||||
|
||||
export const CHILD_VIEW = 'child';
|
||||
|
|
|
@ -11,6 +11,12 @@ export default {
|
|||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
|
||||
sectionTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -84,7 +90,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<gl-dropdown-section-header>
|
||||
<gl-dropdown-section-header v-if="showHeader">
|
||||
<div class="gl-display-flex align-items-center" data-testid="section-header">
|
||||
<span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
|
||||
<gl-badge variant="neutral">{{ totalCountText }}</gl-badge>
|
||||
|
|
|
@ -8,9 +8,16 @@ import {
|
|||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import { debounce, isArray } from 'lodash';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { SEARCH_DEBOUNCE_MS, DEFAULT_I18N } from '../constants';
|
||||
import {
|
||||
ALL_REF_TYPES,
|
||||
SEARCH_DEBOUNCE_MS,
|
||||
DEFAULT_I18N,
|
||||
REF_TYPE_BRANCHES,
|
||||
REF_TYPE_TAGS,
|
||||
REF_TYPE_COMMITS,
|
||||
} from '../constants';
|
||||
import createStore from '../stores';
|
||||
import RefResultsSection from './ref_results_section.vue';
|
||||
|
||||
|
@ -28,6 +35,20 @@ export default {
|
|||
RefResultsSection,
|
||||
},
|
||||
props: {
|
||||
enabledRefTypes: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => ALL_REF_TYPES,
|
||||
validator: (val) =>
|
||||
// It has to be an arrray
|
||||
isArray(val) &&
|
||||
// with at least one item
|
||||
val.length > 0 &&
|
||||
// and only "REF_TYPE_BRANCHES", "REF_TYPE_TAGS", and "REF_TYPE_COMMITS" are allowed
|
||||
val.every((item) => ALL_REF_TYPES.includes(item)) &&
|
||||
// and no duplicates are allowed
|
||||
val.length === new Set(val).size,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -62,17 +83,29 @@ export default {
|
|||
};
|
||||
},
|
||||
showBranchesSection() {
|
||||
return Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error);
|
||||
return (
|
||||
this.enabledRefTypes.includes(REF_TYPE_BRANCHES) &&
|
||||
Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error)
|
||||
);
|
||||
},
|
||||
showTagsSection() {
|
||||
return Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error);
|
||||
return (
|
||||
this.enabledRefTypes.includes(REF_TYPE_TAGS) &&
|
||||
Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error)
|
||||
);
|
||||
},
|
||||
showCommitsSection() {
|
||||
return Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error);
|
||||
return (
|
||||
this.enabledRefTypes.includes(REF_TYPE_COMMITS) &&
|
||||
Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error)
|
||||
);
|
||||
},
|
||||
showNoResults() {
|
||||
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
|
||||
},
|
||||
showSectionHeaders() {
|
||||
return this.enabledRefTypes.length > 1;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Keep the Vuex store synchronized if the parent
|
||||
|
@ -97,10 +130,18 @@ export default {
|
|||
}, SEARCH_DEBOUNCE_MS);
|
||||
|
||||
this.setProjectId(this.projectId);
|
||||
this.search(this.query);
|
||||
|
||||
this.$watch(
|
||||
'enabledRefTypes',
|
||||
() => {
|
||||
this.setEnabledRefTypes(this.enabledRefTypes);
|
||||
this.search(this.query);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setProjectId', 'setSelectedRef', 'search']),
|
||||
...mapActions(['setEnabledRefTypes', 'setProjectId', 'setSelectedRef', 'search']),
|
||||
focusSearchBox() {
|
||||
this.$refs.searchBox.$el.querySelector('input').focus();
|
||||
},
|
||||
|
@ -170,6 +211,7 @@ export default {
|
|||
:selected-ref="selectedRef"
|
||||
:error="matches.branches.error"
|
||||
:error-message="i18n.branchesErrorMessage"
|
||||
:show-header="showSectionHeaders"
|
||||
data-testid="branches-section"
|
||||
@selected="selectRef($event)"
|
||||
/>
|
||||
|
@ -185,6 +227,7 @@ export default {
|
|||
:selected-ref="selectedRef"
|
||||
:error="matches.tags.error"
|
||||
:error-message="i18n.tagsErrorMessage"
|
||||
:show-header="showSectionHeaders"
|
||||
data-testid="tags-section"
|
||||
@selected="selectRef($event)"
|
||||
/>
|
||||
|
@ -200,6 +243,7 @@ export default {
|
|||
:selected-ref="selectedRef"
|
||||
:error="matches.commits.error"
|
||||
:error-message="i18n.commitsErrorMessage"
|
||||
:show-header="showSectionHeaders"
|
||||
data-testid="commits-section"
|
||||
@selected="selectRef($event)"
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES';
|
||||
export const REF_TYPE_TAGS = 'REF_TYPE_TAGS';
|
||||
export const REF_TYPE_COMMITS = 'REF_TYPE_COMMITS';
|
||||
export const ALL_REF_TYPES = Object.freeze([REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]);
|
||||
|
||||
export const X_TOTAL_HEADER = 'x-total';
|
||||
|
||||
export const SEARCH_DEBOUNCE_MS = 250;
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
import Api from '~/api';
|
||||
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS } from '../constants';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const setEnabledRefTypes = ({ commit }, refTypes) =>
|
||||
commit(types.SET_ENABLED_REF_TYPES, refTypes);
|
||||
|
||||
export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId);
|
||||
|
||||
export const setSelectedRef = ({ commit }, selectedRef) =>
|
||||
commit(types.SET_SELECTED_REF, selectedRef);
|
||||
|
||||
export const search = ({ dispatch, commit }, query) => {
|
||||
export const search = ({ state, dispatch, commit }, query) => {
|
||||
commit(types.SET_QUERY, query);
|
||||
|
||||
dispatch('searchBranches');
|
||||
dispatch('searchTags');
|
||||
dispatch('searchCommits');
|
||||
const dispatchIfRefTypeEnabled = (refType, action) => {
|
||||
if (state.enabledRefTypes.includes(refType)) {
|
||||
dispatch(action);
|
||||
}
|
||||
};
|
||||
dispatchIfRefTypeEnabled(REF_TYPE_BRANCHES, 'searchBranches');
|
||||
dispatchIfRefTypeEnabled(REF_TYPE_TAGS, 'searchTags');
|
||||
dispatchIfRefTypeEnabled(REF_TYPE_COMMITS, 'searchCommits');
|
||||
};
|
||||
|
||||
export const searchBranches = ({ commit, state }) => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const SET_ENABLED_REF_TYPES = 'SET_ENABLED_REF_TYPES';
|
||||
|
||||
export const SET_PROJECT_ID = 'SET_PROJECT_ID';
|
||||
export const SET_SELECTED_REF = 'SET_SELECTED_REF';
|
||||
export const SET_QUERY = 'SET_QUERY';
|
||||
|
|
|
@ -4,6 +4,9 @@ import { X_TOTAL_HEADER } from '../constants';
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_ENABLED_REF_TYPES](state, refTypes) {
|
||||
state.enabledRefTypes = refTypes;
|
||||
},
|
||||
[types.SET_PROJECT_ID](state, projectId) {
|
||||
state.projectId = projectId;
|
||||
},
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
const createRefTypeState = () => ({
|
||||
list: [],
|
||||
totalCount: 0,
|
||||
error: null,
|
||||
});
|
||||
|
||||
export default () => ({
|
||||
enabledRefTypes: [],
|
||||
projectId: null,
|
||||
|
||||
query: '',
|
||||
matches: {
|
||||
branches: {
|
||||
list: [],
|
||||
totalCount: 0,
|
||||
error: null,
|
||||
},
|
||||
tags: {
|
||||
list: [],
|
||||
totalCount: 0,
|
||||
error: null,
|
||||
},
|
||||
commits: {
|
||||
list: [],
|
||||
totalCount: 0,
|
||||
error: null,
|
||||
},
|
||||
branches: createRefTypeState(),
|
||||
tags: createRefTypeState(),
|
||||
commits: createRefTypeState(),
|
||||
},
|
||||
selectedRef: null,
|
||||
requestCount: 0,
|
||||
|
|
|
@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:codequality_backend_comparison, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml)
|
||||
|
||||
record_experiment_user(:invite_members_version_a)
|
||||
record_experiment_user(:invite_members_version_b)
|
||||
|
|
|
@ -46,6 +46,8 @@ class RootController < Dashboard::ProjectsController
|
|||
redirect_to(activity_dashboard_path)
|
||||
when 'starred_project_activity'
|
||||
redirect_to(activity_dashboard_path(filter: 'starred'))
|
||||
when 'followed_user_activity'
|
||||
redirect_to(activity_dashboard_path(filter: 'followed'))
|
||||
when 'groups'
|
||||
redirect_to(dashboard_groups_path)
|
||||
when 'todos'
|
||||
|
|
|
@ -29,6 +29,7 @@ module PreferencesHelper
|
|||
stars: _("Starred Projects"),
|
||||
project_activity: _("Your Projects' Activity"),
|
||||
starred_project_activity: _("Starred Projects' Activity"),
|
||||
followed_user_activity: _("Followed Users' Activity"),
|
||||
groups: _("Your Groups"),
|
||||
todos: _("Your To-Do List"),
|
||||
issues: _("Assigned Issues"),
|
||||
|
|
|
@ -272,7 +272,7 @@ class User < ApplicationRecord
|
|||
enum layout: { fixed: 0, fluid: 1 }
|
||||
|
||||
# User's Dashboard preference
|
||||
enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8 }
|
||||
enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8, followed_user_activity: 9 }
|
||||
|
||||
# User's Project preference
|
||||
enum project_view: { readme: 0, activity: 1, files: 2 }
|
||||
|
|
|
@ -25,7 +25,11 @@
|
|||
•
|
||||
- if total_count > recent_releases.count
|
||||
•
|
||||
= link_to n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }, project_releases_path(milestone.project)
|
||||
- more_text = n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }
|
||||
- if milestone.project_milestone?
|
||||
= link_to more_text, project_releases_path(milestone.project)
|
||||
- else
|
||||
= more_text
|
||||
%div
|
||||
= render('shared/milestone_expired', milestone: milestone)
|
||||
- if milestone.group_milestone?
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "Add 'Followed User Activity' as dashboard user choices"
|
||||
merge_request: 55165
|
||||
author: Benj Fassbind @randombenj
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix button alignment in design management header
|
||||
merge_request: 48003
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add rake task to cleanup description templates cache in batches
|
||||
merge_request: 54706
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add runners api context metadata
|
||||
merge_request: 55089
|
||||
author:
|
||||
type: changed
|
|
@ -13,13 +13,13 @@ GitLab provides Rake tasks for general maintenance.
|
|||
This command gathers information about your GitLab installation and the system it runs on.
|
||||
These may be useful when asking for help or reporting issues.
|
||||
|
||||
**Omnibus Installation**
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:env:info
|
||||
```
|
||||
|
||||
**Source Installation**
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
bundle exec rake gitlab:env:info RAILS_ENV=production
|
||||
|
@ -76,13 +76,13 @@ installations: a license cannot be installed into GitLab Community Edition.
|
|||
These may be useful when raising tickets with Support, or for programmatically
|
||||
checking your license parameters.
|
||||
|
||||
**Omnibus Installation**
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:license:info
|
||||
```
|
||||
|
||||
**Source Installation**
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
bundle exec rake gitlab:license:info RAILS_ENV=production
|
||||
|
@ -119,13 +119,13 @@ You may also have a look at our troubleshooting guides for:
|
|||
|
||||
To run `gitlab:check`, run:
|
||||
|
||||
**Omnibus Installation**
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:check
|
||||
```
|
||||
|
||||
**Source Installation**
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
bundle exec rake gitlab:check RAILS_ENV=production
|
||||
|
@ -182,13 +182,13 @@ Checking GitLab ... Finished
|
|||
|
||||
In some case it is necessary to rebuild the `authorized_keys` file. To do this, run:
|
||||
|
||||
**Omnibus Installation**
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:shell:setup
|
||||
```
|
||||
|
||||
**Source Installation**
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
|
@ -203,18 +203,64 @@ You will lose any data stored in authorized_keys file.
|
|||
Do you want to continue (yes/no)? yes
|
||||
```
|
||||
|
||||
## Clear issue and merge request description template names cache
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54706) in GitLab 13.10.
|
||||
|
||||
If the issue or merge request description template names in the dropdown
|
||||
do not reflect the actual description template names in the repository, consider clearing
|
||||
the Redis cache that stores the template names information.
|
||||
You can clear the cache of
|
||||
[all issues and merge request templates in the installation](#clear-cache-for-all-issue-and-merge-request-template-names)
|
||||
or [in a specific project](#clear-cache-for-issue-and-merge-request-template-names-in-specific-projects).
|
||||
|
||||
### Clear cache for all issue and merge request template names
|
||||
|
||||
If you want to refresh issue and merge request templates for all projects:
|
||||
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake cache:clear:description_templates
|
||||
```
|
||||
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
sudo -u git -H bundle exec rake cache:clear:description_templates RAILS_ENV=production
|
||||
```
|
||||
|
||||
### Clear cache for issue and merge request template names in specific projects
|
||||
|
||||
If you want to refresh issue and merge request templates for specific projects,
|
||||
provide a comma-separated list of IDs as the `project_ids` parameter to the Rake task.
|
||||
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake cache:clear:description_templates project_ids=10,25,35
|
||||
```
|
||||
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
sudo -u git -H bundle exec rake cache:clear:description_templates project_ids=10,25,35 RAILS_ENV=production
|
||||
```
|
||||
|
||||
## Clear Redis cache
|
||||
|
||||
If for some reason the dashboard displays the wrong information, you might want to
|
||||
clear Redis' cache. To do this, run:
|
||||
|
||||
**Omnibus Installation**
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake cache:clear
|
||||
```
|
||||
|
||||
**Source Installation**
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
|
@ -229,7 +275,7 @@ missing some icons. In that case, try to precompile the assets again.
|
|||
This only applies to source installations and does NOT apply to
|
||||
Omnibus packages.
|
||||
|
||||
**Source Installation**
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
|
@ -249,13 +295,13 @@ Sometimes you need to know if your GitLab installation can connect to a TCP
|
|||
service on another machine - perhaps a PostgreSQL or HTTPS server. A Rake task
|
||||
is included to help you with this:
|
||||
|
||||
**Omnibus Installation**
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:tcp_check[example.com,80]
|
||||
```
|
||||
|
||||
**Source Installation**
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
|
|
|
@ -171,8 +171,7 @@ certain arguments may also increase the complexity of a query.
|
|||
|
||||
NOTE:
|
||||
The complexity limits may be revised in future, and additionally, the complexity
|
||||
of a query may be altered. Changes to complexity can happen on `X.0` or `X.6`
|
||||
releases without a deprecation period.
|
||||
of a query may be altered.
|
||||
|
||||
### Request timeout
|
||||
|
||||
|
|
|
@ -30,10 +30,10 @@ Metrics for a branch are read from the latest metrics report artifact (default f
|
|||
|
||||
For an MR, the values of these metrics from the feature branch are compared to the values from the target branch. Then they are displayed in the MR widget in this order:
|
||||
|
||||
- Metrics that have been added by the MR. Marked with a **New** badge.
|
||||
- Existing metrics with changed values.
|
||||
- Existing metrics with unchanged values.
|
||||
- Metrics that have been added by the MR. Marked with a **New** badge.
|
||||
- Metrics that have been removed by the MR. Marked with a **Removed** badge.
|
||||
- Existing metrics with unchanged values.
|
||||
|
||||
## How to set it up
|
||||
|
||||
|
|
|
@ -135,12 +135,12 @@ These variables are available when:
|
|||
| `CI_MERGE_REQUEST_PROJECT_URL` | 11.6 | all | The URL of the project of the merge request. For example, `http://192.168.10.15:3000/namespace/awesome-project`. |
|
||||
| `CI_MERGE_REQUEST_REF_PATH` | 11.6 | all | The ref path of the merge request. For example, `refs/merge-requests/1/head`. |
|
||||
| `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` | 11.6 | all | The source branch name of the merge request. |
|
||||
| `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the source branch of the merge request. Only available in [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
|
||||
| `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the source branch of the merge request. The variable is empty in merge request pipelines. The SHA is present only in [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
|
||||
| `CI_MERGE_REQUEST_SOURCE_PROJECT_ID` | 11.6 | all | The ID of the source project of the merge request. |
|
||||
| `CI_MERGE_REQUEST_SOURCE_PROJECT_PATH` | 11.6 | all | The path of the source project of the merge request. |
|
||||
| `CI_MERGE_REQUEST_SOURCE_PROJECT_URL` | 11.6 | all | The URL of the source project of the merge request. |
|
||||
| `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` | 11.6 | all | The target branch name of the merge request. |
|
||||
| `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the target branch of the merge request. Only available in [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
|
||||
| `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the target branch of the merge request. The variable is empty in merge request pipelines. The SHA is present only in [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
|
||||
| `CI_MERGE_REQUEST_TITLE` | 11.9 | all | The title of the merge request. |
|
||||
| `CI_MERGE_REQUEST_EVENT_TYPE` | 12.3 | all | The event type of the merge request. Can be `detached`, `merged_result` or `merge_train`. |
|
||||
| `CI_MERGE_REQUEST_DIFF_ID` | 13.7 | all | The version of the merge request diff. |
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Manage
|
||||
group: Access
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
type: reference, howto, concepts
|
||||
---
|
||||
|
|
|
@ -40,7 +40,7 @@ Prerequisites:
|
|||
- You need to [authenticate with the API](../../../api/README.md#authentication). If authenticating with a deploy token, it must be configured with the `write_package_registry` scope.
|
||||
|
||||
```plaintext
|
||||
PUT /projects/:id/packages/generic/:package_name/:package_version/:file_name
|
||||
PUT /projects/:id/packages/generic/:package_name/:package_version/:file_name?status=:status
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
@ -58,7 +58,7 @@ Example request:
|
|||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--upload-file path/to/file.txt \
|
||||
"https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt"
|
||||
"https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt?status=hidden"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
|
@ -108,12 +108,13 @@ select few, the amount of activity on the default Dashboard page can be
|
|||
overwhelming. Changing this setting allows you to redefine your default
|
||||
dashboard.
|
||||
|
||||
You have 8 options here that you can use for your default dashboard view:
|
||||
You can include the following options for your default dashboard view:
|
||||
|
||||
- Your projects (default)
|
||||
- Starred projects
|
||||
- Your projects' activity
|
||||
- Starred projects' activity
|
||||
- Followed Users' Activity
|
||||
- Your groups
|
||||
- Your [To-Do List](../todos.md)
|
||||
- Assigned Issues
|
||||
|
|
|
@ -34,12 +34,12 @@ module API
|
|||
if runner_registration_token_valid?
|
||||
# Create shared runner. Requires admin access
|
||||
attributes.merge(runner_type: :instance_type)
|
||||
elsif project = Project.find_by_runners_token(params[:token])
|
||||
elsif @project = Project.find_by_runners_token(params[:token])
|
||||
# Create a specific runner for the project
|
||||
attributes.merge(runner_type: :project_type, projects: [project])
|
||||
elsif group = Group.find_by_runners_token(params[:token])
|
||||
attributes.merge(runner_type: :project_type, projects: [@project])
|
||||
elsif @group = Group.find_by_runners_token(params[:token])
|
||||
# Create a specific runner for the group
|
||||
attributes.merge(runner_type: :group_type, groups: [group])
|
||||
attributes.merge(runner_type: :group_type, groups: [@group])
|
||||
else
|
||||
forbidden!
|
||||
end
|
||||
|
@ -81,12 +81,7 @@ module API
|
|||
end
|
||||
|
||||
resource :jobs do
|
||||
before do
|
||||
Gitlab::ApplicationContext.push(
|
||||
user: -> { current_job&.user },
|
||||
project: -> { current_job&.project }
|
||||
)
|
||||
end
|
||||
before { set_application_context }
|
||||
|
||||
desc 'Request a job' do
|
||||
success Entities::JobRequest::Response
|
||||
|
|
|
@ -71,6 +71,26 @@ module API
|
|||
header 'Job-Status', job.status
|
||||
forbidden!(reason)
|
||||
end
|
||||
|
||||
def set_application_context
|
||||
if current_job
|
||||
Gitlab::ApplicationContext.push(
|
||||
user: -> { current_job.user },
|
||||
project: -> { current_job.project }
|
||||
)
|
||||
elsif current_runner&.project_type?
|
||||
Gitlab::ApplicationContext.push(
|
||||
project: -> do
|
||||
projects = current_runner.projects.limit(2) # rubocop: disable CodeReuse/ActiveRecord
|
||||
projects.first if projects.length == 1
|
||||
end
|
||||
)
|
||||
elsif current_runner&.group_type?
|
||||
Gitlab::ApplicationContext.push(
|
||||
namespace: -> { current_runner.groups.first }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,8 @@ module Gitlab
|
|||
class ProjectPipelineStatus
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
ALL_PIPELINES_STATUS_PATTERN = 'projects/*/pipeline_status'
|
||||
|
||||
attr_accessor :sha, :status, :ref, :project, :loaded
|
||||
|
||||
def self.load_for_project(project)
|
||||
|
|
|
@ -52,7 +52,7 @@ module Gitlab
|
|||
return unless included?
|
||||
|
||||
strong_memoize(:errors) do
|
||||
needs_errors
|
||||
[needs_errors, variable_expansion_errors].compact.flatten
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -153,6 +153,12 @@ module Gitlab
|
|||
@pipeline.project.actual_limits.ci_needs_size_limit
|
||||
end
|
||||
|
||||
def variable_expansion_errors
|
||||
sorted_collection = evaluate_context.variables.sorted_collection(@pipeline.project)
|
||||
errors = sorted_collection.errors
|
||||
["#{name}: #{errors}"] if errors
|
||||
end
|
||||
|
||||
def pipeline_attributes
|
||||
{
|
||||
pipeline: @pipeline,
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Cleanup
|
||||
module Redis
|
||||
class BatchDeleteByPattern
|
||||
REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
|
||||
REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
|
||||
|
||||
attr_reader :patterns
|
||||
|
||||
def initialize(patterns)
|
||||
raise ArgumentError.new('Argument should be an Array of patterns') unless patterns.is_a?(Array)
|
||||
|
||||
@patterns = patterns
|
||||
end
|
||||
|
||||
def execute
|
||||
return if patterns.blank?
|
||||
|
||||
batch_delete_cache_keys
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def batch_delete_cache_keys
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
patterns.each do |match|
|
||||
cursor = REDIS_SCAN_START_STOP
|
||||
loop do
|
||||
cursor, keys = redis.scan(
|
||||
cursor,
|
||||
match: match,
|
||||
count: REDIS_CLEAR_BATCH_SIZE
|
||||
)
|
||||
|
||||
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
||||
redis.del(*keys) if keys.any?
|
||||
end
|
||||
|
||||
break if cursor == REDIS_SCAN_START_STOP
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Cleanup
|
||||
module Redis
|
||||
class DescriptionTemplatesCacheKeysPatternBuilder
|
||||
# project_ids - a list of project_ids for which to compute description templates cache keys or `:all` to compute
|
||||
# a pattern that cover all description templates cache keys.
|
||||
#
|
||||
# Example
|
||||
# * ::Gitlab::Cleanup::Redis::BatchDeleteDescriptionTemplates.new(:all).execute - to get 2
|
||||
# patterns for all issue and merge request description templates cache keys.
|
||||
#
|
||||
# * ::Gitlab::Cleanup::Redis::BatchDeleteDescriptionTemplates.new([1,2,3,4]).execute - to get an array of
|
||||
# patterns for each project's issue and merge request description templates cache keys.
|
||||
def initialize(project_ids)
|
||||
raise ArgumentError.new('project_ids can either be an array of project IDs or :all') if project_ids != :all && !project_ids.is_a?(Array)
|
||||
|
||||
@project_ids = parse_project_ids(project_ids)
|
||||
end
|
||||
|
||||
def execute
|
||||
case project_ids
|
||||
when :all
|
||||
all_instance_patterns
|
||||
else
|
||||
project_patterns
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project_ids
|
||||
|
||||
def parse_project_ids(project_ids)
|
||||
return project_ids if project_ids == :all
|
||||
|
||||
project_ids.map { |id| Integer(id) }
|
||||
rescue ArgumentError
|
||||
raise ArgumentError.new('Invalid Project ID. Please ensure all passed in project ids values are valid integer project ids.')
|
||||
end
|
||||
|
||||
def project_patterns
|
||||
cache_key_patterns = []
|
||||
Project.id_in(project_ids).each_batch do |batch|
|
||||
cache_key_patterns << batch.map do |pr|
|
||||
next unless pr.repository.exists?
|
||||
|
||||
cache = Gitlab::RepositoryCache.new(pr.repository)
|
||||
|
||||
[repo_issue_templates_cache_key(cache), repo_merge_request_templates_cache_key(cache)]
|
||||
end
|
||||
end
|
||||
|
||||
cache_key_patterns.flatten.compact
|
||||
end
|
||||
|
||||
def all_instance_patterns
|
||||
[all_issue_templates_cache_key, all_merge_request_templates_cache_key]
|
||||
end
|
||||
|
||||
def issue_templates_cache_key
|
||||
Repository::METHOD_CACHES_FOR_FILE_TYPES[:issue_template]
|
||||
end
|
||||
|
||||
def merge_request_templates_cache_key
|
||||
Repository::METHOD_CACHES_FOR_FILE_TYPES[:merge_request_template]
|
||||
end
|
||||
|
||||
def all_issue_templates_cache_key
|
||||
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{issue_templates_cache_key}:*"
|
||||
end
|
||||
|
||||
def all_merge_request_templates_cache_key
|
||||
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{merge_request_templates_cache_key}:*"
|
||||
end
|
||||
|
||||
def repo_issue_templates_cache_key(cache)
|
||||
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{cache.cache_key(issue_templates_cache_key)}"
|
||||
end
|
||||
|
||||
def repo_merge_request_templates_cache_key(cache)
|
||||
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{cache.cache_key(merge_request_templates_cache_key)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -115,7 +115,7 @@ module Gitlab
|
|||
|
||||
config[:storage] = storages
|
||||
|
||||
internal_socket_dir = File.join(gitaly_dir, 'internal_sockets')
|
||||
internal_socket_dir = options[:internal_socket_dir] || File.join(gitaly_dir, 'internal_sockets')
|
||||
FileUtils.mkdir(internal_socket_dir) unless File.exist?(internal_socket_dir)
|
||||
config[:internal_socket_dir] = internal_socket_dir
|
||||
|
||||
|
|
|
@ -2,32 +2,22 @@
|
|||
|
||||
namespace :cache do
|
||||
namespace :clear do
|
||||
REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
|
||||
REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
|
||||
|
||||
desc "GitLab | Cache | Clear redis cache"
|
||||
task redis: :environment do
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
|
||||
projects/*/pipeline_status]
|
||||
cache_key_patterns = %W[
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
|
||||
#{Gitlab::Cache::Ci::ProjectPipelineStatus::ALL_PIPELINES_STATUS_PATTERN}
|
||||
]
|
||||
|
||||
cache_key_pattern.each do |match|
|
||||
cursor = REDIS_SCAN_START_STOP
|
||||
loop do
|
||||
cursor, keys = redis.scan(
|
||||
cursor,
|
||||
match: match,
|
||||
count: REDIS_CLEAR_BATCH_SIZE
|
||||
)
|
||||
::Gitlab::Cleanup::Redis::BatchDeleteByPattern.new(cache_key_patterns).execute
|
||||
end
|
||||
|
||||
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
||||
redis.del(*keys) if keys.any?
|
||||
end
|
||||
desc "GitLab | Cache | Clear description templates redis cache"
|
||||
task description_templates: :environment do
|
||||
project_ids = Array(ENV['project_ids']&.split(',')).map!(&:squish)
|
||||
|
||||
break if cursor == REDIS_SCAN_START_STOP
|
||||
end
|
||||
end
|
||||
end
|
||||
cache_key_patterns = ::Gitlab::Cleanup::Redis::DescriptionTemplatesCacheKeysPatternBuilder.new(project_ids).execute
|
||||
::Gitlab::Cleanup::Redis::BatchDeleteByPattern.new(cache_key_patterns).execute
|
||||
end
|
||||
|
||||
task all: [:redis]
|
||||
|
|
|
@ -13201,6 +13201,9 @@ msgstr ""
|
|||
msgid "Follow"
|
||||
msgstr ""
|
||||
|
||||
msgid "Followed Users' Activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Followed users"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -68,6 +68,18 @@ RSpec.describe RootController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'who has customized their dashboard setting for followed user activities' do
|
||||
before do
|
||||
user.dashboard = 'followed_user_activity'
|
||||
end
|
||||
|
||||
it 'redirects to the activity list' do
|
||||
get :index
|
||||
|
||||
expect(response).to redirect_to activity_dashboard_path(filter: 'followed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'who has customized their dashboard setting for groups' do
|
||||
before do
|
||||
user.dashboard = 'groups'
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('Batch delete button component', () => {
|
|||
expect(findButton().attributes('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('emits `deleteSelectedDesigns` event on modal ok click', () => {
|
||||
it('emits `delete-selected-designs` event on modal ok click', () => {
|
||||
createComponent();
|
||||
findButton().vm.$emit('click');
|
||||
return wrapper.vm
|
||||
|
@ -46,7 +46,7 @@ describe('Batch delete button component', () => {
|
|||
return wrapper.vm.$nextTick();
|
||||
})
|
||||
.then(() => {
|
||||
expect(wrapper.emitted().deleteSelectedDesigns).toBeTruthy();
|
||||
expect(wrapper.emitted('delete-selected-designs')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -106,11 +106,11 @@ describe('Design management toolbar component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('emits `delete` event on deleteButton `deleteSelectedDesigns` event', () => {
|
||||
it('emits `delete` event on deleteButton `delete-selected-designs` event', () => {
|
||||
createComponent();
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
wrapper.find(DeleteButton).vm.$emit('deleteSelectedDesigns');
|
||||
wrapper.find(DeleteButton).vm.$emit('delete-selected-designs');
|
||||
expect(wrapper.emitted().delete).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ exports[`Design management upload button component renders inverted upload desig
|
|||
|
||||
<input
|
||||
accept="image/*"
|
||||
class="hide"
|
||||
class="gl-display-none"
|
||||
multiple="multiple"
|
||||
name="design_file"
|
||||
type="file"
|
||||
|
@ -44,7 +44,7 @@ exports[`Design management upload button component renders upload design button
|
|||
|
||||
<input
|
||||
accept="image/*"
|
||||
class="hide"
|
||||
class="gl-display-none"
|
||||
multiple="multiple"
|
||||
name="design_file"
|
||||
type="file"
|
||||
|
|
|
@ -97,7 +97,7 @@ describe('Design management index page', () => {
|
|||
let moveDesignHandler;
|
||||
|
||||
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
|
||||
const findSelectAllButton = () => wrapper.find('.js-select-all');
|
||||
const findSelectAllButton = () => wrapper.find('[data-testid="select-all-designs-button"');
|
||||
const findToolbar = () => wrapper.find('.qa-selector-toolbar');
|
||||
const findDesignCollectionIsCopying = () =>
|
||||
wrapper.find('[data-testid="design-collection-is-copying"');
|
||||
|
@ -542,7 +542,9 @@ describe('Design management index page', () => {
|
|||
await nextTick();
|
||||
expect(findDeleteButton().exists()).toBe(true);
|
||||
expect(findSelectAllButton().text()).toBe('Deselect all');
|
||||
findDeleteButton().vm.$emit('deleteSelectedDesigns');
|
||||
|
||||
findDeleteButton().vm.$emit('delete-selected-designs');
|
||||
|
||||
const [{ variables }] = mutate.mock.calls[0];
|
||||
expect(variables.filenames).toStrictEqual([mockDesigns[0].filename, mockDesigns[1].filename]);
|
||||
});
|
||||
|
|
|
@ -35,8 +35,8 @@ describe('Pipelines Triggerer', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should render a table cell', () => {
|
||||
expect(wrapper.find('.table-section').exists()).toBe(true);
|
||||
it('should render pipeline triggerer table cell', () => {
|
||||
expect(wrapper.find('[data-testid="pipeline-triggerer"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass triggerer information when triggerer is provided', () => {
|
||||
|
|
|
@ -7,6 +7,7 @@ const projectPath = 'test/test';
|
|||
describe('Pipeline Url Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findTableCell = () => wrapper.find('[data-testid="pipeline-url-table-cell"]');
|
||||
const findPipelineUrlLink = () => wrapper.find('[data-testid="pipeline-url-link"]');
|
||||
const findScheduledTag = () => wrapper.find('[data-testid="pipeline-url-scheduled"]');
|
||||
const findLatestTag = () => wrapper.find('[data-testid="pipeline-url-latest"]');
|
||||
|
@ -43,10 +44,10 @@ describe('Pipeline Url Component', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
it('should render a table cell', () => {
|
||||
it('should render pipeline url table cell', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.attributes('class')).toContain('table-section');
|
||||
expect(findTableCell().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render a link the provided path and id', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui';
|
||||
import { GlButton, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { chunk } from 'lodash';
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { GlTable } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
|
||||
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
|
||||
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
|
||||
import PipelinesStatusBadge from '~/pipelines/components/pipelines_list/pipelines_status_badge.vue';
|
||||
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
|
||||
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
|
||||
import CommitComponent from '~/vue_shared/components/commit.vue';
|
||||
|
||||
describe('Pipelines Table', () => {
|
||||
let pipeline;
|
||||
|
@ -29,7 +35,22 @@ describe('Pipelines Table', () => {
|
|||
|
||||
const findRows = () => wrapper.findAll('.commit.gl-responsive-table-row');
|
||||
const findGlTable = () => wrapper.findComponent(GlTable);
|
||||
const findLegacyTable = () => wrapper.findByTestId('ci-table');
|
||||
const findStatusBadge = () => wrapper.findComponent(PipelinesStatusBadge);
|
||||
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
|
||||
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
|
||||
const findCommit = () => wrapper.findComponent(CommitComponent);
|
||||
const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago);
|
||||
const findActions = () => wrapper.findComponent(PipelineOperations);
|
||||
|
||||
const findLegacyTable = () => wrapper.findByTestId('legacy-ci-table');
|
||||
const findTableRows = () => wrapper.findAll('[data-testid="pipeline-table-row"]');
|
||||
const findStatusTh = () => wrapper.findByTestId('status-th');
|
||||
const findPipelineTh = () => wrapper.findByTestId('pipeline-th');
|
||||
const findTriggererTh = () => wrapper.findByTestId('triggerer-th');
|
||||
const findCommitTh = () => wrapper.findByTestId('commit-th');
|
||||
const findStagesTh = () => wrapper.findByTestId('stages-th');
|
||||
const findTimeAgoTh = () => wrapper.findByTestId('timeago-th');
|
||||
const findActionsTh = () => wrapper.findByTestId('actions-th');
|
||||
|
||||
preloadFixtures(jsonFixtureName);
|
||||
|
||||
|
@ -82,11 +103,82 @@ describe('Pipelines Table', () => {
|
|||
});
|
||||
|
||||
describe('table with feature flag on', () => {
|
||||
it('displays new table', () => {
|
||||
createComponent(defaultProps, true);
|
||||
beforeEach(() => {
|
||||
createComponent({ pipelines: [pipeline], viewType: 'root' }, true);
|
||||
});
|
||||
|
||||
it('displays new table', () => {
|
||||
expect(findGlTable().exists()).toBe(true);
|
||||
expect(findLegacyTable().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render table head with correct columns', () => {
|
||||
expect(findStatusTh().text()).toBe('Status');
|
||||
expect(findPipelineTh().text()).toBe('Pipeline');
|
||||
expect(findTriggererTh().text()).toBe('Triggerer');
|
||||
expect(findCommitTh().text()).toBe('Commit');
|
||||
expect(findStagesTh().text()).toBe('Stages');
|
||||
expect(findTimeAgoTh().text()).toBe('Duration');
|
||||
|
||||
// last column should have no text in th
|
||||
expect(findActionsTh().text()).toBe('');
|
||||
});
|
||||
|
||||
it('should display a table row', () => {
|
||||
expect(findTableRows()).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('status cell', () => {
|
||||
it('should render a status badge', () => {
|
||||
expect(findStatusBadge().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render status badge with correct path', () => {
|
||||
expect(findStatusBadge().attributes('href')).toBe(pipeline.path);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipeline cell', () => {
|
||||
it('should render pipeline information', () => {
|
||||
expect(findPipelineInfo().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display the pipeline id', () => {
|
||||
expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('triggerer cell', () => {
|
||||
it('should render the pipeline triggerer', () => {
|
||||
expect(findTriggerer().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit cell', () => {
|
||||
it('should render commit information', () => {
|
||||
expect(findCommit().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display and link to commit', () => {
|
||||
expect(findCommit().text()).toContain(pipeline.commit.short_id);
|
||||
expect(findCommit().props('commitUrl')).toBe(pipeline.commit.commit_path);
|
||||
});
|
||||
|
||||
it('should display the commit author', () => {
|
||||
expect(findCommit().props('author')).toEqual(pipeline.commit.author);
|
||||
});
|
||||
});
|
||||
|
||||
describe('duration cell', () => {
|
||||
it('should render duration information', () => {
|
||||
expect(findTimeAgo().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('operations cell', () => {
|
||||
it('should render pipeline operations', () => {
|
||||
expect(findActions().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,13 @@ import { trimText } from 'helpers/text_helper';
|
|||
import { ENTER_KEY } from '~/lib/utils/keys';
|
||||
import { sprintf } from '~/locale';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants';
|
||||
import {
|
||||
X_TOTAL_HEADER,
|
||||
DEFAULT_I18N,
|
||||
REF_TYPE_BRANCHES,
|
||||
REF_TYPE_TAGS,
|
||||
REF_TYPE_COMMITS,
|
||||
} from '~/ref/constants';
|
||||
import createStore from '~/ref/stores/';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
@ -26,6 +32,7 @@ describe('Ref selector component', () => {
|
|||
let branchesApiCallSpy;
|
||||
let tagsApiCallSpy;
|
||||
let commitApiCallSpy;
|
||||
let requestSpies;
|
||||
|
||||
const createComponent = (props = {}, attrs = {}) => {
|
||||
wrapper = mount(RefSelector, {
|
||||
|
@ -58,6 +65,7 @@ describe('Ref selector component', () => {
|
|||
.mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
|
||||
tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
|
||||
commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]);
|
||||
requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy };
|
||||
|
||||
mock
|
||||
.onGet(`/api/v4/projects/${projectId}/repository/branches`)
|
||||
|
@ -592,4 +600,86 @@ describe('Ref selector component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with non-default ref types', () => {
|
||||
it.each`
|
||||
enabledRefTypes | reqsCalled | reqsNotCalled
|
||||
${[REF_TYPE_BRANCHES]} | ${['branchesApiCallSpy']} | ${['tagsApiCallSpy', 'commitApiCallSpy']}
|
||||
${[REF_TYPE_TAGS]} | ${['tagsApiCallSpy']} | ${['branchesApiCallSpy', 'commitApiCallSpy']}
|
||||
${[REF_TYPE_COMMITS]} | ${[]} | ${['branchesApiCallSpy', 'tagsApiCallSpy', 'commitApiCallSpy']}
|
||||
${[REF_TYPE_TAGS, REF_TYPE_COMMITS]} | ${['tagsApiCallSpy']} | ${['branchesApiCallSpy', 'commitApiCallSpy']}
|
||||
`(
|
||||
'only calls $reqsCalled requests when $enabledRefTypes are enabled',
|
||||
async ({ enabledRefTypes, reqsCalled, reqsNotCalled }) => {
|
||||
createComponent({ enabledRefTypes });
|
||||
|
||||
await waitForRequests();
|
||||
|
||||
reqsCalled.forEach((req) => expect(requestSpies[req]).toHaveBeenCalledTimes(1));
|
||||
reqsNotCalled.forEach((req) => expect(requestSpies[req]).not.toHaveBeenCalled());
|
||||
},
|
||||
);
|
||||
|
||||
it('only calls commitApiCallSpy when REF_TYPE_COMMITS is enabled', async () => {
|
||||
createComponent({ enabledRefTypes: [REF_TYPE_COMMITS] });
|
||||
updateQuery('abcd1234');
|
||||
|
||||
await waitForRequests();
|
||||
|
||||
expect(commitApiCallSpy).toHaveBeenCalledTimes(1);
|
||||
expect(branchesApiCallSpy).not.toHaveBeenCalled();
|
||||
expect(tagsApiCallSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('triggers another search if enabled ref types change', async () => {
|
||||
createComponent({ enabledRefTypes: [REF_TYPE_BRANCHES] });
|
||||
await waitForRequests();
|
||||
|
||||
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
|
||||
expect(tagsApiCallSpy).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.setProps({
|
||||
enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
|
||||
});
|
||||
await waitForRequests();
|
||||
|
||||
expect(branchesApiCallSpy).toHaveBeenCalledTimes(2);
|
||||
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => {
|
||||
createComponent({ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] });
|
||||
updateQuery('abcd1234');
|
||||
await waitForRequests();
|
||||
|
||||
expect(findBranchesSection().exists()).toBe(true);
|
||||
expect(findCommitsSection().exists()).toBe(true);
|
||||
|
||||
wrapper.setProps({ enabledRefTypes: [REF_TYPE_COMMITS] });
|
||||
await waitForRequests();
|
||||
|
||||
expect(findBranchesSection().exists()).toBe(false);
|
||||
expect(findCommitsSection().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it.each`
|
||||
enabledRefType | findVisibleSection | findHiddenSections
|
||||
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
|
||||
${REF_TYPE_TAGS} | ${findTagsSection} | ${[findBranchesSection, findCommitsSection]}
|
||||
${REF_TYPE_COMMITS} | ${findCommitsSection} | ${[findBranchesSection, findTagsSection]}
|
||||
`(
|
||||
'hides section headers if a single ref type is enabled',
|
||||
async ({ enabledRefType, findVisibleSection, findHiddenSections }) => {
|
||||
createComponent({ enabledRefTypes: [enabledRefType] });
|
||||
updateQuery('abcd1234');
|
||||
await waitForRequests();
|
||||
|
||||
expect(findVisibleSection().exists()).toBe(true);
|
||||
expect(findVisibleSection().find('[data-testid="section-header"]').exists()).toBe(false);
|
||||
findHiddenSections.forEach((findHiddenSection) =>
|
||||
expect(findHiddenSection().exists()).toBe(false),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import testAction from 'helpers/vuex_action_helper';
|
||||
import { ALL_REF_TYPES, REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS } from '~/ref/constants';
|
||||
import * as actions from '~/ref/stores/actions';
|
||||
import * as types from '~/ref/stores/mutation_types';
|
||||
import createState from '~/ref/stores/state';
|
||||
|
@ -25,6 +26,14 @@ describe('Ref selector Vuex store actions', () => {
|
|||
state = createState();
|
||||
});
|
||||
|
||||
describe('setEnabledRefTypes', () => {
|
||||
it(`commits ${types.SET_ENABLED_REF_TYPES} with the enabled ref types`, () => {
|
||||
testAction(actions.setProjectId, ALL_REF_TYPES, state, [
|
||||
{ type: types.SET_PROJECT_ID, payload: ALL_REF_TYPES },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setProjectId', () => {
|
||||
it(`commits ${types.SET_PROJECT_ID} with the new project ID`, () => {
|
||||
const projectId = '4';
|
||||
|
@ -46,12 +55,23 @@ describe('Ref selector Vuex store actions', () => {
|
|||
describe('search', () => {
|
||||
it(`commits ${types.SET_QUERY} with the new search query`, () => {
|
||||
const query = 'hello';
|
||||
testAction(actions.search, query, state, [{ type: types.SET_QUERY, payload: query }]);
|
||||
});
|
||||
|
||||
it.each`
|
||||
enabledRefTypes | expectedActions
|
||||
${[REF_TYPE_BRANCHES]} | ${['searchBranches']}
|
||||
${[REF_TYPE_COMMITS]} | ${['searchCommits']}
|
||||
${[REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]} | ${['searchBranches', 'searchTags', 'searchCommits']}
|
||||
`(`dispatches fetch actions for enabled ref types`, ({ enabledRefTypes, expectedActions }) => {
|
||||
const query = 'hello';
|
||||
state.enabledRefTypes = enabledRefTypes;
|
||||
testAction(
|
||||
actions.search,
|
||||
query,
|
||||
state,
|
||||
[{ type: types.SET_QUERY, payload: query }],
|
||||
[{ type: 'searchBranches' }, { type: 'searchTags' }, { type: 'searchCommits' }],
|
||||
expectedActions.map((type) => ({ type })),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { X_TOTAL_HEADER } from '~/ref/constants';
|
||||
import { X_TOTAL_HEADER, ALL_REF_TYPES } from '~/ref/constants';
|
||||
import * as types from '~/ref/stores/mutation_types';
|
||||
import mutations from '~/ref/stores/mutations';
|
||||
import createState from '~/ref/stores/state';
|
||||
|
@ -13,6 +13,7 @@ describe('Ref selector Vuex store mutations', () => {
|
|||
describe('initial state', () => {
|
||||
it('is created with the correct structure and initial values', () => {
|
||||
expect(state).toEqual({
|
||||
enabledRefTypes: [],
|
||||
projectId: null,
|
||||
|
||||
query: '',
|
||||
|
@ -39,6 +40,14 @@ describe('Ref selector Vuex store mutations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe(`${types.SET_ENABLED_REF_TYPES}`, () => {
|
||||
it('sets the enabled ref types', () => {
|
||||
mutations[types.SET_ENABLED_REF_TYPES](state, ALL_REF_TYPES);
|
||||
|
||||
expect(state.enabledRefTypes).toBe(ALL_REF_TYPES);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.SET_PROJECT_ID}`, () => {
|
||||
it('updates the project ID', () => {
|
||||
const newProjectId = '4';
|
||||
|
|
|
@ -29,6 +29,7 @@ RSpec.describe PreferencesHelper do
|
|||
['Starred Projects', 'stars'],
|
||||
["Your Projects' Activity", 'project_activity'],
|
||||
["Starred Projects' Activity", 'starred_project_activity'],
|
||||
["Followed Users' Activity", 'followed_user_activity'],
|
||||
["Your Groups", 'groups'],
|
||||
["Your To-Do List", 'todos'],
|
||||
["Assigned Issues", 'issues'],
|
||||
|
|
|
@ -1025,4 +1025,75 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'applying pipeline variables' do
|
||||
subject { seed_build }
|
||||
|
||||
let(:pipeline_variables) { [] }
|
||||
let(:pipeline) do
|
||||
build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables)
|
||||
end
|
||||
|
||||
context 'containing variable references' do
|
||||
let(:pipeline_variables) do
|
||||
[
|
||||
build(:ci_pipeline_variable, key: 'A', value: '$B'),
|
||||
build(:ci_pipeline_variable, key: 'B', value: '$C')
|
||||
]
|
||||
end
|
||||
|
||||
context 'when FF :variable_inside_variable is enabled' do
|
||||
before do
|
||||
stub_feature_flags(variable_inside_variable: [project])
|
||||
end
|
||||
|
||||
it "does not have errors" do
|
||||
expect(subject.errors).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'containing cyclic reference' do
|
||||
let(:pipeline_variables) do
|
||||
[
|
||||
build(:ci_pipeline_variable, key: 'A', value: '$B'),
|
||||
build(:ci_pipeline_variable, key: 'B', value: '$C'),
|
||||
build(:ci_pipeline_variable, key: 'C', value: '$A')
|
||||
]
|
||||
end
|
||||
|
||||
context 'when FF :variable_inside_variable is disabled' do
|
||||
before do
|
||||
stub_feature_flags(variable_inside_variable: false)
|
||||
end
|
||||
|
||||
it "does not have errors" do
|
||||
expect(subject.errors).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when FF :variable_inside_variable is enabled' do
|
||||
before do
|
||||
stub_feature_flags(variable_inside_variable: [project])
|
||||
end
|
||||
|
||||
it "returns an error" do
|
||||
expect(subject.errors).to contain_exactly(
|
||||
'rspec: circular variable reference detected: ["A", "B", "C"]')
|
||||
end
|
||||
|
||||
context 'with job:rules:[if:]' do
|
||||
let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } }
|
||||
|
||||
it "included? does not raise" do
|
||||
expect { subject.included? }.not_to raise_error
|
||||
end
|
||||
|
||||
it "included? returns true" do
|
||||
expect(subject.included?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Cleanup::Redis::BatchDeleteByPattern, :clean_gitlab_redis_cache do
|
||||
subject { described_class.new(patterns) }
|
||||
|
||||
describe 'execute' do
|
||||
context 'when no patterns are passed' do
|
||||
before do
|
||||
expect(Gitlab::Redis::Cache).not_to receive(:with)
|
||||
end
|
||||
|
||||
context 'with nil patterns' do
|
||||
let(:patterns) { nil }
|
||||
|
||||
specify { expect { subject }.to raise_error(ArgumentError, 'Argument should be an Array of patterns') }
|
||||
end
|
||||
|
||||
context 'with empty array patterns' do
|
||||
let(:patterns) { [] }
|
||||
|
||||
specify { subject.execute }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with patterns' do
|
||||
context 'when key is not found' do
|
||||
let(:patterns) { ['key'] }
|
||||
|
||||
before do
|
||||
expect_any_instance_of(Redis).not_to receive(:del) # rubocop:disable RSpec/AnyInstanceOf
|
||||
end
|
||||
|
||||
specify { subject.execute }
|
||||
end
|
||||
|
||||
context 'with cache data' do
|
||||
let(:cache_keys) { %w[key-test1 key-test2 key-test3 key-test4] }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::REDIS_CLEAR_BATCH_SIZE", 2)
|
||||
|
||||
write_to_cache
|
||||
end
|
||||
|
||||
context 'with one key' do
|
||||
let(:patterns) { ['key-test1'] }
|
||||
|
||||
it 'deletes the key' do
|
||||
expect_any_instance_of(Redis).to receive(:del).with(patterns.first).once # rubocop:disable RSpec/AnyInstanceOf
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'with many keys' do
|
||||
let(:patterns) { %w[key-test1 key-test2] }
|
||||
|
||||
it 'deletes keys for each pattern separatelly' do
|
||||
expect_any_instance_of(Redis).to receive(:del).with(patterns.first).once # rubocop:disable RSpec/AnyInstanceOf
|
||||
expect_any_instance_of(Redis).to receive(:del).with(patterns.last).once # rubocop:disable RSpec/AnyInstanceOf
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cache_keys over batch size' do
|
||||
let(:patterns) { %w[key-test*] }
|
||||
|
||||
it 'deletes matched keys in batches' do
|
||||
# redis scan returns the values in random order so just checking it is being called twice meaning
|
||||
# scan returned results in 2 batches, which is what we expect
|
||||
key_like = start_with('key-test')
|
||||
expect_any_instance_of(Redis).to receive(:del).with(key_like, key_like).twice # rubocop:disable RSpec/AnyInstanceOf
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_to_cache
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
cache_keys.each_with_index do |cache_key, index|
|
||||
redis.set(cache_key, index)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,94 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Cleanup::Redis::DescriptionTemplatesCacheKeysPatternBuilder, :clean_gitlab_redis_cache do
|
||||
subject { described_class.new(project_ids).execute }
|
||||
|
||||
describe 'execute' do
|
||||
context 'when build pattern for all description templates' do
|
||||
RSpec.shared_examples 'all issue and merge request templates pattern' do
|
||||
it 'builds pattern to remove all issue and merge request templates keys' do
|
||||
expect(subject.count).to eq(2)
|
||||
expect(subject).to match_array(%W[
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:issue_template_names_hash:*
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:merge_request_template_names_hash:*
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with project_ids == :all' do
|
||||
let(:project_ids) { :all }
|
||||
|
||||
it_behaves_like 'all issue and merge request templates pattern'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with project_ids' do
|
||||
let_it_be(:project1) { create(:project, :repository) }
|
||||
let_it_be(:project2) { create(:project, :repository) }
|
||||
|
||||
context 'with nil project_ids' do
|
||||
let(:project_ids) { nil }
|
||||
|
||||
specify { expect { subject }.to raise_error(ArgumentError, 'project_ids can either be an array of project IDs or :all') }
|
||||
end
|
||||
|
||||
context 'with project_ids as string' do
|
||||
let(:project_ids) { '1' }
|
||||
|
||||
specify { expect { subject }.to raise_error(ArgumentError, 'project_ids can either be an array of project IDs or :all') }
|
||||
end
|
||||
|
||||
context 'with invalid project_ids as array of strings' do
|
||||
let(:project_ids) { %w[a b] }
|
||||
|
||||
specify { expect { subject }.to raise_error(ArgumentError, 'Invalid Project ID. Please ensure all passed in project ids values are valid integer project ids.') }
|
||||
end
|
||||
|
||||
context 'with non existent project id' do
|
||||
let(:project_ids) { [non_existing_record_id] }
|
||||
|
||||
it 'no patterns are built' do
|
||||
expect(subject.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with one project_id' do
|
||||
let(:project_ids) { [project1.id] }
|
||||
|
||||
it 'builds patterns for the project' do
|
||||
expect(subject.count).to eq(2)
|
||||
expect(subject).to match_array(%W[
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:issue_template_names_hash:#{project1.full_path}:#{project1.id}
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:merge_request_template_names_hash:#{project1.full_path}:#{project1.id}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with many project_ids' do
|
||||
let(:project_ids) { [project1.id, project2.id] }
|
||||
|
||||
RSpec.shared_examples 'builds patterns for the given projects' do
|
||||
it 'builds patterns for the given projects' do
|
||||
expect(subject.count).to eq(4)
|
||||
expect(subject).to match_array(%W[
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:issue_template_names_hash:#{project1.full_path}:#{project1.id}
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:merge_request_template_names_hash:#{project1.full_path}:#{project1.id}
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:issue_template_names_hash:#{project2.full_path}:#{project2.id}
|
||||
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:merge_request_template_names_hash:#{project2.full_path}:#{project2.id}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'builds patterns for the given projects'
|
||||
|
||||
context 'with project_ids as string' do
|
||||
let(:project_ids) { [project1.id.to_s, project2.id.to_s] }
|
||||
|
||||
it_behaves_like 'builds patterns for the given projects'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -797,6 +797,50 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'setting the application context' do
|
||||
subject { request_job }
|
||||
|
||||
context 'when triggered by a user' do
|
||||
let(:job) { create(:ci_build, user: user, project: project) }
|
||||
|
||||
subject { request_job(id: job.id) }
|
||||
|
||||
it_behaves_like 'storing arguments in the application context' do
|
||||
let(:expected_params) { { user: user.username, project: project.full_path } }
|
||||
end
|
||||
|
||||
it_behaves_like 'not executing any extra queries for the application context', 3 do
|
||||
# Extra queries: User, Project, Route
|
||||
let(:subject_proc) { proc { request_job(id: job.id) } }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the runner is of project type' do
|
||||
it_behaves_like 'storing arguments in the application context' do
|
||||
let(:expected_params) { { project: project.full_path } }
|
||||
end
|
||||
|
||||
it_behaves_like 'not executing any extra queries for the application context', 2 do
|
||||
# Extra queries: Project, Route
|
||||
let(:subject_proc) { proc { request_job } }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the runner is of group type' do
|
||||
let(:group) { create(:group) }
|
||||
let(:runner) { create(:ci_runner, :group, groups: [group]) }
|
||||
|
||||
it_behaves_like 'storing arguments in the application context' do
|
||||
let(:expected_params) { { root_namespace: group.full_path_components.first } }
|
||||
end
|
||||
|
||||
it_behaves_like 'not executing any extra queries for the application context', 2 do
|
||||
# Extra queries: Group, Route
|
||||
let(:subject_proc) { proc { request_job } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def request_job(token = runner.token, **params)
|
||||
new_params = params.merge(token: token, last_update: last_update)
|
||||
post api('/jobs/request'), params: new_params.to_json, headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' }
|
||||
|
|
|
@ -35,6 +35,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
context 'when valid token is provided' do
|
||||
def request
|
||||
post api('/runners'), params: { token: token }
|
||||
end
|
||||
|
||||
it 'creates runner with default values' do
|
||||
post api('/runners'), params: { token: registration_token }
|
||||
|
||||
|
@ -51,9 +55,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
|
||||
context 'when project token is used' do
|
||||
let(:project) { create(:project) }
|
||||
let(:token) { project.runners_token }
|
||||
|
||||
it 'creates project runner' do
|
||||
post api('/runners'), params: { token: project.runners_token }
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(project.runners.size).to eq(1)
|
||||
|
@ -62,13 +67,24 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
expect(runner.token).not_to eq(project.runners_token)
|
||||
expect(runner).to be_project_type
|
||||
end
|
||||
|
||||
it_behaves_like 'storing arguments in the application context' do
|
||||
subject { request }
|
||||
|
||||
let(:expected_params) { { project: project.full_path } }
|
||||
end
|
||||
|
||||
it_behaves_like 'not executing any extra queries for the application context' do
|
||||
let(:subject_proc) { proc { request } }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group token is used' do
|
||||
let(:group) { create(:group) }
|
||||
let(:token) { group.runners_token }
|
||||
|
||||
it 'creates a group runner' do
|
||||
post api('/runners'), params: { token: group.runners_token }
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(group.runners.reload.size).to eq(1)
|
||||
|
@ -77,6 +93,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
expect(runner.token).not_to eq(group.runners_token)
|
||||
expect(runner).to be_group_type
|
||||
end
|
||||
|
||||
it_behaves_like 'storing arguments in the application context' do
|
||||
subject { request }
|
||||
|
||||
let(:expected_params) { { root_namespace: group.full_path_components.first } }
|
||||
end
|
||||
|
||||
it_behaves_like 'not executing any extra queries for the application context' do
|
||||
let(:subject_proc) { proc { request } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -172,8 +172,13 @@ module TestEnv
|
|||
Gitlab::SetupHelper::Gitaly.create_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
|
||||
Gitlab::SetupHelper::Gitaly.create_configuration(
|
||||
gitaly_dir,
|
||||
{ 'default' => repos_path }, force: true,
|
||||
options: { gitaly_socket: "gitaly2.socket", config_filename: "gitaly2.config.toml" }
|
||||
{ 'default' => repos_path },
|
||||
force: true,
|
||||
options: {
|
||||
internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"),
|
||||
gitaly_socket: "gitaly2.socket",
|
||||
config_filename: "gitaly2.config.toml"
|
||||
}
|
||||
)
|
||||
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
|
||||
end
|
||||
|
|
|
@ -22,3 +22,19 @@ RSpec.shared_examples 'storing arguments in the application context' do
|
|||
hash.transform_keys! { |key| "meta.#{key}" }
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'not executing any extra queries for the application context' do |expected_extra_queries = 0|
|
||||
it 'does not execute more queries than without adding anything to the application context' do
|
||||
# Call the subject once to memoize all factories being used for the spec, so they won't
|
||||
# add any queries to the expectation.
|
||||
subject_proc.call
|
||||
|
||||
expect do
|
||||
allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
|
||||
subject_proc.call
|
||||
end.to issue_same_number_of_queries_as {
|
||||
allow(Gitlab::ApplicationContext).to receive(:push)
|
||||
subject_proc.call
|
||||
}.with_threshold(expected_extra_queries).ignoring_cached_queries
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rake_helper'
|
||||
|
||||
RSpec.describe 'clearing redis cache' do
|
||||
RSpec.describe 'clearing redis cache', :clean_gitlab_redis_cache do
|
||||
before do
|
||||
Rake.application.rake_require 'tasks/cache'
|
||||
end
|
||||
|
@ -21,4 +21,27 @@ RSpec.describe 'clearing redis cache' do
|
|||
expect { run_rake_task('cache:clear:redis') }.to change { pipeline_status.has_cache? }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'invoking clear description templates cache rake task' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
before do
|
||||
stub_env('project_ids', project_ids) if project_ids
|
||||
service = double(:service, execute: true)
|
||||
|
||||
expect(Gitlab::Cleanup::Redis::DescriptionTemplatesCacheKeysPatternBuilder).to receive(:new).with(expected_project_ids).and_return(service)
|
||||
expect(Gitlab::Cleanup::Redis::BatchDeleteByPattern).to receive(:new).and_return(service)
|
||||
end
|
||||
|
||||
where(:project_ids, :expected_project_ids) do
|
||||
nil | [] # this acts as no argument is being passed
|
||||
'1' | %w[1]
|
||||
'1, 2, 3' | %w[1 2 3]
|
||||
'1, 2, some-string, 3' | %w[1 2 some-string 3]
|
||||
end
|
||||
|
||||
with_them do
|
||||
specify { run_rake_task('cache:clear:description_templates') }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue