Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e0b023e388
commit
c5d67a0495
49 changed files with 1035 additions and 350 deletions
|
@ -1,9 +1,3 @@
|
|||
/* Error constants */
|
||||
export const PARSE_FAILURE = 'parse_failure';
|
||||
export const LOAD_FAILURE = 'load_failure';
|
||||
export const UNSUPPORTED_DATA = 'unsupported_data';
|
||||
export const DEFAULT = 'default';
|
||||
|
||||
/* Interaction handles */
|
||||
export const IS_HIGHLIGHTED = 'dag-highlighted';
|
||||
export const LINK_SELECTOR = 'dag-link';
|
||||
|
|
|
@ -6,16 +6,9 @@ import { fetchPolicies } from '~/lib/graphql';
|
|||
import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
|
||||
import DagGraph from './dag_graph.vue';
|
||||
import DagAnnotations from './dag_annotations.vue';
|
||||
import {
|
||||
DEFAULT,
|
||||
PARSE_FAILURE,
|
||||
LOAD_FAILURE,
|
||||
UNSUPPORTED_DATA,
|
||||
ADD_NOTE,
|
||||
REMOVE_NOTE,
|
||||
REPLACE_NOTES,
|
||||
} from './constants';
|
||||
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
|
||||
import { parseData } from './parsing_utils';
|
||||
import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
<script>
|
||||
import * as d3 from 'd3';
|
||||
import { uniqueId } from 'lodash';
|
||||
import {
|
||||
LINK_SELECTOR,
|
||||
NODE_SELECTOR,
|
||||
PARSE_FAILURE,
|
||||
ADD_NOTE,
|
||||
REMOVE_NOTE,
|
||||
REPLACE_NOTES,
|
||||
} from './constants';
|
||||
import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
|
||||
import {
|
||||
currentIsLive,
|
||||
getLiveLinksAsDict,
|
||||
|
@ -19,6 +12,7 @@ import {
|
|||
} from './interactions';
|
||||
import { getMaxNodes, removeOrphanNodes } from './parsing_utils';
|
||||
import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
|
||||
import { PARSE_FAILURE } from '../../constants';
|
||||
|
||||
export default {
|
||||
viewOptions: {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
|
||||
import ciHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import { GlAlert, GlButton, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import ciHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
|
||||
import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql';
|
||||
import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
|
||||
|
||||
const DELETE_MODAL_ID = 'pipeline-delete-modal';
|
||||
|
||||
|
@ -10,57 +13,143 @@ export default {
|
|||
name: 'PipelineHeaderSection',
|
||||
components: {
|
||||
ciHeader,
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
errorTexts: {
|
||||
[LOAD_FAILURE]: __('We are currently unable to fetch data for the pipeline header.'),
|
||||
[POST_FAILURE]: __('An error occurred while making the request.'),
|
||||
[DELETE_FAILURE]: __('An error occurred while deleting the pipeline.'),
|
||||
[DEFAULT]: __('An unknown error occurred.'),
|
||||
},
|
||||
inject: {
|
||||
// Receive `cancel`, `delete`, `fullProject` and `retry`
|
||||
paths: {
|
||||
default: {},
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
pipelineId: {
|
||||
default: '',
|
||||
},
|
||||
pipelineIid: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
pipeline: {
|
||||
query: getPipelineQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.paths.fullProject,
|
||||
iid: this.pipelineIid,
|
||||
};
|
||||
},
|
||||
update: data => data.project.pipeline,
|
||||
error() {
|
||||
this.reportFailure(LOAD_FAILURE);
|
||||
},
|
||||
pollInterval: 10000,
|
||||
watchLoading(isLoading) {
|
||||
if (!isLoading) {
|
||||
// To ensure apollo has updated the cache,
|
||||
// we only remove the loading state in sync with GraphQL
|
||||
this.isCanceling = false;
|
||||
this.isRetrying = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pipeline: null,
|
||||
failureType: null,
|
||||
isCanceling: false,
|
||||
isRetrying: false,
|
||||
isDeleting: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
status() {
|
||||
return this.pipeline.details && this.pipeline.details.status;
|
||||
},
|
||||
shouldRenderContent() {
|
||||
return !this.isLoading && Object.keys(this.pipeline).length;
|
||||
},
|
||||
deleteModalConfirmationText() {
|
||||
return __(
|
||||
'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.',
|
||||
);
|
||||
},
|
||||
hasError() {
|
||||
return this.failureType;
|
||||
},
|
||||
hasPipelineData() {
|
||||
return Boolean(this.pipeline);
|
||||
},
|
||||
isLoadingInitialQuery() {
|
||||
return this.$apollo.queries.pipeline.loading && !this.hasPipelineData;
|
||||
},
|
||||
status() {
|
||||
return this.pipeline?.status;
|
||||
},
|
||||
shouldRenderContent() {
|
||||
return !this.isLoadingInitialQuery && this.hasPipelineData;
|
||||
},
|
||||
failure() {
|
||||
switch (this.failureType) {
|
||||
case LOAD_FAILURE:
|
||||
return {
|
||||
text: this.$options.errorTexts[LOAD_FAILURE],
|
||||
variant: 'danger',
|
||||
};
|
||||
case POST_FAILURE:
|
||||
return {
|
||||
text: this.$options.errorTexts[POST_FAILURE],
|
||||
variant: 'danger',
|
||||
};
|
||||
case DELETE_FAILURE:
|
||||
return {
|
||||
text: this.$options.errorTexts[DELETE_FAILURE],
|
||||
variant: 'danger',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: this.$options.errorTexts[DEFAULT],
|
||||
variant: 'danger',
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cancelPipeline() {
|
||||
reportFailure(errorType) {
|
||||
this.failureType = errorType;
|
||||
},
|
||||
async postAction(path) {
|
||||
try {
|
||||
await axios.post(path);
|
||||
this.$apollo.queries.pipeline.refetch();
|
||||
} catch {
|
||||
this.reportFailure(POST_FAILURE);
|
||||
}
|
||||
},
|
||||
async cancelPipeline() {
|
||||
this.isCanceling = true;
|
||||
eventHub.$emit('headerPostAction', this.pipeline.cancel_path);
|
||||
this.postAction(this.paths.cancel);
|
||||
},
|
||||
retryPipeline() {
|
||||
async retryPipeline() {
|
||||
this.isRetrying = true;
|
||||
eventHub.$emit('headerPostAction', this.pipeline.retry_path);
|
||||
this.postAction(this.paths.retry);
|
||||
},
|
||||
deletePipeline() {
|
||||
async deletePipeline() {
|
||||
this.isDeleting = true;
|
||||
eventHub.$emit('headerDeleteAction', this.pipeline.delete_path);
|
||||
this.$apollo.queries.pipeline.stopPolling();
|
||||
|
||||
try {
|
||||
const { request } = await axios.delete(this.paths.delete);
|
||||
redirectTo(setUrlFragment(request.responseURL, 'delete_success'));
|
||||
} catch {
|
||||
this.$apollo.queries.pipeline.startPolling();
|
||||
this.reportFailure(DELETE_FAILURE);
|
||||
this.isDeleting = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
DELETE_MODAL_ID,
|
||||
|
@ -68,54 +157,53 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div class="pipeline-header-container">
|
||||
<gl-alert v-if="hasError" :variant="failure.variant">{{ failure.text }}</gl-alert>
|
||||
<ci-header
|
||||
v-if="shouldRenderContent"
|
||||
:status="status"
|
||||
:item-id="pipeline.id"
|
||||
:time="pipeline.created_at"
|
||||
:status="pipeline.detailedStatus"
|
||||
:time="pipeline.createdAt"
|
||||
:user="pipeline.user"
|
||||
:item-id="Number(pipelineId)"
|
||||
item-name="Pipeline"
|
||||
>
|
||||
<gl-button
|
||||
v-if="pipeline.retry_path"
|
||||
v-if="pipeline.retryable"
|
||||
:loading="isRetrying"
|
||||
:disabled="isRetrying"
|
||||
data-testid="retryButton"
|
||||
category="secondary"
|
||||
variant="info"
|
||||
data-testid="retryPipeline"
|
||||
class="js-retry-button"
|
||||
@click="retryPipeline()"
|
||||
>
|
||||
{{ __('Retry') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.cancel_path"
|
||||
v-if="pipeline.cancelable"
|
||||
:loading="isCanceling"
|
||||
:disabled="isCanceling"
|
||||
data-testid="cancelPipeline"
|
||||
class="gl-ml-3"
|
||||
category="primary"
|
||||
variant="danger"
|
||||
data-testid="cancelPipeline"
|
||||
@click="cancelPipeline()"
|
||||
>
|
||||
{{ __('Cancel running') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.delete_path"
|
||||
v-if="pipeline.userPermissions.destroyPipeline"
|
||||
v-gl-modal="$options.DELETE_MODAL_ID"
|
||||
:loading="isDeleting"
|
||||
:disabled="isDeleting"
|
||||
data-testid="deletePipeline"
|
||||
class="gl-ml-3"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
category="secondary"
|
||||
data-testid="deletePipeline"
|
||||
>
|
||||
{{ __('Delete') }}
|
||||
</gl-button>
|
||||
</ci-header>
|
||||
|
||||
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" />
|
||||
<gl-loading-icon v-if="isLoadingInitialQuery" size="lg" class="gl-mt-3 gl-mb-3" />
|
||||
|
||||
<gl-modal
|
||||
:modal-id="$options.DELETE_MODAL_ID"
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
|
||||
import ciHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
const DELETE_MODAL_ID = 'pipeline-delete-modal';
|
||||
|
||||
export default {
|
||||
name: 'PipelineHeaderSection',
|
||||
components: {
|
||||
ciHeader,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCanceling: false,
|
||||
isRetrying: false,
|
||||
isDeleting: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
status() {
|
||||
return this.pipeline.details && this.pipeline.details.status;
|
||||
},
|
||||
shouldRenderContent() {
|
||||
return !this.isLoading && Object.keys(this.pipeline).length;
|
||||
},
|
||||
deleteModalConfirmationText() {
|
||||
return __(
|
||||
'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.',
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cancelPipeline() {
|
||||
this.isCanceling = true;
|
||||
eventHub.$emit('headerPostAction', this.pipeline.cancel_path);
|
||||
},
|
||||
retryPipeline() {
|
||||
this.isRetrying = true;
|
||||
eventHub.$emit('headerPostAction', this.pipeline.retry_path);
|
||||
},
|
||||
deletePipeline() {
|
||||
this.isDeleting = true;
|
||||
eventHub.$emit('headerDeleteAction', this.pipeline.delete_path);
|
||||
},
|
||||
},
|
||||
DELETE_MODAL_ID,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="pipeline-header-container">
|
||||
<ci-header
|
||||
v-if="shouldRenderContent"
|
||||
:status="status"
|
||||
:item-id="pipeline.id"
|
||||
:time="pipeline.created_at"
|
||||
:user="pipeline.user"
|
||||
item-name="Pipeline"
|
||||
>
|
||||
<gl-button
|
||||
v-if="pipeline.retry_path"
|
||||
:loading="isRetrying"
|
||||
:disabled="isRetrying"
|
||||
data-testid="retryButton"
|
||||
category="secondary"
|
||||
variant="info"
|
||||
@click="retryPipeline()"
|
||||
>
|
||||
{{ __('Retry') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.cancel_path"
|
||||
:loading="isCanceling"
|
||||
:disabled="isCanceling"
|
||||
data-testid="cancelPipeline"
|
||||
class="gl-ml-3"
|
||||
category="primary"
|
||||
variant="danger"
|
||||
@click="cancelPipeline()"
|
||||
>
|
||||
{{ __('Cancel running') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.delete_path"
|
||||
v-gl-modal="$options.DELETE_MODAL_ID"
|
||||
:loading="isDeleting"
|
||||
:disabled="isDeleting"
|
||||
data-testid="deletePipeline"
|
||||
class="gl-ml-3"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
>
|
||||
{{ __('Delete') }}
|
||||
</gl-button>
|
||||
</ci-header>
|
||||
|
||||
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" />
|
||||
|
||||
<gl-modal
|
||||
:modal-id="$options.DELETE_MODAL_ID"
|
||||
:title="__('Delete pipeline')"
|
||||
:ok-title="__('Delete pipeline')"
|
||||
ok-variant="danger"
|
||||
@ok="deletePipeline()"
|
||||
>
|
||||
<p>
|
||||
{{ deleteModalConfirmationText }}
|
||||
</p>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
|
@ -21,3 +21,11 @@ export const FETCH_TAG_ERROR_MESSAGE = __('There was a problem fetching project
|
|||
export const RAW_TEXT_WARNING = s__(
|
||||
'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
|
||||
);
|
||||
|
||||
/* Error constants shared across graphs */
|
||||
export const DEFAULT = 'default';
|
||||
export const DELETE_FAILURE = 'delete_pipeline_failure';
|
||||
export const LOAD_FAILURE = 'load_failure';
|
||||
export const PARSE_FAILURE = 'parse_failure';
|
||||
export const POST_FAILURE = 'post_failure';
|
||||
export const UNSUPPORTED_DATA = 'unsupported_data';
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
|
||||
project(fullPath: $fullPath) {
|
||||
pipeline(iid: $iid) {
|
||||
id
|
||||
status
|
||||
retryable
|
||||
cancelable
|
||||
userPermissions {
|
||||
destroyPipeline
|
||||
}
|
||||
detailedStatus {
|
||||
detailsPath
|
||||
icon
|
||||
group
|
||||
text
|
||||
}
|
||||
createdAt
|
||||
user {
|
||||
name
|
||||
webPath
|
||||
email
|
||||
avatarUrl
|
||||
status {
|
||||
message
|
||||
emoji
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,10 +7,11 @@ import pipelineGraph from './components/graph/graph_component.vue';
|
|||
import createDagApp from './pipeline_details_dag';
|
||||
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
|
||||
import PipelinesMediator from './pipeline_details_mediator';
|
||||
import pipelineHeader from './components/header_component.vue';
|
||||
import legacyPipelineHeader from './components/legacy_header_component.vue';
|
||||
import eventHub from './event_hub';
|
||||
import TestReports from './components/test_reports/test_reports.vue';
|
||||
import createTestReportsStore from './stores/test_reports';
|
||||
import { createPipelineHeaderApp } from './pipeline_details_header';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
|
@ -56,7 +57,7 @@ const createPipelinesDetailApp = mediator => {
|
|||
});
|
||||
};
|
||||
|
||||
const createPipelineHeaderApp = mediator => {
|
||||
const createLegacyPipelineHeaderApp = mediator => {
|
||||
if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
|
||||
return;
|
||||
}
|
||||
|
@ -64,7 +65,7 @@ const createPipelineHeaderApp = mediator => {
|
|||
new Vue({
|
||||
el: SELECTORS.PIPELINE_HEADER,
|
||||
components: {
|
||||
pipelineHeader,
|
||||
legacyPipelineHeader,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -95,7 +96,7 @@ const createPipelineHeaderApp = mediator => {
|
|||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('pipeline-header', {
|
||||
return createElement('legacy-pipeline-header', {
|
||||
props: {
|
||||
isLoading: this.mediator.state.isLoading,
|
||||
pipeline: this.mediator.store.state.pipeline,
|
||||
|
@ -132,7 +133,12 @@ export default () => {
|
|||
mediator.fetchPipeline();
|
||||
|
||||
createPipelinesDetailApp(mediator);
|
||||
createPipelineHeaderApp(mediator);
|
||||
|
||||
if (gon.features.graphqlPipelineHeader) {
|
||||
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER);
|
||||
} else {
|
||||
createLegacyPipelineHeaderApp(mediator);
|
||||
}
|
||||
createTestDetails();
|
||||
createDagApp();
|
||||
};
|
||||
|
|
41
app/assets/javascripts/pipelines/pipeline_details_header.js
Normal file
41
app/assets/javascripts/pipelines/pipeline_details_header.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import pipelineHeader from './components/header_component.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
export const createPipelineHeaderApp = elSelector => {
|
||||
const el = document.querySelector(elSelector);
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { cancelPath, deletePath, fullPath, pipelineId, pipelineIid, retryPath } = el?.dataset;
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
components: {
|
||||
pipelineHeader,
|
||||
},
|
||||
apolloProvider,
|
||||
provide: {
|
||||
paths: {
|
||||
cancel: cancelPath,
|
||||
delete: deletePath,
|
||||
fullProject: fullPath,
|
||||
retry: retryPath,
|
||||
},
|
||||
pipelineId,
|
||||
pipelineIid,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('pipeline-header', {});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { escape } from 'lodash';
|
||||
import simplePoll from '../../../lib/utils/simple_poll';
|
||||
import eventHub from '../../event_hub';
|
||||
|
@ -12,7 +12,7 @@ export default {
|
|||
name: 'MRWidgetRebase',
|
||||
components: {
|
||||
statusIcon,
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
mr: {
|
||||
|
@ -109,29 +109,29 @@ export default {
|
|||
|
||||
<div class="rebase-state-find-class-convention media media-body space-children">
|
||||
<template v-if="mr.rebaseInProgress || isMakingRequest">
|
||||
<span class="bold">{{ __('Rebase in progress') }}</span>
|
||||
<span class="bold" data-testid="rebase-message">{{ __('Rebase in progress') }}</span>
|
||||
</template>
|
||||
<template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch">
|
||||
<span class="bold" v-html="fastForwardMergeText"></span>
|
||||
<span class="bold" data-testid="rebase-message" v-html="fastForwardMergeText"></span>
|
||||
</template>
|
||||
<template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest">
|
||||
<div
|
||||
class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"
|
||||
>
|
||||
<button
|
||||
:disabled="isMakingRequest"
|
||||
type="button"
|
||||
class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button"
|
||||
<gl-button
|
||||
:loading="isMakingRequest"
|
||||
variant="success"
|
||||
class="qa-mr-rebase-button"
|
||||
@click="rebase"
|
||||
>
|
||||
<gl-loading-icon v-if="isMakingRequest" />{{ __('Rebase') }}
|
||||
</button>
|
||||
<span v-if="!rebasingError" class="bold">{{
|
||||
{{ __('Rebase') }}
|
||||
</gl-button>
|
||||
<span v-if="!rebasingError" class="bold" data-testid="rebase-message">{{
|
||||
__(
|
||||
'Fast-forward merge is not possible. Rebase the source branch onto the target branch.',
|
||||
)
|
||||
}}</span>
|
||||
<span v-else class="bold danger">{{ rebasingError }}</span>
|
||||
<span v-else class="bold danger" data-testid="rebase-message">{{ rebasingError }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import CiIcon from './ci_icon.vue';
|
|||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-foss/pipelines/8150156" // url
|
||||
* details_path or detailsPath: "/gitlab-org/gitlab-foss/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
|
@ -46,6 +46,13 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return !this.showText ? this.status?.text : '';
|
||||
},
|
||||
detailsPath() {
|
||||
// For now, this can either come from graphQL with camelCase or REST API in snake_case
|
||||
return this.status.detailsPath || this.status.details_path;
|
||||
},
|
||||
cssClass() {
|
||||
const className = this.status.group;
|
||||
return className ? `ci-status ci-${className} qa-status-badge` : 'ci-status qa-status-badge';
|
||||
|
@ -54,12 +61,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
v-gl-tooltip
|
||||
:href="status.details_path"
|
||||
:class="cssClass"
|
||||
:title="!showText ? status.text : ''"
|
||||
>
|
||||
<a v-gl-tooltip :href="detailsPath" :class="cssClass" :title="title">
|
||||
<ci-icon :status="status" :css-classes="iconClasses" />
|
||||
|
||||
<template v-if="showText">
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlTooltip } from '@gitlab/ui';
|
||||
import CiIconBadge from './ci_badge_link.vue';
|
||||
import TimeagoTooltip from './time_ago_tooltip.vue';
|
||||
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
|
||||
import { glEmojiTag } from '../../emoji';
|
||||
import { __, sprintf } from '../../locale';
|
||||
|
||||
/**
|
||||
* Renders header component for job and pipeline page based on UI mockups
|
||||
|
@ -20,10 +21,12 @@ export default {
|
|||
UserAvatarImage,
|
||||
GlLink,
|
||||
GlDeprecatedButton,
|
||||
GlTooltip,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
EMOJI_REF: 'EMOJI_REF',
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
|
@ -62,6 +65,27 @@ export default {
|
|||
userAvatarAltText() {
|
||||
return sprintf(__(`%{username}'s avatar`), { username: this.user.name });
|
||||
},
|
||||
userPath() {
|
||||
// GraphQL returns `webPath` and Rest `path`
|
||||
return this.user?.webPath || this.user?.path;
|
||||
},
|
||||
avatarUrl() {
|
||||
// GraphQL returns `avatarUrl` and Rest `avatar_url`
|
||||
return this.user?.avatarUrl || this.user?.avatar_url;
|
||||
},
|
||||
statusTooltipHTML() {
|
||||
// Rest `status_tooltip_html` which is a ready to work
|
||||
// html for the emoji and the status text inside a tooltip.
|
||||
// GraphQL returns `status.emoji` and `status.message` which
|
||||
// needs to be combined to make the html we want.
|
||||
const { emoji } = this.user?.status || {};
|
||||
const emojiHtml = emoji ? glEmojiTag(emoji) : '';
|
||||
|
||||
return emojiHtml || this.user?.status_tooltip_html;
|
||||
},
|
||||
message() {
|
||||
return this.user?.status?.message;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -73,7 +97,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<header class="page-content-header ci-header-container">
|
||||
<header class="page-content-header ci-header-container" data-testid="pipeline-header-content">
|
||||
<section class="header-main-content">
|
||||
<ci-icon-badge :status="status" />
|
||||
|
||||
|
@ -89,12 +113,12 @@ export default {
|
|||
<template v-if="user">
|
||||
<gl-link
|
||||
v-gl-tooltip
|
||||
:href="user.path"
|
||||
:href="userPath"
|
||||
:title="user.email"
|
||||
class="js-user-link commit-committer-link"
|
||||
>
|
||||
<user-avatar-image
|
||||
:img-src="user.avatar_url"
|
||||
:img-src="avatarUrl"
|
||||
:img-alt="userAvatarAltText"
|
||||
:tooltip-text="user.name"
|
||||
:img-size="24"
|
||||
|
@ -102,7 +126,15 @@ export default {
|
|||
|
||||
{{ user.name }}
|
||||
</gl-link>
|
||||
<span v-if="user.status_tooltip_html" v-html="user.status_tooltip_html"></span>
|
||||
<gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]">
|
||||
{{ message }}
|
||||
</gl-tooltip>
|
||||
<span
|
||||
v-if="statusTooltipHTML"
|
||||
:ref="$options.EMOJI_REF"
|
||||
:data-testid="message"
|
||||
v-html="statusTooltipHTML"
|
||||
></span>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -868,9 +868,6 @@ $add-to-slack-popup-max-width: 400px;
|
|||
$add-to-slack-gif-max-width: 850px;
|
||||
$add-to-slack-well-max-width: 750px;
|
||||
$add-to-slack-logo-size: 100px;
|
||||
$double-headed-arrow-width: 100px;
|
||||
$double-headed-arrow-height: 25px;
|
||||
$right-arrow-size: 16px;
|
||||
|
||||
/*
|
||||
Popup
|
||||
|
|
|
@ -447,20 +447,3 @@ table.u2f-registrations,
|
|||
width: 100%;
|
||||
max-width: $add-to-slack-popup-max-width;
|
||||
}
|
||||
|
||||
.gitlab-slack-right-arrow svg {
|
||||
fill: $white-dark;
|
||||
width: $right-arrow-size;
|
||||
height: $right-arrow-size;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.gitlab-slack-double-headed-arrow {
|
||||
vertical-align: text-top;
|
||||
|
||||
svg {
|
||||
fill: $gray-darker;
|
||||
width: $double-headed-arrow-width;
|
||||
height: $double-headed-arrow-height;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,11 +28,6 @@ class Projects::JobsController < Projects::ApplicationController
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def show
|
||||
@pipeline = @build.pipeline
|
||||
@builds = @pipeline.builds
|
||||
.order('id DESC')
|
||||
.present(current_user: current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
|
|
|
@ -16,6 +16,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:pipelines_security_report_summary, project)
|
||||
push_frontend_feature_flag(:new_pipeline_form)
|
||||
push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false)
|
||||
end
|
||||
before_action :ensure_pipeline, only: [:show]
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ module DesignManagement
|
|||
def execute
|
||||
return error("Not allowed!") unless can_create_designs?
|
||||
return error("Only #{MAX_FILES} files are allowed simultaneously") if files.size > MAX_FILES
|
||||
return error("Duplicate filenames are not allowed!") if files.map(&:original_filename).uniq.length != files.length
|
||||
|
||||
uploaded_designs, version = upload_designs!
|
||||
skipped_designs = designs - uploaded_designs
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
- pipeline_has_errors = @pipeline.builds.empty? && @pipeline.yaml_errors.present?
|
||||
|
||||
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
|
||||
#js-pipeline-header-vue.pipeline-header-container
|
||||
|
||||
#js-pipeline-header-vue.pipeline-header-container{ data: {full_path: @project.full_path, retry_path: retry_project_pipeline_path(@pipeline.project, @pipeline), cancel_path: cancel_project_pipeline_path(@pipeline.project, @pipeline), delete_path: project_pipeline_path(@pipeline.project, @pipeline), pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id} }
|
||||
- if @pipeline.commit.present?
|
||||
= render "projects/pipelines/info", commit: @pipeline.commit
|
||||
|
||||
|
|
5
changelogs/unreleased/khanchi-designs-patch.yml
Normal file
5
changelogs/unreleased/khanchi-designs-patch.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Designs: return an error if uploading designs with duplicate names'
|
||||
merge_request: 42514
|
||||
author: Sushil Khanchi
|
||||
type: fixed
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: graphql_milestone_stats
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35066
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: graphql_pipeline_header
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39494
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/254235
|
||||
group: group::pipeline authoring
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: graphql_release_data
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30753
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: graphql_releases_page
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33095
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: new_release_page
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35367
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: release_asset_link_editing
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26821
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: release_asset_link_type
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33643
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: release_issue_summary
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19451
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: release_show_page
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23792
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -129,6 +129,9 @@ Note the following when promoting a secondary:
|
|||
```
|
||||
|
||||
1. Promote the **secondary** node to the **primary** node.
|
||||
|
||||
DANGER: **Danger:**
|
||||
In GitLab 13.2 and later versions, promoting a secondary node to a primary while the secondary is paused fails. We are [investigating the issue](https://gitlab.com/gitlab-org/gitlab/-/issues/225173). Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting.
|
||||
|
||||
To promote the secondary node to primary along with preflight checks:
|
||||
|
||||
|
@ -159,6 +162,9 @@ conjunction with multiple servers, as it can only
|
|||
perform changes on a **secondary** with only a single machine. Instead, you must
|
||||
do this manually.
|
||||
|
||||
DANGER: **Danger:**
|
||||
In GitLab 13.2 and later versions, promoting a secondary node to a primary while the secondary is paused fails. We are [investigating the issue](https://gitlab.com/gitlab-org/gitlab/-/issues/225173). Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting.
|
||||
|
||||
1. SSH in to the database node in the **secondary** and trigger PostgreSQL to
|
||||
promote to read-write:
|
||||
|
||||
|
@ -201,6 +207,9 @@ an external PostgreSQL database, as it can only perform changes on a **secondary
|
|||
node with GitLab and the database on the same machine. As a result, a manual process is
|
||||
required:
|
||||
|
||||
DANGER: **Danger:**
|
||||
In GitLab 13.2 and later versions, promoting a secondary node to a primary while the secondary is paused fails. We are [investigating the issue](https://gitlab.com/gitlab-org/gitlab/-/issues/225173). Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting.
|
||||
|
||||
1. Promote the replica database associated with the **secondary** site. This will
|
||||
set the database to read-write:
|
||||
- Amazon RDS - [Promoting a Read Replica to Be a Standalone DB Instance](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReadRepl.html#USER_ReadRepl.Promote)
|
||||
|
|
|
@ -195,6 +195,9 @@ For information on how to update your Geo nodes to the latest GitLab version, se
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35913) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
||||
|
||||
DANGER: **Danger:**
|
||||
In GitLab 13.2 and later versions, promoting a secondary node to a primary while the secondary is paused fails. We are [investigating the issue](https://gitlab.com/gitlab-org/gitlab/-/issues/225173). Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting.
|
||||
|
||||
In some circumstances, like during [upgrades](replication/updating_the_geo_nodes.md) or a [planned failover](disaster_recovery/planned_failover.md), it is desirable to pause replication between the primary and secondary.
|
||||
|
||||
Pausing and resuming replication is done via a command line tool from the secondary node.
|
||||
|
@ -261,6 +264,7 @@ This list of limitations only reflects the latest version of GitLab. If you are
|
|||
- Object pools for forked project deduplication work only on the **primary** node, and are duplicated on the **secondary** node.
|
||||
- [External merge request diffs](../merge_request_diffs.md) will not be replicated if they are on-disk, and viewing merge requests will fail. However, external MR diffs in object storage **are** supported. The default configuration (in-database) does work.
|
||||
- GitLab Runners cannot register with a **secondary** node. Support for this is [planned for the future](https://gitlab.com/gitlab-org/gitlab/-/issues/3294).
|
||||
- Geo **secondary** nodes can not be configured to [use high-availability configurations of PostgreSQL](https://gitlab.com/groups/gitlab-org/-/epics/2536).
|
||||
|
||||
### Limitations on replication/verification
|
||||
|
||||
|
|
|
@ -84,8 +84,8 @@ Note that this service requires a login, so this use case is most useful in a
|
|||
corporate installation where all users have access to Office 365.
|
||||
|
||||
```ruby
|
||||
gitlab_rails['gravatar_plain_url'] = 'http://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=%{email}&size=HR120x120'
|
||||
gitlab_rails['gravatar_ssl_url'] = 'https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=%{email}&size=HR120x120'
|
||||
gitlab_rails['gravatar_plain_url'] = 'http://outlook.office.com/owa/service.svc/s/GetPersonaPhoto?email=%{email}&size=HR120x120'
|
||||
gitlab_rails['gravatar_ssl_url'] = 'https://outlook.office.com/owa/service.svc/s/GetPersonaPhoto?email=%{email}&size=HR120x120'
|
||||
```
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
|
125
doc/administration/read_only_gitlab.md
Normal file
125
doc/administration/read_only_gitlab.md
Normal file
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
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/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Place GitLab into a read-only state **(CORE ONLY)**
|
||||
|
||||
CAUTION: **Warning:**
|
||||
This document should be used as a temporary solution.
|
||||
There's work in progress to make this
|
||||
[possible with Geo](https://gitlab.com/groups/gitlab-org/-/epics/2149).
|
||||
|
||||
In some cases, you might want to place GitLab under a read-only state.
|
||||
The configuration for doing so depends on your desired outcome.
|
||||
|
||||
## Make the repositories read-only
|
||||
|
||||
The first thing you'll want to accomplish is to ensure that no changes can be
|
||||
made to your repositories. There's two ways you can accomplish that:
|
||||
|
||||
- Either stop Unicorn/Puma to make the internal API unreachable:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl stop puma # or unicorn
|
||||
```
|
||||
|
||||
- Or, open up a Rails console:
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails console
|
||||
```
|
||||
|
||||
And set the repositories for all projects read-only:
|
||||
|
||||
```ruby
|
||||
Project.all.find_each { |project| project.update!(repository_read_only: true) }
|
||||
```
|
||||
|
||||
When you're ready to revert this, you can do so with the following command:
|
||||
|
||||
```ruby
|
||||
Project.all.find_each { |project| project.update!(repository_read_only: false) }
|
||||
```
|
||||
|
||||
## Shut down the GitLab UI
|
||||
|
||||
If you don't mind shutting down the GitLab UI, then the easiest approach is to
|
||||
stop `sidekiq` and `puma`/`unicorn`, and you'll effectively ensure that no
|
||||
changes can be made to GitLab:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl stop sidekiq
|
||||
sudo gitlab-ctl stop puma # or unicorn
|
||||
```
|
||||
|
||||
When you're ready to revert this:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl start sidekiq
|
||||
sudo gitlab-ctl start puma # or unicorn
|
||||
```
|
||||
|
||||
## Make the database read-only
|
||||
|
||||
If you want to allow users to use the GitLab UI, then you'll need to ensure that
|
||||
the database is read-only:
|
||||
|
||||
1. Take a [GitLab backup](../raketasks/backup_restore.md#back-up-gitlab)
|
||||
in case things don't go as expected.
|
||||
1. Enter PostgreSQL on the console as an admin user:
|
||||
|
||||
```shell
|
||||
sudo \
|
||||
-u gitlab-psql /opt/gitlab/embedded/bin/psql \
|
||||
-h /var/opt/gitlab/postgresql gitlabhq_production
|
||||
```
|
||||
|
||||
1. Create the `gitlab_read_only` user. Note that the password is set to `mypassword`,
|
||||
change that to your liking:
|
||||
|
||||
```sql
|
||||
-- NOTE: Use the password defined earlier
|
||||
CREATE USER gitlab_read_only WITH password 'mypassword';
|
||||
GRANT CONNECT ON DATABASE gitlabhq_production to gitlab_read_only;
|
||||
GRANT USAGE ON SCHEMA public TO gitlab_read_only;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO gitlab_read_only;
|
||||
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO gitlab_read_only;
|
||||
|
||||
-- Tables created by "gitlab" should be made read-only for "gitlab_read_only"
|
||||
-- automatically.
|
||||
ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON TABLES TO gitlab_read_only;
|
||||
ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON SEQUENCES TO gitlab_read_only;
|
||||
```
|
||||
|
||||
1. Get the hashed password of the `gitlab_read_only` user and copy the result:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl pg-password-md5 gitlab_read_only
|
||||
```
|
||||
|
||||
1. Edit `/etc/gitlab/gitlab.rb` and add the password from the previous step:
|
||||
|
||||
```ruby
|
||||
postgresql['sql_user_password'] = 'a2e20f823772650f039284619ab6f239'
|
||||
postgresql['sql_user'] = "gitlab_read_only"
|
||||
```
|
||||
|
||||
1. Reconfigure GitLab and restart PostgreSQL:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl reconfigure
|
||||
sudo gitlab-ctl restart postgresql
|
||||
```
|
||||
|
||||
When you're ready to revert the read-only state, you'll need to remove the added
|
||||
lines in `/etc/gitlab/gitlab.rb`, and reconfigure GitLab and restart PostgreSQL:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl reconfigure
|
||||
sudo gitlab-ctl restart postgresql
|
||||
```
|
||||
|
||||
Once you verify all works as expected, you can remove the `gitlab_read_only`
|
||||
user from the database.
|
|
@ -105,7 +105,8 @@ ID for the feature to be enabled. See the [Ruby example](#ruby-application-examp
|
|||
|
||||
### User IDs
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8240) in GitLab 12.2. [Updated](https://gitlab.com/gitlab-org/gitlab/-/issues/34363) to be defined per environment in GitLab 12.6.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8240) in GitLab 12.2.
|
||||
> - [Updated](https://gitlab.com/gitlab-org/gitlab/-/issues/34363) to be defined per environment in GitLab 12.6.
|
||||
|
||||
Enables the feature for a list of target users. It is implemented
|
||||
using the Unleash [`userWithId`](https://unleash.github.io/docs/activation_strategy#userwithid)
|
||||
|
@ -352,8 +353,10 @@ end
|
|||
|
||||
## Feature Flag Related Issues **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36617) in GitLab 13.2.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36617) in GitLab 13.2.
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/251234) in GitLab 13.5.
|
||||
|
||||
You can link related issues to a feature flag. In the **Linked issues** section, click the `+` button and input the issue reference number or the full URL of the issue.
|
||||
You can link related issues to a feature flag. In the **Linked issues** section,
|
||||
click the `+` button and input the issue reference number or the full URL of the issue.
|
||||
|
||||
This feature is similar to the [related issues](../user/project/issues/related_issues.md) feature.
|
||||
|
|
|
@ -6,37 +6,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# GitLab Managed Apps
|
||||
|
||||
GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can
|
||||
be added directly to your configured cluster.
|
||||
|
||||
These applications are needed for [Review Apps](../../ci/review_apps/index.md)
|
||||
and [deployments](../../ci/environments/index.md) when using [Auto DevOps](../../topics/autodevops/index.md).
|
||||
|
||||
You can install them after you
|
||||
[create a cluster](../project/clusters/add_remove_clusters.md).
|
||||
GitLab provides **GitLab Managed Apps**, a one-click install for various
|
||||
applications which can be added directly to your configured cluster. These
|
||||
applications are needed for [Review Apps](../../ci/review_apps/index.md) and
|
||||
[deployments](../../ci/environments/index.md) when using [Auto DevOps](../../topics/autodevops/index.md).
|
||||
You can install them after you [create a cluster](../project/clusters/add_remove_clusters.md).
|
||||
|
||||
## Installing applications
|
||||
|
||||
Applications managed by GitLab will be installed onto the `gitlab-managed-apps` namespace.
|
||||
|
||||
This namespace:
|
||||
Applications managed by GitLab are installed onto the `gitlab-managed-apps`
|
||||
namespace. This namespace:
|
||||
|
||||
- Is different from the namespace used for project deployments.
|
||||
- Is created once.
|
||||
- Has a non-configurable name.
|
||||
|
||||
To see a list of available applications to install. For a:
|
||||
To view a list of available applications to install for a:
|
||||
|
||||
- [Project-level cluster](../project/clusters/index.md), navigate to your project's
|
||||
**Operations > Kubernetes**.
|
||||
- [Group-level cluster](../group/clusters/index.md), navigate to your group's
|
||||
**Kubernetes** page.
|
||||
|
||||
NOTE: **Note:**
|
||||
As of GitLab 11.6, Helm will be upgraded to the latest version supported
|
||||
by GitLab before installing any of the applications.
|
||||
|
||||
The following applications can be installed:
|
||||
You can install the following applications:
|
||||
|
||||
- [Helm](#helm)
|
||||
- [Ingress](#ingress)
|
||||
|
@ -49,10 +41,9 @@ The following applications can be installed:
|
|||
- [Elastic Stack](#elastic-stack)
|
||||
- [Fluentd](#fluentd)
|
||||
|
||||
With the exception of Knative, the applications will be installed in a dedicated
|
||||
With the exception of Knative, the applications are installed in a dedicated
|
||||
namespace called `gitlab-managed-apps`.
|
||||
|
||||
NOTE: **Note:**
|
||||
Some applications are installable only for a project-level cluster.
|
||||
Support for installing these applications in a group-level cluster is
|
||||
planned for future releases.
|
||||
|
@ -65,6 +56,9 @@ you should be careful as GitLab cannot detect it. In this case, installing
|
|||
Helm via the applications will result in the cluster having it twice, which
|
||||
can lead to confusion during deployments.
|
||||
|
||||
In GitLab versions 11.6 and greater, Helm is upgraded to the latest version
|
||||
supported by GitLab before installing any of the applications.
|
||||
|
||||
### Helm
|
||||
|
||||
> - Introduced in GitLab 10.2 for project-level clusters.
|
||||
|
@ -81,7 +75,6 @@ applications. Prior to [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issu
|
|||
GitLab used an in-cluster Tiller server in the `gitlab-managed-apps`
|
||||
namespace. This server can now be safely removed.
|
||||
|
||||
NOTE: **Note:**
|
||||
GitLab's Helm integration does not support installing applications behind a proxy,
|
||||
but a [workaround](../../topics/autodevops/index.md#install-applications-behind-a-proxy)
|
||||
is available.
|
||||
|
@ -90,26 +83,25 @@ is available.
|
|||
|
||||
> Introduced in GitLab 11.6 for project- and group-level clusters.
|
||||
|
||||
[cert-manager](https://cert-manager.io/docs/) is a native
|
||||
Kubernetes certificate management controller that helps with issuing
|
||||
certificates. Installing cert-manager on your cluster will issue a
|
||||
certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that
|
||||
certificates are valid and up-to-date.
|
||||
[cert-manager](https://cert-manager.io/docs/) is a native Kubernetes certificate
|
||||
management controller that helps with issuing certificates. Installing
|
||||
cert-manager on your cluster issues a certificate by [Let's Encrypt](https://letsencrypt.org/)
|
||||
and ensures that certificates are valid and up-to-date.
|
||||
|
||||
The chart used to install this application depends on the version of GitLab used. In:
|
||||
|
||||
- GitLab 12.3 and newer, the [jetstack/cert-manager](https://github.com/jetstack/cert-manager)
|
||||
- GitLab 12.3 and newer, the [`jetstack/cert-manager`](https://github.com/jetstack/cert-manager)
|
||||
chart is used with a [`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/cert_manager/values.yaml)
|
||||
file.
|
||||
- GitLab 12.2 and older, the [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager)
|
||||
- GitLab 12.2 and older, the [`stable/cert-manager`](https://gi2wthub.com/helm/charts/tree/master/stable/cert-manager)
|
||||
chart was used.
|
||||
|
||||
If you have installed cert-manager prior to GitLab 12.3, Let's Encrypt will
|
||||
[block requests from older versions of cert-manager](https://community.letsencrypt.org/t/blocking-old-cert-manager-versions/98753).
|
||||
If you installed cert-manager prior to GitLab 12.3, Let's Encrypt
|
||||
[blocks requests](https://community.letsencrypt.org/t/blocking-old-cert-manager-versions/98753)
|
||||
from older versions of `cert-manager`. To resolve this:
|
||||
|
||||
To resolve this:
|
||||
|
||||
1. Uninstall cert-manager (consider [backing up any additional configuration](https://cert-manager.io/docs/tutorials/backup/)).
|
||||
1. [Back up any additional configuration](https://cert-manager.io/docs/tutorials/backup/).
|
||||
1. Uninstall cert-manager.
|
||||
1. Install cert-manager again.
|
||||
|
||||
### GitLab Runner
|
||||
|
@ -117,26 +109,21 @@ To resolve this:
|
|||
> - Introduced in GitLab 10.6 for project-level clusters.
|
||||
> - Introduced in GitLab 11.10 for group-level clusters.
|
||||
|
||||
[GitLab Runner](https://docs.gitlab.com/runner/) is the open source
|
||||
project that is used to run your jobs and send the results back to
|
||||
GitLab. It is used in conjunction with [GitLab
|
||||
CI/CD](../../ci/README.md), the open-source continuous integration
|
||||
service included with GitLab that coordinates the jobs.
|
||||
[GitLab Runner](https://docs.gitlab.com/runner/) is the open source project that
|
||||
is used to run your jobs and send the results back to GitLab. It's used in
|
||||
conjunction with [GitLab CI/CD](../../ci/README.md), the open-source continuous
|
||||
integration service included with GitLab that coordinates the jobs.
|
||||
|
||||
If the project is on GitLab.com, shared runners are available
|
||||
(the first 2000 minutes are free, you can
|
||||
[buy more later](../../subscriptions/gitlab_com/index.md#purchase-additional-ci-minutes))
|
||||
and you do not have to deploy one if they are enough for your needs. If a
|
||||
project-specific runner is desired, or there are no shared runners, it is easy
|
||||
to deploy one.
|
||||
If the project is on GitLab.com, [shared runners](../gitlab_com/index.md#shared-runners)
|
||||
are available, and you do not have to deploy one if they are enough for your
|
||||
needs. If a project-specific runner is desired, or there are no shared runners,
|
||||
you can deploy one.
|
||||
|
||||
Note that the deployed runner will be set as **privileged**, which means it will essentially
|
||||
have root access to the underlying machine. This is required to build Docker images,
|
||||
so it is the default. Make sure you read the
|
||||
[security implications](../project/clusters/index.md#security-implications)
|
||||
The deployed runner is set as **privileged**. Root access to the underlying
|
||||
server is required to build Docker images, so it is the default. Be sure to read
|
||||
the [security implications](../project/clusters/index.md#security-implications)
|
||||
before deploying one.
|
||||
|
||||
NOTE: **Note:**
|
||||
The [`runner/gitlab-runner`](https://gitlab.com/gitlab-org/charts/gitlab-runner)
|
||||
chart is used to install this application, using
|
||||
[a preconfigured `values.yaml`](https://gitlab.com/gitlab-org/charts/gitlab-runner/-/blob/master/values.yaml)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 29 KiB |
|
@ -8,80 +8,80 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
|
||||
|
||||
NOTE: **Note:**
|
||||
This is the user guide. In order to use the dependency proxy, an administrator
|
||||
must first [configure it](../../../administration/packages/dependency_proxy.md).
|
||||
The GitLab Dependency Proxy is a local proxy you can use for your frequently-accessed
|
||||
upstream images.
|
||||
|
||||
For many organizations, it is desirable to have a local proxy for frequently used
|
||||
upstream images/packages. In the case of CI/CD, the proxy is responsible for
|
||||
receiving a request and returning the upstream image from a registry, acting
|
||||
as a pull-through cache.
|
||||
In the case of CI/CD, the Dependency Proxy receives a request and returns the
|
||||
upstream image from a registry, acting as a pull-through cache.
|
||||
|
||||
The dependency proxy is available in the group level. To access it, navigate to
|
||||
a group's **Packages & Registries > Dependency Proxy**.
|
||||
## Prerequisites
|
||||
|
||||
![Dependency Proxy group page](img/group_dependency_proxy.png)
|
||||
To use the Dependency Proxy:
|
||||
|
||||
## Supported dependency proxies
|
||||
- Your group must be public. Authentication for private groups is [not supported yet](https://gitlab.com/gitlab-org/gitlab/-/issues/11582).
|
||||
|
||||
NOTE: **Note:**
|
||||
For a list of the upcoming additions to the proxies, visit the
|
||||
[direction page](https://about.gitlab.com/direction/package/dependency_proxy/#top-vision-items).
|
||||
### Supported images and packages
|
||||
|
||||
The following dependency proxies are supported.
|
||||
The following images and packages are supported.
|
||||
|
||||
| Dependency proxy | GitLab version |
|
||||
| Image/Package | GitLab version |
|
||||
| ---------------- | -------------- |
|
||||
| Docker | 11.11+ |
|
||||
|
||||
## Using the Docker dependency proxy
|
||||
For a list of planned additions, view the
|
||||
[direction page](https://about.gitlab.com/direction/package/dependency_proxy/#top-vision-items).
|
||||
|
||||
With the Docker dependency proxy, you can use GitLab as a source for a Docker image.
|
||||
To get a Docker image into the dependency proxy:
|
||||
## View the Dependency Proxy
|
||||
|
||||
1. Find the proxy URL on your group's page under **Packages & Registries > Dependency Proxy**,
|
||||
for example `gitlab.com/groupname/dependency_proxy/containers`.
|
||||
1. Trigger GitLab to pull the Docker image you want (e.g., `alpine:latest` or
|
||||
`linuxserver/nextcloud:latest`) and store it in the proxy storage by using
|
||||
one of the following ways:
|
||||
To view the Dependency Proxy:
|
||||
|
||||
- Manually pulling the Docker image:
|
||||
- Go to your group's **Packages & Registries > Dependency Proxy**.
|
||||
|
||||
The Dependency Proxy is not available for projects.
|
||||
|
||||
## Use the Dependency Proxy for Docker images
|
||||
|
||||
You can use GitLab as a source for your Docker images.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Your images must be stored on [Docker Hub](https://hub.docker.com/).
|
||||
- Docker Hub must be available. Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/241639)
|
||||
for progress on accessing images when Docker Hub is down.
|
||||
|
||||
To store a Docker image in Dependency Proxy storage:
|
||||
|
||||
1. Go to your group's **Packages & Registries > Dependency Proxy**.
|
||||
1. Copy the **Dependency Proxy URL**.
|
||||
1. Use one of these commands. In these examples, the image is `alpine:latest`.
|
||||
|
||||
- Add the URL to your [`.gitlab-ci.yml`](../../../ci/yaml/README.md#image) file:
|
||||
|
||||
```shell
|
||||
docker pull gitlab.com/groupname/dependency_proxy/containers/alpine:latest
|
||||
image: gitlab.example.com/groupname/dependency_proxy/containers/alpine:latest
|
||||
```
|
||||
|
||||
- From a `Dockerfile`:
|
||||
- Manually pull the Docker image:
|
||||
|
||||
```shell
|
||||
FROM gitlab.com/groupname/dependency_proxy/containers/alpine:latest
|
||||
docker pull gitlab.example.com/groupname/dependency_proxy/containers/alpine:latest
|
||||
```
|
||||
|
||||
- In [`.gitlab-ci.yml`](../../../ci/yaml/README.md#image):
|
||||
- Add the URL to a `Dockerfile`:
|
||||
|
||||
```shell
|
||||
image: gitlab.com/groupname/dependency_proxy/containers/alpine:latest
|
||||
FROM gitlab.example.com/groupname/dependency_proxy/containers/alpine:latest
|
||||
```
|
||||
|
||||
GitLab pulls the Docker image from Docker Hub and caches the blobs
|
||||
on the GitLab server. The next time you pull the same image, GitLab gets the latest
|
||||
information about the image from Docker Hub but serves the existing blobs
|
||||
information about the image from Docker Hub, but serves the existing blobs
|
||||
from the GitLab server.
|
||||
|
||||
The blobs are kept forever, and there is no hard limit on how much data can be
|
||||
## Clear the Dependency Proxy cache
|
||||
|
||||
Blobs are kept forever on the GitLab server, and there is no hard limit on how much data can be
|
||||
stored.
|
||||
|
||||
## Clearing the cache
|
||||
|
||||
It is possible to use the GitLab API to purge the dependency proxy cache for a
|
||||
given group to gain back disk space that may be taken up by image blobs that
|
||||
are no longer needed. See the [dependency proxy API documentation](../../../api/dependency_proxy.md)
|
||||
for more details.
|
||||
|
||||
## Limitations
|
||||
|
||||
The following limitations apply:
|
||||
|
||||
- Only [public groups are supported](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) (authentication is not supported yet).
|
||||
- Only Docker Hub is supported.
|
||||
- This feature requires Docker Hub being available.
|
||||
To reclaim disk space used by image blobs that are no longer needed, use
|
||||
the [Dependency Proxy API](../../../api/dependency_proxy.md).
|
||||
|
|
|
@ -72,10 +72,10 @@ module Gitlab
|
|||
end
|
||||
|
||||
def with_lock_retries(&block)
|
||||
Gitlab::Database::WithLockRetries.new(**{
|
||||
Gitlab::Database::WithLockRetries.new(
|
||||
klass: self.class,
|
||||
logger: Gitlab::AppLogger
|
||||
}).run(&block)
|
||||
).run(&block)
|
||||
end
|
||||
|
||||
def connection
|
||||
|
|
|
@ -99,7 +99,7 @@ module Gitlab
|
|||
def with_lock_retries(&block)
|
||||
arguments = { klass: self.class, logger: logger }
|
||||
|
||||
Gitlab::Database::WithLockRetries.new(arguments).run(raise_on_exhaustion: true, &block)
|
||||
Gitlab::Database::WithLockRetries.new(**arguments).run(raise_on_exhaustion: true, &block)
|
||||
end
|
||||
|
||||
delegate :execute, to: :connection
|
||||
|
|
|
@ -68,10 +68,10 @@ module Gitlab
|
|||
end
|
||||
|
||||
def with_lock_retries(&block)
|
||||
Gitlab::Database::WithLockRetries.new({
|
||||
Gitlab::Database::WithLockRetries.new(
|
||||
klass: self.class,
|
||||
logger: Gitlab::BackgroundMigration::Logger
|
||||
}).run(&block)
|
||||
).run(&block)
|
||||
end
|
||||
|
||||
def assert_not_in_transaction_block(scope:)
|
||||
|
|
|
@ -3004,6 +3004,9 @@ msgstr ""
|
|||
msgid "An unknown error occurred while loading this graph."
|
||||
msgstr ""
|
||||
|
||||
msgid "An unknown error occurred."
|
||||
msgstr ""
|
||||
|
||||
msgid "Analytics"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28472,6 +28475,9 @@ msgstr ""
|
|||
msgid "Warning: Displaying this diagram might cause performance issues on this page."
|
||||
msgstr ""
|
||||
|
||||
msgid "We are currently unable to fetch data for the pipeline header."
|
||||
msgstr ""
|
||||
|
||||
msgid "We are currently unable to fetch data for this graph."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -121,13 +121,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(assigns(:build).id).to eq(job.id)
|
||||
end
|
||||
|
||||
it 'has the correct build collection' do
|
||||
builds = assigns(:builds).map(&:id)
|
||||
|
||||
expect(builds).to include(job.id, second_job.id)
|
||||
expect(builds).not_to include(third_job.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job does not exist' do
|
||||
|
|
|
@ -140,6 +140,7 @@ RSpec.describe 'Commits' do
|
|||
|
||||
context 'when accessing internal project with disallowed access', :js do
|
||||
before do
|
||||
stub_feature_flags(graphql_pipeline_header: false)
|
||||
project.update(
|
||||
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
|
||||
public_builds: false)
|
||||
|
|
|
@ -172,10 +172,17 @@ RSpec.describe 'Pipeline', :js do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'showing user status' do
|
||||
let(:user_with_status) { pipeline.user }
|
||||
describe 'pipelines details view' do
|
||||
let!(:status) { create(:user_status, user: pipeline.user, emoji: 'smirk', message: 'Authoring this object') }
|
||||
|
||||
subject { visit project_pipeline_path(project, pipeline) }
|
||||
it 'pipeline header shows the user status and emoji' do
|
||||
visit project_pipeline_path(project, pipeline)
|
||||
|
||||
within '[data-testid="pipeline-header-content"]' do
|
||||
expect(page).to have_selector("[data-testid='#{status.message}']")
|
||||
expect(page).to have_selector("[data-name='#{status.emoji}']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'pipeline graph' do
|
||||
|
@ -400,7 +407,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
|
||||
context 'when retrying' do
|
||||
before do
|
||||
find('[data-testid="retryButton"]').click
|
||||
find('[data-testid="retryPipeline"]').click
|
||||
end
|
||||
|
||||
it 'does not show a "Retry" button', :sidekiq_might_not_need_inline do
|
||||
|
@ -902,7 +909,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
|
||||
context 'when retrying' do
|
||||
before do
|
||||
find('[data-testid="retryButton"]').click
|
||||
find('[data-testid="retryPipeline"]').click
|
||||
end
|
||||
|
||||
it 'does not show a "Retry" button', :sidekiq_might_not_need_inline do
|
||||
|
|
|
@ -4,13 +4,8 @@ import Dag from '~/pipelines/components/dag/dag.vue';
|
|||
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
|
||||
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
|
||||
|
||||
import {
|
||||
ADD_NOTE,
|
||||
REMOVE_NOTE,
|
||||
REPLACE_NOTES,
|
||||
PARSE_FAILURE,
|
||||
UNSUPPORTED_DATA,
|
||||
} from '~/pipelines/components/dag//constants';
|
||||
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants';
|
||||
import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/pipelines/constants';
|
||||
import {
|
||||
mockParsedGraphQLNodes,
|
||||
tooSmallGraph,
|
||||
|
|
|
@ -1,115 +1,164 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import {
|
||||
mockCancelledPipelineHeader,
|
||||
mockFailedPipelineHeader,
|
||||
mockRunningPipelineHeader,
|
||||
mockSuccessfulPipelineHeader,
|
||||
} from './mock_data';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import HeaderComponent from '~/pipelines/components/header_component.vue';
|
||||
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import eventHub from '~/pipelines/event_hub';
|
||||
|
||||
describe('Pipeline details header', () => {
|
||||
let wrapper;
|
||||
let glModalDirective;
|
||||
|
||||
const threeWeeksAgo = new Date();
|
||||
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
|
||||
let mockAxios;
|
||||
|
||||
const findDeleteModal = () => wrapper.find(GlModal);
|
||||
const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]');
|
||||
const findCancelButton = () => wrapper.find('[data-testid="cancelPipeline"]');
|
||||
const findDeleteButton = () => wrapper.find('[data-testid="deletePipeline"]');
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
|
||||
const defaultProps = {
|
||||
pipeline: {
|
||||
details: {
|
||||
status: {
|
||||
group: 'failed',
|
||||
icon: 'status_failed',
|
||||
label: 'failed',
|
||||
text: 'failed',
|
||||
details_path: 'path',
|
||||
},
|
||||
},
|
||||
id: 123,
|
||||
created_at: threeWeeksAgo.toISOString(),
|
||||
user: {
|
||||
web_url: 'path',
|
||||
name: 'Foo',
|
||||
username: 'foobar',
|
||||
email: 'foo@bar.com',
|
||||
avatar_url: 'link',
|
||||
},
|
||||
retry_path: 'retry',
|
||||
cancel_path: 'cancel',
|
||||
delete_path: 'delete',
|
||||
const defaultProvideOptions = {
|
||||
pipelineId: 14,
|
||||
pipelineIid: 1,
|
||||
paths: {
|
||||
retry: '/retry',
|
||||
cancel: '/cancel',
|
||||
delete: '/delete',
|
||||
fullProject: '/namespace/my-project',
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
const createComponent = (pipelineMock = mockRunningPipelineHeader, { isLoading } = false) => {
|
||||
glModalDirective = jest.fn();
|
||||
|
||||
wrapper = shallowMount(HeaderComponent, {
|
||||
propsData: {
|
||||
...props,
|
||||
const $apollo = {
|
||||
queries: {
|
||||
pipeline: {
|
||||
loading: isLoading,
|
||||
stopPolling: jest.fn(),
|
||||
startPolling: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return shallowMount(HeaderComponent, {
|
||||
data() {
|
||||
return {
|
||||
pipeline: pipelineMock,
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
...defaultProvideOptions,
|
||||
},
|
||||
directives: {
|
||||
glModal: {
|
||||
bind(el, { value }) {
|
||||
bind(_, { value }) {
|
||||
glModalDirective(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
mocks: { $apollo },
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
|
||||
createComponent(defaultProps);
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios.onGet('*').replyOnce(200);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
eventHub.$off();
|
||||
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
|
||||
mockAxios.restore();
|
||||
});
|
||||
|
||||
it('should render provided pipeline info', () => {
|
||||
expect(wrapper.find(CiHeader).props()).toMatchObject({
|
||||
status: defaultProps.pipeline.details.status,
|
||||
itemId: defaultProps.pipeline.id,
|
||||
time: defaultProps.pipeline.created_at,
|
||||
user: defaultProps.pipeline.user,
|
||||
describe('initial loading', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(null, { isLoading: true });
|
||||
});
|
||||
|
||||
it('shows a loading state while graphQL is fetching initial data', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action buttons', () => {
|
||||
it('should not trigger eventHub when nothing happens', () => {
|
||||
expect(eventHub.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
describe('visible state', () => {
|
||||
it.each`
|
||||
state | pipelineData | retryValue | cancelValue
|
||||
${'cancelled'} | ${mockCancelledPipelineHeader} | ${true} | ${false}
|
||||
${'failed'} | ${mockFailedPipelineHeader} | ${true} | ${false}
|
||||
${'running'} | ${mockRunningPipelineHeader} | ${false} | ${true}
|
||||
${'successful'} | ${mockSuccessfulPipelineHeader} | ${false} | ${false}
|
||||
`(
|
||||
'with a $state pipeline, it will show actions: retry $retryValue and cancel $cancelValue',
|
||||
({ pipelineData, retryValue, cancelValue }) => {
|
||||
wrapper = createComponent(pipelineData);
|
||||
|
||||
it('should call postAction when retry button action is clicked', () => {
|
||||
wrapper.find('[data-testid="retryButton"]').vm.$emit('click');
|
||||
expect(findRetryButton().exists()).toBe(retryValue);
|
||||
expect(findCancelButton().exists()).toBe(cancelValue);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
|
||||
});
|
||||
|
||||
it('should call postAction when cancel button action is clicked', () => {
|
||||
wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
|
||||
});
|
||||
|
||||
it('does not show delete modal', () => {
|
||||
expect(findDeleteModal()).not.toBeVisible();
|
||||
});
|
||||
|
||||
describe('when delete button action is clicked', () => {
|
||||
it('displays delete modal', () => {
|
||||
expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
|
||||
expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
|
||||
describe('actions', () => {
|
||||
describe('Retry action', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(mockCancelledPipelineHeader);
|
||||
});
|
||||
|
||||
it('should call delete when modal is submitted', () => {
|
||||
it('should call axios with the right path when retry button is clicked', async () => {
|
||||
jest.spyOn(axios, 'post');
|
||||
findRetryButton().vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(defaultProvideOptions.paths.retry);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cancel action', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(mockRunningPipelineHeader);
|
||||
});
|
||||
|
||||
it('should call axios with the right path when cancel button is clicked', async () => {
|
||||
jest.spyOn(axios, 'post');
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(defaultProvideOptions.paths.cancel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete action', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(mockFailedPipelineHeader);
|
||||
});
|
||||
|
||||
it('displays delete modal when clicking on delete and does not call the delete action', async () => {
|
||||
jest.spyOn(axios, 'delete');
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
|
||||
expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
|
||||
expect(axios.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call delete path when modal is submitted', async () => {
|
||||
jest.spyOn(axios, 'delete');
|
||||
findDeleteModal().vm.$emit('ok');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(axios.delete).toHaveBeenCalledWith(defaultProvideOptions.paths.delete);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
116
spec/frontend/pipelines/legacy_header_component_spec.js
Normal file
116
spec/frontend/pipelines/legacy_header_component_spec.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import LegacyHeaderComponent from '~/pipelines/components/legacy_header_component.vue';
|
||||
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import eventHub from '~/pipelines/event_hub';
|
||||
|
||||
describe('Pipeline details header', () => {
|
||||
let wrapper;
|
||||
let glModalDirective;
|
||||
|
||||
const threeWeeksAgo = new Date();
|
||||
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
|
||||
|
||||
const findDeleteModal = () => wrapper.find(GlModal);
|
||||
|
||||
const defaultProps = {
|
||||
pipeline: {
|
||||
details: {
|
||||
status: {
|
||||
group: 'failed',
|
||||
icon: 'status_failed',
|
||||
label: 'failed',
|
||||
text: 'failed',
|
||||
details_path: 'path',
|
||||
},
|
||||
},
|
||||
id: 123,
|
||||
created_at: threeWeeksAgo.toISOString(),
|
||||
user: {
|
||||
web_url: 'path',
|
||||
name: 'Foo',
|
||||
username: 'foobar',
|
||||
email: 'foo@bar.com',
|
||||
avatar_url: 'link',
|
||||
},
|
||||
retry_path: 'retry',
|
||||
cancel_path: 'cancel',
|
||||
delete_path: 'delete',
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
glModalDirective = jest.fn();
|
||||
|
||||
wrapper = shallowMount(LegacyHeaderComponent, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
directives: {
|
||||
glModal: {
|
||||
bind(el, { value }) {
|
||||
glModalDirective(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
|
||||
createComponent(defaultProps);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
eventHub.$off();
|
||||
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('should render provided pipeline info', () => {
|
||||
expect(wrapper.find(CiHeader).props()).toMatchObject({
|
||||
status: defaultProps.pipeline.details.status,
|
||||
itemId: defaultProps.pipeline.id,
|
||||
time: defaultProps.pipeline.created_at,
|
||||
user: defaultProps.pipeline.user,
|
||||
});
|
||||
});
|
||||
|
||||
describe('action buttons', () => {
|
||||
it('should not trigger eventHub when nothing happens', () => {
|
||||
expect(eventHub.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call postAction when retry button action is clicked', () => {
|
||||
wrapper.find('[data-testid="retryButton"]').vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
|
||||
});
|
||||
|
||||
it('should call postAction when cancel button action is clicked', () => {
|
||||
wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
|
||||
});
|
||||
|
||||
it('does not show delete modal', () => {
|
||||
expect(findDeleteModal()).not.toBeVisible();
|
||||
});
|
||||
|
||||
describe('when delete button action is clicked', () => {
|
||||
it('displays delete modal', () => {
|
||||
expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
|
||||
expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
|
||||
});
|
||||
|
||||
it('should call delete when modal is submitted', () => {
|
||||
findDeleteModal().vm.$emit('ok');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,3 +1,7 @@
|
|||
const PIPELINE_RUNNING = 'RUNNING';
|
||||
const PIPELINE_CANCELED = 'CANCELED';
|
||||
const PIPELINE_FAILED = 'FAILED';
|
||||
|
||||
export const pipelineWithStages = {
|
||||
id: 20333396,
|
||||
user: {
|
||||
|
@ -320,6 +324,80 @@ export const pipelineWithStages = {
|
|||
triggered: [],
|
||||
};
|
||||
|
||||
const threeWeeksAgo = new Date();
|
||||
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
|
||||
|
||||
export const mockPipelineHeader = {
|
||||
detailedStatus: {},
|
||||
id: 123,
|
||||
userPermissions: {
|
||||
destroyPipeline: true,
|
||||
},
|
||||
createdAt: threeWeeksAgo.toISOString(),
|
||||
user: {
|
||||
name: 'Foo',
|
||||
username: 'foobar',
|
||||
email: 'foo@bar.com',
|
||||
avatarUrl: 'link',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockFailedPipelineHeader = {
|
||||
...mockPipelineHeader,
|
||||
status: PIPELINE_FAILED,
|
||||
retryable: true,
|
||||
cancelable: false,
|
||||
detailedStatus: {
|
||||
group: 'failed',
|
||||
icon: 'status_failed',
|
||||
label: 'failed',
|
||||
text: 'failed',
|
||||
detailsPath: 'path',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockRunningPipelineHeader = {
|
||||
...mockPipelineHeader,
|
||||
status: PIPELINE_RUNNING,
|
||||
retryable: false,
|
||||
cancelable: true,
|
||||
detailedStatus: {
|
||||
group: 'running',
|
||||
icon: 'status_running',
|
||||
label: 'running',
|
||||
text: 'running',
|
||||
detailsPath: 'path',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockCancelledPipelineHeader = {
|
||||
...mockPipelineHeader,
|
||||
status: PIPELINE_CANCELED,
|
||||
retryable: true,
|
||||
cancelable: false,
|
||||
detailedStatus: {
|
||||
group: 'cancelled',
|
||||
icon: 'status_cancelled',
|
||||
label: 'cancelled',
|
||||
text: 'cancelled',
|
||||
detailsPath: 'path',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockSuccessfulPipelineHeader = {
|
||||
...mockPipelineHeader,
|
||||
status: 'SUCCESS',
|
||||
retryable: false,
|
||||
cancelable: false,
|
||||
detailedStatus: {
|
||||
group: 'success',
|
||||
icon: 'status_success',
|
||||
label: 'success',
|
||||
text: 'success',
|
||||
detailsPath: 'path',
|
||||
},
|
||||
};
|
||||
|
||||
export const stageReply = {
|
||||
name: 'deploy',
|
||||
title: 'deploy: running',
|
||||
|
|
|
@ -6,6 +6,10 @@ import component from '~/vue_merge_request_widget/components/states/mr_widget_re
|
|||
describe('Merge request widget rebase component', () => {
|
||||
let Component;
|
||||
let vm;
|
||||
|
||||
const findRebaseMessageEl = () => vm.$el.querySelector('[data-testid="rebase-message"]');
|
||||
const findRebaseMessageElText = () => findRebaseMessageEl().textContent.trim();
|
||||
|
||||
beforeEach(() => {
|
||||
Component = Vue.extend(component);
|
||||
});
|
||||
|
@ -21,9 +25,7 @@ describe('Merge request widget rebase component', () => {
|
|||
service: {},
|
||||
});
|
||||
|
||||
expect(
|
||||
vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(),
|
||||
).toContain('Rebase in progress');
|
||||
expect(findRebaseMessageElText()).toContain('Rebase in progress');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -39,9 +41,7 @@ describe('Merge request widget rebase component', () => {
|
|||
});
|
||||
|
||||
it('it should render rebase button and warning message', () => {
|
||||
const text = vm.$el
|
||||
.querySelector('.rebase-state-find-class-convention span')
|
||||
.textContent.trim();
|
||||
const text = findRebaseMessageElText();
|
||||
|
||||
expect(text).toContain('Fast-forward merge is not possible.');
|
||||
expect(text.replace(/\s\s+/g, ' ')).toContain(
|
||||
|
@ -53,9 +53,7 @@ describe('Merge request widget rebase component', () => {
|
|||
vm.rebasingError = 'Something went wrong!';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(),
|
||||
).toContain('Something went wrong!');
|
||||
expect(findRebaseMessageElText()).toContain('Something went wrong!');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -72,9 +70,7 @@ describe('Merge request widget rebase component', () => {
|
|||
service: {},
|
||||
});
|
||||
|
||||
const text = vm.$el
|
||||
.querySelector('.rebase-state-find-class-convention span')
|
||||
.textContent.trim();
|
||||
const text = findRebaseMessageElText();
|
||||
|
||||
expect(text).toContain('Fast-forward merge is not possible.');
|
||||
expect(text).toContain('Rebase the source branch onto');
|
||||
|
@ -93,7 +89,7 @@ describe('Merge request widget rebase component', () => {
|
|||
service: {},
|
||||
});
|
||||
|
||||
const elem = vm.$el.querySelector('.rebase-state-find-class-convention span');
|
||||
const elem = findRebaseMessageEl();
|
||||
|
||||
expect(elem.innerHTML).toContain(
|
||||
`Fast-forward merge is not possible. Rebase the source branch onto <span class="label-branch">${targetBranch}</span> to allow this merge request to be merged.`,
|
||||
|
|
|
@ -271,6 +271,14 @@ RSpec.describe DesignManagement::SaveDesignsService do
|
|||
expect(response[:message]).to match(/only \d+ files are allowed simultaneously/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when uploading duplicate files' do
|
||||
let(:files) { [rails_sample, dk_png, rails_sample] }
|
||||
|
||||
it 'returns the correct error' do
|
||||
expect(response[:message]).to match('Duplicate filenames are not allowed!')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is not allowed to upload designs' do
|
||||
|
|
Loading…
Reference in a new issue