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/issue_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/projects/topic_type.rb
|
||||
- app/graphql/types/release_type.rb
|
||||
|
|
|
@ -42,6 +42,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
coverageLoaded: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -83,14 +88,15 @@ export default {
|
|||
if (!props.inline || !props.line.left) return {};
|
||||
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(
|
||||
(props) => {
|
||||
if (!props.line.right) return {};
|
||||
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(
|
||||
(props) => {
|
||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', ['commitId', 'fileLineCoverage']),
|
||||
...mapState('diffs', ['codequalityDiff', 'highlightedRow']),
|
||||
...mapState('diffs', ['codequalityDiff', 'highlightedRow', 'coverageLoaded']),
|
||||
...mapState({
|
||||
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
|
||||
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
|
||||
|
@ -180,6 +180,7 @@ export default {
|
|||
:index="index"
|
||||
:is-highlighted="isHighlighted(line)"
|
||||
:file-line-coverage="fileLineCoverage"
|
||||
:coverage-loaded="coverageLoaded"
|
||||
@showCommentForm="(code) => singleLineComment(code, line)"
|
||||
@setHighlightedRow="setHighlightedRow"
|
||||
@toggleLineDiscussions="
|
||||
|
|
|
@ -21,6 +21,7 @@ export default () => ({
|
|||
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
|
||||
diffFiles: [],
|
||||
coverageFiles: {},
|
||||
coverageLoaded: false,
|
||||
mergeRequestDiffs: [],
|
||||
mergeRequestDiff: null,
|
||||
diffViewType: getViewTypeFromQueryString() || viewTypeFromCookie || defaultViewType,
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
},
|
||||
|
||||
[types.SET_COVERAGE_DATA](state, coverageFiles) {
|
||||
Object.assign(state, { coverageFiles });
|
||||
Object.assign(state, { coverageFiles, coverageLoaded: true });
|
||||
},
|
||||
|
||||
[types.RENDER_FILE](state, file) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { s__, __ } from '~/locale';
|
||||
import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
|
||||
|
||||
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.`,
|
||||
);
|
||||
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>
|
||||
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 ActionsCell from './cells/actions_cell.vue';
|
||||
import DurationCell from './cells/duration_cell.vue';
|
||||
import JobCell from './cells/job_cell.vue';
|
||||
import PipelineCell from './cells/pipeline_cell.vue';
|
||||
|
||||
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!`;
|
||||
import { DEFAULT_FIELDS } from './constants';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
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: {
|
||||
ActionsCell,
|
||||
CiBadge,
|
||||
|
@ -83,6 +25,11 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
tableFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => DEFAULT_FIELDS,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatCoverage(coverage) {
|
||||
|
@ -95,7 +42,7 @@ export default {
|
|||
<template>
|
||||
<gl-table
|
||||
:items="jobs"
|
||||
:fields="$options.fields"
|
||||
:fields="tableFields"
|
||||
:tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
|
||||
:empty-text="$options.i18n.emptyText"
|
||||
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 { createPipelinesDetailApp } from './pipeline_details_graph';
|
||||
import { createPipelineHeaderApp } from './pipeline_details_header';
|
||||
import { createPipelineJobsApp } from './pipeline_details_jobs';
|
||||
import { apolloProvider } from './pipeline_shared_client';
|
||||
import { createTestDetails } from './pipeline_test_details';
|
||||
|
||||
|
@ -11,6 +12,7 @@ const SELECTORS = {
|
|||
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
|
||||
PIPELINE_HEADER: '#js-pipeline-header-vue',
|
||||
PIPELINE_TESTS: '#js-pipeline-tests-detail',
|
||||
PIPELINE_JOBS: '#js-pipeline-jobs-vue',
|
||||
};
|
||||
|
||||
export default async function initPipelineDetailsBundle() {
|
||||
|
@ -55,4 +57,14 @@ export default async function initPipelineDetailsBundle() {
|
|||
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() {
|
||||
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: {
|
||||
isCollapsed(newVal) {
|
||||
|
@ -182,7 +196,13 @@ export default {
|
|||
<div class="gl-flex-grow-1">
|
||||
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</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>
|
||||
<actions
|
||||
:widget="$options.label || $options.name"
|
||||
|
|
|
@ -388,6 +388,10 @@ const fileExtensionIcons = {
|
|||
log: 'log',
|
||||
};
|
||||
|
||||
const twoFileExtensionIcons = {
|
||||
'gradle.kts': 'gradle',
|
||||
};
|
||||
|
||||
const fileNameIcons = {
|
||||
'.jscsrc': 'json',
|
||||
'.jshintrc': 'json',
|
||||
|
@ -598,6 +602,9 @@ const fileNameIcons = {
|
|||
|
||||
export default function getIconForFile(name) {
|
||||
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 :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
|
||||
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.'
|
||||
field :title, GraphQL::Types::String, null: false,
|
||||
description: 'Title of the merge request.'
|
||||
markdown_field :title_html, null: true
|
||||
field :description, GraphQL::Types::String, null: true,
|
||||
description: 'Description of the merge request (Markdown rendered as HTML for caching).'
|
||||
markdown_field :description_html, null: true
|
||||
field :state, MergeRequestStateEnum, null: false,
|
||||
description: 'State of the merge request.'
|
||||
field :created_at, Types::TimeType, null: false,
|
||||
|
@ -202,6 +200,9 @@ module Types
|
|||
field :timelogs, Types::TimelogType.connection_type, null: false,
|
||||
description: 'Timelogs on the merge request.'
|
||||
|
||||
markdown_field :title_html, null: true
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
def approved_by
|
||||
object.approved_by_users
|
||||
end
|
||||
|
|
|
@ -20,7 +20,6 @@ module Types
|
|||
|
||||
field :description, GraphQL::Types::String, null: true,
|
||||
description: 'Description of the namespace.'
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
field :visibility, GraphQL::Types::String, null: true,
|
||||
description: 'Visibility of the namespace.'
|
||||
|
@ -47,6 +46,8 @@ module Types
|
|||
null: true,
|
||||
description: "Shared runners availability for the namespace and its descendants."
|
||||
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
def root_storage_statistics
|
||||
Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find
|
||||
end
|
||||
|
|
|
@ -33,8 +33,6 @@ module Types
|
|||
method: :note,
|
||||
description: 'Content of the note.'
|
||||
|
||||
markdown_field :body_html, null: true, method: :note
|
||||
|
||||
field :created_at, Types::TimeType, null: false,
|
||||
description: 'Timestamp of the note creation.'
|
||||
field :updated_at, Types::TimeType, null: false,
|
||||
|
@ -50,6 +48,8 @@ module Types
|
|||
null: true,
|
||||
description: 'URL to view this Note in the Web UI.'
|
||||
|
||||
markdown_field :body_html, null: true, method: :note
|
||||
|
||||
def url
|
||||
::Gitlab::UrlBuilder.build(object)
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ class GravatarService
|
|||
return unless identifier
|
||||
|
||||
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,
|
||||
hash: hash,
|
||||
|
|
|
@ -29,17 +29,20 @@
|
|||
|
||||
#js-tab-builds.tab-pane
|
||||
- if stages.present?
|
||||
.table-holder.pipeline-holder
|
||||
%table.table.ci-table.pipeline
|
||||
%thead
|
||||
%tr
|
||||
%th= _('Status')
|
||||
%th= _('Name')
|
||||
%th= _('Job ID')
|
||||
%th
|
||||
%th= _('Coverage')
|
||||
%th
|
||||
= render partial: "projects/stage/stage", collection: stages, as: :stage
|
||||
- if Feature.enabled?(:jobs_tab_vue, @project, default_enabled: :yaml)
|
||||
#js-pipeline-jobs-vue{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
|
||||
- else
|
||||
.table-holder.pipeline-holder
|
||||
%table.table.ci-table.pipeline
|
||||
%thead
|
||||
%tr
|
||||
%th= _('Status')
|
||||
%th= _('Name')
|
||||
%th= _('Job ID')
|
||||
%th
|
||||
%th= _('Coverage')
|
||||
%th
|
||||
= render partial: "projects/stage/stage", collection: stages, as: :stage
|
||||
|
||||
- if @pipeline.failed_builds.present?
|
||||
#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,
|
||||
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
|
||||
|
||||
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. |
|
||||
| `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 `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`
|
||||
|
||||
## 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:
|
||||
- [`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/)
|
||||
|
||||
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
|
||||
conversational index is a measure that reports back to self-managed instances
|
||||
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
|
||||
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
|
||||
errors in [Version App](https://gitlab.com/gitlab-services/version-gitlab-com)
|
||||
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
|
||||
can be safely removed from Service Ping. Use this
|
||||
[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,
|
||||
update the attributes of the metric's YAML definition:
|
||||
|
|
|
@ -1,165 +1,9 @@
|
|||
---
|
||||
stage: Release
|
||||
group: Release
|
||||
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)."
|
||||
redirect_to: 'custom_domains_ssl_tls_certification/lets_encrypt_integration.md'
|
||||
remove_date: '2022-03-14'
|
||||
---
|
||||
|
||||
# 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 method is still valid but was **deprecated** in favor of the
|
||||
[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
|
||||
```
|
||||
<!-- This redirect file can be deleted after <2022-03-14>. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
|
||||
|
|
|
@ -289,6 +289,8 @@ In these issues, you can also see our friendly neighborhood [Support Bot](#suppo
|
|||
|
||||
### 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
|
||||
the GitLab instance. They just send an email to the address they are given, and
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occured while fetching the pipelines jobs."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred adding a draft to the thread."
|
||||
msgstr ""
|
||||
|
||||
|
@ -3860,6 +3863,9 @@ msgstr ""
|
|||
msgid "An error occurred while loading projects."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while loading the Jobs tab."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while loading the Needs tab."
|
||||
msgstr ""
|
||||
|
||||
|
@ -31357,6 +31363,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|New policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|No rules defined - policy will not run."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -31441,9 +31450,6 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|view results"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrhestration|No rules defined - policy will not run."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityPolicies|+%{count} more"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -26,11 +26,11 @@ RSpec.describe 'mail_room.yml' do
|
|||
|
||||
before do
|
||||
stub_env('GITLAB_REDIS_QUEUES_CONFIG_FILE', absolute_path(queues_config_path))
|
||||
clear_queues_raw_config
|
||||
redis_clear_raw_config!(Gitlab::Redis::Queues)
|
||||
end
|
||||
|
||||
after do
|
||||
clear_queues_raw_config
|
||||
redis_clear_raw_config!(Gitlab::Redis::Queues)
|
||||
end
|
||||
|
||||
context 'when incoming email is disabled' do
|
||||
|
@ -103,12 +103,6 @@ RSpec.describe 'mail_room.yml' do
|
|||
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)
|
||||
Rails.root.join(path).to_s
|
||||
end
|
||||
|
|
|
@ -24,14 +24,15 @@ RSpec.describe 'Commits' do
|
|||
end
|
||||
|
||||
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
|
||||
project.add_reporter(user)
|
||||
end
|
||||
|
||||
describe 'Commit builds' do
|
||||
describe 'Commit builds with jobs_tab_feature flag off' do
|
||||
before do
|
||||
stub_feature_flags(jobs_tab_vue: false)
|
||||
visit pipeline_path(pipeline)
|
||||
end
|
||||
|
||||
|
@ -89,8 +90,9 @@ RSpec.describe 'Commits' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'Download artifacts' do
|
||||
context 'Download artifacts with jobs_tab_vue feature flag off' do
|
||||
before do
|
||||
stub_feature_flags(jobs_tab_vue: false)
|
||||
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
|
||||
end
|
||||
|
||||
|
@ -118,8 +120,9 @@ RSpec.describe 'Commits' do
|
|||
end
|
||||
end
|
||||
|
||||
context "when logged as reporter" do
|
||||
context "when logged as reporter and with jobs_tab_vue feature flag off" do
|
||||
before do
|
||||
stub_feature_flags(jobs_tab_vue: false)
|
||||
project.add_reporter(user)
|
||||
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
|
||||
visit pipeline_path(pipeline)
|
||||
|
|
|
@ -53,6 +53,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
pipeline: pipeline,
|
||||
name: 'jenkins',
|
||||
stage: 'external',
|
||||
ref: 'master',
|
||||
target_url: 'http://gitlab.com/status')
|
||||
end
|
||||
end
|
||||
|
@ -915,7 +916,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
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'
|
||||
|
||||
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) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(jobs_tab_vue: false)
|
||||
visit builds_project_pipeline_path(project, pipeline)
|
||||
end
|
||||
|
||||
|
|
|
@ -625,7 +625,7 @@ RSpec.describe 'Pipelines', :js do
|
|||
create_build('test', 1, 'audit', :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)
|
||||
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);
|
||||
|
||||
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 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'
|
||||
|
||||
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
|
||||
before do
|
||||
|
@ -42,25 +42,51 @@ RSpec.describe Gitlab::Redis::Sessions do
|
|||
end
|
||||
|
||||
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
|
||||
it 'instantiates ::Redis instance' do
|
||||
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
|
||||
|
||||
context 'when redis.sessions configuration is provided' do
|
||||
let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
|
||||
let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
|
||||
|
||||
before do
|
||||
redis_clear_raw_config!(Gitlab::Redis::Sessions)
|
||||
redis_clear_raw_config!(Gitlab::Redis::SharedState)
|
||||
allow(described_class).to receive(:config_fallback?).and_return(false)
|
||||
end
|
||||
|
||||
it 'instantiates an instance of MultiStore' do
|
||||
expect(subject).to be_instance_of(::Gitlab::Redis::MultiStore)
|
||||
after do
|
||||
redis_clear_raw_config!(Gitlab::Redis::Sessions)
|
||||
redis_clear_raw_config!(Gitlab::Redis::SharedState)
|
||||
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
|
||||
|
|
|
@ -32,4 +32,11 @@ module RedisHelpers
|
|||
def redis_sessions_cleanup!
|
||||
Gitlab::Redis::Sessions.with(&:flushdb)
|
||||
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
|
||||
|
|
|
@ -8,13 +8,13 @@ RSpec.shared_examples "redis_new_instance_shared_examples" do |name, fallback_cl
|
|||
let(:fallback_config_file) { nil }
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
after do
|
||||
fallback_class.remove_instance_variable(:@_raw_config) rescue nil
|
||||
redis_clear_raw_config!(fallback_class)
|
||||
end
|
||||
|
||||
it_behaves_like "redis_shared_examples"
|
||||
|
|
|
@ -20,11 +20,11 @@ RSpec.shared_examples "redis_shared_examples" do
|
|||
|
||||
before do
|
||||
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
|
||||
|
||||
after do
|
||||
clear_raw_config
|
||||
redis_clear_raw_config!(described_class)
|
||||
end
|
||||
|
||||
describe '.config_file_name' do
|
||||
|
@ -399,12 +399,6 @@ RSpec.shared_examples "redis_shared_examples" do
|
|||
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
|
||||
described_class.remove_instance_variable(:@pool)
|
||||
rescue NameError
|
||||
|
|
Loading…
Reference in a new issue