Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-18 12:13:06 +00:00
parent 31522c5182
commit c7b2529418
58 changed files with 1770 additions and 692 deletions

View file

@ -234,7 +234,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/administration/integration/kroki.md @msedlakjakubowski
/doc/administration/integration/mailgun.md @kpaizee
/doc/administration/integration/plantuml.md @aqualls
/doc/administration/integration/terminal.md @kpaizee
/doc/administration/integration/terminal.md @ashrafkhamis
/doc/administration/invalidate_markdown_cache.md @msedlakjakubowski
/doc/administration/issue_closing_pattern.md @aqualls
/doc/administration/job_artifacts.md @marcel.amirault
@ -278,11 +278,11 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/administration/smime_signing_email.md @axil
/doc/administration/snippets/index.md @aqualls
/doc/administration/static_objects_external_storage.md @aqualls
/doc/administration/system_hooks.md @kpaizee
/doc/administration/system_hooks.md @ashrafkhamis
/doc/administration/terraform_state.md @sselhorn
/doc/administration/timezone.md @axil
/doc/administration/troubleshooting/ @axil
/doc/administration/troubleshooting/elasticsearch.md @sselhorn
/doc/administration/troubleshooting/elasticsearch.md @ashrafkhamis
/doc/administration/troubleshooting/postgresql.md @aqualls
/doc/administration/uploads.md @axil
/doc/administration/user_settings.md @eread
@ -291,7 +291,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/access_requests.md @eread
/doc/api/admin_sidekiq_queues.md @axil
/doc/api/alert_management_alerts.md @msedlakjakubowski
/doc/api/api_resources.md @kpaizee
/doc/api/api_resources.md @ashrafkhamis
/doc/api/appearance.md @eread
/doc/api/applications.md @eread
/doc/api/audit_events.md @eread
@ -304,7 +304,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/cluster_agents.md @sselhorn
/doc/api/commits.md @aqualls
/doc/api/container_registry.md @claytoncornell
/doc/api/custom_attributes.md @kpaizee
/doc/api/custom_attributes.md @ashrafkhamis
/doc/api/dependencies.md @rdickenson
/doc/api/dependency_proxy.md @claytoncornell
/doc/api/deploy_keys.md @rdickenson
@ -326,7 +326,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/features.md @rdickenson
/doc/api/freeze_periods.md @rdickenson
/doc/api/geo_nodes.md @axil
/doc/api/graphql/ @kpaizee
/doc/api/graphql/ @ashrafkhamis
/doc/api/graphql/custom_emoji.md @msedlakjakubowski
/doc/api/graphql/sample_issue_boards.md @msedlakjakubowski
/doc/api/group_access_tokens.md @eread
@ -346,10 +346,10 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/group_wikis.md @aqualls
/doc/api/groups.md @fneill
/doc/api/import.md @eread
/doc/api/index.md @kpaizee
/doc/api/index.md @ashrafkhamis
/doc/api/instance_clusters.md @sselhorn
/doc/api/instance_level_ci_variables.md @marcel.amirault
/doc/api/integrations.md @kpaizee
/doc/api/integrations.md @ashrafkhamis
/doc/api/invitations.md @kpaizee
/doc/api/issue_links.md @msedlakjakubowski
/doc/api/issues_statistics.md @msedlakjakubowski
@ -376,7 +376,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/notes.md @msedlakjakubowski
/doc/api/notification_settings.md @msedlakjakubowski
/doc/api/oauth2.md @eread
/doc/api/openapi/openapi_interactive.md @kpaizee
/doc/api/openapi/openapi_interactive.md @ashrafkhamis
/doc/api/packages.md @claytoncornell
/doc/api/packages/ @claytoncornell
/doc/api/pages_domains.md @aqualls
@ -425,7 +425,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/statistics.md @eread
/doc/api/status_checks.md @eread
/doc/api/suggestions.md @aqualls
/doc/api/system_hooks.md @kpaizee
/doc/api/system_hooks.md @ashrafkhamis
/doc/api/tags.md @aqualls
/doc/api/templates/dockerfiles.md @aqualls
/doc/api/templates/gitignores.md @aqualls
@ -435,7 +435,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/topics.md @fneill
/doc/api/usage_data.md @claytoncornell
/doc/api/users.md @eread
/doc/api/version.md @kpaizee
/doc/api/version.md @ashrafkhamis
/doc/api/visual_review_discussions.md @marcel.amirault
/doc/api/vulnerabilities.md @claytoncornell
/doc/api/vulnerability_exports.md @claytoncornell
@ -521,7 +521,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/development/documentation/redirects.md @dianalogan
/doc/development/documentation/review_apps.md @dianalogan
/doc/development/documentation/testing.md @dianalogan
/doc/development/elasticsearch.md @sselhorn
/doc/development/elasticsearch.md @ashrafkhamis
/doc/development/experiment_guide/gitlab_experiment.md @kpaizee
/doc/development/experiment_guide/index.md @kpaizee
/doc/development/export_csv.md @eread
@ -539,14 +539,14 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/development/gitaly.md @eread
/doc/development/gitlab_flavored_markdown/index.md @aqualls
/doc/development/gitlab_flavored_markdown/specification_guide/index.md @aqualls
/doc/development/graphql_guide/ @kpaizee
/doc/development/graphql_guide/ @ashrafkhamis
/doc/development/graphql_guide/batchloader.md @aqualls
/doc/development/hash_indexes.md @aqualls
/doc/development/i18n/ @eread
/doc/development/image_scaling.md @sselhorn
/doc/development/import_export.md @eread
/doc/development/index.md @sselhorn
/doc/development/integrations/ @kpaizee
/doc/development/integrations/ @ashrafkhamis
/doc/development/integrations/codesandbox.md @sselhorn
/doc/development/integrations/secure_partner_integration.md @rdickenson
/doc/development/integrations/secure.md @claytoncornell
@ -584,9 +584,9 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/downgrade_ee_to_ce/index.md @axil
/doc/gitlab-basics/ @aqualls
/doc/install/ @axil
/doc/integration/ @kpaizee
/doc/integration/advanced_search/ @sselhorn
/doc/integration/elasticsearch.md @sselhorn
/doc/integration/ @ashrafkhamis
/doc/integration/advanced_search/ @ashrafkhamis
/doc/integration/elasticsearch.md @ashrafkhamis
/doc/integration/gitpod.md @aqualls
/doc/integration/kerberos.md @eread
/doc/integration/mattermost/index.md @axil
@ -645,7 +645,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/admin_area/settings/index.md @aqualls
/doc/user/admin_area/settings/instance_template_repository.md @aqualls
/doc/user/admin_area/settings/package_registry_rate_limits.md @claytoncornell
/doc/user/admin_area/settings/project_integration_management.md @kpaizee
/doc/user/admin_area/settings/project_integration_management.md @ashrafkhamis
/doc/user/admin_area/settings/push_event_activities_limit.md @aqualls
/doc/user/admin_area/settings/rate_limit_on_issues_creation.md @msedlakjakubowski
/doc/user/admin_area/settings/rate_limit_on_notes_creation.md @msedlakjakubowski
@ -732,7 +732,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/project/import/ @eread
/doc/user/project/import/jira.md @msedlakjakubowski
/doc/user/project/index.md @fneill
/doc/user/project/integrations/ @kpaizee
/doc/user/project/integrations/ @ashrafkhamis
/doc/user/project/integrations/prometheus_library/ @msedlakjakubowski
/doc/user/project/integrations/prometheus.md @msedlakjakubowski
/doc/user/project/issue_board.md @msedlakjakubowski
@ -774,9 +774,9 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/project/working_with_projects.md @fneill
/doc/user/public_access.md @fneill
/doc/user/reserved_names.md @fneill
/doc/user/search/advanced_search.md @sselhorn
/doc/user/search/global_search/advanced_search_syntax.md @sselhorn
/doc/user/search/index.md @sselhorn
/doc/user/search/advanced_search.md @ashrafkhamis
/doc/user/search/global_search/advanced_search_syntax.md @ashrafkhamis
/doc/user/search/index.md @ashrafkhamis
/doc/user/shortcuts.md @aqualls
/doc/user/snippets.md @aqualls
/doc/user/ssh.md @eread

View file

@ -31,6 +31,9 @@
.if-auto-deploy-branches: &if-auto-deploy-branches
if: '$CI_COMMIT_BRANCH =~ /^\d+-\d+-auto-deploy-\d+$/'
.if-tag: &if-tag
if: '$CI_COMMIT_TAG'
.if-default-branch-or-tag: &if-default-branch-or-tag
if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG'
@ -636,6 +639,7 @@
changes: *code-qa-patterns
- <<: *if-dot-com-gitlab-org-default-branch
changes: *code-qa-patterns
- <<: *if-tag
- <<: *if-dot-com-gitlab-org-schedule
- <<: *if-force-ci

View file

