Commit 75d94c4c authored by Paul Slaughter's avatar Paul Slaughter

Redesign of MR header sections (CE Port)

- https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/8593
- Create mr-widget-icon to encapsulate circle icons
- Decouple workflow L&F from mr-widget-heading
- Decouple container L&F from mr-widget-heading
parent 77c26777
...@@ -112,7 +112,7 @@ export default { ...@@ -112,7 +112,7 @@ export default {
</script> </script>
<template> <template>
<div class="mr-widget-heading deploy-heading append-bottom-default"> <div class="deploy-heading">
<div class="ci-widget media"> <div class="ci-widget media">
<div class="media-body"> <div class="media-body">
<div class="deploy-body"> <div class="deploy-body">
......
<template>
<div class="mr-widget-heading">
<div class="mr-widget-content"><slot name="default"></slot></div>
<slot name="footer"></slot>
</div>
</template>
...@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import MrWidgetIcon from './mr_widget_icon.vue';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
Icon, Icon,
clipboardButton, clipboardButton,
TooltipOnTruncate, TooltipOnTruncate,
MrWidgetIcon,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -76,7 +78,7 @@ export default { ...@@ -76,7 +78,7 @@ export default {
</script> </script>
<template> <template>
<div class="mr-source-target append-bottom-default"> <div class="mr-source-target append-bottom-default">
<div class="git-merge-icon-container append-right-default"><icon name="git-merge" /></div> <mr-widget-icon name="git-merge" />
<div class="git-merge-container d-flex"> <div class="git-merge-container d-flex">
<div class="normal"> <div class="normal">
<strong> <strong>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: { Icon },
props: {
name: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="circle-icon-container append-right-default"><icon :name="name" /></div>
</template>
...@@ -79,67 +79,65 @@ export default { ...@@ -79,67 +79,65 @@ export default {
</script> </script>
<template> <template>
<div v-if="hasPipeline || hasCIError" class="mr-widget-heading append-bottom-default"> <div v-if="hasPipeline || hasCIError" class="ci-widget media">
<div class="ci-widget media"> <template v-if="hasCIError">
<template v-if="hasCIError"> <div
<div class="add-border ci-status-icon ci-status-icon-failed ci-error
class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default"
js-ci-error append-right-default" >
> <icon :size="32" name="status_failed_borderless" />
<icon :size="32" name="status_failed_borderless" /> </div>
</div> <div class="media-body" v-html="errorText"></div>
<div class="media-body" v-html="errorText"></div> </template>
</template> <template v-else-if="hasPipeline">
<template v-else-if="hasPipeline"> <a :href="status.details_path" class="align-self-start append-right-default">
<a :href="status.details_path" class="align-self-start append-right-default"> <ci-icon :status="status" :size="32" :borderless="true" class="add-border" />
<ci-icon :status="status" :size="32" :borderless="true" class="add-border" /> </a>
</a> <div class="ci-widget-container d-flex">
<div class="ci-widget-container d-flex"> <div class="ci-widget-content">
<div class="ci-widget-content"> <div class="media-body">
<div class="media-body"> <div class="font-weight-bold">
<div class="font-weight-bold"> Pipeline
Pipeline <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
<a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" >#{{ pipeline.id }}</a
>#{{ pipeline.id }}</a >
>
{{ pipeline.details.status.label }} {{ pipeline.details.status.label }}
<template v-if="hasCommitInfo"> <template v-if="hasCommitInfo">
for for
<a <a
:href="pipeline.commit.commit_path" :href="pipeline.commit.commit_path"
class="commit-sha js-commit-link font-weight-normal" class="commit-sha js-commit-link font-weight-normal"
> >
{{ pipeline.commit.short_id }}</a {{ pipeline.commit.short_id }}</a
> >
on on
<tooltip-on-truncate <tooltip-on-truncate
:title="sourceBranch" :title="sourceBranch"
truncate-target="child" truncate-target="child"
class="label-branch label-truncate" class="label-branch label-truncate"
v-html="sourceBranchLink" v-html="sourceBranchLink"
/> />
</template> </template>
</div>
<div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div>
</div> </div>
<div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div>
</div> </div>
<div> </div>
<span class="mr-widget-pipeline-graph"> <div>
<span v-if="hasStages" class="stage-cell"> <span class="mr-widget-pipeline-graph">
<div <span v-if="hasStages" class="stage-cell">
v-for="(stage, i) in pipeline.details.stages" <div
:key="i" v-for="(stage, i) in pipeline.details.stages"
class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages" :key="i"
> class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
<pipeline-stage :stage="stage" /> >
</div> <pipeline-stage :stage="stage" />
</span> </div>
</span> </span>
</div> </span>
</div> </div>
</template> </div>
</div> </template>
</div> </div>
</template> </template>
<script>
import Deployment from './deployment.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
/**
* Renders the pipeline and related deployments from the store.
*
* | Props | Description
* |---------------|-------------
* | `mr` | This is the mr_widget store
* | `isPostMerge` | If true, show the "post merge" pipeline and deployments
*/
export default {
name: 'MrWidgetPipelineContainer',
components: {
Deployment,
MrWidgetContainer,
MrWidgetPipeline,
},
props: {
mr: {
type: Object,
required: true,
},
isPostMerge: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
pipeline() {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
branch() {
return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranch;
},
branchLink() {
return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranchLink;
},
deployments() {
return this.isPostMerge ? this.mr.postMergeDeployments : this.mr.deployments;
},
deploymentClass() {
return this.isPostMerge ? 'js-post-deployment' : 'js-pre-deployment';
},
hasDeploymentMetrics() {
return this.isPostMerge;
},
},
};
</script>
<template>
<mr-widget-container>
<mr-widget-pipeline
:pipeline="pipeline"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
:source-branch="branch"
:source-branch-link="branchLink"
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<div v-if="deployments.length" slot="footer" class="mr-widget-extension">
<deployment
v-for="deployment in deployments"
:key="deployment.id"
:class="deploymentClass"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
/>
</div>
</mr-widget-container>
</template>
...@@ -6,7 +6,7 @@ import SmartInterval from '~/smart_interval'; ...@@ -6,7 +6,7 @@ import SmartInterval from '~/smart_interval';
import createFlash from '../flash'; import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue'; import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import WidgetPipeline from './components/mr_widget_pipeline.vue'; import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import Deployment from './components/deployment.vue'; import Deployment from './components/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue'; import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MergedState from './components/states/mr_widget_merged.vue'; import MergedState from './components/states/mr_widget_merged.vue';
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
components: { components: {
'mr-widget-header': WidgetHeader, 'mr-widget-header': WidgetHeader,
'mr-widget-merge-help': WidgetMergeHelp, 'mr-widget-merge-help': WidgetMergeHelp,
'mr-widget-pipeline': WidgetPipeline, MrWidgetPipelineContainer,
Deployment, Deployment,
'mr-widget-related-links': WidgetRelatedLinks, 'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState, 'mr-widget-merged': MergedState,
...@@ -296,23 +296,12 @@ export default { ...@@ -296,23 +296,12 @@ export default {
<template> <template>
<div class="mr-state-widget prepend-top-default"> <div class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" /> <mr-widget-header :mr="mr" />
<mr-widget-pipeline <mr-widget-pipeline-container
v-if="shouldRenderPipelines" v-if="shouldRenderPipelines"
:pipeline="mr.pipeline" class="mr-widget-workflow"
:ci-status="mr.ciStatus" :mr="mr"
:has-ci="mr.hasCI"
:source-branch="mr.sourceBranch"
:source-branch-link="mr.sourceBranchLink"
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
/> />
<deployment <div class="mr-section-container mr-widget-workflow">
v-for="deployment in mr.deployments"
:key="`pre-merge-deploy-${deployment.id}`"
class="js-pre-merge-deploy"
:deployment="deployment"
:show-metrics="false"
/>
<div class="mr-section-container">
<grouped-test-reports-app <grouped-test-reports-app
v-if="mr.testResultsPath" v-if="mr.testResultsPath"
class="js-reports-container" class="js-reports-container"
...@@ -336,24 +325,11 @@ export default { ...@@ -336,24 +325,11 @@ export default {
</div> </div>
<div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div> <div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div>
</div> </div>
<mr-widget-pipeline-container
<template v-if="shouldRenderMergedPipeline"> v-if="shouldRenderMergedPipeline"
<mr-widget-pipeline class="js-post-merge-pipeline mr-widget-workflow"
class="js-post-merge-pipeline prepend-top-default" :mr="mr"
:pipeline="mr.mergePipeline" :is-post-merge="true"
:ci-status="mr.ciStatus" />
:has-ci="mr.hasCI"
:source-branch="mr.targetBranch"
:source-branch-link="mr.targetBranch"
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<deployment
v-for="postMergeDeployment in mr.postMergeDeployments"
:key="`post-merge-deploy-${postMergeDeployment.id}`"
:deployment="postMergeDeployment"
:show-metrics="true"
class="js-post-deployment"
/>
</template>
</div> </div>
</template> </template>
...@@ -80,3 +80,15 @@ ...@@ -80,3 +80,15 @@
.user-avatar-link { .user-avatar-link {
text-decoration: none; text-decoration: none;
} }
.circle-icon-container {
$border-size: 1px;
display: flex;
align-items: center;
justify-content: center;
border: $border-size solid $theme-gray-400;
border-radius: 50%;
padding: $gl-padding-8 - $border-size;
color: $theme-gray-700;
}
...@@ -50,9 +50,19 @@ ...@@ -50,9 +50,19 @@
.mr-widget-heading { .mr-widget-heading {
position: relative; position: relative;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: 4px; border-radius: $border-radius-default;
}
&:not(.deploy-heading)::before { .mr-widget-extension {
border-top: 1px solid $border-color;
background-color: $gray-light;
}
.mr-widget-workflow {
margin-top: $gl-padding;
position: relative;
&::before {
content: ''; content: '';
border-left: 1px solid $theme-gray-200; border-left: 1px solid $theme-gray-200;
position: absolute; position: absolute;
...@@ -68,8 +78,8 @@ ...@@ -68,8 +78,8 @@
border-top: 0; border-top: 0;
} }
.mr-widget-heading,
.mr-widget-section, .mr-widget-section,
.mr-widget-content,
.mr-widget-footer { .mr-widget-footer {
padding: $gl-padding; padding: $gl-padding;
} }
...@@ -560,19 +570,6 @@ ...@@ -560,19 +570,6 @@
color: $gl-text-color; color: $gl-text-color;
} }
.git-merge-icon-container {
border: 1px solid $theme-gray-400;
border-radius: 50%;
height: 32px;
width: 32px;
color: $theme-gray-700;
line-height: 28px;
.ic-git-merge {
vertical-align: middle;
width: 31px;
}
}
.git-merge-container { .git-merge-container {
justify-content: space-between; justify-content: space-between;
...@@ -854,11 +851,6 @@ ...@@ -854,11 +851,6 @@
} }
.deploy-heading { .deploy-heading {
margin-top: -19px;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-color: $gray-light;
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
padding: $gl-padding-8 $gl-padding; padding: $gl-padding-8 $gl-padding;
} }
...@@ -868,6 +860,10 @@ ...@@ -868,6 +860,10 @@
font-size: 12px; font-size: 12px;
margin-left: 48px; margin-left: 48px;
} }
&:not(:last-child) {
border-bottom: 1px solid $border-color;
}
} }
.deploy-body { .deploy-body {
......
---
title: Redesign of MR header sections (CE)
merge_request: 23465
author:
type: changed
...@@ -60,7 +60,7 @@ describe 'Merge request > User sees merge widget', :js do ...@@ -60,7 +60,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows environments link' do it 'shows environments link' do
wait_for_requests wait_for_requests
page.within('.js-pre-merge-deploy') do page.within('.js-pre-deployment') do
expect(page).to have_content("Deployed to #{environment.name}") expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url) expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end end
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import MrWidgetContainer from '~/vue_merge_request_widget/components/mr_widget_container.vue';
const BODY_HTML = '<div class="test-body">Hello World</div>';
const FOOTER_HTML = '<div class="test-footer">Goodbye!</div>';
describe('MrWidgetContainer', () => {
let wrapper;
const factory = (options = {}) => {
const localVue = createLocalVue();
wrapper = shallowMount(localVue.extend(MrWidgetContainer), {
localVue,
...options,
});
};
afterEach(() => {
wrapper.destroy();
});
it('has layout', () => {
factory();
expect(wrapper.is('.mr-widget-heading')).toBe(true);
expect(wrapper.contains('.mr-widget-content')).toBe(true);
});
it('accepts default slot', () => {
factory({
slots: {
default: BODY_HTML,
},
});
expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true);
});
it('accepts footer slot', () => {
factory({
slots: {
default: BODY_HTML,
footer: FOOTER_HTML,
},
});
expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true);
expect(wrapper.contains('.test-footer')).toBe(true);
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
const TEST_ICON = 'commit';
describe('MrWidgetIcon', () => {
let wrapper;
beforeEach(() => {
const localVue = createLocalVue();
wrapper = shallowMount(localVue.extend(MrWidgetIcon), {
propsData: {
name: TEST_ICON,
},
sync: false,
localVue,
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders icon and container', () => {
expect(wrapper.is('.circle-icon-container')).toBe(true);
expect(wrapper.find(Icon).props('name')).toEqual(TEST_ICON);
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import { mockStore } from '../mock_data';
describe('MrWidgetPipelineContainer', () => {
let wrapper;
const factory = (props = {}) => {
const localVue = createLocalVue();
wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), {
propsData: {
mr: Object.assign({}, mockStore),
...props,
},
localVue,
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when pre merge', () => {
beforeEach(() => {
factory();
});
it('renders pipeline', () => {
expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
jasmine.objectContaining({
pipeline: mockStore.pipeline,
ciStatus: mockStore.ciStatus,
hasCi: mockStore.hasCI,
sourceBranch: mockStore.sourceBranch,
sourceBranchLink: mockStore.sourceBranchLink,
}),
);
});
it('renders deployments', () => {
const expectedProps = mockStore.deployments.map(dep =>
jasmine.objectContaining({
deployment: dep,
showMetrics: false,
}),
);
const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment');
expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
});
});
describe('when post merge', () => {
beforeEach(() => {
factory({
isPostMerge: true,
});
});
it('renders pipeline', () => {
expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
jasmine.objectContaining({
pipeline: mockStore.mergePipeline,
ciStatus: mockStore.ciStatus,
hasCi: mockStore.hasCI,
sourceBranch: mockStore.targetBranch,
sourceBranchLink: mockStore.targetBranch,
}),
);
});
it('renders deployments', () => {
const expectedProps = mockStore.postMergeDeployments.map(dep =>
jasmine.objectContaining({
deployment: dep,
showMetrics: true,
}),
);
const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment');
expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
});
});
});
...@@ -222,3 +222,16 @@ export default { ...@@ -222,3 +222,16 @@ export default {
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help', troubleshooting_docs_path: 'help',
}; };
export const mockStore = {
pipeline: { id: 0 },
mergePipeline: { id: 1 },
targetBranch: 'target-branch',
sourceBranch: 'source-branch',
sourceBranchLink: 'source-branch-link',
deployments: [{ id: 0, name: 'bogus' }, { id: 1, name: 'bogus-docs' }],
postMergeDeployments: [{ id: 0, name: 'prod' }, { id: 1, name: 'prod-docs' }],
troubleshootingDocsPath: 'troubleshooting-docs-path',
ciStatus: 'ci-status',
hasCI: true,
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment