Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5d92a0af93
commit
e303f963d0
38 changed files with 757 additions and 358 deletions
|
@ -5,9 +5,6 @@ GraphQL/FieldDefinitions:
|
||||||
- app/graphql/types/group_type.rb
|
- app/graphql/types/group_type.rb
|
||||||
- app/graphql/types/issue_type.rb
|
- app/graphql/types/issue_type.rb
|
||||||
- app/graphql/types/label_type.rb
|
- app/graphql/types/label_type.rb
|
||||||
- app/graphql/types/merge_request_type.rb
|
|
||||||
- app/graphql/types/namespace_type.rb
|
|
||||||
- app/graphql/types/notes/note_type.rb
|
|
||||||
- app/graphql/types/project_type.rb
|
- app/graphql/types/project_type.rb
|
||||||
- app/graphql/types/projects/topic_type.rb
|
- app/graphql/types/projects/topic_type.rb
|
||||||
- app/graphql/types/release_type.rb
|
- app/graphql/types/release_type.rb
|
||||||
|
|
|
@ -42,6 +42,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
coverageLoaded: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
inline: {
|
inline: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -83,14 +88,15 @@ export default {
|
||||||
if (!props.inline || !props.line.left) return {};
|
if (!props.inline || !props.line.left) return {};
|
||||||
return props.fileLineCoverage(props.filePath, props.line.left.new_line);
|
return props.fileLineCoverage(props.filePath, props.line.left.new_line);
|
||||||
},
|
},
|
||||||
(props) => [props.inline, props.filePath, props.line.left?.new_line].join(':'),
|
(props) =>
|
||||||
|
[props.inline, props.filePath, props.line.left?.new_line, props.coverageLoaded].join(':'),
|
||||||
),
|
),
|
||||||
coverageStateRight: memoize(
|
coverageStateRight: memoize(
|
||||||
(props) => {
|
(props) => {
|
||||||
if (!props.line.right) return {};
|
if (!props.line.right) return {};
|
||||||
return props.fileLineCoverage(props.filePath, props.line.right.new_line);
|
return props.fileLineCoverage(props.filePath, props.line.right.new_line);
|
||||||
},
|
},
|
||||||
(props) => [props.line.right?.new_line, props.filePath].join(':'),
|
(props) => [props.line.right?.new_line, props.filePath, props.coverageLoaded].join(':'),
|
||||||
),
|
),
|
||||||
showCodequalityLeft: memoize(
|
showCodequalityLeft: memoize(
|
||||||
(props) => {
|
(props) => {
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('diffs', ['commitId', 'fileLineCoverage']),
|
...mapGetters('diffs', ['commitId', 'fileLineCoverage']),
|
||||||
...mapState('diffs', ['codequalityDiff', 'highlightedRow']),
|
...mapState('diffs', ['codequalityDiff', 'highlightedRow', 'coverageLoaded']),
|
||||||
...mapState({
|
...mapState({
|
||||||
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
|
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
|
||||||
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
|
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
|
||||||
|
@ -180,6 +180,7 @@ export default {
|
||||||
:index="index"
|
:index="index"
|
||||||
:is-highlighted="isHighlighted(line)"
|
:is-highlighted="isHighlighted(line)"
|
||||||
:file-line-coverage="fileLineCoverage"
|
:file-line-coverage="fileLineCoverage"
|
||||||
|
:coverage-loaded="coverageLoaded"
|
||||||
@showCommentForm="(code) => singleLineComment(code, line)"
|
@showCommentForm="(code) => singleLineComment(code, line)"
|
||||||
@setHighlightedRow="setHighlightedRow"
|
@setHighlightedRow="setHighlightedRow"
|
||||||
@toggleLineDiscussions="
|
@toggleLineDiscussions="
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default () => ({
|
||||||
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
|
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
|
||||||
diffFiles: [],
|
diffFiles: [],
|
||||||
coverageFiles: {},
|
coverageFiles: {},
|
||||||
|
coverageLoaded: false,
|
||||||
mergeRequestDiffs: [],
|
mergeRequestDiffs: [],
|
||||||
mergeRequestDiff: null,
|
mergeRequestDiff: null,
|
||||||
diffViewType: getViewTypeFromQueryString() || viewTypeFromCookie || defaultViewType,
|
diffViewType: getViewTypeFromQueryString() || viewTypeFromCookie || defaultViewType,
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_COVERAGE_DATA](state, coverageFiles) {
|
[types.SET_COVERAGE_DATA](state, coverageFiles) {
|
||||||
Object.assign(state, { coverageFiles });
|
Object.assign(state, { coverageFiles, coverageLoaded: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.RENDER_FILE](state, file) {
|
[types.RENDER_FILE](state, file) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { s__, __ } from '~/locale';
|
import { s__, __ } from '~/locale';
|
||||||
|
import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
|
||||||
|
|
||||||
export const GRAPHQL_PAGE_SIZE = 30;
|
export const GRAPHQL_PAGE_SIZE = 30;
|
||||||
|
|
||||||
|
@ -33,3 +34,66 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
|
||||||
`DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after its timer finishes.`,
|
`DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after its timer finishes.`,
|
||||||
);
|
);
|
||||||
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
|
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
|
||||||
|
|
||||||
|
/* Table constants */
|
||||||
|
|
||||||
|
const defaultTableClasses = {
|
||||||
|
tdClass: 'gl-p-5!',
|
||||||
|
thClass: DEFAULT_TH_CLASSES,
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`;
|
||||||
|
|
||||||
|
export const DEFAULT_FIELDS = [
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: __('Status'),
|
||||||
|
...defaultTableClasses,
|
||||||
|
columnClass: 'gl-w-10p',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'job',
|
||||||
|
label: __('Job'),
|
||||||
|
...defaultTableClasses,
|
||||||
|
columnClass: 'gl-w-20p',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pipeline',
|
||||||
|
label: __('Pipeline'),
|
||||||
|
...defaultTableClasses,
|
||||||
|
columnClass: 'gl-w-10p',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'stage',
|
||||||
|
label: __('Stage'),
|
||||||
|
...defaultTableClasses,
|
||||||
|
columnClass: 'gl-w-10p',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
label: __('Name'),
|
||||||
|
...defaultTableClasses,
|
||||||
|
columnClass: 'gl-w-15p',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'duration',
|
||||||
|
label: __('Duration'),
|
||||||
|
...defaultTableClasses,
|
||||||
|
columnClass: 'gl-w-15p',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'coverage',
|
||||||
|
label: __('Coverage'),
|
||||||
|
tdClass: coverageTdClasses,
|
||||||
|
thClass: defaultTableClasses.thClass,
|
||||||
|
columnClass: 'gl-w-10p',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
label: '',
|
||||||
|
...defaultTableClasses,
|
||||||
|
columnClass: 'gl-w-10p',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const JOBS_TAB_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'pipeline');
|
||||||
|
|
|
@ -1,75 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTable } from '@gitlab/ui';
|
import { GlTable } from '@gitlab/ui';
|
||||||
import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
|
import { s__ } from '~/locale';
|
||||||
import { s__, __ } from '~/locale';
|
|
||||||
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
||||||
import ActionsCell from './cells/actions_cell.vue';
|
import ActionsCell from './cells/actions_cell.vue';
|
||||||
import DurationCell from './cells/duration_cell.vue';
|
import DurationCell from './cells/duration_cell.vue';
|
||||||
import JobCell from './cells/job_cell.vue';
|
import JobCell from './cells/job_cell.vue';
|
||||||
import PipelineCell from './cells/pipeline_cell.vue';
|
import PipelineCell from './cells/pipeline_cell.vue';
|
||||||
|
import { DEFAULT_FIELDS } from './constants';
|
||||||
const defaultTableClasses = {
|
|
||||||
tdClass: 'gl-p-5!',
|
|
||||||
thClass: DEFAULT_TH_CLASSES,
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
|
||||||
const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`;
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
i18n: {
|
i18n: {
|
||||||
emptyText: s__('Jobs|No jobs to show'),
|
emptyText: s__('Jobs|No jobs to show'),
|
||||||
},
|
},
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: 'status',
|
|
||||||
label: __('Status'),
|
|
||||||
...defaultTableClasses,
|
|
||||||
columnClass: 'gl-w-10p',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'job',
|
|
||||||
label: __('Job'),
|
|
||||||
...defaultTableClasses,
|
|
||||||
columnClass: 'gl-w-20p',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'pipeline',
|
|
||||||
label: __('Pipeline'),
|
|
||||||
...defaultTableClasses,
|
|
||||||
columnClass: 'gl-w-10p',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'stage',
|
|
||||||
label: __('Stage'),
|
|
||||||
...defaultTableClasses,
|
|
||||||
columnClass: 'gl-w-10p',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'name',
|
|
||||||
label: __('Name'),
|
|
||||||
...defaultTableClasses,
|
|
||||||
columnClass: 'gl-w-15p',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'duration',
|
|
||||||
label: __('Duration'),
|
|
||||||
...defaultTableClasses,
|
|
||||||
columnClass: 'gl-w-15p',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'coverage',
|
|
||||||
label: __('Coverage'),
|
|
||||||
tdClass: coverageTdClasses,
|
|
||||||
thClass: defaultTableClasses.thClass,
|
|
||||||
columnClass: 'gl-w-10p',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'actions',
|
|
||||||
label: '',
|
|
||||||
...defaultTableClasses,
|
|
||||||
columnClass: 'gl-w-10p',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
components: {
|
components: {
|
||||||
ActionsCell,
|
ActionsCell,
|
||||||
CiBadge,
|
CiBadge,
|
||||||
|
@ -83,6 +25,11 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
tableFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => DEFAULT_FIELDS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatCoverage(coverage) {
|
formatCoverage(coverage) {
|
||||||
|
@ -95,7 +42,7 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<gl-table
|
<gl-table
|
||||||
:items="jobs"
|
:items="jobs"
|
||||||
:fields="$options.fields"
|
:fields="tableFields"
|
||||||
:tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
|
:tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
|
||||||
:empty-text="$options.i18n.emptyText"
|
:empty-text="$options.i18n.emptyText"
|
||||||
show-empty
|
show-empty
|
||||||
|
|
121
app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
Normal file
121
app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<script>
|
||||||
|
import { GlIntersectionObserver, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
|
||||||
|
import produce from 'immer';
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import eventHub from '~/jobs/components/table/event_hub';
|
||||||
|
import JobsTable from '~/jobs/components/table/jobs_table.vue';
|
||||||
|
import { JOBS_TAB_FIELDS } from '~/jobs/components/table/constants';
|
||||||
|
import getPipelineJobs from '../../graphql/queries/get_pipeline_jobs.query.graphql';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fields: JOBS_TAB_FIELDS,
|
||||||
|
components: {
|
||||||
|
GlIntersectionObserver,
|
||||||
|
GlLoadingIcon,
|
||||||
|
GlSkeletonLoader,
|
||||||
|
JobsTable,
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
fullPath: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
pipelineIid: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
jobs: {
|
||||||
|
query: getPipelineJobs,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
...this.queryVariables,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
update(data) {
|
||||||
|
return data.project?.pipeline?.jobs?.nodes || [];
|
||||||
|
},
|
||||||
|
result({ data }) {
|
||||||
|
this.jobsPageInfo = data.project?.pipeline?.jobs?.pageInfo || {};
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
createFlash({ message: __('An error occured while fetching the pipelines jobs.') });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
jobs: [],
|
||||||
|
jobsPageInfo: {},
|
||||||
|
firstLoad: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
queryVariables() {
|
||||||
|
return {
|
||||||
|
fullPath: this.fullPath,
|
||||||
|
iid: this.pipelineIid,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
eventHub.$on('jobActionPerformed', this.handleJobAction);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
eventHub.$off('jobActionPerformed', this.handleJobAction);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleJobAction() {
|
||||||
|
this.firstLoad = true;
|
||||||
|
|
||||||
|
this.$apollo.queries.jobs.refetch();
|
||||||
|
},
|
||||||
|
fetchMoreJobs() {
|
||||||
|
this.firstLoad = false;
|
||||||
|
|
||||||
|
this.$apollo.queries.jobs.fetchMore({
|
||||||
|
variables: {
|
||||||
|
...this.queryVariables,
|
||||||
|
after: this.jobsPageInfo.endCursor,
|
||||||
|
},
|
||||||
|
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||||
|
const results = produce(fetchMoreResult, (draftData) => {
|
||||||
|
draftData.project.pipeline.jobs.nodes = [
|
||||||
|
...previousResult.project.pipeline.jobs.nodes,
|
||||||
|
...draftData.project.pipeline.jobs.nodes,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="$apollo.loading && firstLoad" class="gl-mt-5">
|
||||||
|
<gl-skeleton-loader :width="1248" :height="73">
|
||||||
|
<circle cx="748.031" cy="37.7193" r="15.0307" />
|
||||||
|
<circle cx="787.241" cy="37.7193" r="15.0307" />
|
||||||
|
<circle cx="827.759" cy="37.7193" r="15.0307" />
|
||||||
|
<circle cx="866.969" cy="37.7193" r="15.0307" />
|
||||||
|
<circle cx="380" cy="37" r="18" />
|
||||||
|
<rect x="432" y="19" width="126.587" height="15" />
|
||||||
|
<rect x="432" y="41" width="247" height="15" />
|
||||||
|
<rect x="158" y="19" width="86.1" height="15" />
|
||||||
|
<rect x="158" y="41" width="168" height="15" />
|
||||||
|
<rect x="22" y="19" width="96" height="36" />
|
||||||
|
<rect x="924" y="30" width="96" height="15" />
|
||||||
|
<rect x="1057" y="20" width="166" height="35" />
|
||||||
|
</gl-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<jobs-table v-else :jobs="jobs" :table-fields="$options.fields" />
|
||||||
|
|
||||||
|
<gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs">
|
||||||
|
<gl-loading-icon v-if="$apollo.loading" size="md" />
|
||||||
|
</gl-intersection-observer>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,70 @@
|
||||||
|
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
|
||||||
|
|
||||||
|
query getPipelineJobs($fullPath: ID!, $iid: ID!, $after: String) {
|
||||||
|
project(fullPath: $fullPath) {
|
||||||
|
id
|
||||||
|
pipeline(iid: $iid) {
|
||||||
|
id
|
||||||
|
jobs(after: $after, first: 20) {
|
||||||
|
pageInfo {
|
||||||
|
...PageInfo
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
artifacts {
|
||||||
|
nodes {
|
||||||
|
downloadPath
|
||||||
|
fileType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allowFailure
|
||||||
|
status
|
||||||
|
scheduledAt
|
||||||
|
manualJob
|
||||||
|
triggered
|
||||||
|
createdByTag
|
||||||
|
detailedStatus {
|
||||||
|
id
|
||||||
|
detailsPath
|
||||||
|
group
|
||||||
|
icon
|
||||||
|
label
|
||||||
|
text
|
||||||
|
tooltip
|
||||||
|
action {
|
||||||
|
id
|
||||||
|
buttonTitle
|
||||||
|
icon
|
||||||
|
method
|
||||||
|
path
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id
|
||||||
|
refName
|
||||||
|
refPath
|
||||||
|
tags
|
||||||
|
shortSha
|
||||||
|
commitPath
|
||||||
|
stage {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
name
|
||||||
|
duration
|
||||||
|
finishedAt
|
||||||
|
coverage
|
||||||
|
retryable
|
||||||
|
playable
|
||||||
|
cancelable
|
||||||
|
active
|
||||||
|
stuck
|
||||||
|
userPermissions {
|
||||||
|
readBuild
|
||||||
|
readJobArtifacts
|
||||||
|
updateBuild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { __ } from '~/locale';
|
||||||
import createDagApp from './pipeline_details_dag';
|
import createDagApp from './pipeline_details_dag';
|
||||||
import { createPipelinesDetailApp } from './pipeline_details_graph';
|
import { createPipelinesDetailApp } from './pipeline_details_graph';
|
||||||
import { createPipelineHeaderApp } from './pipeline_details_header';
|
import { createPipelineHeaderApp } from './pipeline_details_header';
|
||||||
|
import { createPipelineJobsApp } from './pipeline_details_jobs';
|
||||||
import { apolloProvider } from './pipeline_shared_client';
|
import { apolloProvider } from './pipeline_shared_client';
|
||||||
import { createTestDetails } from './pipeline_test_details';
|
import { createTestDetails } from './pipeline_test_details';
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ const SELECTORS = {
|
||||||
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
|
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
|
||||||
PIPELINE_HEADER: '#js-pipeline-header-vue',
|
PIPELINE_HEADER: '#js-pipeline-header-vue',
|
||||||
PIPELINE_TESTS: '#js-pipeline-tests-detail',
|
PIPELINE_TESTS: '#js-pipeline-tests-detail',
|
||||||
|
PIPELINE_JOBS: '#js-pipeline-jobs-vue',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function initPipelineDetailsBundle() {
|
export default async function initPipelineDetailsBundle() {
|
||||||
|
@ -55,4 +57,14 @@ export default async function initPipelineDetailsBundle() {
|
||||||
message: __('An error occurred while loading the Test Reports tab.'),
|
message: __('An error occurred while loading the Test Reports tab.'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (gon.features?.jobsTabVue) {
|
||||||
|
createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
createFlash({
|
||||||
|
message: __('An error occurred while loading the Jobs tab.'),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
34
app/assets/javascripts/pipelines/pipeline_details_jobs.js
Normal file
34
app/assets/javascripts/pipelines/pipeline_details_jobs.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { GlToast } from '@gitlab/ui';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import createDefaultClient from '~/lib/graphql';
|
||||||
|
import JobsApp from './components/jobs/jobs_app.vue';
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
Vue.use(GlToast);
|
||||||
|
|
||||||
|
const apolloProvider = new VueApollo({
|
||||||
|
defaultClient: createDefaultClient(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createPipelineJobsApp = (selector) => {
|
||||||
|
const containerEl = document.querySelector(selector);
|
||||||
|
|
||||||
|
if (!containerEl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fullPath, pipelineIid } = containerEl.dataset;
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el: containerEl,
|
||||||
|
apolloProvider,
|
||||||
|
provide: {
|
||||||
|
fullPath,
|
||||||
|
pipelineIid,
|
||||||
|
},
|
||||||
|
render(createElement) {
|
||||||
|
return createElement(JobsApp);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -94,6 +94,20 @@ export default {
|
||||||
tertiaryActionsButtons() {
|
tertiaryActionsButtons() {
|
||||||
return this.tertiaryButtons ? this.tertiaryButtons() : undefined;
|
return this.tertiaryButtons ? this.tertiaryButtons() : undefined;
|
||||||
},
|
},
|
||||||
|
hydratedSummary() {
|
||||||
|
const structuredOutput = this.summary(this.collapsedData);
|
||||||
|
const summary = {
|
||||||
|
subject: generateText(
|
||||||
|
typeof structuredOutput === 'string' ? structuredOutput : structuredOutput.subject,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (structuredOutput.meta) {
|
||||||
|
summary.meta = generateText(structuredOutput.meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
isCollapsed(newVal) {
|
isCollapsed(newVal) {
|
||||||
|
@ -182,7 +196,13 @@ export default {
|
||||||
<div class="gl-flex-grow-1">
|
<div class="gl-flex-grow-1">
|
||||||
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
|
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
|
||||||
<template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
|
<template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
|
||||||
<div v-else v-safe-html="generateText(summary(collapsedData))"></div>
|
<div v-else>
|
||||||
|
<span v-safe-html="hydratedSummary.subject"></span>
|
||||||
|
<template v-if="hydratedSummary.meta">
|
||||||
|
<br />
|
||||||
|
<span v-safe-html="hydratedSummary.meta" class="gl-font-sm"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<actions
|
<actions
|
||||||
:widget="$options.label || $options.name"
|
:widget="$options.label || $options.name"
|
||||||
|
|
|
@ -388,6 +388,10 @@ const fileExtensionIcons = {
|
||||||
log: 'log',
|
log: 'log',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const twoFileExtensionIcons = {
|
||||||
|
'gradle.kts': 'gradle',
|
||||||
|
};
|
||||||
|
|
||||||
const fileNameIcons = {
|
const fileNameIcons = {
|
||||||
'.jscsrc': 'json',
|
'.jscsrc': 'json',
|
||||||
'.jshintrc': 'json',
|
'.jshintrc': 'json',
|
||||||
|
@ -598,6 +602,9 @@ const fileNameIcons = {
|
||||||
|
|
||||||
export default function getIconForFile(name) {
|
export default function getIconForFile(name) {
|
||||||
return (
|
return (
|
||||||
fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop().toLowerCase() : ''] || ''
|
fileNameIcons[name] ||
|
||||||
|
twoFileExtensionIcons[name ? name.split('.').slice(-2).join('.') : ''] ||
|
||||||
|
fileExtensionIcons[name ? name.split('.').pop().toLowerCase() : ''] ||
|
||||||
|
''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,10 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||||
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
|
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
|
||||||
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
|
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
push_frontend_feature_flag(:jobs_tab_vue, @project, default_enabled: :yaml)
|
||||||
|
end
|
||||||
|
|
||||||
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
|
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
|
||||||
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
|
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,8 @@ module Types
|
||||||
description: 'Internal ID of the merge request.'
|
description: 'Internal ID of the merge request.'
|
||||||
field :title, GraphQL::Types::String, null: false,
|
field :title, GraphQL::Types::String, null: false,
|
||||||
description: 'Title of the merge request.'
|
description: 'Title of the merge request.'
|
||||||
markdown_field :title_html, null: true
|
|
||||||
field :description, GraphQL::Types::String, null: true,
|
field :description, GraphQL::Types::String, null: true,
|
||||||
description: 'Description of the merge request (Markdown rendered as HTML for caching).'
|
description: 'Description of the merge request (Markdown rendered as HTML for caching).'
|
||||||
markdown_field :description_html, null: true
|
|
||||||
field :state, MergeRequestStateEnum, null: false,
|
field :state, MergeRequestStateEnum, null: false,
|
||||||
description: 'State of the merge request.'
|
description: 'State of the merge request.'
|
||||||
field :created_at, Types::TimeType, null: false,
|
field :created_at, Types::TimeType, null: false,
|
||||||
|
@ -202,6 +200,9 @@ module Types
|
||||||
field :timelogs, Types::TimelogType.connection_type, null: false,
|
field :timelogs, Types::TimelogType.connection_type, null: false,
|
||||||
description: 'Timelogs on the merge request.'
|
description: 'Timelogs on the merge request.'
|
||||||
|
|
||||||
|
markdown_field :title_html, null: true
|
||||||
|
markdown_field :description_html, null: true
|
||||||
|
|
||||||
def approved_by
|
def approved_by
|
||||||
object.approved_by_users
|
object.approved_by_users
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,7 +20,6 @@ module Types
|
||||||
|
|
||||||
field :description, GraphQL::Types::String, null: true,
|
field :description, GraphQL::Types::String, null: true,
|
||||||
description: 'Description of the namespace.'
|
description: 'Description of the namespace.'
|
||||||
markdown_field :description_html, null: true
|
|
||||||
|
|
||||||
field :visibility, GraphQL::Types::String, null: true,
|
field :visibility, GraphQL::Types::String, null: true,
|
||||||
description: 'Visibility of the namespace.'
|
description: 'Visibility of the namespace.'
|
||||||
|
@ -47,6 +46,8 @@ module Types
|
||||||
null: true,
|
null: true,
|
||||||
description: "Shared runners availability for the namespace and its descendants."
|
description: "Shared runners availability for the namespace and its descendants."
|
||||||
|
|
||||||
|
markdown_field :description_html, null: true
|
||||||
|
|
||||||
def root_storage_statistics
|
def root_storage_statistics
|
||||||
Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find
|
Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,8 +33,6 @@ module Types
|
||||||
method: :note,
|
method: :note,
|
||||||
description: 'Content of the note.'
|
description: 'Content of the note.'
|
||||||
|
|
||||||
markdown_field :body_html, null: true, method: :note
|
|
||||||
|
|
||||||
field :created_at, Types::TimeType, null: false,
|
field :created_at, Types::TimeType, null: false,
|
||||||
description: 'Timestamp of the note creation.'
|
description: 'Timestamp of the note creation.'
|
||||||
field :updated_at, Types::TimeType, null: false,
|
field :updated_at, Types::TimeType, null: false,
|
||||||
|
@ -50,6 +48,8 @@ module Types
|
||||||
null: true,
|
null: true,
|
||||||
description: 'URL to view this Note in the Web UI.'
|
description: 'URL to view this Note in the Web UI.'
|
||||||
|
|
||||||
|
markdown_field :body_html, null: true, method: :note
|
||||||
|
|
||||||
def url
|
def url
|
||||||
::Gitlab::UrlBuilder.build(object)
|
::Gitlab::UrlBuilder.build(object)
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ class GravatarService
|
||||||
return unless identifier
|
return unless identifier
|
||||||
|
|
||||||
hash = Digest::MD5.hexdigest(identifier.strip.downcase)
|
hash = Digest::MD5.hexdigest(identifier.strip.downcase)
|
||||||
size = 40 unless size && size > 0
|
size = Groups::GroupMembersHelper::AVATAR_SIZE unless size && size > 0
|
||||||
|
|
||||||
sprintf gravatar_url,
|
sprintf gravatar_url,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
|
|
|
@ -29,17 +29,20 @@
|
||||||
|
|
||||||
#js-tab-builds.tab-pane
|
#js-tab-builds.tab-pane
|
||||||
- if stages.present?
|
- if stages.present?
|
||||||
.table-holder.pipeline-holder
|
- if Feature.enabled?(:jobs_tab_vue, @project, default_enabled: :yaml)
|
||||||
%table.table.ci-table.pipeline
|
#js-pipeline-jobs-vue{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
|
||||||
%thead
|
- else
|
||||||
%tr
|
.table-holder.pipeline-holder
|
||||||
%th= _('Status')
|
%table.table.ci-table.pipeline
|
||||||
%th= _('Name')
|
%thead
|
||||||
%th= _('Job ID')
|
%tr
|
||||||
%th
|
%th= _('Status')
|
||||||
%th= _('Coverage')
|
%th= _('Name')
|
||||||
%th
|
%th= _('Job ID')
|
||||||
= render partial: "projects/stage/stage", collection: stages, as: :stage
|
%th
|
||||||
|
%th= _('Coverage')
|
||||||
|
%th
|
||||||
|
= render partial: "projects/stage/stage", collection: stages, as: :stage
|
||||||
|
|
||||||
- if @pipeline.failed_builds.present?
|
- if @pipeline.failed_builds.present?
|
||||||
#js-tab-failures.build-failures.tab-pane.build-page
|
#js-tab-failures.build-failures.tab-pane.build-page
|
||||||
|
|
8
config/feature_flags/development/jobs_tab_vue.yml
Normal file
8
config/feature_flags/development/jobs_tab_vue.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: jobs_tab_vue
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76146
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347371
|
||||||
|
milestone: '14.6'
|
||||||
|
type: development
|
||||||
|
group: group::pipeline execution
|
||||||
|
default_enabled: false
|
|
@ -66,6 +66,24 @@ This solution is relatively simple to set up: you just need to create an email
|
||||||
address dedicated to receive your users' replies to GitLab notifications. However,
|
address dedicated to receive your users' replies to GitLab notifications. However,
|
||||||
this method only supports replies, and not the other features of [incoming email](#incoming-email).
|
this method only supports replies, and not the other features of [incoming email](#incoming-email).
|
||||||
|
|
||||||
|
## Accepted headers
|
||||||
|
|
||||||
|
Email is processed correctly when a configured email address is present in one of the following headers:
|
||||||
|
|
||||||
|
- `To`
|
||||||
|
- `Delivered-To`
|
||||||
|
- `Envelope-To` or `X-Envelope-To`
|
||||||
|
|
||||||
|
In GitLab 14.6 and later, [Service Desk](../user/project/service_desk.md)
|
||||||
|
also checks these additional headers.
|
||||||
|
|
||||||
|
Usually, the "To" field contains the email address of the primary receiver.
|
||||||
|
However, it might not include the configured GitLab email address if:
|
||||||
|
|
||||||
|
- The address is in the "CC" field.
|
||||||
|
- The address was included when using "Reply all".
|
||||||
|
- The email was forwarded.
|
||||||
|
|
||||||
## Set it up
|
## Set it up
|
||||||
|
|
||||||
If you want to use Gmail / Google Apps for incoming email, make sure you have
|
If you want to use Gmail / Google Apps for incoming email, make sure you have
|
||||||
|
|
|
@ -103,7 +103,7 @@ POST /groups/:id/protected_environments
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) maintained by the authenticated user. |
|
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) maintained by the authenticated user. |
|
||||||
| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).|
|
| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).|
|
||||||
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. Here, `group_id` must be of a sub-group of the protecting group.|
|
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. |
|
||||||
|
|
||||||
The assignable `user_id` are the users who belong to the given group with the Maintainer role (or above).
|
The assignable `user_id` are the users who belong to the given group with the Maintainer role (or above).
|
||||||
The assignable `group_id` are the sub-groups under the given group.
|
The assignable `group_id` are the sub-groups under the given group.
|
||||||
|
|
|
@ -55,23 +55,24 @@ The correct approach is to add a new metric for GitLab 12.6 release with updated
|
||||||
|
|
||||||
and update existing business analysis artefacts to use `example_metric_without_archived` instead of `example_metric`
|
and update existing business analysis artefacts to use `example_metric_without_archived` instead of `example_metric`
|
||||||
|
|
||||||
## Deprecate a metric
|
## Remove a metric
|
||||||
|
|
||||||
If a metric is obsolete and you no longer use it, you can mark it as deprecated.
|
WARNING:
|
||||||
|
If a metric is not used in Sisense or any other system after 6 months, the
|
||||||
|
Product Intelligence team marks it as inactive and assigns it to the group owner for review.
|
||||||
|
|
||||||
For an example of the metric deprecation process take a look at this [example merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59899)
|
We are working on automating this process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/338466) for details.
|
||||||
|
|
||||||
To deprecate a metric:
|
Product Intelligence removes metrics from Service Ping if they are not used in any Sisense dashboard.
|
||||||
|
|
||||||
|
For an example of the metric removal process, see this [example issue](https://gitlab.com/gitlab-org/gitlab/-/issues/297029).
|
||||||
|
|
||||||
|
To remove a metric:
|
||||||
|
|
||||||
1. Check the following YAML files and verify the metric is not used in an aggregate:
|
1. Check the following YAML files and verify the metric is not used in an aggregate:
|
||||||
- [`config/metrics/aggregates/*.yaml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/aggregates/)
|
- [`config/metrics/aggregates/*.yaml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/aggregates/)
|
||||||
- [`ee/config/metrics/aggregates/*.yaml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/aggregates/)
|
- [`ee/config/metrics/aggregates/*.yaml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/aggregates/)
|
||||||
|
|
||||||
1. Create an issue in the [GitLab Data Team
|
|
||||||
project](https://gitlab.com/gitlab-data/analytics/-/issues). Ask for
|
|
||||||
confirmation that the metric is not used by other teams, or in any of the SiSense
|
|
||||||
dashboards.
|
|
||||||
|
|
||||||
1. Verify the metric is not used to calculate the conversational index. The
|
1. Verify the metric is not used to calculate the conversational index. The
|
||||||
conversational index is a measure that reports back to self-managed instances
|
conversational index is a measure that reports back to self-managed instances
|
||||||
to inform administrators of the progress of DevOps adoption for the instance.
|
to inform administrators of the progress of DevOps adoption for the instance.
|
||||||
|
@ -81,70 +82,6 @@ To deprecate a metric:
|
||||||
to view the metrics that are used. The metrics are represented
|
to view the metrics that are used. The metrics are represented
|
||||||
as the keys that are passed as a field argument into the `get_value` method.
|
as the keys that are passed as a field argument into the `get_value` method.
|
||||||
|
|
||||||
1. Document the deprecation in the metric's YAML definition. Set
|
|
||||||
the `status:` attribute to `deprecated`, for example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
key_path: analytics_unique_visits.analytics_unique_visits_for_any_target_monthly
|
|
||||||
description: Visits to any of the pages listed above per month
|
|
||||||
product_section: dev
|
|
||||||
product_stage: manage
|
|
||||||
product_group: group::analytics
|
|
||||||
product_category:
|
|
||||||
value_type: number
|
|
||||||
status: deprecated
|
|
||||||
time_frame: 28d
|
|
||||||
data_source:
|
|
||||||
distribution:
|
|
||||||
- ce
|
|
||||||
tier:
|
|
||||||
- free
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Replace the metric's instrumentation with a fixed value. This avoids wasting
|
|
||||||
resources to calculate the deprecated metric. In
|
|
||||||
[`lib/gitlab/usage_data.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data.rb)
|
|
||||||
or
|
|
||||||
[`ee/lib/ee/gitlab/usage_data.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/ee/gitlab/usage_data.rb),
|
|
||||||
replace the code that calculates the metric's value with a fixed value that
|
|
||||||
indicates it's deprecated:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
module Gitlab
|
|
||||||
class UsageData
|
|
||||||
DEPRECATED_VALUE = -1000
|
|
||||||
|
|
||||||
def analytics_unique_visits_data
|
|
||||||
results['analytics_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) }
|
|
||||||
results['analytics_unique_visits_for_any_target_monthly'] = DEPRECATED_VALUE
|
|
||||||
|
|
||||||
{ analytics_unique_visits: results }
|
|
||||||
end
|
|
||||||
# ...
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Remove a metric
|
|
||||||
|
|
||||||
### Removal policy
|
|
||||||
|
|
||||||
WARNING:
|
|
||||||
A metric that is not used in Sisense or any other system after 6 months is marked by the
|
|
||||||
Product Intelligence team as inactive and is assigned to the group owner for review.
|
|
||||||
|
|
||||||
We are working on automating this process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/338466) for details.
|
|
||||||
|
|
||||||
Metrics can be removed from Service Ping if they:
|
|
||||||
|
|
||||||
- Were previously [deprecated](#deprecate-a-metric).
|
|
||||||
- Are not used in any Sisense dashboard.
|
|
||||||
|
|
||||||
For an example of the metric removal process take a look at this [example issue](https://gitlab.com/gitlab-org/gitlab/-/issues/297029)
|
|
||||||
|
|
||||||
### To remove a deprecated metric
|
|
||||||
|
|
||||||
1. Verify that removing the metric from the Service Ping payload does not cause
|
1. Verify that removing the metric from the Service Ping payload does not cause
|
||||||
errors in [Version App](https://gitlab.com/gitlab-services/version-gitlab-com)
|
errors in [Version App](https://gitlab.com/gitlab-services/version-gitlab-com)
|
||||||
when the updated payload is collected and processed. Version App collects
|
when the updated payload is collected and processed. Version App collects
|
||||||
|
@ -159,9 +96,6 @@ For an example of the metric removal process take a look at this [example issue]
|
||||||
Ask for confirmation that the metric is not referred to in any SiSense dashboards and
|
Ask for confirmation that the metric is not referred to in any SiSense dashboards and
|
||||||
can be safely removed from Service Ping. Use this
|
can be safely removed from Service Ping. Use this
|
||||||
[example issue](https://gitlab.com/gitlab-data/analytics/-/issues/7539) for guidance.
|
[example issue](https://gitlab.com/gitlab-data/analytics/-/issues/7539) for guidance.
|
||||||
This step can be skipped if verification done during [deprecation process](#deprecate-a-metric)
|
|
||||||
reported that metric is not required by any data transformation in Snowflake data warehouse nor it is
|
|
||||||
used by any of SiSense dashboards.
|
|
||||||
|
|
||||||
1. After you verify the metric can be safely removed,
|
1. After you verify the metric can be safely removed,
|
||||||
update the attributes of the metric's YAML definition:
|
update the attributes of the metric's YAML definition:
|
||||||
|
|
|
@ -1,165 +1,9 @@
|
||||||
---
|
---
|
||||||
stage: Release
|
redirect_to: 'custom_domains_ssl_tls_certification/lets_encrypt_integration.md'
|
||||||
group: Release
|
remove_date: '2022-03-14'
|
||||||
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
|
|
||||||
description: "How to secure GitLab Pages websites with Let's Encrypt (manual process, deprecated)."
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Let's Encrypt for GitLab Pages (manual process, deprecated) **(FREE)**
|
This file was moved to [another location](custom_domains_ssl_tls_certification/lets_encrypt_integration.md).
|
||||||
|
|
||||||
WARNING:
|
<!-- This redirect file can be deleted after <2022-03-14>. -->
|
||||||
This method is still valid but was **deprecated** in favor of the
|
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
|
||||||
[Let's Encrypt integration](custom_domains_ssl_tls_certification/lets_encrypt_integration.md)
|
|
||||||
introduced in GitLab 12.1.
|
|
||||||
|
|
||||||
If you have a GitLab Pages website served under your own domain,
|
|
||||||
you might want to secure it with a SSL/TLS certificate.
|
|
||||||
|
|
||||||
[Let's Encrypt](https://letsencrypt.org) is a free, automated, and
|
|
||||||
open source Certificate Authority.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
To follow along with this tutorial, we assume you already have:
|
|
||||||
|
|
||||||
- [Created a project](index.md#getting-started) in GitLab
|
|
||||||
containing your website's source code.
|
|
||||||
- Acquired a domain (`example.com`) and added a [DNS entry](custom_domains_ssl_tls_certification/index.md#set-up-pages-with-a-custom-domain)
|
|
||||||
pointing it to your Pages website.
|
|
||||||
- [Added your domain to your Pages project](custom_domains_ssl_tls_certification/index.md#steps)
|
|
||||||
and verified your ownership.
|
|
||||||
- Cloned your project into your computer.
|
|
||||||
- Your website up and running, served under HTTP protocol at `http://example.com`.
|
|
||||||
|
|
||||||
## Obtaining a Let's Encrypt certificate
|
|
||||||
|
|
||||||
Once you have the requirements addressed, follow the instructions
|
|
||||||
below to learn how to obtain the certificate.
|
|
||||||
|
|
||||||
Note that these instructions were tested on macOS Mojave. For other operating systems the steps
|
|
||||||
might be slightly different. Follow the
|
|
||||||
[CertBot instructions](https://certbot.eff.org/) according to your OS.
|
|
||||||
|
|
||||||
1. On your computer, open a terminal and navigate to your repository's
|
|
||||||
root directory:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cd path/to/dir
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Install CertBot (the tool Let's Encrypt uses to issue certificates):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
brew install certbot
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Request a certificate for your domain (`example.com`) and
|
|
||||||
provide an email account (`your@email.com`) to receive notifications:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo certbot certonly -a manual -d example.com --email your@email.com
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, you can register without adding an email account,
|
|
||||||
but you aren't notified about the certificate expiration's date:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo certbot certonly -a manual -d example.com --register-unsafely-without-email
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE:
|
|
||||||
Read through CertBot's documentation on their
|
|
||||||
[command line options](https://eff-certbot.readthedocs.io/using.html#certbot-command-line-options).
|
|
||||||
|
|
||||||
1. You're prompted with a message to agree with their terms.
|
|
||||||
Press `A` to agree and `Y` to let they log your IP.
|
|
||||||
|
|
||||||
CertBot then prompts you with the following message:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
Create a file containing just this data:
|
|
||||||
|
|
||||||
Rxnv6WKo95hsuLVX3osmT6LgmzsJKSaK9htlPToohOP.HUGNKk82jlsmOOfphlt8Jy69iuglsn095nxOMH9j3Yb
|
|
||||||
|
|
||||||
And make it available on your web server at this URL:
|
|
||||||
|
|
||||||
http://example.com/.well-known/acme-challenge/Rxnv6WKo95hsuLVX3osmT6LgmzsJKSaK9htlPToohOP
|
|
||||||
|
|
||||||
Press Enter to Continue
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **Do not press Enter yet.** Let's Encrypt needs to verify your
|
|
||||||
domain ownership before issuing the certificate. To do so, create 3
|
|
||||||
consecutive directories under your website's root:
|
|
||||||
`/.well-known/acme-challenge/Rxnv6WKo95hsuLVX3osmT6LgmzsJKSaK9htlPToohOP/`
|
|
||||||
and add to the last folder an `index.html` file containing the content
|
|
||||||
referred on the previous prompt message:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
Rxnv6WKo95hsuLVX3osmT6LgmzsJKSaK9htlPToohOP.HUGNKk82jlsmOOfphlt8Jy69iuglsn095nxOMH9j3Yb
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that this file needs to be accessed under
|
|
||||||
`http://example.com/.well-known/acme-challenge/Rxnv6WKo95hsuLVX3osmT6LgmzsJKSaK9htlPToohOP`
|
|
||||||
to allow Let's Encrypt to verify the ownership of your domain,
|
|
||||||
therefore, it needs to be part of the website content under the
|
|
||||||
repository's [`public`](index.md#how-it-works) folder.
|
|
||||||
|
|
||||||
1. Add, commit, and push the file into your repository in GitLab. Once the pipeline
|
|
||||||
passes, press **Enter** on your terminal to continue issuing your
|
|
||||||
certificate. CertBot then prompts you with the following message:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
Waiting for verification...
|
|
||||||
Cleaning up challenges
|
|
||||||
|
|
||||||
IMPORTANT NOTES:
|
|
||||||
- Congratulations! Your certificate and chain have been saved at:
|
|
||||||
/etc/letsencrypt/live/example.com/fullchain.pem
|
|
||||||
Your key file has been saved at:
|
|
||||||
/etc/letsencrypt/live/example.com/privkey.pem
|
|
||||||
Your cert will expire on 2019-03-12. To obtain a new or tweaked
|
|
||||||
version of this certificate in the future, simply run certbot
|
|
||||||
again. To non-interactively renew *all* of your certificates, run
|
|
||||||
"certbot renew"
|
|
||||||
- If you like Certbot, please consider supporting our work by:
|
|
||||||
|
|
||||||
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
|
|
||||||
Donating to EFF: https://eff.org/donate-le
|
|
||||||
```
|
|
||||||
|
|
||||||
## Add your certificate to GitLab Pages
|
|
||||||
|
|
||||||
Now that your certificate has been issued, let's add it to your Pages site:
|
|
||||||
|
|
||||||
1. Back at GitLab, navigate to your project's **Settings > Pages**,
|
|
||||||
find your domain and click **Details** and **Edit** to add your certificate.
|
|
||||||
1. From your terminal, copy and paste the certificate into the first field
|
|
||||||
**Certificate (PEM)**:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo cat /etc/letsencrypt/live/example.com/fullchain.pem | pbcopy
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Copy and paste the private key into the second field **Key (PEM)**:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo cat /etc/letsencrypt/live/example.com/privkey.pem | pbcopy
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Click **Save changes** to apply them to your website.
|
|
||||||
1. Wait a few minutes for the configuration changes to take effect.
|
|
||||||
1. Visit your website at `https://example.com`.
|
|
||||||
|
|
||||||
To force `https` connections on your site, navigate to your
|
|
||||||
project's **Settings > Pages** and check **Force HTTPS (requires
|
|
||||||
valid certificates)**.
|
|
||||||
|
|
||||||
## Renewal
|
|
||||||
|
|
||||||
Let's Encrypt certificates expire every 90 days and you must
|
|
||||||
renew them periodically. To renew all your certificates at once, run:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo certbot renew
|
|
||||||
```
|
|
||||||
|
|
|
@ -289,6 +289,8 @@ In these issues, you can also see our friendly neighborhood [Support Bot](#suppo
|
||||||
|
|
||||||
### As an end user (issue creator)
|
### As an end user (issue creator)
|
||||||
|
|
||||||
|
> Support for additional email headers [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/346600) in GitLab 14.6.
|
||||||
|
> In earlier versions, the Service Desk email address had to be in the "To" field.
|
||||||
To create a Service Desk issue, an end user does not need to know anything about
|
To create a Service Desk issue, an end user does not need to know anything about
|
||||||
the GitLab instance. They just send an email to the address they are given, and
|
the GitLab instance. They just send an email to the address they are given, and
|
||||||
receive an email back confirming receipt:
|
receive an email back confirming receipt:
|
||||||
|
@ -304,6 +306,9 @@ are sent as emails:
|
||||||
|
|
||||||
Any responses they send via email are displayed in the issue itself.
|
Any responses they send via email are displayed in the issue itself.
|
||||||
|
|
||||||
|
For information about headers used for treating email, see
|
||||||
|
[the incoming email documentation](../../administration/incoming_email.md#accepted-headers).
|
||||||
|
|
||||||
### As a responder to the issue
|
### As a responder to the issue
|
||||||
|
|
||||||
For responders to the issue, everything works just like other GitLab issues.
|
For responders to the issue, everything works just like other GitLab issues.
|
||||||
|
|
|
@ -3653,6 +3653,9 @@ msgstr ""
|
||||||
msgid "An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present."
|
msgid "An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "An error occured while fetching the pipelines jobs."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred adding a draft to the thread."
|
msgid "An error occurred adding a draft to the thread."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -3860,6 +3863,9 @@ msgstr ""
|
||||||
msgid "An error occurred while loading projects."
|
msgid "An error occurred while loading projects."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "An error occurred while loading the Jobs tab."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred while loading the Needs tab."
|
msgid "An error occurred while loading the Needs tab."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -31357,6 +31363,9 @@ msgstr ""
|
||||||
msgid "SecurityOrchestration|New policy"
|
msgid "SecurityOrchestration|New policy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SecurityOrchestration|No rules defined - policy will not run."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
|
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -31441,9 +31450,6 @@ msgstr ""
|
||||||
msgid "SecurityOrchestration|view results"
|
msgid "SecurityOrchestration|view results"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "SecurityOrhestration|No rules defined - policy will not run."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "SecurityPolicies|+%{count} more"
|
msgid "SecurityPolicies|+%{count} more"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -26,11 +26,11 @@ RSpec.describe 'mail_room.yml' do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_env('GITLAB_REDIS_QUEUES_CONFIG_FILE', absolute_path(queues_config_path))
|
stub_env('GITLAB_REDIS_QUEUES_CONFIG_FILE', absolute_path(queues_config_path))
|
||||||
clear_queues_raw_config
|
redis_clear_raw_config!(Gitlab::Redis::Queues)
|
||||||
end
|
end
|
||||||
|
|
||||||
after do
|
after do
|
||||||
clear_queues_raw_config
|
redis_clear_raw_config!(Gitlab::Redis::Queues)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when incoming email is disabled' do
|
context 'when incoming email is disabled' do
|
||||||
|
@ -103,12 +103,6 @@ RSpec.describe 'mail_room.yml' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_queues_raw_config
|
|
||||||
Gitlab::Redis::Queues.remove_instance_variable(:@_raw_config)
|
|
||||||
rescue NameError
|
|
||||||
# raised if @_raw_config was not set; ignore
|
|
||||||
end
|
|
||||||
|
|
||||||
def absolute_path(path)
|
def absolute_path(path)
|
||||||
Rails.root.join(path).to_s
|
Rails.root.join(path).to_s
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,14 +24,15 @@ RSpec.describe 'Commits' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'commit status is Generic Commit Status' do
|
context 'commit status is Generic Commit Status' do
|
||||||
let!(:status) { create(:generic_commit_status, pipeline: pipeline) }
|
let!(:status) { create(:generic_commit_status, pipeline: pipeline, ref: pipeline.ref) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.add_reporter(user)
|
project.add_reporter(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Commit builds' do
|
describe 'Commit builds with jobs_tab_feature flag off' do
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(jobs_tab_vue: false)
|
||||||
visit pipeline_path(pipeline)
|
visit pipeline_path(pipeline)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -89,8 +90,9 @@ RSpec.describe 'Commits' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'Download artifacts' do
|
context 'Download artifacts with jobs_tab_vue feature flag off' do
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(jobs_tab_vue: false)
|
||||||
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
|
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -118,8 +120,9 @@ RSpec.describe 'Commits' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when logged as reporter" do
|
context "when logged as reporter and with jobs_tab_vue feature flag off" do
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(jobs_tab_vue: false)
|
||||||
project.add_reporter(user)
|
project.add_reporter(user)
|
||||||
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
|
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
|
||||||
visit pipeline_path(pipeline)
|
visit pipeline_path(pipeline)
|
||||||
|
|
|
@ -53,6 +53,7 @@ RSpec.describe 'Pipeline', :js do
|
||||||
pipeline: pipeline,
|
pipeline: pipeline,
|
||||||
name: 'jenkins',
|
name: 'jenkins',
|
||||||
stage: 'external',
|
stage: 'external',
|
||||||
|
ref: 'master',
|
||||||
target_url: 'http://gitlab.com/status')
|
target_url: 'http://gitlab.com/status')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -915,7 +916,7 @@ RSpec.describe 'Pipeline', :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /:project/-/pipelines/:id/builds' do
|
describe 'GET /:project/-/pipelines/:id/builds with jobs_tab_vue feature flag turned off' do
|
||||||
include_context 'pipeline builds'
|
include_context 'pipeline builds'
|
||||||
|
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
@ -923,6 +924,7 @@ RSpec.describe 'Pipeline', :js do
|
||||||
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
|
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(jobs_tab_vue: false)
|
||||||
visit builds_project_pipeline_path(project, pipeline)
|
visit builds_project_pipeline_path(project, pipeline)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -625,7 +625,7 @@ RSpec.describe 'Pipelines', :js do
|
||||||
create_build('test', 1, 'audit', :created)
|
create_build('test', 1, 'audit', :created)
|
||||||
create_build('deploy', 2, 'production', :created)
|
create_build('deploy', 2, 'production', :created)
|
||||||
|
|
||||||
create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
|
create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3, ref: 'master')
|
||||||
|
|
||||||
visit project_pipeline_path(project, pipeline)
|
visit project_pipeline_path(project, pipeline)
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
|
@ -277,3 +277,36 @@ describe('DiffRow', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('coverage state memoization', () => {
|
||||||
|
it('updates when coverage is loaded', () => {
|
||||||
|
const lineWithoutCoverage = {};
|
||||||
|
const lineWithCoverage = {
|
||||||
|
text: 'Test coverage: 5 hits',
|
||||||
|
class: 'coverage',
|
||||||
|
};
|
||||||
|
|
||||||
|
const unchangedProps = {
|
||||||
|
inline: true,
|
||||||
|
filePath: 'file/path',
|
||||||
|
line: { left: { new_line: 3 } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const noCoverageProps = {
|
||||||
|
fileLineCoverage: () => lineWithoutCoverage,
|
||||||
|
coverageLoaded: false,
|
||||||
|
...unchangedProps,
|
||||||
|
};
|
||||||
|
const coverageProps = {
|
||||||
|
fileLineCoverage: () => lineWithCoverage,
|
||||||
|
coverageLoaded: true,
|
||||||
|
...unchangedProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
// this caches no coverage for the line
|
||||||
|
expect(DiffRow.coverageStateLeft(noCoverageProps)).toStrictEqual(lineWithoutCoverage);
|
||||||
|
|
||||||
|
// this retrieves coverage for the line because it has been recached
|
||||||
|
expect(DiffRow.coverageStateLeft(coverageProps)).toStrictEqual(lineWithCoverage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -112,6 +112,7 @@ describe('DiffsStoreMutations', () => {
|
||||||
mutations[types.SET_COVERAGE_DATA](state, coverage);
|
mutations[types.SET_COVERAGE_DATA](state, coverage);
|
||||||
|
|
||||||
expect(state.coverageFiles).toEqual(coverage);
|
expect(state.coverageFiles).toEqual(coverage);
|
||||||
|
expect(state.coverageLoaded).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
106
spec/frontend/pipelines/components/jobs/jobs_app_spec.js
Normal file
106
spec/frontend/pipelines/components/jobs/jobs_app_spec.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { GlIntersectionObserver, GlSkeletonLoader } from '@gitlab/ui';
|
||||||
|
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
|
||||||
|
import JobsTable from '~/jobs/components/table/jobs_table.vue';
|
||||||
|
import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql';
|
||||||
|
import { mockPipelineJobsQueryResponse } from '../../mock_data';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(VueApollo);
|
||||||
|
|
||||||
|
jest.mock('~/flash');
|
||||||
|
|
||||||
|
describe('Jobs app', () => {
|
||||||
|
let wrapper;
|
||||||
|
let resolverSpy;
|
||||||
|
|
||||||
|
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||||
|
const findJobsTable = () => wrapper.findComponent(JobsTable);
|
||||||
|
|
||||||
|
const triggerInfiniteScroll = () =>
|
||||||
|
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
|
||||||
|
|
||||||
|
const createMockApolloProvider = (resolver) => {
|
||||||
|
const requestHandlers = [[getPipelineJobsQuery, resolver]];
|
||||||
|
|
||||||
|
return createMockApollo(requestHandlers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComponent = (resolver) => {
|
||||||
|
wrapper = shallowMount(JobsApp, {
|
||||||
|
provide: {
|
||||||
|
fullPath: 'root/ci-project',
|
||||||
|
pipelineIid: 1,
|
||||||
|
},
|
||||||
|
localVue,
|
||||||
|
apolloProvider: createMockApolloProvider(resolver),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resolverSpy = jest.fn().mockResolvedValue(mockPipelineJobsQueryResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the loading state', () => {
|
||||||
|
createComponent(resolverSpy);
|
||||||
|
|
||||||
|
expect(findSkeletonLoader().exists()).toBe(true);
|
||||||
|
expect(findJobsTable().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the jobs table', async () => {
|
||||||
|
createComponent(resolverSpy);
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findJobsTable().exists()).toBe(true);
|
||||||
|
expect(findSkeletonLoader().exists()).toBe(false);
|
||||||
|
expect(createFlash).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles job fetch error correctly', async () => {
|
||||||
|
resolverSpy = jest.fn().mockRejectedValue(new Error('GraphQL error'));
|
||||||
|
|
||||||
|
createComponent(resolverSpy);
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(createFlash).toHaveBeenCalledWith({
|
||||||
|
message: 'An error occured while fetching the pipelines jobs.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles infinite scrolling by calling fetchMore', async () => {
|
||||||
|
createComponent(resolverSpy);
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
triggerInfiniteScroll();
|
||||||
|
|
||||||
|
expect(resolverSpy).toHaveBeenCalledWith({
|
||||||
|
after: 'eyJpZCI6Ijg0NyJ9',
|
||||||
|
fullPath: 'root/ci-project',
|
||||||
|
iid: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not display main loading state again after fetchMore', async () => {
|
||||||
|
createComponent(resolverSpy);
|
||||||
|
|
||||||
|
expect(findSkeletonLoader().exists()).toBe(true);
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
triggerInfiniteScroll();
|
||||||
|
|
||||||
|
expect(findSkeletonLoader().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -505,3 +505,132 @@ export const mockSearch = [
|
||||||
export const mockBranchesAfterMap = ['branch-1', 'branch-10', 'branch-11'];
|
export const mockBranchesAfterMap = ['branch-1', 'branch-10', 'branch-11'];
|
||||||
|
|
||||||
export const mockTagsAfterMap = ['tag-3', 'tag-2', 'tag-1', 'main-tag'];
|
export const mockTagsAfterMap = ['tag-3', 'tag-2', 'tag-1', 'main-tag'];
|
||||||
|
|
||||||
|
export const mockPipelineJobsQueryResponse = {
|
||||||
|
data: {
|
||||||
|
project: {
|
||||||
|
id: 'gid://gitlab/Project/20',
|
||||||
|
__typename: 'Project',
|
||||||
|
pipeline: {
|
||||||
|
id: 'gid://gitlab/Ci::Pipeline/224',
|
||||||
|
__typename: 'Pipeline',
|
||||||
|
jobs: {
|
||||||
|
__typename: 'CiJobConnection',
|
||||||
|
pageInfo: {
|
||||||
|
endCursor: 'eyJpZCI6Ijg0NyJ9',
|
||||||
|
hasNextPage: true,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
startCursor: 'eyJpZCI6IjYyMCJ9',
|
||||||
|
__typename: 'PageInfo',
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
artifacts: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
downloadPath: '/root/ci-project/-/jobs/620/artifacts/download?file_type=trace',
|
||||||
|
fileType: 'TRACE',
|
||||||
|
__typename: 'CiJobArtifact',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
__typename: 'CiJobArtifactConnection',
|
||||||
|
},
|
||||||
|
allowFailure: false,
|
||||||
|
status: 'SUCCESS',
|
||||||
|
scheduledAt: null,
|
||||||
|
manualJob: false,
|
||||||
|
triggered: null,
|
||||||
|
createdByTag: false,
|
||||||
|
detailedStatus: {
|
||||||
|
id: 'success-620-620',
|
||||||
|
detailsPath: '/root/ci-project/-/jobs/620',
|
||||||
|
group: 'success',
|
||||||
|
icon: 'status_success',
|
||||||
|
label: 'passed',
|
||||||
|
text: 'passed',
|
||||||
|
tooltip: 'passed (retried)',
|
||||||
|
action: null,
|
||||||
|
__typename: 'DetailedStatus',
|
||||||
|
},
|
||||||
|
id: 'gid://gitlab/Ci::Build/620',
|
||||||
|
refName: 'main',
|
||||||
|
refPath: '/root/ci-project/-/commits/main',
|
||||||
|
tags: [],
|
||||||
|
shortSha: '5acce24b',
|
||||||
|
commitPath: '/root/ci-project/-/commit/5acce24b3737d4f0d649ad0a26ae1903a2b35f5e',
|
||||||
|
stage: { id: 'gid://gitlab/Ci::Stage/148', name: 'test', __typename: 'CiStage' },
|
||||||
|
name: 'coverage_job',
|
||||||
|
duration: 4,
|
||||||
|
finishedAt: '2021-12-06T14:13:49Z',
|
||||||
|
coverage: 82.71,
|
||||||
|
retryable: false,
|
||||||
|
playable: false,
|
||||||
|
cancelable: false,
|
||||||
|
active: false,
|
||||||
|
stuck: false,
|
||||||
|
userPermissions: {
|
||||||
|
readBuild: true,
|
||||||
|
readJobArtifacts: true,
|
||||||
|
updateBuild: true,
|
||||||
|
__typename: 'JobPermissions',
|
||||||
|
},
|
||||||
|
__typename: 'CiJob',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
artifacts: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
downloadPath: '/root/ci-project/-/jobs/619/artifacts/download?file_type=trace',
|
||||||
|
fileType: 'TRACE',
|
||||||
|
__typename: 'CiJobArtifact',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
__typename: 'CiJobArtifactConnection',
|
||||||
|
},
|
||||||
|
allowFailure: false,
|
||||||
|
status: 'SUCCESS',
|
||||||
|
scheduledAt: null,
|
||||||
|
manualJob: false,
|
||||||
|
triggered: null,
|
||||||
|
createdByTag: false,
|
||||||
|
detailedStatus: {
|
||||||
|
id: 'success-619-619',
|
||||||
|
detailsPath: '/root/ci-project/-/jobs/619',
|
||||||
|
group: 'success',
|
||||||
|
icon: 'status_success',
|
||||||
|
label: 'passed',
|
||||||
|
text: 'passed',
|
||||||
|
tooltip: 'passed (retried)',
|
||||||
|
action: null,
|
||||||
|
__typename: 'DetailedStatus',
|
||||||
|
},
|
||||||
|
id: 'gid://gitlab/Ci::Build/619',
|
||||||
|
refName: 'main',
|
||||||
|
refPath: '/root/ci-project/-/commits/main',
|
||||||
|
tags: [],
|
||||||
|
shortSha: '5acce24b',
|
||||||
|
commitPath: '/root/ci-project/-/commit/5acce24b3737d4f0d649ad0a26ae1903a2b35f5e',
|
||||||
|
stage: { id: 'gid://gitlab/Ci::Stage/148', name: 'test', __typename: 'CiStage' },
|
||||||
|
name: 'test_job_two',
|
||||||
|
duration: 4,
|
||||||
|
finishedAt: '2021-12-06T14:13:44Z',
|
||||||
|
coverage: null,
|
||||||
|
retryable: false,
|
||||||
|
playable: false,
|
||||||
|
cancelable: false,
|
||||||
|
active: false,
|
||||||
|
stuck: false,
|
||||||
|
userPermissions: {
|
||||||
|
readBuild: true,
|
||||||
|
readJobArtifacts: true,
|
||||||
|
updateBuild: true,
|
||||||
|
__typename: 'JobPermissions',
|
||||||
|
},
|
||||||
|
__typename: 'CiJob',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::Redis::Sessions do
|
RSpec.describe Gitlab::Redis::Sessions do
|
||||||
include_examples "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState
|
it_behaves_like "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState
|
||||||
|
|
||||||
describe 'redis instance used in connection pool' do
|
describe 'redis instance used in connection pool' do
|
||||||
before do
|
before do
|
||||||
|
@ -42,25 +42,51 @@ RSpec.describe Gitlab::Redis::Sessions do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#store' do
|
describe '#store' do
|
||||||
subject { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
|
subject(:store) { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
|
||||||
|
|
||||||
context 'when redis.sessions configuration is NOT provided' do
|
context 'when redis.sessions configuration is NOT provided' do
|
||||||
it 'instantiates ::Redis instance' do
|
it 'instantiates ::Redis instance' do
|
||||||
expect(described_class).to receive(:config_fallback?).and_return(true)
|
expect(described_class).to receive(:config_fallback?).and_return(true)
|
||||||
expect(subject).to be_instance_of(::Redis::Store)
|
expect(store).to be_instance_of(::Redis::Store)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when redis.sessions configuration is provided' do
|
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
|
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)
|
allow(described_class).to receive(:config_fallback?).and_return(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'instantiates an instance of MultiStore' do
|
after do
|
||||||
expect(subject).to be_instance_of(::Gitlab::Redis::MultiStore)
|
redis_clear_raw_config!(Gitlab::Redis::Sessions)
|
||||||
|
redis_clear_raw_config!(Gitlab::Redis::SharedState)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_sessions, :use_primary_store_as_default_for_sessions
|
# 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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,4 +32,11 @@ module RedisHelpers
|
||||||
def redis_sessions_cleanup!
|
def redis_sessions_cleanup!
|
||||||
Gitlab::Redis::Sessions.with(&:flushdb)
|
Gitlab::Redis::Sessions.with(&:flushdb)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Usage: reset cached instance config
|
||||||
|
def redis_clear_raw_config!(instance_class)
|
||||||
|
instance_class.remove_instance_variable(:@_raw_config)
|
||||||
|
rescue NameError
|
||||||
|
# raised if @_raw_config was not set; ignore
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,13 +8,13 @@ RSpec.shared_examples "redis_new_instance_shared_examples" do |name, fallback_cl
|
||||||
let(:fallback_config_file) { nil }
|
let(:fallback_config_file) { nil }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
fallback_class.remove_instance_variable(:@_raw_config) rescue nil
|
redis_clear_raw_config!(fallback_class)
|
||||||
|
|
||||||
allow(fallback_class).to receive(:config_file_name).and_return(fallback_config_file)
|
allow(fallback_class).to receive(:config_file_name).and_return(fallback_config_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
after do
|
after do
|
||||||
fallback_class.remove_instance_variable(:@_raw_config) rescue nil
|
redis_clear_raw_config!(fallback_class)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like "redis_shared_examples"
|
it_behaves_like "redis_shared_examples"
|
||||||
|
|
|
@ -20,11 +20,11 @@ RSpec.shared_examples "redis_shared_examples" do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(described_class).to receive(:config_file_name).and_return(Rails.root.join(config_file_name).to_s)
|
allow(described_class).to receive(:config_file_name).and_return(Rails.root.join(config_file_name).to_s)
|
||||||
clear_raw_config
|
redis_clear_raw_config!(described_class)
|
||||||
end
|
end
|
||||||
|
|
||||||
after do
|
after do
|
||||||
clear_raw_config
|
redis_clear_raw_config!(described_class)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.config_file_name' do
|
describe '.config_file_name' do
|
||||||
|
@ -399,12 +399,6 @@ RSpec.shared_examples "redis_shared_examples" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_raw_config
|
|
||||||
described_class.remove_instance_variable(:@_raw_config)
|
|
||||||
rescue NameError
|
|
||||||
# raised if @_raw_config was not set; ignore
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_pool
|
def clear_pool
|
||||||
described_class.remove_instance_variable(:@pool)
|
described_class.remove_instance_variable(:@pool)
|
||||||
rescue NameError
|
rescue NameError
|
||||||
|
|
Loading…
Reference in a new issue