Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-23 18:10:15 +00:00
parent e0b023e388
commit c5d67a0495
49 changed files with 1035 additions and 350 deletions

View file

@ -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';

View file

@ -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

View file

@ -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: {

View file

@ -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"

View file

@ -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>

View file

@ -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';

View file

@ -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
}
}
}
}
}

View file

@ -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();
};

View 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', {});
},
});
};

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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

View file

@ -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;
}
}

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
---
title: 'Designs: return an error if uploading designs with duplicate names'
merge_request: 42514
author: Sushil Khanchi
type: fixed

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View 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.

View file

@ -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.

View file

@ -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

View file

@ -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).

View file

@ -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

View file

@ -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

View file

@ -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:)

View file

@ -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 ""

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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);
});
});
});

View 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');
});
});
});
});

View file

@ -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',

View file

@ -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.`,

View file

@ -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