@ -8,8 +8,6 @@ import {
REACHED_LIMIT_MESSAGE,
REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
CLOSE_TO_LIMIT_MESSAGE,
CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE,
DANGER_ALERT_TITLE_PERSONAL_NAMESPACE,
} from '../constants';
export default {
@ -52,13 +50,6 @@ export default {
});
},
dangerAlertTitle() {
if (this.usersLimitDataset.userNamespace) {
return sprintf(DANGER_ALERT_TITLE_PERSONAL_NAMESPACE, {
count: this.freeUsersLimit,
members: this.pluralMembers(this.freeUsersLimit),
});
}
return sprintf(DANGER_ALERT_TITLE, {
count: this.freeUsersLimit,
members: this.pluralMembers(this.freeUsersLimit),
@ -71,20 +62,9 @@ export default {
title() {
return this.reachedLimit ? this.dangerAlertTitle : this.warningAlertTitle;
},
reachedLimitMessage() {
if (this.usersLimitDataset.userNamespace) {
return this.$options.i18n.reachedLimitMessage;
}
return this.$options.i18n.reachedLimitUpgradeSuggestionMessage;
},
message() {
if (this.reachedLimit) {
return this.reachedLimitMessage;
}
if (this.usersLimitDataset.userNamespace) {
return this.$options.i18n.closeToLimitMessagePersonalNamespace;
return this.$options.i18n.reachedLimitUpgradeSuggestionMessage;
}
return this.$options.i18n.closeToLimitMessage;
@ -99,7 +79,6 @@ export default {
reachedLimitMessage: REACHED_LIMIT_MESSAGE,
reachedLimitUpgradeSuggestionMessage: REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
closeToLimitMessage: CLOSE_TO_LIMIT_MESSAGE,
closeToLimitMessagePersonalNamespace: CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE,
},
};
</script>

View file

@ -146,10 +146,6 @@ export const DANGER_ALERT_TITLE = s__(
"InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
);
export const DANGER_ALERT_TITLE_PERSONAL_NAMESPACE = s__(
"InviteMembersModal|You've reached your %{count} %{members} limit for your personal projects",
);
export const REACHED_LIMIT_MESSAGE = s__(
'InviteMembersModal|You cannot add more members, but you can remove members who no longer need access.',
);
@ -163,6 +159,3 @@ export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.co
export const CLOSE_TO_LIMIT_MESSAGE = s__(
'InviteMembersModal|To get more members an owner of the group can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
);
export const CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE = s__(
'InviteMembersModal|To make more space, you can remove members who no longer need access.',
);

View file

@ -20,8 +20,6 @@ export default (function initInviteMembersModal() {
return false;
}
const usersLimitDataset = JSON.parse(el.dataset.usersLimitDataset || '{}');
inviteMembersModal = new Vue({
el,
name: 'InviteMembersModalRoot',
@ -40,10 +38,9 @@ export default (function initInviteMembersModal() {
projects: JSON.parse(el.dataset.projects || '[]'),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
usersLimitDataset: convertObjectPropsToCamelCase({
...usersLimitDataset,
user_namespace: parseBoolean(usersLimitDataset.user_namespace),
}),
usersLimitDataset: convertObjectPropsToCamelCase(
JSON.parse(el.dataset.usersLimitDataset || '{}'),
),
},
}),
});

View file

@ -1,6 +1,6 @@
<script>
import { __ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '../../constants';
@ -10,8 +10,6 @@ export default {
},
components: {
PipelineMiniGraph,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
inject: ['projectFullPath'],
props: {
@ -47,9 +45,6 @@ export default {
downstreamPipelines() {
return this.linkedPipelines?.downstream?.nodes || [];
},
hasDownstreamPipelines() {
return this.downstreamPipelines.length > 0;
},
hasPipelineStages() {
return this.pipelineStages.length > 0;
},
@ -87,23 +82,11 @@ export default {
</script>
<template>
<div
<pipeline-mini-graph
v-if="hasPipelineStages"
class="gl-align-items-center gl-display-inline-flex gl-flex-wrap stage-cell gl-mr-5"
>
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
upstreamPipeline,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="pipeline-editor-mini-graph-upstream"
/>
<pipeline-mini-graph :stages="pipelineStages" />
<linked-pipelines-mini-list
v-if="hasDownstreamPipelines"
:triggered="downstreamPipelines"
:pipeline-path="pipelinePath"
data-testid="pipeline-editor-mini-graph-downstream"
/>
</div>
:downstream-pipelines="downstreamPipelines"
:pipeline-path="pipelinePath"
:stages="pipelineStages"
:upstream-pipeline="upstreamPipeline"
/>
</template>

View file

@ -174,7 +174,7 @@ export default {
<div class="gl-display-flex gl-flex-wrap">
<pipeline-editor-mini-graph :pipeline="pipeline" v-on="$listeners" />
<gl-button
class="gl-mt-2 gl-md-mt-0"
class="gl-ml-3"
category="secondary"
variant="confirm"
:href="status.detailsPath"

View file

@ -0,0 +1,14 @@
import { get } from 'lodash';
export const accessors = {
rest: {
detailedStatus: ['details', 'status'],
},
graphql: {
detailedStatus: 'detailedStatus',
},
};
export const accessValue = (pipeline, dataMethod, path) => {
return get(pipeline, accessors[dataMethod][path]);
};

View file

@ -0,0 +1,132 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { accessValue } from './accessors/linked_pipelines_accessors';
/**
* Renders the upstream/downstream portions of the pipeline mini graph.
*/
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
CiIcon,
},
inject: {
dataMethod: {
default: 'rest',
},
},
props: {
triggeredBy: {
type: Array,
required: false,
default: () => [],
},
triggered: {
type: Array,
required: false,
default: () => [],
},
pipelinePath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
maxRenderedPipelines: 3,
};
},
computed: {
// Exactly one of these (triggeredBy and triggered) must be truthy. Never both. Never neither.
isUpstream() {
return Boolean(this.triggeredBy.length) && !this.triggered.length;
},
isDownstream() {
return !this.triggeredBy.length && Boolean(this.triggered.length);
},
linkedPipelines() {
return this.isUpstream ? this.triggeredBy : this.triggered;
},
totalPipelineCount() {
return this.linkedPipelines.length;
},
linkedPipelinesTrimmed() {
return this.totalPipelineCount > this.maxRenderedPipelines
? this.linkedPipelines.slice(0, this.maxRenderedPipelines)
: this.linkedPipelines;
},
shouldRenderCounter() {
return this.isDownstream && this.linkedPipelines.length > this.maxRenderedPipelines;
},
counterLabel() {
return `+${this.linkedPipelines.length - this.maxRenderedPipelines}`;
},
counterTooltipText() {
return sprintf(s__('LinkedPipelines|%{counterLabel} more downstream pipelines'), {
counterLabel: this.counterLabel,
});
},
},
methods: {
pipelineTooltipText(pipeline) {
const { label } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
return `${pipeline.project.name} - ${label}`;
},
pipelineStatus(pipeline) {
// detailedStatus is graphQL, details.status is REST
return pipeline?.detailedStatus || pipeline?.details?.status;
},
triggerButtonClass(pipeline) {
const { group } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
return `ci-status-icon-${group}`;
},
},
};
</script>
<template>
<span
v-if="linkedPipelines"
:class="{
'is-upstream': isUpstream,
'is-downstream': isDownstream,
}"
class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
>
<a
v-for="pipeline in linkedPipelinesTrimmed"
:key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
:href="pipeline.path"
:class="triggerButtonClass(pipeline)"
class="linked-pipeline-mini-item gl-display-inline-block gl-h-6 gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
data-testid="linked-pipeline-mini-item"
>
<ci-icon
is-borderless
is-interactive
css-classes="gl-rounded-full"
:size="24"
:status="pipelineStatus(pipeline)"
class="gl-align-items-center gl-border gl-display-inline-flex"
/>
</a>
<a
v-if="shouldRenderCounter"
v-gl-tooltip="{ title: counterTooltipText }"
:title="counterTooltipText"
:href="pipelinePath"
class="gl-align-items-center gl-bg-gray-50 gl-display-inline-flex gl-font-sm gl-h-6 gl-justify-content-center gl-rounded-pill gl-text-decoration-none gl-text-gray-500 gl-w-7 linked-pipelines-counter linked-pipeline-mini-item"
data-testid="linked-pipeline-counter"
>
{{ counterLabel }}
</a>
</span>
</template>

View file

@ -0,0 +1,102 @@
<script>
import { GlIcon } from '@gitlab/ui';
import PipelineStages from './pipeline_stages.vue';
import LinkedPipelinesMiniList from './linked_pipelines_mini_list.vue';
/**
* Renders the pipeline mini graph.
*/
export default {
components: {
GlIcon,
LinkedPipelinesMiniList,
PipelineStages,
},
arrowStyles: [
'arrow-icon gl-display-inline-block gl-mx-1 gl-text-gray-500 gl-vertical-align-middle!',
],
props: {
downstreamPipelines: {
type: Array,
required: false,
default: () => [],
},
isMergeTrain: {
type: Boolean,
required: false,
default: false,
},
pipelinePath: {
type: String,
required: false,
default: '',
},
stages: {
type: Array,
required: true,
default: () => [],
},
stagesClass: {
type: [Array, Object, String],
required: false,
default: '',
},
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
upstreamPipeline: {
type: Object,
required: false,
default: () => {},
},
},
computed: {
hasDownstreamPipelines() {
return Boolean(this.downstreamPipelines.length);
},
},
methods: {
onPipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
},
},
};
</script>
<template>
<div class="stage-cell" data-testid="pipeline-mini-graph">
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
upstreamPipeline,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="pipeline-mini-graph-upstream"
/>
<gl-icon
v-if="upstreamPipeline"
:class="$options.arrowStyles"
name="long-arrow"
data-testid="upstream-arrow-icon"
/>
<pipeline-stages
:is-merge-train="isMergeTrain"
:stages="stages"
:update-dropdown="updateDropdown"
:stages-class="stagesClass"
data-testid="pipeline-stages"
@pipelineActionRequestComplete="onPipelineActionRequestComplete"
/>
<gl-icon
v-if="hasDownstreamPipelines"
:class="$options.arrowStyles"
name="long-arrow"
data-testid="downstream-arrow-icon"
/>
<linked-pipelines-mini-list
v-if="hasDownstreamPipelines"
:triggered="downstreamPipelines"
:pipeline-path="pipelinePath"
data-testid="pipeline-mini-graph-downstream"
/>
</div>
</template>

View file

@ -1,7 +1,7 @@
<script>
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import PipelineStage from './pipeline_stage.vue';
/**
* Renders the pipeline mini graph.
* Renders the pipeline stages portion of the pipeline mini graph.
*/
export default {
components: {
@ -36,7 +36,7 @@ export default {
};
</script>
<template>
<div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle">
<div data-testid="pipeline-stages" class="gl-display-inline gl-vertical-align-middle">
<div
v-for="stage in stages"
:key="stage.name"

View file

@ -1,8 +1,8 @@
<script>
import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import eventHub from '../../event_hub';
import PipelineMiniGraph from './pipeline_mini_graph.vue';
import PipelineOperations from './pipeline_operations.vue';
import PipelineStopModal from './pipeline_stop_modal.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
@ -17,8 +17,6 @@ const DEFAULT_TH_CLASSES =
export default {
components: {
GlTableLite,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
PipelineMiniGraph,
PipelineOperations,
PipelinesStatusBadge,
@ -169,29 +167,14 @@ export default {
</template>
<template #cell(stages)="{ item }">
<div class="stage-cell">
<!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 -->
<div></div>
<linked-pipelines-mini-list
v-if="item.triggered_by"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
item.triggered_by,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="mini-graph-upstream"
/>
<pipeline-mini-graph
v-if="item.details && item.details.stages && item.details.stages.length > 0"
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
@pipelineActionRequestComplete="onPipelineActionRequestComplete"
/>
<linked-pipelines-mini-list
v-if="item.triggered.length"
:triggered="item.triggered"
:pipeline-path="item.path"
data-testid="mini-graph-downstream"
/>
</div>
<pipeline-mini-graph
:downstream-pipelines="item.triggered"
:pipeline-path="item.path"
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
:upstream-pipeline="item.triggered_by"
@pipelineActionRequestComplete="onPipelineActionRequestComplete"
/>
</template>
<template #cell(actions)="{ item }">

View file

@ -2,11 +2,11 @@
import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import {
getQueryHeaders,
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { formatStages } from '../utils';
import getLinkedPipelinesQuery from '../graphql/queries/get_linked_pipelines.query.graphql';
import getPipelineStagesQuery from '../graphql/queries/get_pipeline_stages.query.graphql';
@ -21,8 +21,6 @@ export default {
components: {
GlLoadingIcon,
PipelineMiniGraph,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
inject: {
fullPath: {
@ -92,12 +90,12 @@ export default {
};
},
computed: {
hasDownstream() {
return this.pipeline?.downstream?.nodes.length > 0;
},
downstreamPipelines() {
return this.pipeline?.downstream?.nodes;
},
pipelinePath() {
return this.pipeline?.path ?? '';
},
upstreamPipeline() {
return this.pipeline?.upstream;
},
@ -128,23 +126,13 @@ export default {
<template>
<div class="gl-pt-2">
<gl-loading-icon v-if="$apollo.queries.pipeline.loading" />
<div v-else class="gl-align-items-center gl-display-flex">
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
upstreamPipeline,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="commit-box-mini-graph-upstream"
/>
<pipeline-mini-graph :stages="formattedStages" data-testid="commit-box-mini-graph" />
<linked-pipelines-mini-list
v-if="hasDownstream"
:triggered="downstreamPipelines"
:pipeline-path="pipeline.path"
data-testid="commit-box-mini-graph-downstream"
/>
</div>
<pipeline-mini-graph
v-else
data-testid="commit-box-pipeline-mini-graph"
:downstream-pipelines="downstreamPipelines"
:pipeline-path="pipelinePath"
:stages="formattedStages"
:upstream-pipeline="upstreamPipeline"
/>
</div>
</template>

View file

@ -9,11 +9,11 @@ import {
GlTooltipDirective,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
import mrWidgetPipelineMixin from '~/vue_merge_request_widget/mixins/mr_widget_pipeline';
import { s__, n__ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { MT_MERGE_STRATEGY } from '../constants';
@ -31,8 +31,6 @@ export default {
PipelineMiniGraph,
TimeAgoTooltip,
TooltipOnTruncate,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@ -276,17 +274,15 @@ export default {
</div>
</div>
<div>
<span class="gl-align-items-center gl-display-inline-flex mr-widget-pipeline-graph">
<span class="gl-align-items-center gl-display-inline-flex gl-flex-wrap stage-cell">
<linked-pipelines-mini-list v-if="triggeredBy.length" :triggered-by="triggeredBy" />
<pipeline-mini-graph
v-if="hasStages"
stages-class="mr-widget-pipeline-stages"
:stages="pipeline.details.stages"
:is-merge-train="isMergeTrain"
/>
</span>
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
<span class="gl-align-items-center gl-display-inline-flex">
<pipeline-mini-graph
v-if="pipeline.details.stages"
:downstream-pipelines="triggered"
:is-merge-train="isMergeTrain"
:stages="pipeline.details.stages"
:upstream-pipeline="triggeredBy[0]"
stages-class="mr-widget-pipeline-stages"
/>
<pipeline-artifacts :pipeline-id="pipeline.id" :artifacts="artifacts" class="gl-ml-3" />
</span>
</div>

View file

@ -5,7 +5,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
import StatusIcon from '../extensions/status_icon.vue';
import { EXTENSION_ICON_NAMES } from '../../constants';
import { EXTENSION_ICONS } from '../../constants';
const FETCH_TYPE_COLLAPSED = 'collapsed';
@ -66,7 +66,7 @@ export default {
type: String,
default: 'neutral',
required: false,
validator: (value) => Object.keys(EXTENSION_ICON_NAMES).indexOf(value) > -1,
validator: (value) => Object.keys(EXTENSION_ICONS).indexOf(value) > -1,
},
isCollapsible: {
type: Boolean,
@ -84,6 +84,11 @@ export default {
error: null,
};
},
computed: {
statusIcon() {
return this.error ? EXTENSION_ICONS.failed : this.statusIconName;
},
},
watch: {
isLoading(newValue) {
this.$emit('is-loading', newValue);
@ -147,18 +152,14 @@ export default {
<template>
<section class="media-section" data-testid="widget-extension">
<div class="media gl-p-5">
<status-icon
:level="1"
:name="widgetName"
:is-loading="isLoading"
:icon-name="statusIconName"
/>
<status-icon :level="1" :name="widgetName" :is-loading="isLoading" :icon-name="statusIcon" />
<div
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
data-testid="widget-extension-top-level"
>
<div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
<slot name="summary">{{ isLoading ? loadingText : summary }}</slot>
<slot v-if="!error" name="summary">{{ isLoading ? loadingText : summary }}</slot>
<span v-else>{{ error }}</span>
</div>
<!-- actions will go here -->
<div

View file

@ -427,10 +427,10 @@
padding-inline-start: 28px;
margin-inline-start: 0 !important;
> input.task-list-item-checkbox {
input.task-list-item-checkbox {
position: absolute;
inset-inline-start: 8px;
top: 5px;
inset-inline-start: $gl-padding-8;
inset-block-start: 5px;
}
}
}

View file

@ -1,77 +1,24 @@
@import 'mixins_and_variables_and_functions';
.description {
ul,
ol {
/* We're changing list-style-position to inside because the default of
* outside doesn't move negative margin to the left of the bullet. */
list-style-position: inside;
}
li {
position: relative;
/* In the browser, the li element comes after (to the right of) the bullet point, so hovering
* over the left of the bullet point doesn't trigger a row hover. To trigger hovering on the
* left, we're applying negative margin here to shift the li element left. */
margin-inline-start: -1rem;
padding-inline-start: 2.5rem;
margin-inline-start: 2.25rem;
.drag-icon {
position: absolute;
inset-block-start: 0.3rem;
inset-inline-start: 1rem;
}
/* The inside bullet aligns itself to the bottom, which we see when text to the right of
* a multi-line list item wraps. We fix this by aligning it to the top, and excluding
* other elements. Targeting ::marker doesn't seem to work, instead we exclude custom elements
* or anything with a class */
> *:not(gl-emoji, code, [class]) {
vertical-align: top;
}
/* The inside bullet is treated like an element inside the li element, so when we have a
* multi-paragraph list item, the text doesn't start on the right of the bullet because
* it is a block level p element. We make it inline to fix this. */
> p:first-of-type {
display: inline-block;
max-width: calc(100% - 1.5rem);
}
/* We fix the other paragraphs not indenting to the
* right of the bullet due to the inside bullet. */
p ~ a,
p ~ blockquote,
p ~ code,
p ~ details,
p ~ dl,
p ~ h1,
p ~ h2,
p ~ h3,
p ~ h4,
p ~ h5,
p ~ h6,
p ~ hr,
p ~ ol,
p ~ p,
p ~ table:not(.code), /* We need :not(.code) to override typography.scss */
p ~ ul,
p ~ .markdown-code-block {
margin-inline-start: 1rem;
inset-inline-start: -2.3rem;
padding-inline-end: 1rem;
width: 2rem;
}
}
ul.task-list {
> li.task-list-item {
/* We're using !important to override the same selector in typography.scss */
margin-inline-start: -1rem !important;
padding-inline-start: 2.5rem;
ul.task-list > li.task-list-item {
margin-inline-start: 0.5rem !important; /* Override typography.scss */
> input.task-list-item-checkbox {
position: static;
vertical-align: middle;
margin-block-start: -2px;
}
> .drag-icon {
inset-inline-start: -0.6rem;
}
}
}

View file

@ -398,12 +398,6 @@ $tabs-holder-z-index: 250;
display: block;
}
.mr-widget-pipeline-graph {
.dropdown-menu {
z-index: $zindex-dropdown-menu;
}
}
.normal {
flex: 1;
flex-basis: auto;

View file

@ -33,12 +33,6 @@
height: 22px;
}
}
.mr-widget-pipeline-graph {
.dropdown-menu {
margin-top: 11px;
}
}
}
.branch-info .commit-icon {

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::HookLogsController < Admin::ApplicationController
include ::Integrations::HooksExecution
include ::WebHooks::HookExecutionNotice
before_action :hook, only: [:show, :retry]
before_action :hook_log, only: [:show, :retry]

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::HooksController < Admin::ApplicationController
include ::Integrations::HooksExecution
include ::WebHooks::HookActions
before_action :hook_logs, only: :edit

View file

@ -1,95 +0,0 @@
# frozen_string_literal: true
module Integrations::HooksExecution
extend ActiveSupport::Concern
included do
attr_writer :hooks, :hook
end
def index
self.hooks = relation.select(&:persisted?)
self.hook = relation.new
end
def create
self.hook = relation.new(hook_params)
hook.save
unless hook.valid?
self.hooks = relation.select(&:persisted?)
flash[:alert] = hook.errors.full_messages.join.html_safe
end
redirect_to action: :index
end
def update
if hook.update(hook_params)
flash[:notice] = _('Hook was successfully updated.')
redirect_to action: :index
else
render 'edit'
end
end
def destroy
destroy_hook(hook)
redirect_to action: :index, status: :found
end
def edit
redirect_to(action: :index) unless hook
end
private
def hook_params
permitted = hook_param_names + trigger_values
permitted << { url_variables: [:key, :value] }
ps = params.require(:hook).permit(*permitted).to_h
ps[:url_variables] = ps[:url_variables].to_h { [_1[:key], _1[:value].presence] } if ps.key?(:url_variables)
if action_name == 'update' && ps.key?(:url_variables)
supplied = ps[:url_variables]
ps[:url_variables] = hook.url_variables.merge(supplied).compact
end
ps
end
def hook_param_names
%i[enable_ssl_verification token url push_events_branch_filter]
end
def destroy_hook(hook)
result = WebHooks::DestroyService.new(current_user).execute(hook)
if result[:status] == :success
flash[:notice] =
if result[:async]
_("%{hook_type} was scheduled for deletion") % { hook_type: hook.model_name.human }
else
_("%{hook_type} was deleted") % { hook_type: hook.model_name.human }
end
else
flash[:alert] = result[:message]
end
end
def set_hook_execution_notice(result)
http_status = result[:http_status]
message = result[:message]
if http_status && http_status >= 200 && http_status < 400
flash[:notice] = "Hook executed successfully: HTTP #{http_status}"
elsif http_status
flash[:alert] = "Hook executed successfully but returned HTTP #{http_status} #{message}"
else
flash[:alert] = "Hook execution failed: #{message}"
end
end
end

View file

@ -0,0 +1,85 @@
# frozen_string_literal: true
module WebHooks
module HookActions
extend ActiveSupport::Concern
include HookExecutionNotice
included do
attr_writer :hooks, :hook
end
def index
self.hooks = relation.select(&:persisted?)
self.hook = relation.new
end
def create
self.hook = relation.new(hook_params)
hook.save
unless hook.valid?
self.hooks = relation.select(&:persisted?)
flash[:alert] = hook.errors.full_messages.join.html_safe
end
redirect_to action: :index
end
def update
if hook.update(hook_params)
flash[:notice] = _('Hook was successfully updated.')
redirect_to action: :index
else
render 'edit'
end
end
def destroy
destroy_hook(hook)
redirect_to action: :index, status: :found
end
def edit
redirect_to(action: :index) unless hook
end
private
def hook_params
permitted = hook_param_names + trigger_values
permitted << { url_variables: [:key, :value] }
ps = params.require(:hook).permit(*permitted).to_h
ps[:url_variables] = ps[:url_variables].to_h { [_1[:key], _1[:value].presence] } if ps.key?(:url_variables)
if action_name == 'update' && ps.key?(:url_variables)
supplied = ps[:url_variables]
ps[:url_variables] = hook.url_variables.merge(supplied).compact
end
ps
end
def hook_param_names
%i[enable_ssl_verification token url push_events_branch_filter]
end
def destroy_hook(hook)
result = WebHooks::DestroyService.new(current_user).execute(hook)
if result[:status] == :success
flash[:notice] =
if result[:async]
format(_("%{hook_type} was scheduled for deletion"), hook_type: hook.model_name.human)
else
format(_("%{hook_type} was deleted"), hook_type: hook.model_name.human)
end
else
flash[:alert] = result[:message]
end
end
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
module WebHooks
module HookExecutionNotice
private
def set_hook_execution_notice(result)
http_status = result[:http_status]
message = result[:message]
if http_status && http_status >= 200 && http_status < 400
flash[:notice] = "Hook executed successfully: HTTP #{http_status}"
elsif http_status
flash[:alert] = "Hook executed successfully but returned HTTP #{http_status} #{message}"
else
flash[:alert] = "Hook execution failed: #{message}"
end
end
end
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Projects::HookLogsController < Projects::ApplicationController
include ::Integrations::HooksExecution
include ::WebHooks::HookExecutionNotice
before_action :authorize_admin_project!

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Projects::HooksController < Projects::ApplicationController
include ::Integrations::HooksExecution
include ::WebHooks::HookActions
# Authorize
before_action :authorize_admin_project!

View file

@ -197,6 +197,9 @@ module UsersHelper
banned_badge = { text: s_('AdminUsers|Banned'), variant: 'danger' }
return banned_badge if user.banned?
ldap_blocked_badge = { text: s_('AdminUsers|LDAP Blocked'), variant: 'danger' }
return ldap_blocked_badge if user.ldap_blocked?
{ text: s_('AdminUsers|Blocked'), variant: 'danger' }
end

View file

@ -5,7 +5,7 @@
.gl-flex-grow-1
%h3= s_('BackgroundMigrations|Background Migrations')
%p.light.gl-mb-0
- learnmore_link = help_page_path('development/database/batched_background_migrations')
- learnmore_link = help_page_path('user/admin_area/monitoring/background_migrations')
- learnmore_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: learnmore_link }
= html_escape(s_('BackgroundMigrations|Background migrations are used to perform data migrations whenever a migration exceeds the time limits in our guidelines. %{linkStart}Learn more%{linkEnd}')) % { linkStart: learnmore_link_start, linkEnd: '</a>'.html_safe }

View file

@ -1,7 +1,5 @@
- page_title s_("UsageQuota|Usage")
= render_if_exists 'namespaces/free_user_cap/projects/usage_quota_limitations_banner'
= render Pajamas::AlertComponent.new(title: _('Repository usage recalculation started'),
variant: :info,
alert_options: { class: 'js-recalculation-started-alert gl-mt-4 gl-mb-5 gl-display-none' }) do |c|

View file

@ -120,7 +120,7 @@ module Gitlab
stage: stage_value,
extends: extends,
rules: rules_value,
job_variables: variables_entry.value_with_data,
job_variables: variables_value.to_h,
root_variables_inheritance: root_variables_inheritance,
only: only_value,
except: except_value,

View file

@ -18,9 +18,7 @@ module Gitlab
end
def value
@config.to_h do |key, data|
[key.to_s, expand_data(data)[:value]]
end
@config.to_h { |key, value| [key.to_s, expand_value(value)[:value]] }
end
def self.default(**)
@ -28,9 +26,7 @@ module Gitlab
end
def value_with_data
@config.to_h do |key, data|
[key.to_s, expand_data(data)]
end
@config.to_h { |key, value| [key.to_s, expand_value(value)] }
end
def use_value_data?
@ -39,11 +35,11 @@ module Gitlab
private
def expand_data(data)
if data.is_a?(Hash)
{ value: data[:value].to_s, description: data[:description] }.compact
def expand_value(value)
if value.is_a?(Hash)
{ value: value[:value].to_s, description: value[:description] }
else
{ value: data.to_s }
{ value: value.to_s, description: nil }
end
end
end

View file

@ -6,24 +6,26 @@ module Gitlab
module Helpers
class << self
def merge_variables(current_vars, new_vars)
return current_vars if new_vars.blank?
current_vars = transform_from_yaml_variables(current_vars)
new_vars = transform_from_yaml_variables(new_vars)
current_vars = transform_to_array(current_vars) if current_vars.is_a?(Hash)
new_vars = transform_to_array(new_vars) if new_vars.is_a?(Hash)
(new_vars + current_vars).uniq { |var| var[:key] }
transform_to_yaml_variables(
current_vars.merge(new_vars)
)
end
def transform_to_array(vars)
vars.to_h.map do |key, data|
if data.is_a?(Hash)
{ key: key.to_s, **data.except(:key) }
else
{ key: key.to_s, value: data }
end
def transform_to_yaml_variables(vars)
vars.to_h.map do |key, value|
{ key: key.to_s, value: value, public: true }
end
end
def transform_from_yaml_variables(vars)
return vars.stringify_keys.transform_values(&:to_s) if vars.is_a?(Hash)
vars.to_a.to_h { |var| [var[:key].to_s, var[:value]] }
end
def inherit_yaml_variables(from:, to:, inheritance:)
merge_variables(apply_inheritance(from, inheritance), to)
end
@ -33,7 +35,7 @@ module Gitlab
def apply_inheritance(variables, inheritance)
case inheritance
when true then variables
when false then []
when false then {}
when Array then variables.select { |var| inheritance.include?(var[:key]) }
end
end

View file

@ -43,7 +43,7 @@ module Gitlab
end
def root_variables
@root_variables ||= transform_to_array(variables)
@root_variables ||= transform_to_yaml_variables(variables)
end
def jobs
@ -70,7 +70,7 @@ module Gitlab
environment: job[:environment_name],
coverage_regex: job[:coverage],
# yaml_variables is calculated with using job_variables in Seed::Build
job_variables: transform_to_array(job[:job_variables]),
job_variables: transform_to_yaml_variables(job[:job_variables]),
root_variables_inheritance: job[:root_variables_inheritance],
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
@ -114,7 +114,7 @@ module Gitlab
Gitlab::Ci::Variables::Helpers.inherit_yaml_variables(
from: root_variables,
to: job[:job_variables],
to: transform_to_yaml_variables(job[:job_variables]),
inheritance: job.fetch(:root_variables_inheritance, true)
)
end
@ -137,8 +137,8 @@ module Gitlab
job[:release]
end
def transform_to_array(variables)
::Gitlab::Ci::Variables::Helpers.transform_to_array(variables)
def transform_to_yaml_variables(variables)
::Gitlab::Ci::Variables::Helpers.transform_to_yaml_variables(variables)
end
end
end

View file

@ -1056,9 +1056,6 @@ msgstr[1] ""
msgid "%{strong_start}%{human_size}%{strong_end} Project Storage"
msgstr ""
msgid "%{strong_start}%{project_name}%{strong_end} is a personal project, so you cant upgrade to a paid plan or start a free trial to lift these limits. We recommend %{move_to_group_link}moving this project to a group%{end_link} to unlock these options. You can %{manage_members_link}manage the members of this project%{end_link}, but dont forget that all unique members in your personal namespace %{strong_start}%{namespace_name}%{strong_end} count towards total seats in use."
msgstr ""
msgid "%{strong_start}%{release_count}%{strong_end} Release"
msgid_plural "%{strong_start}%{release_count}%{strong_end} Releases"
msgstr[0] ""
@ -3123,6 +3120,9 @@ msgstr ""
msgid "AdminUsers|It's you!"
msgstr ""
msgid "AdminUsers|LDAP Blocked"
msgstr ""
msgid "AdminUsers|Learn more about %{link_start}banned users.%{link_end}"
msgstr ""
@ -21615,9 +21615,6 @@ msgstr ""
msgid "InviteMembersModal|To get more members an owner of the group can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier."
msgstr ""
msgid "InviteMembersModal|To make more space, you can remove members who no longer need access."
msgstr ""
msgid "InviteMembersModal|Username or email address"
msgstr ""
@ -21642,9 +21639,6 @@ msgstr ""
msgid "InviteMembersModal|You've reached your %{count} %{members} limit for %{name}"
msgstr ""
msgid "InviteMembersModal|You've reached your %{count} %{members} limit for your personal projects"
msgstr ""
msgid "InviteMembers|Invite a group"
msgstr ""
@ -45506,9 +45500,6 @@ msgstr ""
msgid "Your profile"
msgstr ""
msgid "Your project has limited quotas and features"
msgstr ""
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
msgstr ""

View file

@ -189,7 +189,7 @@ RSpec.describe "Admin > Admin sees background migrations" do
visit admin_background_migrations_path
within '#content-body' do
expect(page).to have_link('Learn more', href: help_page_path('development/database/batched_background_migrations'))
expect(page).to have_link('Learn more', href: help_page_path('user/admin_area/monitoring/background_migrations'))
end
end

View file

@ -27,7 +27,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do
end
it 'displays a mini pipeline graph' do
expect(page).to have_selector('[data-testid="commit-box-mini-graph"]')
expect(page).to have_selector('[data-testid="commit-box-pipeline-mini-graph"]')
first('[data-testid="mini-pipeline-graph-dropdown"]').click

View file

@ -1,14 +1,24 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { COMMIT_BOX_POLL_INTERVAL } from '~/projects/commit_box/info/constants';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import getPipelineStagesQuery from '~/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql';
import { mockPipelineStagesQueryResponse, mockStages } from './mock_data';
import * as graphQlUtils from '~/pipelines/components/graph/utils';
import {
mockDownstreamQueryResponse,
mockPipelineStagesQueryResponse,
mockStages,
mockUpstreamDownstreamQueryResponse,
mockUpstreamQueryResponse,
} from './mock_data';
jest.mock('~/flash');
@ -17,61 +27,219 @@ Vue.use(VueApollo);
describe('Commit box pipeline mini graph', () => {
let wrapper;
const findMiniGraph = () => wrapper.findByTestId('commit-box-mini-graph');
const findUpstream = () => wrapper.findByTestId('commit-box-mini-graph-upstream');
const findDownstream = () => wrapper.findByTestId('commit-box-mini-graph-downstream');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const downstreamHandler = jest.fn().mockResolvedValue(mockDownstreamQueryResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const stagesHandler = jest.fn().mockResolvedValue(mockPipelineStagesQueryResponse);
const upstreamDownstreamHandler = jest
.fn()
.mockResolvedValue(mockUpstreamDownstreamQueryResponse);
const upstreamHandler = jest.fn().mockResolvedValue(mockUpstreamQueryResponse);
const advanceToNextFetch = () => {
jest.advanceTimersByTime(COMMIT_BOX_POLL_INTERVAL);
};
const createComponent = ({ props = {} } = {}) => {
const handlers = [
[getLinkedPipelinesQuery, {}],
const fullPath = 'gitlab-org/gitlab';
const iid = '315';
const createMockApolloProvider = (handler = downstreamHandler) => {
const requestHandlers = [
[getLinkedPipelinesQuery, handler],
[getPipelineStagesQuery, stagesHandler],
];
return createMockApollo(requestHandlers);
};
const createComponent = (handler) => {
wrapper = extendedWrapper(
shallowMount(CommitBoxPipelineMiniGraph, {
propsData: {
stages: mockStages,
...props,
},
apolloProvider: createMockApollo(handlers),
provide: {
fullPath,
iid,
dataMethod: 'graphql',
graphqlResourceEtag: '/api/graphql:pipelines/id/320',
},
apolloProvider: createMockApolloProvider(handler),
}),
);
return waitForPromises();
};
afterEach(() => {
wrapper.destroy();
});
describe('linked pipelines', () => {
describe('loading state', () => {
it('should display loading state when loading', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(true);
expect(findPipelineMiniGraph().exists()).toBe(false);
});
});
describe('loaded state', () => {
beforeEach(async () => {
await createComponent();
});
it('should display the mini pipeine graph', () => {
expect(findMiniGraph().exists()).toBe(true);
it('should not display loading state after the query is resolved', async () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findPipelineMiniGraph().exists()).toBe(true);
});
it('should not display linked pipelines', () => {
expect(findUpstream().exists()).toBe(false);
expect(findDownstream().exists()).toBe(false);
it('should display the pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(true);
});
});
describe('when data is mismatched', () => {
beforeEach(async () => {
await createComponent({ props: { stages: [] } });
describe('load upstream/downstream', () => {
const samplePipeline = {
__typename: expect.any(String),
id: expect.any(String),
path: expect.any(String),
project: expect.any(Object),
detailedStatus: expect.any(Object),
};
it('formatted stages should be passed to the pipeline mini graph', async () => {
const stage = mockStages[0];
const expectedStages = [
{
name: stage.name,
status: {
__typename: 'DetailedStatus',
id: stage.status.id,
icon: stage.status.icon,
group: stage.status.group,
},
dropdown_path: stage.dropdown_path,
title: stage.title,
},
];
createComponent();
await waitForPromises();
expect(findPipelineMiniGraph().props('stages')).toEqual(expectedStages);
});
it('calls create flash with expected arguments', () => {
it('should render a downstream pipeline only', async () => {
createComponent(downstreamHandler);
await waitForPromises();
const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline');
expect(downstreamPipelines).toEqual(expect.any(Array));
expect(upstreamPipeline).toEqual(null);
});
it('should pass the pipeline path prop for the counter badge', async () => {
createComponent(downstreamHandler);
await waitForPromises();
const expectedPath = mockDownstreamQueryResponse.data.project.pipeline.path;
const pipelinePath = findPipelineMiniGraph().props('pipelinePath');
expect(pipelinePath).toBe(expectedPath);
});
it('should render an upstream pipeline only', async () => {
createComponent(upstreamHandler);
await waitForPromises();
const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline');
expect(upstreamPipeline).toEqual(samplePipeline);
expect(downstreamPipelines).toHaveLength(0);
});
it('should render downstream and upstream pipelines', async () => {
createComponent(upstreamDownstreamHandler);
await waitForPromises();
const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline');
expect(upstreamPipeline).toEqual(samplePipeline);
expect(downstreamPipelines).toEqual(expect.arrayContaining([samplePipeline]));
});
});
describe('error state', () => {
it('createFlash should show if there is an error fetching the data', async () => {
createComponent({ handler: failedHandler });
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'There was a problem handling the pipeline data.',
captureError: true,
error: new Error('Rest stages and graphQl stages must be the same length'),
message: 'There was a problem fetching linked pipelines.',
});
});
});
describe('polling', () => {
it('polling interval is set for linked pipelines', () => {
createComponent();
const expectedInterval = wrapper.vm.$apollo.queries.pipeline.options.pollInterval;
expect(expectedInterval).toBe(COMMIT_BOX_POLL_INTERVAL);
});
it('polling interval is set for pipeline stages', () => {
createComponent();
const expectedInterval = wrapper.vm.$apollo.queries.pipelineStages.options.pollInterval;
expect(expectedInterval).toBe(COMMIT_BOX_POLL_INTERVAL);
});
it('polls for stages and linked pipelines', async () => {
createComponent();
await waitForPromises();
expect(stagesHandler).toHaveBeenCalledTimes(1);
expect(downstreamHandler).toHaveBeenCalledTimes(1);
advanceToNextFetch();
await waitForPromises();
expect(stagesHandler).toHaveBeenCalledTimes(2);
expect(downstreamHandler).toHaveBeenCalledTimes(2);
advanceToNextFetch();
await waitForPromises();
expect(stagesHandler).toHaveBeenCalledTimes(3);
expect(downstreamHandler).toHaveBeenCalledTimes(3);
});
it('toggles query polling with visibility check', async () => {
jest.spyOn(graphQlUtils, 'toggleQueryPollingByVisibility');
createComponent();
await waitForPromises();
expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
wrapper.vm.$apollo.queries.pipelineStages,
);
expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
wrapper.vm.$apollo.queries.pipeline,
);
});
});
});

View file

@ -3,116 +3,21 @@ export const mockStages = [
name: 'build',
title: 'build: passed',
status: {
__typename: 'DetailedStatus',
id: 'success-409-409',
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#build',
details_path: '/root/ci-project/-/pipelines/318#build',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#build',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=build',
},
{
name: 'test',
title: 'test: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#test',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#test',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test',
},
{
name: 'test_two',
title: 'test_two: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#test_two',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#test_two',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test_two',
},
{
name: 'manual',
title: 'manual: skipped',
status: {
icon: 'status_skipped',
text: 'skipped',
label: 'skipped',
group: 'skipped',
tooltip: 'skipped',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#manual',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
action: {
icon: 'play',
title: 'Play all manual',
path: '/root/ci-project/-/pipelines/611/stages/manual/play_manual',
method: 'post',
button_title: 'Play all manual',
},
},
path: '/root/ci-project/-/pipelines/611#manual',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=manual',
},
{
name: 'deploy',
title: 'deploy: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#deploy',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#deploy',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=deploy',
},
{
name: 'qa',
title: 'qa: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#qa',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#qa',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=qa',
path: '/root/ci-project/-/pipelines/318#build',
dropdown_path: '/root/ci-project/-/pipelines/318/stage.json?stage=build',
},
];
@ -161,3 +66,109 @@ export const mockPipelineStatusResponse = {
},
},
};
export const mockDownstreamQueryResponse = {
data: {
project: {
id: '1',
pipeline: {
path: '/root/ci-project/-/pipelines/790',
id: 'pipeline-1',
downstream: {
nodes: [
{
id: 'gid://gitlab/Ci::Pipeline/612',
path: '/root/job-log-sections/-/pipelines/612',
project: { id: '1', name: 'job-log-sections', __typename: 'Project' },
detailedStatus: {
id: 'status-1',
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
__typename: 'Pipeline',
},
],
__typename: 'PipelineConnection',
},
upstream: null,
},
__typename: 'Project',
},
},
};
export const mockUpstreamDownstreamQueryResponse = {
data: {
project: {
id: '1',
pipeline: {
id: 'pipeline-1',
path: '/root/ci-project/-/pipelines/790',
downstream: {
nodes: [
{
id: 'gid://gitlab/Ci::Pipeline/612',
path: '/root/job-log-sections/-/pipelines/612',
project: { id: '1', name: 'job-log-sections', __typename: 'Project' },
detailedStatus: {
id: 'status-1',
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
__typename: 'Pipeline',
},
],
__typename: 'PipelineConnection',
},
upstream: {
id: 'gid://gitlab/Ci::Pipeline/610',
path: '/root/trigger-downstream/-/pipelines/610',
project: { id: '1', name: 'trigger-downstream', __typename: 'Project' },
detailedStatus: {
id: 'status-1',
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
__typename: 'Pipeline',
},
},
__typename: 'Project',
},
},
};
export const mockUpstreamQueryResponse = {
data: {
project: {
id: '1',
pipeline: {
id: 'pipeline-1',
path: '/root/ci-project/-/pipelines/790',
downstream: {
nodes: [],
__typename: 'PipelineConnection',
},
upstream: {
id: 'gid://gitlab/Ci::Pipeline/610',
path: '/root/trigger-downstream/-/pipelines/610',
project: { id: '1', name: 'trigger-downstream', __typename: 'Project' },
detailedStatus: {
id: 'status-1',
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
__typename: 'Pipeline',
},
},
__typename: 'Project',
},
},
};

View file

@ -1,12 +1,7 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
import {
REACHED_LIMIT_MESSAGE,
REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
} from '~/invite_members/constants';
import { REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE } from '~/invite_members/constants';
import { freeUsersLimit, membersCount } from '../mock_data/member_modal';
const WARNING_ALERT_TITLE = 'You only have space for 2 more members in name';
@ -52,22 +47,6 @@ describe('UserLimitNotification', () => {
});
});
describe('when close to limit within a personal namepace', () => {
beforeEach(() => {
createComponent(true, false, { membersCount: 3, userNamespace: true });
});
it('renders the limit for a personal namespace', () => {
const alert = findAlert();
expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE);
expect(alert.text()).toEqual(
'To make more space, you can remove members who no longer need access.',
);
});
});
describe('when close to limit within a group', () => {
it("renders user's limit notification", () => {
createComponent(true, false, { membersCount: 3 });
@ -91,19 +70,5 @@ describe('UserLimitNotification', () => {
expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for name");
expect(alert.text()).toEqual(REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE);
});
describe('when free user namespace', () => {
it("renders user's limit notification", () => {
createComponent(true, true, { userNamespace: true });
const alert = findAlert();
expect(alert.attributes('title')).toEqual(
"You've reached your 5 members limit for your personal projects",
);
expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE);
});
});
});
});

View file

@ -0,0 +1,109 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
Vue.use(VueApollo);
describe('Pipeline Status', () => {
let wrapper;
let mockApollo;
let mockLinkedPipelinesQuery;
const createComponent = ({ hasStages = true, options } = {}) => {
wrapper = shallowMount(PipelineEditorMiniGraph, {
provide: {
dataMethod: 'graphql',
projectFullPath: mockProjectFullPath,
},
propsData: {
pipeline: mockProjectPipeline({ hasStages }).pipeline,
},
...options,
});
};
const createComponentWithApollo = (hasStages = true) => {
const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]];
mockApollo = createMockApollo(handlers);
createComponent({
hasStages,
options: {
apolloProvider: mockApollo,
},
});
};
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
beforeEach(() => {
mockLinkedPipelinesQuery = jest.fn();
});
afterEach(() => {
mockLinkedPipelinesQuery.mockReset();
wrapper.destroy();
});
describe('when there are stages', () => {
beforeEach(() => {
createComponent();
});
it('renders pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(true);
});
});
describe('when there are no stages', () => {
beforeEach(() => {
createComponent({ hasStages: false });
});
it('does not render pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(false);
});
});
describe('when querying upstream and downstream pipelines', () => {
describe('when query succeeds', () => {
beforeEach(() => {
mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
createComponentWithApollo();
});
it('should call the query with the correct variables', () => {
expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1);
expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({
fullPath: mockProjectFullPath,
iid: mockProjectPipeline().pipeline.iid,
});
});
});
describe('when query fails', () => {
beforeEach(async () => {
mockLinkedPipelinesQuery.mockRejectedValue(new Error());
createComponentWithApollo();
await waitForPromises();
});
it('should emit an error event when query fails', async () => {
expect(wrapper.emitted('showError')).toHaveLength(1);
expect(wrapper.emitted('showError')[0]).toEqual([
{
type: PIPELINE_FAILURE,
reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError],
},
]);
});
});
});
});

View file

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';

View file

@ -0,0 +1,176 @@
import { mount } from '@vue/test-utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import LinkedPipelinesMiniList from '~/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue';
import mockData from './linked_pipelines_mock_data';
describe('Linked pipeline mini list', () => {
let wrapper;
const findCiIcon = () => wrapper.findComponent(CiIcon);
const findCiIcons = () => wrapper.findAllComponents(CiIcon);
const findLinkedPipelineCounter = () => wrapper.find('[data-testid="linked-pipeline-counter"]');
const findLinkedPipelineMiniItem = () =>
wrapper.find('[data-testid="linked-pipeline-mini-item"]');
const findLinkedPipelineMiniItems = () =>
wrapper.findAll('[data-testid="linked-pipeline-mini-item"]');
const findLinkedPipelineMiniList = () => wrapper.findComponent(LinkedPipelinesMiniList);
const createComponent = (props = {}) => {
wrapper = mount(LinkedPipelinesMiniList, {
directives: {
GlTooltip: createMockDirective(),
},
propsData: {
...props,
},
});
};
describe('when passed an upstream pipeline as prop', () => {
beforeEach(() => {
createComponent({
triggeredBy: [mockData.triggered_by],
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render one linked pipeline item', () => {
expect(findLinkedPipelineMiniItem().exists()).toBe(true);
});
it('should render a linked pipeline with the correct href', () => {
expect(findLinkedPipelineMiniItem().exists()).toBe(true);
expect(findLinkedPipelineMiniItem().attributes('href')).toBe(
'/gitlab-org/gitlab-foss/-/pipelines/129',
);
});
it('should render one ci status icon', () => {
expect(findCiIcon().exists()).toBe(true);
});
it('should render a borderless ci-icon', () => {
expect(findCiIcon().exists()).toBe(true);
expect(findCiIcon().props('isBorderless')).toBe(true);
expect(findCiIcon().classes('borderless')).toBe(true);
});
it('should render a ci-icon with a custom border class', () => {
expect(findCiIcon().exists()).toBe(true);
expect(findCiIcon().classes('gl-border')).toBe(true);
});
it('should render the correct ci status icon', () => {
expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
});
it('should have an activated tooltip', () => {
expect(findLinkedPipelineMiniItem().exists()).toBe(true);
const tooltip = getBinding(findLinkedPipelineMiniItem().element, 'gl-tooltip');
expect(tooltip.value.title).toBe('GitLabCE - running');
});
it('should correctly set is-upstream', () => {
expect(findLinkedPipelineMiniList().exists()).toBe(true);
expect(findLinkedPipelineMiniList().classes('is-upstream')).toBe(true);
});
it('should correctly compute shouldRenderCounter', () => {
expect(findLinkedPipelineMiniList().vm.shouldRenderCounter).toBe(false);
});
it('should not render the pipeline counter', () => {
expect(findLinkedPipelineCounter().exists()).toBe(false);
});
});
describe('when passed downstream pipelines as props', () => {
beforeEach(() => {
createComponent({
triggered: mockData.triggered,
pipelinePath: 'my/pipeline/path',
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render three linked pipeline items', () => {
expect(findLinkedPipelineMiniItems().exists()).toBe(true);
expect(findLinkedPipelineMiniItems().length).toBe(3);
});
it('should render three ci status icons', () => {
expect(findCiIcons().exists()).toBe(true);
expect(findCiIcons().length).toBe(3);
});
it('should render the correct ci status icon', () => {
expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
});
it('should have an activated tooltip', () => {
expect(findLinkedPipelineMiniItem().exists()).toBe(true);
const tooltip = getBinding(findLinkedPipelineMiniItem().element, 'gl-tooltip');
expect(tooltip.value.title).toBe('GitLabCE - running');
});
it('should correctly set is-downstream', () => {
expect(findLinkedPipelineMiniList().exists()).toBe(true);
expect(findLinkedPipelineMiniList().classes('is-downstream')).toBe(true);
});
it('should render a borderless ci-icon', () => {
expect(findCiIcon().exists()).toBe(true);
expect(findCiIcon().props('isBorderless')).toBe(true);
expect(findCiIcon().classes('borderless')).toBe(true);
});
it('should render a ci-icon with a custom border class', () => {
expect(findCiIcon().exists()).toBe(true);
expect(findCiIcon().classes('gl-border')).toBe(true);
});
it('should render the pipeline counter', () => {
expect(findLinkedPipelineCounter().exists()).toBe(true);
});
it('should correctly compute shouldRenderCounter', () => {
expect(findLinkedPipelineMiniList().vm.shouldRenderCounter).toBe(true);
});
it('should correctly trim linkedPipelines', () => {
expect(findLinkedPipelineMiniList().props('triggered').length).toBe(6);
expect(findLinkedPipelineMiniList().vm.linkedPipelinesTrimmed.length).toBe(3);
});
it('should set the correct pipeline path', () => {
expect(findLinkedPipelineCounter().exists()).toBe(true);
expect(findLinkedPipelineCounter().attributes('href')).toBe('my/pipeline/path');
});
it('should render the correct counterTooltipText', () => {
expect(findLinkedPipelineCounter().exists()).toBe(true);
const tooltip = getBinding(findLinkedPipelineCounter().element, 'gl-tooltip');
expect(tooltip.value.title).toBe(findLinkedPipelineMiniList().vm.counterTooltipText);
});
});
});

View file

@ -0,0 +1,407 @@
export default {
triggered_by: {
id: 129,
active: true,
path: '/gitlab-org/gitlab-foss/-/pipelines/129',
project: {
name: 'GitLabCE',
},
details: {
status: {
icon: 'status_running',
text: 'running',
label: 'running',
group: 'running',
has_details: true,
details_path: '/gitlab-org/gitlab-foss/-/pipelines/129',
favicon:
'/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
},
},
flags: {
latest: false,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: true,
cancelable: true,
},
ref: {
name: '7-5-stable',
path: '/gitlab-org/gitlab-foss/commits/7-5-stable',
tag: false,
branch: true,
},
commit: {
id: '23433d4d8b20d7e45c103d0b6048faad38a130ab',
short_id: '23433d4d',
title: 'Version 7.5.0.rc1',
created_at: '2014-11-17T15:44:14.000+01:00',
parent_ids: ['30ac909f30f58d319b42ed1537664483894b18cd'],
message: 'Version 7.5.0.rc1\n',
author_name: 'Jacob Vosmaer',
author_email: 'contact@jacobvosmaer.nl',
authored_date: '2014-11-17T15:44:14.000+01:00',
committer_name: 'Jacob Vosmaer',
committer_email: 'contact@jacobvosmaer.nl',
committed_date: '2014-11-17T15:44:14.000+01:00',
author_gravatar_url:
'http://www.gravatar.com/avatar/e66d11c0eedf8c07b3b18fca46599807?s=80&d=identicon',
commit_url:
'http://localhost:3000/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab',
commit_path: '/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab',
},
retry_path: '/gitlab-org/gitlab-foss/-/pipelines/129/retry',
cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/129/cancel',
created_at: '2017-05-24T14:46:20.090Z',
updated_at: '2017-05-24T14:46:29.906Z',
},
triggered: [
{
id: 132,
active: true,
path: '/gitlab-org/gitlab-foss/-/pipelines/132',
project: {
name: 'GitLabCE',
},
details: {
status: {
icon: 'status_running',
text: 'running',
label: 'running',
group: 'running',
has_details: true,
details_path: '/gitlab-org/gitlab-foss/-/pipelines/132',
favicon:
'/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
},
},
flags: {
latest: false,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: true,
cancelable: true,
},
ref: {
name: 'crowd',
path: '/gitlab-org/gitlab-foss/commits/crowd',
tag: false,
branch: true,
},
commit: {
id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
short_id: 'b9d58c4c',
title: 'getting user keys publically through http without any authentication, the github…',
created_at: '2013-10-03T12:50:33.000+05:30',
parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
message:
'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n',
author_name: 'devaroop',
author_email: 'devaroop123@yahoo.co.in',
authored_date: '2013-10-02T20:39:29.000+05:30',
committer_name: 'devaroop',
committer_email: 'devaroop123@yahoo.co.in',
committed_date: '2013-10-03T12:50:33.000+05:30',
author_gravatar_url:
'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
commit_url:
'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
},
retry_path: '/gitlab-org/gitlab-foss/-/pipelines/132/retry',
cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/132/cancel',
created_at: '2017-05-24T14:46:24.644Z',
updated_at: '2017-05-24T14:48:55.226Z',
},
{
id: 133,
active: true,
path: '/gitlab-org/gitlab-foss/-/pipelines/133',
project: {
name: 'GitLabCE',
},
details: {
status: {
icon: 'status_running',
text: 'running',
label: 'running',
group: 'running',
has_details: true,
details_path: '/gitlab-org/gitlab-foss/-/pipelines/133',
favicon:
'/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
},
},
flags: {
latest: false,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: true,
cancelable: true,
},
ref: {
name: 'crowd',
path: '/gitlab-org/gitlab-foss/commits/crowd',
tag: false,
branch: true,
},
commit: {
id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b',
short_id: 'b6bd4856',
title: 'getting user keys publically through http without any authentication, the github…',
created_at: '2013-10-02T20:39:29.000+05:30',
parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
message:
'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n',
author_name: 'devaroop',
author_email: 'devaroop123@yahoo.co.in',
authored_date: '2013-10-02T20:39:29.000+05:30',
committer_name: 'devaroop',
committer_email: 'devaroop123@yahoo.co.in',
committed_date: '2013-10-02T20:39:29.000+05:30',
author_gravatar_url:
'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
commit_url:
'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
},
retry_path: '/gitlab-org/gitlab-foss/-/pipelines/133/retry',
cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/133/cancel',
created_at: '2017-05-24T14:46:24.648Z',
updated_at: '2017-05-24T14:48:59.673Z',
},
{
id: 130,
active: true,
path: '/gitlab-org/gitlab-foss/-/pipelines/130',
project: {
name: 'GitLabCE',
},
details: {
status: {
icon: 'status_running',
text: 'running',
label: 'running',
group: 'running',
has_details: true,
details_path: '/gitlab-org/gitlab-foss/-/pipelines/130',
favicon:
'/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
},
},
flags: {
latest: false,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: true,
cancelable: true,
},
ref: {
name: 'crowd',
path: '/gitlab-org/gitlab-foss/commits/crowd',
tag: false,
branch: true,
},
commit: {
id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f',
short_id: '6d7ced4a',
title: 'Whitespace fixes to patch',
created_at: '2013-10-08T13:53:22.000-05:00',
parent_ids: ['1875141a963a4238bda29011d8f7105839485253'],
message: 'Whitespace fixes to patch\n',
author_name: 'Dale Hamel',
author_email: 'dale.hamel@srvthe.net',
authored_date: '2013-10-08T13:53:22.000-05:00',
committer_name: 'Dale Hamel',
committer_email: 'dale.hamel@invenia.ca',
committed_date: '2013-10-08T13:53:22.000-05:00',
author_gravatar_url:
'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon',
commit_url:
'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
},
retry_path: '/gitlab-org/gitlab-foss/-/pipelines/130/retry',
cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/130/cancel',
created_at: '2017-05-24T14:46:24.630Z',
updated_at: '2017-05-24T14:49:45.091Z',
},
{
id: 131,
active: true,
path: '/gitlab-org/gitlab-foss/-/pipelines/132',
project: {
name: 'GitLabCE',
},
details: {
status: {
icon: 'status_running',
text: 'running',
label: 'running',
group: 'running',
has_details: true,
details_path: '/gitlab-org/gitlab-foss/-/pipelines/132',
favicon:
'/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
},
},
flags: {
latest: false,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: true,
cancelable: true,
},
ref: {
name: 'crowd',
path: '/gitlab-org/gitlab-foss/commits/crowd',
tag: false,
branch: true,
},
commit: {
id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
short_id: 'b9d58c4c',
title: 'getting user keys publically through http without any authentication, the github…',
created_at: '2013-10-03T12:50:33.000+05:30',
parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
message:
'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n',
author_name: 'devaroop',
author_email: 'devaroop123@yahoo.co.in',
authored_date: '2013-10-02T20:39:29.000+05:30',
committer_name: 'devaroop',
committer_email: 'devaroop123@yahoo.co.in',
committed_date: '2013-10-03T12:50:33.000+05:30',
author_gravatar_url:
'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
commit_url:
'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e',
},
retry_path: '/gitlab-org/gitlab-foss/-/pipelines/132/retry',
cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/132/cancel',
created_at: '2017-05-24T14:46:24.644Z',
updated_at: '2017-05-24T14:48:55.226Z',
},
{
id: 134,
active: true,
path: '/gitlab-org/gitlab-foss/-/pipelines/133',
project: {
name: 'GitLabCE',
},
details: {
status: {
icon: 'status_running',
text: 'running',
label: 'running',
group: 'running',
has_details: true,
details_path: '/gitlab-org/gitlab-foss/-/pipelines/133',
favicon:
'/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
},
},
flags: {
latest: false,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: true,
cancelable: true,
},
ref: {
name: 'crowd',
path: '/gitlab-org/gitlab-foss/commits/crowd',
tag: false,
branch: true,
},
commit: {
id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b',
short_id: 'b6bd4856',
title: 'getting user keys publically through http without any authentication, the github…',
created_at: '2013-10-02T20:39:29.000+05:30',
parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'],
message:
'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n',
author_name: 'devaroop',
author_email: 'devaroop123@yahoo.co.in',
authored_date: '2013-10-02T20:39:29.000+05:30',
committer_name: 'devaroop',
committer_email: 'devaroop123@yahoo.co.in',
committed_date: '2013-10-02T20:39:29.000+05:30',
author_gravatar_url:
'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon',
commit_url:
'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b',
},
retry_path: '/gitlab-org/gitlab-foss/-/pipelines/133/retry',
cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/133/cancel',
created_at: '2017-05-24T14:46:24.648Z',
updated_at: '2017-05-24T14:48:59.673Z',
},
{
id: 135,
active: true,
path: '/gitlab-org/gitlab-foss/-/pipelines/130',
project: {
name: 'GitLabCE',
},
details: {
status: {
icon: 'status_running',
text: 'running',
label: 'running',
group: 'running',
has_details: true,
details_path: '/gitlab-org/gitlab-foss/-/pipelines/130',
favicon:
'/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
},
},
flags: {
latest: false,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: true,
cancelable: true,
},
ref: {
name: 'crowd',
path: '/gitlab-org/gitlab-foss/commits/crowd',
tag: false,
branch: true,
},
commit: {
id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f',
short_id: '6d7ced4a',
title: 'Whitespace fixes to patch',
created_at: '2013-10-08T13:53:22.000-05:00',
parent_ids: ['1875141a963a4238bda29011d8f7105839485253'],
message: 'Whitespace fixes to patch\n',
author_name: 'Dale Hamel',
author_email: 'dale.hamel@srvthe.net',
authored_date: '2013-10-08T13:53:22.000-05:00',
committer_name: 'Dale Hamel',
committer_email: 'dale.hamel@invenia.ca',
committed_date: '2013-10-08T13:53:22.000-05:00',
author_gravatar_url:
'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon',
commit_url:
'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f',
},
retry_path: '/gitlab-org/gitlab-foss/-/pipelines/130/retry',
cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/130/cancel',
created_at: '2017-05-24T14:46:24.630Z',
updated_at: '2017-05-24T14:49:45.091Z',
},
],
};

View file

@ -0,0 +1,149 @@
import { mount } from '@vue/test-utils';
import { pipelines } from 'test_fixtures/pipelines/pipelines.json';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue';
import mockLinkedPipelines from './linked_pipelines_mock_data';
const mockStages = pipelines[0].details.stages;
describe('Pipeline Mini Graph', () => {
let wrapper;
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findPipelineStages = () => wrapper.findComponent(PipelineStages);
const findLinkedPipelineUpstream = () =>
wrapper.findComponent('[data-testid="pipeline-mini-graph-upstream"]');
const findLinkedPipelineDownstream = () =>
wrapper.findComponent('[data-testid="pipeline-mini-graph-downstream"]');
const findDownstreamArrowIcon = () => wrapper.find('[data-testid="downstream-arrow-icon"]');
const findUpstreamArrowIcon = () => wrapper.find('[data-testid="upstream-arrow-icon"]');
const createComponent = (props = {}) => {
wrapper = mount(PipelineMiniGraph, {
propsData: {
stages: mockStages,
...props,
},
});
};
describe('rendered state without upstream or downstream pipelines', () => {
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render the pipeline stages', () => {
expect(findPipelineStages().exists()).toBe(true);
});
it('should have the correct props', () => {
expect(findPipelineMiniGraph().props()).toMatchObject({
downstreamPipelines: [],
isMergeTrain: false,
pipelinePath: '',
stages: expect.any(Array),
stagesClass: '',
updateDropdown: false,
upstreamPipeline: undefined,
});
});
it('should have no linked pipelines', () => {
expect(findLinkedPipelineDownstream().exists()).toBe(false);
expect(findLinkedPipelineUpstream().exists()).toBe(false);
});
it('should not render arrow icons', () => {
expect(findUpstreamArrowIcon().exists()).toBe(false);
expect(findDownstreamArrowIcon().exists()).toBe(false);
});
it('triggers events in "action request complete"', () => {
createComponent();
findPipelineMiniGraph(0).vm.$emit('pipelineActionRequestComplete');
findPipelineMiniGraph(1).vm.$emit('pipelineActionRequestComplete');
expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
});
});
describe('rendered state with upstream pipeline', () => {
beforeEach(() => {
createComponent({
upstreamPipeline: mockLinkedPipelines.triggered_by,
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should have the correct props', () => {
expect(findPipelineMiniGraph().props()).toMatchObject({
downstreamPipelines: [],
isMergeTrain: false,
pipelinePath: '',
stages: expect.any(Array),
stagesClass: '',
updateDropdown: false,
upstreamPipeline: expect.any(Object),
});
});
it('should render the upstream linked pipelines mini list only', () => {
expect(findLinkedPipelineUpstream().exists()).toBe(true);
expect(findLinkedPipelineDownstream().exists()).toBe(false);
});
it('should render an upstream arrow icon only', () => {
expect(findDownstreamArrowIcon().exists()).toBe(false);
expect(findUpstreamArrowIcon().exists()).toBe(true);
expect(findUpstreamArrowIcon().props('name')).toBe('long-arrow');
});
});
describe('rendered state with downstream pipelines', () => {
beforeEach(() => {
createComponent({
downstreamPipelines: mockLinkedPipelines.triggered,
pipelinePath: 'my/pipeline/path',
});
});
it('should have the correct props', () => {
expect(findPipelineMiniGraph().props()).toMatchObject({
downstreamPipelines: expect.any(Array),
isMergeTrain: false,
pipelinePath: 'my/pipeline/path',
stages: expect.any(Array),
stagesClass: '',
updateDropdown: false,
upstreamPipeline: undefined,
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render the downstream linked pipelines mini list only', () => {
expect(findLinkedPipelineDownstream().exists()).toBe(true);
expect(findLinkedPipelineUpstream().exists()).toBe(false);
});
it('should render a downstream arrow icon only', () => {
expect(findUpstreamArrowIcon().exists()).toBe(false);
expect(findDownstreamArrowIcon().exists()).toBe(true);
expect(findDownstreamArrowIcon().props('name')).toBe('long-arrow');
});
});
});

View file

@ -4,7 +4,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
import eventHub from '~/pipelines/event_hub';
import waitForPromises from 'helpers/wait_for_promises';
import { stageReply } from '../../mock_data';

View file

@ -1,18 +1,18 @@
import { shallowMount } from '@vue/test-utils';
import { pipelines } from 'test_fixtures/pipelines/pipelines.json';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue';
const mockStages = pipelines[0].details.stages;
describe('Pipeline Mini Graph', () => {
describe('Pipeline Stages', () => {
let wrapper;
const findPipelineStages = () => wrapper.findAll(PipelineStage);
const findPipelineStagesAt = (i) => findPipelineStages().at(i);
const createComponent = (props = {}) => {
wrapper = shallowMount(PipelineMiniGraph, {
wrapper = shallowMount(PipelineStages, {
propsData: {
stages: mockStages,
...props,

View file

@ -3,7 +3,7 @@ import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
@ -113,40 +113,28 @@ describe('Pipelines Table', () => {
});
describe('stages cell', () => {
it('should render a pipeline mini graph', () => {
it('should render pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(true);
});
it('should render the right number of stages', () => {
const stagesLength = pipeline.details.stages.length;
expect(
findPipelineMiniGraph().findAll('[data-testid="mini-pipeline-graph-dropdown"]'),
).toHaveLength(stagesLength);
expect(findPipelineMiniGraph().props('stages').length).toBe(stagesLength);
});
describe('when pipeline does not have stages', () => {
beforeEach(() => {
pipeline = createMockPipeline();
pipeline.details.stages = null;
pipeline.details.stages = [];
createComponent({ pipelines: [pipeline] });
});
it('stages are not rendered', () => {
expect(findPipelineMiniGraph().exists()).toBe(false);
expect(findPipelineMiniGraph().props('stages')).toHaveLength(0);
});
});
it('should not update dropdown', () => {
expect(findPipelineMiniGraph().props('updateDropdown')).toBe(false);
});
it('when update graph dropdown is set, should update graph dropdown', () => {
createComponent({ pipelines: [pipeline], updateGraphDropdown: true });
expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true);
});
it('when action request is complete, should refresh table', () => {
findPipelineMiniGraph().vm.$emit('pipelineActionRequestComplete');

View file

@ -4,9 +4,8 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import MRWidgetPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { SUCCESS } from '~/vue_merge_request_widget/constants';
import mockData from '../mock_data';
@ -30,14 +29,13 @@ describe('MRWidgetPipeline', () => {
const findPipelineInfoContainer = () => wrapper.findByTestId('pipeline-info-container');
const findCommitLink = () => wrapper.findByTestId('commit-link');
const findPipelineFinishedAt = () => wrapper.findByTestId('finished-at');
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findAllPipelineStages = () => wrapper.findAllComponents(PipelineStage);
const findPipelineCoverage = () => wrapper.findByTestId('pipeline-coverage');
const findPipelineCoverageDelta = () => wrapper.findByTestId('pipeline-coverage-delta');
const findPipelineCoverageTooltipText = () =>
wrapper.findByTestId('pipeline-coverage-tooltip').text();
const findPipelineCoverageDeltaTooltipText = () =>
wrapper.findByTestId('pipeline-coverage-delta-tooltip').text();
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findMonitoringPipelineMessage = () => wrapper.findByTestId('monitoring-pipeline-message');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@ -45,7 +43,7 @@ describe('MRWidgetPipeline', () => {
const createWrapper = (props = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
mountFn(PipelineComponent, {
mountFn(MRWidgetPipelineComponent, {
propsData: {
...defaultProps,
...props,
@ -106,8 +104,10 @@ describe('MRWidgetPipeline', () => {
});
it('should render pipeline graph', () => {
const stagesCount = mockData.pipeline.details.stages.length;
expect(findPipelineMiniGraph().exists()).toBe(true);
expect(findAllPipelineStages()).toHaveLength(mockData.pipeline.details.stages.length);
expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount);
});
describe('should render pipeline coverage information', () => {
@ -176,15 +176,11 @@ describe('MRWidgetPipeline', () => {
expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
});
it('should render pipeline graph with correct styles', () => {
it('should render pipeline graph', () => {
const stagesCount = mockData.pipeline.details.stages.length;
expect(findPipelineMiniGraph().exists()).toBe(true);
expect(findPipelineMiniGraph().findAll('.mr-widget-pipeline-stages')).toHaveLength(
stagesCount,
);
expect(findAllPipelineStages()).toHaveLength(stagesCount);
expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount);
});
it('should render coverage information', () => {

View file

@ -48,7 +48,8 @@ describe('MR Widget', () => {
const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
createComponent({ propsData: { fetchCollapsedData } });
await waitForPromises();
expect(wrapper.vm.error).toBe('Failed to load');
expect(wrapper.findByText('Failed to load').exists()).toBe(true);
expect(findStatusIcon().props()).toMatchObject({ iconName: 'failed', isLoading: false });
});
it('displays loading icon until request is made and then displays status icon when the request is complete', async () => {

View file

@ -155,7 +155,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }],
only: { refs: %w(branches tags) },
job_variables: { 'VAR' => { value: 'job' } },
job_variables: { 'VAR' => 'job' },
root_variables_inheritance: true,
after_script: [],
ignore: false,
@ -215,7 +215,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
job_variables: { 'VAR' => { value: 'job' } },
job_variables: { 'VAR' => 'job' },
root_variables_inheritance: true,
ignore: false,
after_script: ['make clean'],

View file

@ -97,15 +97,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:attributes) do
{ name: 'rspec',
ref: 'master',
job_variables: [{ key: 'VAR1', value: 'var 1' },
{ key: 'VAR2', value: 'var 2' }],
job_variables: [{ key: 'VAR1', value: 'var 1', public: true },
{ key: 'VAR2', value: 'var 2', public: true }],
rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] }
end
it do
is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' },
{ key: 'VAR3', value: 'var 3' },
{ key: 'VAR2', value: 'var 2' }])
is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true },
{ key: 'VAR2', value: 'var 2', public: true },
{ key: 'VAR3', value: 'var 3', public: true }])
end
end
@ -114,13 +114,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
{
name: 'rspec',
ref: 'master',
job_variables: [{ key: 'VARIABLE', value: 'value' }],
job_variables: [{ key: 'VARIABLE', value: 'value', public: true }],
tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE']
}
end
it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) }
it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) }
it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value', public: true }]) }
end
context 'with cache:key' do
@ -257,19 +257,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:attributes) do
{ name: 'rspec',
ref: 'master',
yaml_variables: [{ key: 'VAR2', value: 'var 2' },
{ key: 'VAR3', value: 'var 3' }],
job_variables: [{ key: 'VAR2', value: 'var 2' },
{ key: 'VAR3', value: 'var 3' }],
yaml_variables: [{ key: 'VAR2', value: 'var 2', public: true },
{ key: 'VAR3', value: 'var 3', public: true }],
job_variables: [{ key: 'VAR2', value: 'var 2', public: true },
{ key: 'VAR3', value: 'var 3', public: true }],
root_variables_inheritance: root_variables_inheritance }
end
context 'when the pipeline has variables' do
let(:root_variables) do
[{ key: 'VAR1', value: 'var overridden pipeline 1' },
{ key: 'VAR2', value: 'var pipeline 2' },
{ key: 'VAR3', value: 'var pipeline 3' },
{ key: 'VAR4', value: 'new var pipeline 4' }]
[{ key: 'VAR1', value: 'var overridden pipeline 1', public: true },
{ key: 'VAR2', value: 'var pipeline 2', public: true },
{ key: 'VAR3', value: 'var pipeline 3', public: true },
{ key: 'VAR4', value: 'new var pipeline 4', public: true }]
end
context 'when root_variables_inheritance is true' do
@ -277,10 +277,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it 'returns calculated yaml variables' do
expect(subject[:yaml_variables]).to match_array(
[{ key: 'VAR1', value: 'var overridden pipeline 1' },
{ key: 'VAR2', value: 'var 2' },
{ key: 'VAR3', value: 'var 3' },
{ key: 'VAR4', value: 'new var pipeline 4' }]
[{ key: 'VAR1', value: 'var overridden pipeline 1', public: true },
{ key: 'VAR2', value: 'var 2', public: true },
{ key: 'VAR3', value: 'var 3', public: true },
{ key: 'VAR4', value: 'new var pipeline 4', public: true }]
)
end
end
@ -290,8 +290,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it 'returns job variables' do
expect(subject[:yaml_variables]).to match_array(
[{ key: 'VAR2', value: 'var 2' },
{ key: 'VAR3', value: 'var 3' }]
[{ key: 'VAR2', value: 'var 2', public: true },
{ key: 'VAR3', value: 'var 3', public: true }]
)
end
end
@ -301,9 +301,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it 'returns calculated yaml variables' do
expect(subject[:yaml_variables]).to match_array(
[{ key: 'VAR1', value: 'var overridden pipeline 1' },
{ key: 'VAR2', value: 'var 2' },
{ key: 'VAR3', value: 'var 3' }]
[{ key: 'VAR1', value: 'var overridden pipeline 1', public: true },
{ key: 'VAR2', value: 'var 2', public: true },
{ key: 'VAR3', value: 'var 3', public: true }]
)
end
end
@ -314,8 +314,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it 'returns seed yaml variables' do
expect(subject[:yaml_variables]).to match_array(
[{ key: 'VAR2', value: 'var 2' },
{ key: 'VAR3', value: 'var 3' }])
[{ key: 'VAR2', value: 'var 2', public: true },
{ key: 'VAR3', value: 'var 3', public: true }])
end
end
end
@ -324,8 +324,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:attributes) do
{ name: 'rspec',
ref: 'master',
yaml_variables: [{ key: 'VAR1', value: 'var 1' }],
job_variables: [{ key: 'VAR1', value: 'var 1' }],
yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }],
job_variables: [{ key: 'VAR1', value: 'var 1', public: true }],
root_variables_inheritance: root_variables_inheritance,
rules: rules }
end
@ -338,14 +338,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
it 'recalculates the variables' do
expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' },
{ key: 'VAR2', value: 'new var 2' })
expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true },
{ key: 'VAR2', value: 'new var 2', public: true })
end
end
context 'when the rules use root variables' do
let(:root_variables) do
[{ key: 'VAR2', value: 'var pipeline 2' }]
[{ key: 'VAR2', value: 'var pipeline 2', public: true }]
end
let(:rules) do
@ -353,15 +353,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
it 'recalculates the variables' do
expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' },
{ key: 'VAR2', value: 'overridden var 2' })
expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true },
{ key: 'VAR2', value: 'overridden var 2', public: true })
end
context 'when the root_variables_inheritance is false' do
let(:root_variables_inheritance) { false }
it 'does not recalculate the variables' do
expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' })
expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1', public: true })
end
end
end

View file

@ -15,27 +15,21 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do
end
let(:result) do
[{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value22' },
{ key: 'key3', value: 'value3' }]
[{ key: 'key1', value: 'value1', public: true },
{ key: 'key2', value: 'value22', public: true },
{ key: 'key3', value: 'value3', public: true }]
end
subject { described_class.merge_variables(current_variables, new_variables) }
it { is_expected.to match_array(result) }
it { is_expected.to eq(result) }
context 'when new variables is a hash' do
let(:new_variables) do
{ 'key2' => 'value22', 'key3' => 'value3' }
end
let(:result) do
[{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value22' },
{ key: 'key3', value: 'value3' }]
end
it { is_expected.to match_array(result) }
it { is_expected.to eq(result) }
end
context 'when new variables is a hash with symbol keys' do
@ -43,72 +37,79 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do
{ key2: 'value22', key3: 'value3' }
end
let(:result) do
[{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value22' },
{ key: 'key3', value: 'value3' }]
end
it { is_expected.to match_array(result) }
it { is_expected.to eq(result) }
end
context 'when new variables is nil' do
let(:new_variables) {}
let(:result) do
[{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2' }]
[{ key: 'key1', value: 'value1', public: true },
{ key: 'key2', value: 'value2', public: true }]
end
it { is_expected.to match_array(result) }
it { is_expected.to eq(result) }
end
end
describe '.transform_to_array' do
subject { described_class.transform_to_array(variables) }
context 'when values are strings' do
let(:variables) do
{ 'key1' => 'value1', 'key2' => 'value2' }
end
let(:result) do
[{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2' }]
end
it { is_expected.to match_array(result) }
describe '.transform_to_yaml_variables' do
let(:variables) do
{ 'key1' => 'value1', 'key2' => 'value2' }
end
let(:result) do
[{ key: 'key1', value: 'value1', public: true },
{ key: 'key2', value: 'value2', public: true }]
end
subject { described_class.transform_to_yaml_variables(variables) }
it { is_expected.to eq(result) }
context 'when variables is nil' do
let(:variables) {}
it { is_expected.to match_array([]) }
it { is_expected.to eq([]) }
end
end
describe '.transform_from_yaml_variables' do
let(:variables) do
[{ key: 'key1', value: 'value1', public: true },
{ key: 'key2', value: 'value2', public: true }]
end
context 'when values are hashes' do
let(:result) do
{ 'key1' => 'value1', 'key2' => 'value2' }
end
subject { described_class.transform_from_yaml_variables(variables) }
it { is_expected.to eq(result) }
context 'when variables is nil' do
let(:variables) {}
it { is_expected.to eq({}) }
end
context 'when variables is a hash' do
let(:variables) do
{ 'key1' => { value: 'value1', description: 'var 1' }, 'key2' => { value: 'value2' } }
{ key1: 'value1', 'key2' => 'value2' }
end
let(:result) do
[{ key: 'key1', value: 'value1', description: 'var 1' },
{ key: 'key2', value: 'value2' }]
it { is_expected.to eq(result) }
end
context 'when variables contain integers and symbols' do
let(:variables) do
{ key1: 1, key2: :value2 }
end
it { is_expected.to match_array(result) }
context 'when a value data has `key` as a key' do
let(:variables) do
{ 'key1' => { value: 'value1', key: 'new_key1' }, 'key2' => { value: 'value2' } }
end
let(:result) do
[{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2' }]
end
it { is_expected.to match_array(result) }
let(:result1) do
{ 'key1' => '1', 'key2' => 'value2' }
end
it { is_expected.to eq(result1) }
end
end
@ -126,35 +127,35 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do
let(:inheritance) { true }
let(:result) do
[{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value22' },
{ key: 'key3', value: 'value3' }]
[{ key: 'key1', value: 'value1', public: true },
{ key: 'key2', value: 'value22', public: true },
{ key: 'key3', value: 'value3', public: true }]
end
subject { described_class.inherit_yaml_variables(from: from, to: to, inheritance: inheritance) }
it { is_expected.to match_array(result) }
it { is_expected.to eq(result) }
context 'when inheritance is false' do
let(:inheritance) { false }
let(:result) do
[{ key: 'key2', value: 'value22' },
{ key: 'key3', value: 'value3' }]
[{ key: 'key2', value: 'value22', public: true },
{ key: 'key3', value: 'value3', public: true }]
end
it { is_expected.to match_array(result) }
it { is_expected.to eq(result) }
end
context 'when inheritance is array' do
let(:inheritance) { ['key2'] }
let(:result) do
[{ key: 'key2', value: 'value22' },
{ key: 'key3', value: 'value3' }]
[{ key: 'key2', value: 'value22', public: true },
{ key: 'key3', value: 'value3', public: true }]
end
it { is_expected.to match_array(result) }
it { is_expected.to eq(result) }
end
end
end

View file

@ -72,8 +72,8 @@ module Gitlab
it 'returns calculated variables with root and job variables' do
is_expected.to match_array([
{ key: 'VAR1', value: 'value 11' },
{ key: 'VAR2', value: 'value 2' }
{ key: 'VAR1', value: 'value 11', public: true },
{ key: 'VAR2', value: 'value 2', public: true }
])
end

View file

@ -448,7 +448,7 @@ module Gitlab
it 'parses the root:variables as #root_variables' do
expect(subject.root_variables)
.to contain_exactly({ key: 'SUPPORTED', value: 'parsed' })
.to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true })
end
end
@ -490,7 +490,7 @@ module Gitlab
it 'parses the root:variables as #root_variables' do
expect(subject.root_variables)
.to contain_exactly({ key: 'SUPPORTED', value: 'parsed' })
.to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true })
end
end
@ -1098,8 +1098,8 @@ module Gitlab
it 'returns job variables' do
expect(job_variables).to contain_exactly(
{ key: 'VAR1', value: 'value1' },
{ key: 'VAR2', value: 'value2' }
{ key: 'VAR1', value: 'value1', public: true },
{ key: 'VAR2', value: 'value2', public: true }
)
expect(root_variables_inheritance).to eq(true)
end
@ -1203,21 +1203,21 @@ module Gitlab
expect(config_processor.builds[0]).to include(
name: 'test1',
options: { script: ['test'] },
job_variables: [{ key: 'VAR1', value: 'test1 var 1' },
{ key: 'VAR2', value: 'test2 var 2' }]
job_variables: [{ key: 'VAR1', value: 'test1 var 1', public: true },
{ key: 'VAR2', value: 'test2 var 2', public: true }]
)
expect(config_processor.builds[1]).to include(
name: 'test2',
options: { script: ['test'] },
job_variables: [{ key: 'VAR1', value: 'base var 1' },
{ key: 'VAR2', value: 'test2 var 2' }]
job_variables: [{ key: 'VAR1', value: 'base var 1', public: true },
{ key: 'VAR2', value: 'test2 var 2', public: true }]
)
expect(config_processor.builds[2]).to include(
name: 'test3',
options: { script: ['test'] },
job_variables: [{ key: 'VAR1', value: 'base var 1' }]
job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }]
)
expect(config_processor.builds[3]).to include(

View file

@ -36,7 +36,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
expect(pipeline.statuses).to match_array [test, bridge]
expect(bridge.options).to eq(expected_bridge_options)
expect(bridge.yaml_variables)
.to include(key: 'CROSS', value: 'downstream')
.to include(key: 'CROSS', value: 'downstream', public: true)
end
end

View file

@ -40,8 +40,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac
it 'returns variable list' do
expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
expect(subject['KEY2']).to eq({ value: 'val 2', description: '' })
expect(subject['KEY3']).to eq({ value: 'val 3' })
expect(subject['KEY4']).to eq({ value: 'val 4' })
expect(subject['KEY3']).to eq({ value: 'val 3', description: nil })
expect(subject['KEY4']).to eq({ value: 'val 4', description: nil })
end
end