Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/pipelines/components/dag/constants.js6
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_graph.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue166
-rw-r--r--app/assets/javascripts/pipelines/components/legacy_header_component.vue132
-rw-r--r--app/assets/javascripts/pipelines/constants.js8
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql30
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js16
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_header.js41
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue44
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/pages/profile.scss17
-rw-r--r--app/controllers/projects/jobs_controller.rb5
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/services/design_management/save_designs_service.rb1
-rw-r--r--app/views/projects/pipelines/show.html.haml3
-rw-r--r--changelogs/unreleased/khanchi-designs-patch.yml5
-rw-r--r--config/feature_flags/development/graphql_milestone_stats.yml6
-rw-r--r--config/feature_flags/development/graphql_pipeline_header.yml7
-rw-r--r--config/feature_flags/development/graphql_release_data.yml6
-rw-r--r--config/feature_flags/development/graphql_releases_page.yml6
-rw-r--r--config/feature_flags/development/new_release_page.yml6
-rw-r--r--config/feature_flags/development/release_asset_link_editing.yml6
-rw-r--r--config/feature_flags/development/release_asset_link_type.yml6
-rw-r--r--config/feature_flags/development/release_issue_summary.yml6
-rw-r--r--config/feature_flags/development/release_show_page.yml6
-rw-r--r--doc/administration/geo/disaster_recovery/index.md9
-rw-r--r--doc/administration/geo/index.md4
-rw-r--r--doc/administration/libravatar.md4
-rw-r--r--doc/administration/read_only_gitlab.md125
-rw-r--r--doc/operations/feature_flags.md9
-rw-r--r--doc/user/clusters/applications.md87
-rw-r--r--doc/user/packages/dependency_proxy/img/group_dependency_proxy.pngbin29334 -> 0 bytes
-rw-r--r--doc/user/packages/dependency_proxy/index.md92
-rw-r--r--lib/gitlab/database/partitioning/partition_creator.rb4
-rw-r--r--lib/gitlab/database/reindexing/concurrent_reindex.rb2
-rw-r--r--lib/gitlab/database/schema_helpers.rb4
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb7
-rw-r--r--spec/features/commits_spec.rb1
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb17
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js9
-rw-r--r--spec/frontend/pipelines/header_component_spec.js177
-rw-r--r--spec/frontend/pipelines/legacy_header_component_spec.js116
-rw-r--r--spec/frontend/pipelines/mock_data.js78
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js22
-rw-r--r--spec/services/design_management/save_designs_service_spec.rb8
49 files changed, 1030 insertions, 345 deletions
diff --git a/app/assets/javascripts/pipelines/components/dag/constants.js b/app/assets/javascripts/pipelines/components/dag/constants.js
index b6a98fdc488..cd89055737f 100644
--- a/app/assets/javascripts/pipelines/components/dag/constants.js
+++ b/app/assets/javascripts/pipelines/components/dag/constants.js
@@ -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';
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 8487da3d621..ab736061a2e 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -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
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
index d12baa9617e..34ff89a5e6f 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
@@ -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: {
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index c7b72be36ad..b26f28fa6af 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -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: {},
+ },
+ pipelineId: {
+ default: '',
},
- isLoading: {
- type: Boolean,
- required: true,
+ 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"
diff --git a/app/assets/javascripts/pipelines/components/legacy_header_component.vue b/app/assets/javascripts/pipelines/components/legacy_header_component.vue
new file mode 100644
index 00000000000..c7b72be36ad
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/legacy_header_component.vue
@@ -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>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index abe5e1060c8..701d36b71f2 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -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';
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
new file mode 100644
index 00000000000..06083daeca0
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
@@ -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
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 745f5b886a5..67aec12655a 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -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();
};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/pipelines/pipeline_details_header.js
new file mode 100644
index 00000000000..27fe9ba3f19
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_details_header.js
@@ -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', {});
+ },
+ });
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index ec0934c5b4b..14c2e9fa828 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index d7af3b3298e..1b7e51b7d02 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -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">
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 6ff6f10f786..4679d922861 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -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>
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8cebfc430e0..4dcc6cdb649 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -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
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index ddffcf382c6..3605283245f 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -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;
- }
-}
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 3f7f8da3478..a8ac20cf96b 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -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
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index c1734d2cd8a..0a9dccc605f 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -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]
diff --git a/app/services/design_management/save_designs_service.rb b/app/services/design_management/save_designs_service.rb
index 0446d2f1ee8..f3ac5b74ca5 100644
--- a/app/services/design_management/save_designs_service.rb
+++ b/app/services/design_management/save_designs_service.rb
@@ -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
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index a9c140aee5f..5b8ab455ec8 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -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
diff --git a/changelogs/unreleased/khanchi-designs-patch.yml b/changelogs/unreleased/khanchi-designs-patch.yml
new file mode 100644
index 00000000000..9f675685c30
--- /dev/null
+++ b/changelogs/unreleased/khanchi-designs-patch.yml
@@ -0,0 +1,5 @@
+---
+title: 'Designs: return an error if uploading designs with duplicate names'
+merge_request: 42514
+author: Sushil Khanchi
+type: fixed
diff --git a/config/feature_flags/development/graphql_milestone_stats.yml b/config/feature_flags/development/graphql_milestone_stats.yml
index daa72031879..a3ff14d3245 100644
--- a/config/feature_flags/development/graphql_milestone_stats.yml
+++ b/config/feature_flags/development/graphql_milestone_stats.yml
@@ -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
diff --git a/config/feature_flags/development/graphql_pipeline_header.yml b/config/feature_flags/development/graphql_pipeline_header.yml
new file mode 100644
index 00000000000..99019d4c849
--- /dev/null
+++ b/config/feature_flags/development/graphql_pipeline_header.yml
@@ -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
diff --git a/config/feature_flags/development/graphql_release_data.yml b/config/feature_flags/development/graphql_release_data.yml
index c30fbf7fe13..609c0dc634a 100644
--- a/config/feature_flags/development/graphql_release_data.yml
+++ b/config/feature_flags/development/graphql_release_data.yml
@@ -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
diff --git a/config/feature_flags/development/graphql_releases_page.yml b/config/feature_flags/development/graphql_releases_page.yml
index cd7cca3d71c..607fc7027b5 100644
--- a/config/feature_flags/development/graphql_releases_page.yml
+++ b/config/feature_flags/development/graphql_releases_page.yml
@@ -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
diff --git a/config/feature_flags/development/new_release_page.yml b/config/feature_flags/development/new_release_page.yml
index a17438ba949..b2890b9ebf9 100644
--- a/config/feature_flags/development/new_release_page.yml
+++ b/config/feature_flags/development/new_release_page.yml
@@ -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
diff --git a/config/feature_flags/development/release_asset_link_editing.yml b/config/feature_flags/development/release_asset_link_editing.yml
index 6e6cce2c343..6e5a0ba1b52 100644
--- a/config/feature_flags/development/release_asset_link_editing.yml
+++ b/config/feature_flags/development/release_asset_link_editing.yml
@@ -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
diff --git a/config/feature_flags/development/release_asset_link_type.yml b/config/feature_flags/development/release_asset_link_type.yml
index baeb2e59f82..d49f246d2fa 100644
--- a/config/feature_flags/development/release_asset_link_type.yml
+++ b/config/feature_flags/development/release_asset_link_type.yml
@@ -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
diff --git a/config/feature_flags/development/release_issue_summary.yml b/config/feature_flags/development/release_issue_summary.yml
index ebd8ef2e4d0..93370422f2c 100644
--- a/config/feature_flags/development/release_issue_summary.yml
+++ b/config/feature_flags/development/release_issue_summary.yml
@@ -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
diff --git a/config/feature_flags/development/release_show_page.yml b/config/feature_flags/development/release_show_page.yml
index 5a3f1709452..fd18eca1b25 100644
--- a/config/feature_flags/development/release_show_page.yml
+++ b/config/feature_flags/development/release_show_page.yml
@@ -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
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index f15aeaa7a92..152dd780ed6 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -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)
diff --git a/doc/administration/geo/index.md b/doc/administration/geo/index.md
index c6cd8dd34ca..47d19c1e12c 100644
--- a/doc/administration/geo/index.md
+++ b/doc/administration/geo/index.md
@@ -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
diff --git a/doc/administration/libravatar.md b/doc/administration/libravatar.md
index beecd9e4783..a36ed347f54 100644
--- a/doc/administration/libravatar.md
+++ b/doc/administration/libravatar.md
@@ -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
diff --git a/doc/administration/read_only_gitlab.md b/doc/administration/read_only_gitlab.md
new file mode 100644
index 00000000000..681102a8c39
--- /dev/null
+++ b/doc/administration/read_only_gitlab.md
@@ -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.
diff --git a/doc/operations/feature_flags.md b/doc/operations/feature_flags.md
index 56f2470a1f1..8db1c0945c6 100644
--- a/doc/operations/feature_flags.md
+++ b/doc/operations/feature_flags.md
@@ -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.
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 2243ffa0cb1..18b20e06593 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -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).
-
-To resolve this:
+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:
-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.
-
-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.
-
-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)
+[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](../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.
+
+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)
diff --git a/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png b/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png
deleted file mode 100644
index e550d296d5a..00000000000
--- a/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index e3ee8909165..3fa21ef486b 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -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).
+
+## View the Dependency Proxy
+
+To view the Dependency Proxy:
+
+- 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.
-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:
+Prerequisites:
-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:
+- 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.
- - Manually pulling the Docker image:
+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
-stored.
-
-## Clearing the cache
+## Clear the Dependency Proxy 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:
+Blobs are kept forever on the GitLab server, and there is no hard limit on how much data can be
+stored.
-- 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).
diff --git a/lib/gitlab/database/partitioning/partition_creator.rb b/lib/gitlab/database/partitioning/partition_creator.rb
index fefa3d8e182..547e0b9b957 100644
--- a/lib/gitlab/database/partitioning/partition_creator.rb
+++ b/lib/gitlab/database/partitioning/partition_creator.rb
@@ -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
diff --git a/lib/gitlab/database/reindexing/concurrent_reindex.rb b/lib/gitlab/database/reindexing/concurrent_reindex.rb
index 7e4c5aa622e..24e3f48e08f 100644
--- a/lib/gitlab/database/reindexing/concurrent_reindex.rb
+++ b/lib/gitlab/database/reindexing/concurrent_reindex.rb
@@ -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
diff --git a/lib/gitlab/database/schema_helpers.rb b/lib/gitlab/database/schema_helpers.rb
index dda4d8eecdb..bf8481e1a13 100644
--- a/lib/gitlab/database/schema_helpers.rb
+++ b/lib/gitlab/database/schema_helpers.rb
@@ -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:)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 644b2e8796a..be1f202f38e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 94cce1964ca..d7c22d46e90 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -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
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index e66a40720da..97ee891dbb8 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -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)
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index f59dc5dd074..c8c3a52d427 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -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
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index 989f6c17197..08a43199594 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -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,
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 5388d624d3c..2e10b0f068c 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -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 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 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 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 });
});
- });
- describe('action buttons', () => {
- it('should not trigger eventHub when nothing happens', () => {
- expect(eventHub.$emit).not.toHaveBeenCalled();
+ it('shows a loading state while graphQL is fetching initial data', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
});
+ });
+
+ 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);
+
+ expect(findRetryButton().exists()).toBe(retryValue);
+ expect(findCancelButton().exists()).toBe(cancelValue);
+ },
+ );
+ });
- it('should call postAction when retry button action is clicked', () => {
- wrapper.find('[data-testid="retryButton"]').vm.$emit('click');
+ describe('actions', () => {
+ describe('Retry action', () => {
+ beforeEach(() => {
+ wrapper = createComponent(mockCancelledPipelineHeader);
+ });
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
- });
+ it('should call axios with the right path when retry button is clicked', async () => {
+ jest.spyOn(axios, 'post');
+ findRetryButton().vm.$emit('click');
- it('should call postAction when cancel button action is clicked', () => {
- wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click');
+ await wrapper.vm.$nextTick();
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
+ expect(axios.post).toHaveBeenCalledWith(defaultProvideOptions.paths.retry);
+ });
});
- it('does not show delete modal', () => {
- expect(findDeleteModal()).not.toBeVisible();
+ 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('when delete button action is clicked', () => {
- it('displays delete modal', () => {
+ 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 when modal is submitted', () => {
+ 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);
});
});
});
diff --git a/spec/frontend/pipelines/legacy_header_component_spec.js b/spec/frontend/pipelines/legacy_header_component_spec.js
new file mode 100644
index 00000000000..fb7feb8898a
--- /dev/null
+++ b/spec/frontend/pipelines/legacy_header_component_spec.js
@@ -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');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index e63efc543f1..2afdbb05107 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -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',
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
index 6ec30493f8b..9923434a7dd 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -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.`,
diff --git a/spec/services/design_management/save_designs_service_spec.rb b/spec/services/design_management/save_designs_service_spec.rb
index abba5de2c27..94ec12ab8d1 100644
--- a/spec/services/design_management/save_designs_service_spec.rb
+++ b/spec/services/design_management/save_designs_service_spec.rb
@@ -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