Commit 2d4e4ada authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '10476-remove-pipeline-dashboard-feature-flag' into 'master'

Resolve "Remove Pipeline Dashboard Feature Flag"

Closes #10476

See merge request gitlab-org/gitlab-ee!10302
parents 6d357b2c 11a05b6f
...@@ -12,21 +12,11 @@ export default { ...@@ -12,21 +12,11 @@ export default {
required: false, required: false,
default: 0, default: 0,
}, },
lastAlert: {
type: Object,
required: false,
default: null,
},
alertPath: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
alertClasses() { alertClasses() {
return { return {
'text-success': this.count <= 0, 'text-tertiary': this.count <= 0,
'text-warning': this.count > 0, 'text-warning': this.count > 0,
}; };
}, },
...@@ -36,17 +26,6 @@ export default { ...@@ -36,17 +26,6 @@ export default {
alerts: this.pluralizedAlerts, alerts: this.pluralizedAlerts,
}); });
}, },
alertLinkTitle() {
return sprintf(__('View %{alerts}'), { alerts: this.pluralizedAlerts });
},
lastAlertText() {
if (this.count === 0 || this.lastAlert === null) {
return __('None');
}
const ellipsis = this.count > 1 ? '\u2026' : '';
return `${this.lastAlert.operator} ${this.lastAlert.threshold}${ellipsis}`;
},
pluralizedAlerts() { pluralizedAlerts() {
return n__('Alert', 'Alerts', this.count); return n__('Alert', 'Alerts', this.count);
}, },
...@@ -55,17 +34,14 @@ export default { ...@@ -55,17 +34,14 @@ export default {
</script> </script>
<template> <template>
<div class="row"> <div class="dashboard-card-alert row">
<div class="col-12 d-flex align-items-center"> <div class="col-12">
<icon :class="alertClasses" name="warning" /> <icon
:class="alertClasses"
class="align-text-bottom js-dashboard-alerts-icon"
name="warning"
/>
<span class="js-alert-count text-secondary prepend-left-4"> {{ alertCount }} </span> <span class="js-alert-count text-secondary prepend-left-4"> {{ alertCount }} </span>
</div> </div>
<div class="js-last-alert col-12">
<a v-if="alertPath" :href="alertPath" class="js-alert-link cgray">
<span v-if="lastAlert" class="str-truncated-60"> {{ lastAlert.title }} </span>
<span> {{ lastAlertText }} </span>
</a>
<span v-else> {{ lastAlertText }} </span>
</div>
</div> </div>
</template> </template>
...@@ -2,13 +2,11 @@ ...@@ -2,13 +2,11 @@
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { GlLoadingIcon, GlDashboardSkeleton } from '@gitlab/ui'; import { GlLoadingIcon, GlDashboardSkeleton } from '@gitlab/ui';
import DashboardProject from './project.vue'; import DashboardProject from './project.vue';
import NewDashboardProject from './new_project.vue';
import ProjectSearch from './project_search.vue'; import ProjectSearch from './project_search.vue';
export default { export default {
components: { components: {
DashboardProject, DashboardProject,
NewDashboardProject,
ProjectSearch, ProjectSearch,
GlLoadingIcon, GlLoadingIcon,
GlDashboardSkeleton, GlDashboardSkeleton,
...@@ -36,15 +34,6 @@ export default { ...@@ -36,15 +34,6 @@ export default {
addIsDisabled() { addIsDisabled() {
return !this.projectTokens.length; return !this.projectTokens.length;
}, },
showNewPipelineDashboard() {
return gon && gon.features && gon.features.pipelineDashboard;
},
dashboardClasses() {
return {
'm-0': !this.showNewPipelineDashboard,
'dashboard-cards': this.showNewPipelineDashboard,
};
},
}, },
created() { created() {
this.setProjectEndpoints({ this.setProjectEndpoints({
...@@ -85,21 +74,10 @@ export default { ...@@ -85,21 +74,10 @@ export default {
</div> </div>
</div> </div>
<div class="prepend-top-default"> <div class="prepend-top-default">
<div v-if="projects.length" :class="dashboardClasses" class="row prepend-top-default"> <div v-if="projects.length" class="row prepend-top-default dashboard-cards">
<template v-if="showNewPipelineDashboard"> <div v-for="project in projects" :key="project.id" class="col-12 col-md-6 col-xl-4 px-2">
<div v-for="project in projects" :key="project.id" class="col-12 col-md-6 col-xl-4 px-2"> <dashboard-project :project="project" />
<new-dashboard-project :project="project" /> </div>
</div>
</template>
<template v-else>
<div
v-for="project in projects"
:key="project.id"
class="col-12 col-md-6 odds-md-pad-right evens-md-pad-left"
>
<dashboard-project :project="project" />
</div>
</template>
</div> </div>
<div v-else-if="!isLoadingProjects" class="row prepend-top-20 text-center"> <div v-else-if="!isLoadingProjects" class="row prepend-top-20 text-center">
<div class="col-12 d-flex justify-content-center svg-content"> <div class="col-12 d-flex justify-content-center svg-content">
...@@ -125,8 +103,7 @@ export default { ...@@ -125,8 +103,7 @@ export default {
</a> </a>
</div> </div>
</div> </div>
<gl-dashboard-skeleton v-else-if="showNewPipelineDashboard" /> <gl-dashboard-skeleton v-else />
<gl-loading-icon v-else :size="2" class="prepend-top-20" />
</div> </div>
</div> </div>
</template> </template>
<script>
import { __, n__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
count: {
type: Number,
required: false,
default: 0,
},
},
computed: {
alertClasses() {
return {
'text-tertiary': this.count <= 0,
'text-warning': this.count > 0,
};
},
alertCount() {
return sprintf(__('%{count} %{alerts}'), {
count: this.count,
alerts: this.pluralizedAlerts,
});
},
pluralizedAlerts() {
return n__('Alert', 'Alerts', this.count);
},
},
};
</script>
<template>
<div class="dashboard-card-alert row">
<div class="col-12">
<icon
:class="alertClasses"
class="align-text-bottom js-dashboard-alerts-icon"
name="warning"
/>
<span class="js-alert-count text-secondary prepend-left-4"> {{ alertCount }} </span>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import _ from 'underscore';
import { GlTooltip } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import Commit from '~/vue_shared/components/commit.vue';
import ProjectHeader from './new_project_header.vue';
import Alerts from './new_alerts.vue';
import ProjectPipeline from './project_pipeline.vue';
import { STATUS_FAILED, STATUS_RUNNING } from '../../constants';
export default {
components: {
ProjectHeader,
UserAvatarLink,
Commit,
Alerts,
ProjectPipeline,
GlTooltip,
Icon,
},
mixins: [timeagoMixin],
props: {
project: {
type: Object,
required: true,
},
},
tooltips: {
timeAgo: __('Finished'),
triggerer: __('Triggerer'),
},
computed: {
hasPipelineFailed() {
return (
this.lastPipeline &&
this.lastPipeline.details &&
this.lastPipeline.details.status &&
this.lastPipeline.details.status.group === STATUS_FAILED
);
},
hasPipelineErrors() {
return this.project.alert_count > 0;
},
cardClasses() {
return {
'dashboard-card-body-warning': !this.hasPipelineFailed && this.hasPipelineErrors,
'dashboard-card-body-failed': this.hasPipelineFailed,
'bg-secondary': !this.hasPipelineFailed && !this.hasPipelineErrors,
};
},
noPipelineMessage() {
return __('The branch for this project has no active pipeline configuration.');
},
user() {
return this.lastPipeline && !_.isEmpty(this.lastPipeline.user)
? this.lastPipeline.user
: null;
},
lastPipeline() {
return !_.isEmpty(this.project.last_pipeline) ? this.project.last_pipeline : null;
},
commitRef() {
return this.lastPipeline && !_.isEmpty(this.lastPipeline.ref)
? {
...this.lastPipeline.ref,
ref_url: this.lastPipeline.ref.path,
}
: {};
},
finishedTime() {
return (
this.lastPipeline && this.lastPipeline.details && this.lastPipeline.details.finished_at
);
},
finishedTimeTitle() {
return this.tooltipTitle(this.finishedTime);
},
shouldShowTimeAgo() {
return (
this.lastPipeline &&
this.lastPipeline.details &&
this.lastPipeline.details.status &&
this.lastPipeline.details.status.group !== STATUS_RUNNING &&
this.finishedTime
);
},
},
methods: {
...mapActions(['removeProject']),
},
};
</script>
<template>
<div class="dashboard-card card border-0">
<project-header
:project="project"
:has-pipeline-failed="hasPipelineFailed"
:has-errors="hasPipelineErrors"
@remove="removeProject"
/>
<div :class="cardClasses" class="dashboard-card-body card-body">
<div v-if="lastPipeline" class="row">
<div class="col-1 align-self-center">
<user-avatar-link
v-if="user"
:link-href="user.path"
:img-src="user.avatar_url"
:tooltip-text="user.name"
:img-size="32"
/>
</div>
<div class="col-10 col-sm-6 pr-0 pl-5 align-self-center align-middle ci-table">
<commit
:tag="commitRef.tag"
:commit-ref="commitRef"
:short-sha="lastPipeline.commit.short_id"
:commit-url="lastPipeline.commit.commit_url"
:title="lastPipeline.commit.title"
:author="lastPipeline.commit.author"
:show-branch="true"
/>
</div>
<div class="col-sm-5 pl-0 text-right align-self-center d-none d-sm-block">
<div v-if="shouldShowTimeAgo" class="text-secondary">
<icon
name="clock"
class="dashboard-card-time-ago-icon align-text-bottom js-dashboard-project-clock-icon"
/>
<time ref="timeAgo" class="js-dashboard-project-time-ago">
{{ timeFormated(finishedTime) }}
</time>
<gl-tooltip :target="() => $refs.timeAgo">
<div class="bold">{{ $options.tooltips.timeAgo }}</div>
<div>{{ finishedTimeTitle }}</div>
</gl-tooltip>
</div>
<alerts :count="project.alert_count" />
</div>
<div class="col-12">
<project-pipeline
:project-name="project.name_with_namespace"
:last-pipeline="lastPipeline"
:has-pipeline-failed="hasPipelineFailed"
/>
</div>
</div>
<div v-else class="h-100 d-flex justify-content-center align-items-center">
<div class="text-plain text-metric text-center bold w-75">
{{ noPipelineMessage }}
</div>
</div>
</div>
</div>
</template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import { GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
export default {
components: {
Icon,
ProjectAvatar,
GlButton,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
project: {
type: Object,
required: true,
},
hasPipelineFailed: {
type: Boolean,
required: false,
default: false,
},
hasErrors: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
headerClasses() {
return {
'dashboard-card-header-warning': this.hasErrors,
'dashboard-card-header-failed': this.hasPipelineFailed,
'bg-light': !this.hasErrors && !this.hasPipelineFailed,
};
},
},
methods: {
onRemove() {
this.$emit('remove', this.project.remove_path);
},
},
};
</script>
<template>
<div :class="headerClasses" class="card-header border-0 py-2 d-flex align-items-center">
<project-avatar :project="project" :size="24" class="flex-shrink-0 border rounded" />
<div class="flex-grow-1 block-truncated">
<gl-link
v-gl-tooltip
class="js-project-link cgray"
:href="project.web_url"
:title="project.name_with_namespace"
>
<span class="js-project-namespace">{{ project.namespace.name }} /</span>
<span class="js-project-name bold"> {{ project.name }}</span>
</gl-link>
</div>
<div class="dropdown js-more-actions">
<gl-button
v-gl-tooltip
class="js-more-actions-toggle d-flex align-items-center bg-transparent border-0 p-0 ml-2"
data-toggle="dropdown"
:title="__('More actions')"
>
<icon name="ellipsis_v" class="text-secondary" />
</gl-button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<gl-button class="btn btn-transparent js-remove-button" @click="onRemove">
<span class="text-danger"> {{ __('Remove') }} </span>
</gl-button>
</li>
</ul>
</div>
</div>
</template>
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import timeago from '~/vue_shared/mixins/timeago'; import _ from 'underscore';
import { GlTooltip } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import Commit from '~/vue_shared/components/commit.vue'; import Commit from '~/vue_shared/components/commit.vue';
import DashboardAlerts from './alerts.vue';
import ProjectHeader from './project_header.vue'; import ProjectHeader from './project_header.vue';
import Alerts from './alerts.vue';
import ProjectPipeline from './project_pipeline.vue';
import { STATUS_FAILED, STATUS_RUNNING } from '../../constants';
export default { export default {
components: { components: {
Icon,
Commit,
DashboardAlerts,
ProjectHeader, ProjectHeader,
UserAvatarLink,
Commit,
Alerts,
ProjectPipeline,
GlTooltip,
Icon,
}, },
mixins: [timeago], mixins: [timeagoMixin],
props: { props: {
project: { project: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
tooltips: {
timeAgo: __('Finished'),
triggerer: __('Triggerer'),
},
computed: { computed: {
author() { hasPipelineFailed() {
return this.hasDeployment && this.project.last_deployment.user return (
? { this.lastPipeline &&
avatar_url: this.project.last_deployment.user.avatar_url, this.lastPipeline.details &&
path: this.project.last_deployment.user.web_url, this.lastPipeline.details.status &&
username: this.project.last_deployment.user.username, this.lastPipeline.details.status.group === STATUS_FAILED
} );
},
hasPipelineErrors() {
return this.project.alert_count > 0;
},
cardClasses() {
return {
'dashboard-card-body-warning': !this.hasPipelineFailed && this.hasPipelineErrors,
'dashboard-card-body-failed': this.hasPipelineFailed,
'bg-secondary': !this.hasPipelineFailed && !this.hasPipelineErrors,
};
},
noPipelineMessage() {
return __('The branch for this project has no active pipeline configuration.');
},
user() {
return this.lastPipeline && !_.isEmpty(this.lastPipeline.user)
? this.lastPipeline.user
: null; : null;
}, },
lastPipeline() {
return !_.isEmpty(this.project.last_pipeline) ? this.project.last_pipeline : null;
},
commitRef() { commitRef() {
return this.hasDeployment && this.project.last_deployment.ref return this.lastPipeline && !_.isEmpty(this.lastPipeline.ref)
? { ? {
name: this.project.last_deployment.ref.name, ...this.lastPipeline.ref,
ref_url: this.project.last_deployment.ref.ref_path, ref_url: this.lastPipeline.ref.path,
} }
: null; : {};
},
finishedTime() {
return (
this.lastPipeline && this.lastPipeline.details && this.lastPipeline.details.finished_at
);
}, },
hasDeployment() { finishedTimeTitle() {
return this.project.last_deployment !== null; return this.tooltipTitle(this.finishedTime);
}, },
lastDeployed() { shouldShowTimeAgo() {
return this.hasDeployment ? this.timeFormated(this.project.last_deployment.created_at) : null; return (
this.lastPipeline &&
this.lastPipeline.details &&
this.lastPipeline.details.status &&
this.lastPipeline.details.status.group !== STATUS_RUNNING &&
this.finishedTime
);
}, },
}, },
methods: { methods: {
...@@ -50,41 +94,70 @@ export default { ...@@ -50,41 +94,70 @@ export default {
}, },
}; };
</script> </script>
<template> <template>
<div class="card"> <div class="dashboard-card card border-0">
<project-header :project="project" class="card-header" @remove="removeProject" /> <project-header
<div class="card-body"> :project="project"
<div class="row"> :has-pipeline-failed="hasPipelineFailed"
<div class="col-6 col-sm-4 col-md-6 col-lg-4 pr-1"> :has-errors="hasPipelineErrors"
<dashboard-alerts @remove="removeProject"
:count="project.alert_count" />
:last-alert="project.last_alert"
:alert-path="project.alert_path" <div :class="cardClasses" class="dashboard-card-body card-body">
<div v-if="lastPipeline" class="row">
<div class="col-1 align-self-center">
<user-avatar-link
v-if="user"
:link-href="user.path"
:img-src="user.avatar_url"
:tooltip-text="user.name"
:img-size="32"
/>
</div>
<div class="col-10 col-sm-6 pr-0 pl-5 align-self-center align-middle ci-table">
<commit
:tag="commitRef.tag"
:commit-ref="commitRef"
:short-sha="lastPipeline.commit.short_id"
:commit-url="lastPipeline.commit.commit_url"
:title="lastPipeline.commit.title"
:author="lastPipeline.commit.author"
:show-branch="true"
/> />
</div> </div>
<template v-if="project.last_deployment">
<div class="col-6 col-sm-4 col-md-6 col-lg-4 px-1"> <div class="col-sm-5 pl-0 text-right align-self-center d-none d-sm-block">
<commit <div v-if="shouldShowTimeAgo" class="text-secondary">
:commit-ref="commitRef" <icon
:short-sha="project.last_deployment.commit.short_id" name="clock"
:commit-url="project.last_deployment.commit.commit_url" class="dashboard-card-time-ago-icon align-text-bottom js-dashboard-project-clock-icon"
:title="project.last_deployment.commit.title"
:author="author"
:tag="project.last_deployment.tag"
/> />
<time ref="timeAgo" class="js-dashboard-project-time-ago">
{{ timeFormated(finishedTime) }}
</time>
<gl-tooltip :target="() => $refs.timeAgo">
<div class="bold">{{ $options.tooltips.timeAgo }}</div>
<div>{{ finishedTimeTitle }}</div>
</gl-tooltip>
</div> </div>
<div <alerts :count="project.alert_count" />
class="js-project-container col-12 col-sm-4 col-md-12 col-lg-4 pl-1 d-flex justify-content-end" </div>
>
<div class="d-flex align-items-end justify-content-end"> <div class="col-12">
<div class="prepend-top-default text-secondary d-flex align-items-center flex-wrap"> <project-pipeline
<icon name="calendar" class="append-right-4 js-dashboard-project-calendar-icon" /> :project-name="project.name_with_namespace"
{{ lastDeployed }} :last-pipeline="lastPipeline"
</div> :has-pipeline-failed="hasPipelineFailed"
</div> />
</div> </div>
</template> </div>
<div v-else class="h-100 d-flex justify-content-center align-items-center">
<div class="text-plain text-metric text-center bold w-75">
{{ noPipelineMessage }}
</div>
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
export default { export default {
components: { components: {
Icon, Icon,
ProjectAvatar, ProjectAvatar,
GlButton,
GlLink,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -16,6 +18,25 @@ export default { ...@@ -16,6 +18,25 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
hasPipelineFailed: {
type: Boolean,
required: false,
default: false,
},
hasErrors: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
headerClasses() {
return {
'dashboard-card-header-warning': this.hasErrors,
'dashboard-card-header-failed': this.hasPipelineFailed,
'bg-light': !this.hasErrors && !this.hasPipelineFailed,
};
},
}, },
methods: { methods: {
onRemove() { onRemove() {
...@@ -26,27 +47,33 @@ export default { ...@@ -26,27 +47,33 @@ export default {
</script> </script>
<template> <template>
<div class="project-header d-flex align-items-center"> <div :class="headerClasses" class="card-header border-0 py-2 d-flex align-items-center">
<project-avatar :project="project" :size="20" class="flex-shrink-0" /> <project-avatar :project="project" :size="24" class="flex-shrink-0 border rounded" />
<div class="flex-grow-1"> <div class="flex-grow-1 block-truncated">
<a class="js-project-link cgray" :href="project.web_url"> <gl-link
<span class="js-name-with-namespace bold"> {{ project.name_with_namespace }} </span> v-gl-tooltip
</a> class="js-project-link cgray"
:href="project.web_url"
:title="project.name_with_namespace"
>
<span class="js-project-namespace">{{ project.namespace.name }} /</span>
<span class="js-project-name bold"> {{ project.name }}</span>
</gl-link>
</div> </div>
<div class="dropdown js-more-actions"> <div class="dropdown js-more-actions">
<div <gl-button
v-gl-tooltip v-gl-tooltip
class="js-more-actions-toggle d-flex align-items-center ml-2" class="js-more-actions-toggle d-flex align-items-center bg-transparent border-0 p-0 ml-2"
data-toggle="dropdown" data-toggle="dropdown"
:title="__('More actions')" :title="__('More actions')"
> >
<icon name="ellipsis_v" class="text-secondary" /> <icon name="ellipsis_v" class="text-secondary" />
</div> </gl-button>
<ul class="dropdown-menu dropdown-menu-right"> <ul class="dropdown-menu dropdown-menu-right">
<li> <li>
<button class="btn btn-transparent js-remove-button" type="button" @click="onRemove"> <gl-button class="btn btn-transparent js-remove-button" @click="onRemove">
<span class="text-danger"> {{ __('Remove') }} </span> <span class="text-danger"> {{ __('Remove') }} </span>
</button> </gl-button>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -3,10 +3,6 @@ ...@@ -3,10 +3,6 @@
class OperationsController < ApplicationController class OperationsController < ApplicationController
before_action :authorize_read_operations_dashboard! before_action :authorize_read_operations_dashboard!
before_action do
push_frontend_feature_flag(:pipeline_dashboard)
end
respond_to :json, only: [:list] respond_to :json, only: [:list]
POLLING_INTERVAL = 120_000 POLLING_INTERVAL = 120_000
......
---
title: Removing pipeline dashboard feature flag
merge_request: 10302
author:
type: added
import Vue from 'vue'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import Alerts from 'ee/operations/components/dashboard/alerts.vue'; import Alerts from 'ee/operations/components/dashboard/alerts.vue';
import { removeWhitespace } from 'spec/helpers/vue_component_helper';
import { mockOneProject } from '../../mock_data'; const localVue = createLocalVue();
describe('alerts component', () => { describe('alerts component', () => {
const AlertsComponent = Vue.extend(Alerts); const AlertsComponent = localVue.extend(Alerts);
const mockPath = 'https://mock-alert_path/'; let wrapper;
const mount = (props = {}) => mountComponentWithStore(AlertsComponent, { props });
let vm;
beforeEach(() => { const mount = (propsData = {}) => shallowMount(AlertsComponent, { propsData, sync: false });
vm = mount();
});
afterEach(() => { afterEach(() => {
if (vm.$destroy) { wrapper.destroy();
vm.$destroy();
}
}); });
it('renders multiple alert count when multiple alerts are present', () => { it('renders multiple alert count when multiple alerts are present', () => {
vm = mount({ count: 2 }); wrapper = mount({
count: 2,
});
expect(vm.$el.querySelector('.js-alert-count').innerText.trim()).toBe('2 Alerts'); expect(wrapper.element.querySelector('.js-alert-count').innerText.trim()).toBe('2 Alerts');
}); });
it('renders count for one alert when there is one alert', () => { it('renders count for one alert when there is one alert', () => {
vm = mount({ count: 1 }); wrapper = mount({
expect(vm.$el.querySelector('.js-alert-count').innerText.trim()).toBe('1 Alert');
});
it('renders last alert when one has fired', () => {
const mockAlert = mockOneProject.last_alert;
const alertMessage = `${mockAlert.title} ${mockAlert.operator} ${mockAlert.threshold}`;
vm = mount({
count: 1, count: 1,
alertPath: mockPath,
lastAlert: mockAlert,
}); });
const lastAlert = vm.$el.querySelector('.js-last-alert');
const innerText = removeWhitespace(lastAlert.innerText).trim();
expect(innerText).toBe(alertMessage); expect(wrapper.element.querySelector('.js-alert-count').innerText.trim()).toBe('1 Alert');
});
it('links last alert to metrics page', () => {
vm = mount({ alertPath: mockPath });
expect(vm.$el.querySelector('.js-alert-link').href).toBe(mockPath);
});
it('does not render last alert message when it has not fired', () => {
vm = mount({ alertPath: mockPath });
const lastAlert = vm.$el.querySelector('.js-last-alert');
expect(lastAlert.innerText.trim()).toBe('None');
}); });
describe('wrapped components', () => { describe('wrapped components', () => {
describe('icon', () => { describe('icon', () => {
it('renders warning', () => { it('renders warning', () => {
expect(vm.$el.querySelector('.ic-warning')).not.toBe(null); wrapper = mount({
count: 1,
});
expect(wrapper.element.querySelector('.js-dashboard-alerts-icon')).not.toBe(null);
}); });
}); });
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Alerts from 'ee/operations/components/dashboard/new_alerts.vue';
const localVue = createLocalVue();
describe('alerts component', () => {
const AlertsComponent = localVue.extend(Alerts);
let wrapper;
const mount = (propsData = {}) => shallowMount(AlertsComponent, { propsData, sync: false });
afterEach(() => {
wrapper.destroy();
});
it('renders multiple alert count when multiple alerts are present', () => {
wrapper = mount({
count: 2,
});
expect(wrapper.element.querySelector('.js-alert-count').innerText.trim()).toBe('2 Alerts');
});
it('renders count for one alert when there is one alert', () => {
wrapper = mount({
count: 1,
});
expect(wrapper.element.querySelector('.js-alert-count').innerText.trim()).toBe('1 Alert');
});
describe('wrapped components', () => {
describe('icon', () => {
it('renders warning', () => {
wrapper = mount({
count: 1,
});
expect(wrapper.element.querySelector('.js-dashboard-alerts-icon')).not.toBe(null);
});
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import ProjectHeader from 'ee/operations/components/dashboard/new_project_header.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import { removeWhitespace } from 'spec/helpers/vue_component_helper';
import { mockOneProject, mockText } from '../../new_mock_data';
const localVue = createLocalVue();
describe('project header component', () => {
let wrapper;
const factory = () => {
wrapper = shallowMount(localVue.extend(ProjectHeader), {
propsData: {
project: mockOneProject,
},
localVue,
sync: false,
});
};
beforeEach(() => {
factory();
});
afterEach(() => {
wrapper.destroy();
});
it('renders project name with namespace', () => {
const namespace = wrapper.find('.js-project-namespace').text();
const name = wrapper.find('.js-project-name').text();
expect(removeWhitespace(namespace).trim()).toBe(`${mockOneProject.namespace.name} /`);
expect(removeWhitespace(name).trim()).toBe(mockOneProject.name);
});
it('links project name to project', () => {
const path = mockOneProject.web_url;
expect(wrapper.find('.js-project-link').attributes('href')).toBe(path);
});
describe('wrapped components', () => {
describe('project avatar', () => {
it('renders', () => {
expect(wrapper.findAll(ProjectAvatar).length).toBe(1);
});
it('binds project', () => {
expect(wrapper.find(ProjectAvatar).props('project')).toEqual(mockOneProject);
});
});
});
describe('dropdown menu', () => {
it('renders removal button', () => {
expect(
wrapper
.find('.js-remove-button')
.text()
.trim(),
).toBe(mockText.REMOVE_PROJECT);
});
it('emits project removal link on click', () => {
wrapper.find('.js-remove-button').vm.$emit('click');
expect(wrapper.emittedByOrder()).toEqual([
{ name: 'remove', args: [mockOneProject.remove_path] },
]);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import Commit from '~/vue_shared/components/commit.vue';
import Project from 'ee/operations/components/dashboard/new_project.vue';
import ProjectHeader from 'ee/operations/components/dashboard/new_project_header.vue';
import Alerts from 'ee/operations/components/dashboard/new_alerts.vue';
import store from 'ee/operations/store';
import { mockOneProject } from '../../new_mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('project component', () => {
const ProjectComponent = localVue.extend(Project);
let wrapper;
beforeEach(() => {
wrapper = shallowMount(ProjectComponent, {
sync: false,
store,
localVue,
propsData: { project: mockOneProject },
});
});
afterEach(() => {
wrapper.destroy();
});
describe('wrapped components', () => {
describe('project header', () => {
it('binds project', () => {
const header = wrapper.find(ProjectHeader);
expect(header.props('project')).toEqual(mockOneProject);
});
});
describe('alerts', () => {
it('binds alert count to count', () => {
const alert = wrapper.find(Alerts);
expect(alert.props('count')).toBe(mockOneProject.alert_count);
});
});
describe('commit', () => {
let commit;
beforeEach(() => {
commit = wrapper.find(Commit);
});
it('binds commitRef', () => {
expect(commit.props('commitRef')).toBe(wrapper.vm.commitRef);
});
it('binds short_id to shortSha', () => {
expect(commit.props('shortSha')).toBe(
wrapper.props().project.last_pipeline.commit.short_id,
);
});
it('binds commitUrl', () => {
expect(commit.props('commitUrl')).toBe(
wrapper.props().project.last_pipeline.commit.commit_url,
);
});
it('binds title', () => {
expect(commit.props('title')).toBe(wrapper.props().project.last_pipeline.commit.title);
});
it('binds author', () => {
expect(commit.props('author')).toBe(wrapper.props().project.last_pipeline.commit.author);
});
it('binds tag', () => {
expect(commit.props('tag')).toBe(wrapper.props().project.last_pipeline.ref.tag);
});
});
describe('deploy finished at', () => {
it('renders clock icon', () => {
expect(wrapper.contains('.js-dashboard-project-clock-icon')).toBe(true);
});
it('renders time ago of finished time', () => {
const timeago = '1 day ago';
const container = wrapper.element.querySelector('.js-dashboard-project-time-ago');
expect(container.innerText.trim()).toBe(timeago);
});
});
});
});
...@@ -28,9 +28,11 @@ describe('project header component', () => { ...@@ -28,9 +28,11 @@ describe('project header component', () => {
}); });
it('renders project name with namespace', () => { it('renders project name with namespace', () => {
const name = wrapper.find('.js-name-with-namespace').text(); const namespace = wrapper.find('.js-project-namespace').text();
const name = wrapper.find('.js-project-name').text();
expect(removeWhitespace(name).trim()).toBe(mockOneProject.name_with_namespace); expect(removeWhitespace(namespace).trim()).toBe(`${mockOneProject.namespace.name} /`);
expect(removeWhitespace(name).trim()).toBe(mockOneProject.name);
}); });
it('links project name to project', () => { it('links project name to project', () => {
...@@ -62,7 +64,7 @@ describe('project header component', () => { ...@@ -62,7 +64,7 @@ describe('project header component', () => {
}); });
it('emits project removal link on click', () => { it('emits project removal link on click', () => {
wrapper.find('.js-remove-button').trigger('click'); wrapper.find('.js-remove-button').vm.$emit('click');
expect(wrapper.emittedByOrder()).toEqual([ expect(wrapper.emittedByOrder()).toEqual([
{ name: 'remove', args: [mockOneProject.remove_path] }, { name: 'remove', args: [mockOneProject.remove_path] },
......
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import ProjectPipeline from 'ee/operations/components/dashboard/project_pipeline.vue'; import ProjectPipeline from 'ee/operations/components/dashboard/project_pipeline.vue';
import { mockPipelineData } from '../../new_mock_data'; import { mockPipelineData } from '../../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
......
...@@ -19,9 +19,7 @@ describe('project component', () => { ...@@ -19,9 +19,7 @@ describe('project component', () => {
sync: false, sync: false,
store, store,
localVue, localVue,
propsData: { propsData: { project: mockOneProject },
project: mockOneProject,
},
}); });
}); });
...@@ -39,18 +37,10 @@ describe('project component', () => { ...@@ -39,18 +37,10 @@ describe('project component', () => {
}); });
describe('alerts', () => { describe('alerts', () => {
let alert;
beforeEach(() => {
alert = wrapper.find(Alerts);
});
it('binds alert count to count', () => { it('binds alert count to count', () => {
expect(alert.props('count')).toBe(mockOneProject.alert_count); const alert = wrapper.find(Alerts);
});
it('binds last alert', () => { expect(alert.props('count')).toBe(mockOneProject.alert_count);
expect(alert.props('lastAlert')).toEqual(mockOneProject.last_alert);
}); });
}); });
...@@ -66,38 +56,38 @@ describe('project component', () => { ...@@ -66,38 +56,38 @@ describe('project component', () => {
}); });
it('binds short_id to shortSha', () => { it('binds short_id to shortSha', () => {
expect(commit.props().shortSha).toBe( expect(commit.props('shortSha')).toBe(
wrapper.props().project.last_deployment.commit.short_id, wrapper.props().project.last_pipeline.commit.short_id,
); );
}); });
it('binds commitUrl', () => { it('binds commitUrl', () => {
expect(commit.props().commitUrl).toBe( expect(commit.props('commitUrl')).toBe(
wrapper.props().project.last_deployment.commit.commit_url, wrapper.props().project.last_pipeline.commit.commit_url,
); );
}); });
it('binds title', () => { it('binds title', () => {
expect(commit.props().title).toBe(wrapper.props().project.last_deployment.commit.title); expect(commit.props('title')).toBe(wrapper.props().project.last_pipeline.commit.title);
}); });
it('binds author', () => { it('binds author', () => {
expect(commit.props().author).toBe(wrapper.vm.author); expect(commit.props('author')).toBe(wrapper.props().project.last_pipeline.commit.author);
}); });
it('binds tag', () => { it('binds tag', () => {
expect(commit.props().tag).toBe(wrapper.props().project.last_deployment.tag); expect(commit.props('tag')).toBe(wrapper.props().project.last_pipeline.ref.tag);
}); });
}); });
describe('last deploy', () => { describe('deploy finished at', () => {
it('renders calendar icon', () => { it('renders clock icon', () => {
expect(wrapper.contains('.js-dashboard-project-calendar-icon')).toBe(true); expect(wrapper.contains('.js-dashboard-project-clock-icon')).toBe(true);
}); });
it('renders time ago of last deploy', () => { it('renders time ago of finished time', () => {
const timeago = '1 day ago'; const timeago = '1 day ago';
const container = wrapper.element.querySelector('.js-project-container'); const container = wrapper.element.querySelector('.js-dashboard-project-time-ago');
expect(container.innerText.trim()).toBe(timeago); expect(container.innerText.trim()).toBe(timeago);
}); });
......
import { TEST_HOST } from 'spec/test_constants';
const AVATAR_URL = `${TEST_HOST}/dummy.jpg`;
export const mockText = { export const mockText = {
ADD_PROJECTS: 'Add projects', ADD_PROJECTS: 'Add projects',
ADD_PROJECTS_ERROR: 'Something went wrong, unable to add projects to dashboard', ADD_PROJECTS_ERROR: 'Something went wrong, unable to add projects to dashboard',
...@@ -14,52 +18,104 @@ export const mockText = { ...@@ -14,52 +18,104 @@ export const mockText = {
SEARCH_DESCRIPTION_SUFFIX: 'in projects', SEARCH_DESCRIPTION_SUFFIX: 'in projects',
}; };
export function mockPipelineData(
status = 'success',
id = 1,
finishedTimeStamp = new Date(Date.now() - 86400000).toISOString(),
isTag = false,
) {
return {
id,
user: {
id: 1,
name: 'Test',
username: 'test',
state: 'active',
avatar_url: AVATAR_URL,
web_url: '/test',
status_tooltip_html: null,
path: '/test',
},
active: false,
path: '/test/test-project/pipelines/1',
details: {
status: {
icon: `status_${status}`,
text: status,
label: status,
group: status,
tooltip: status,
has_details: true,
details_path: '/test/test-project/pipelines/1',
illustration: null,
},
finished_at: finishedTimeStamp,
},
ref: {
name: 'master',
path: 'test/test-project/commits/master',
tag: isTag,
branch: true,
merge_request: false,
},
commit: {
id: 'e778416d94deaf75bdabcc8fdd6b7d21f482bcca',
short_id: 'e778416d',
title: "Add new file to the branch I'm working on",
message: "Add new file to the branch I'm working on",
author: {
id: 1,
name: 'Test',
username: 'test',
state: 'active',
avatar_url: AVATAR_URL,
status_tooltip_html: null,
path: '/test',
},
commit_url: '/test/test-project/commit/e778416d94deaf75bdabcc8fdd6b7d21f482bcca',
commit_path: '/test/test-project/commit/e778416d94deaf75bdabcc8fdd6b7d21f482bcca',
},
project: {
full_name: 'Test / test-project',
full_path: '/test/test-project',
name: 'test-project',
},
};
}
export function mockProjectData( export function mockProjectData(
projectCount = 1, projectCount = 1,
deployTimeStamp = `${new Date(Date.now() - 86400000).getTime()}`, currentPipelineStatus = 'success',
alertCount = 1, upstreamStatus = 'success',
isTag = false, alertCount = 0,
) { ) {
return Array(projectCount) return Array(projectCount)
.fill(null) .fill(null)
.map((_, index) => ({ .map((_, index) => ({
id: index, id: index,
name: 'mock-name', description: '',
name_with_namespace: 'mock-namespace / mock-name', name: 'test-project',
path: 'mock-path', name_with_namespace: 'Test / test-project',
path_with_namespace: 'mock-path_with-namespace', path: 'test-project',
path_with_namespace: 'test/test-project',
created_at: '2019-02-01T15:40:27.522Z',
default_branch: 'master',
tag_list: [],
avatar_url: null, avatar_url: null,
last_deployment: { web_url: 'https://mock-web_url/',
created_at: deployTimeStamp, namespace: {
commit: { id: 1,
short_id: 'mock-short_id', name: 'test',
title: 'mock-title', path: 'test',
commit_url: 'https://mock-commit_url/', kind: 'user',
}, full_path: 'user',
tag: isTag, parent_id: null,
user: {
avatar_url: null,
path: 'mock-path',
username: 'mock-username',
web_url: 'https://mock-web_url/',
},
ref: {
name: 'mock-name',
ref_path: 'mock-ref_path',
web_url: 'https://mock-web_url/',
},
}, },
remove_path: '/-/operations?project_id=1',
last_pipeline: mockPipelineData(currentPipelineStatus),
upstream_pipeline: mockPipelineData(upstreamStatus),
downstream_pipelines: [],
alert_count: alertCount, alert_count: alertCount,
alert_path: 'mock-alert_path',
last_alert: {
id: index,
title: 'mock-title',
threshold: 2,
operator: 'mock-operator',
alert_path: 'mock-alert_path',
},
remove_path: 'mock-remove_path',
web_url: 'https://mock-web_url/',
})); }));
} }
......
import { TEST_HOST } from 'spec/test_constants';
const AVATAR_URL = `${TEST_HOST}/dummy.jpg`;
export const mockText = {
ADD_PROJECTS: 'Add projects',
ADD_PROJECTS_ERROR: 'Something went wrong, unable to add projects to dashboard',
REMOVE_PROJECT_ERROR: 'Something went wrong, unable to remove project',
DASHBOARD_TITLE: 'Operations Dashboard',
EMPTY_TITLE: 'Add a project to the dashboard',
EMPTY_SUBTITLE:
"The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses.",
EMPTY_SVG_SOURCE: '/assets/illustrations/operations-dashboard_empty.svg',
NO_SEARCH_RESULTS: 'Sorry, no projects matched your search',
RECEIVE_PROJECTS_ERROR: 'Something went wrong, unable to get operations projects',
REMOVE_PROJECT: 'Remove',
SEARCH_PROJECTS: 'Search your projects',
SEARCH_DESCRIPTION_SUFFIX: 'in projects',
};
export function mockPipelineData(
status = 'success',
id = 1,
finishedTimeStamp = new Date(Date.now() - 86400000).toISOString(),
isTag = false,
) {
return {
id,
user: {
id: 1,
name: 'Test',
username: 'test',
state: 'active',
avatar_url: AVATAR_URL,
web_url: '/test',
status_tooltip_html: null,
path: '/test',
},
active: false,
path: '/test/test-project/pipelines/1',
details: {
status: {
icon: `status_${status}`,
text: status,
label: status,
group: status,
tooltip: status,
has_details: true,
details_path: '/test/test-project/pipelines/1',
illustration: null,
},
finished_at: finishedTimeStamp,
},
ref: {
name: 'master',
path: 'test/test-project/commits/master',
tag: isTag,
branch: true,
merge_request: false,
},
commit: {
id: 'e778416d94deaf75bdabcc8fdd6b7d21f482bcca',
short_id: 'e778416d',
title: "Add new file to the branch I'm working on",
message: "Add new file to the branch I'm working on",
author: {
id: 1,
name: 'Test',
username: 'test',
state: 'active',
avatar_url: AVATAR_URL,
status_tooltip_html: null,
path: '/test',
},
commit_url: '/test/test-project/commit/e778416d94deaf75bdabcc8fdd6b7d21f482bcca',
commit_path: '/test/test-project/commit/e778416d94deaf75bdabcc8fdd6b7d21f482bcca',
},
project: {
full_name: 'Test / test-project',
full_path: '/test/test-project',
name: 'test-project',
},
};
}
export function mockProjectData(
projectCount = 1,
currentPipelineStatus = 'success',
upstreamStatus = 'success',
alertCount = 0,
) {
return Array(projectCount)
.fill(null)
.map((_, index) => ({
id: index,
description: '',
name: 'test-project',
name_with_namespace: 'Test / test-project',
path: 'test-project',
path_with_namespace: 'test/test-project',
created_at: '2019-02-01T15:40:27.522Z',
default_branch: 'master',
tag_list: [],
avatar_url: null,
web_url: 'https://mock-web_url/',
namespace: {
id: 1,
name: 'test',
path: 'test',
kind: 'user',
full_path: 'user',
parent_id: null,
},
remove_path: '/-/operations?project_id=1',
last_pipeline: mockPipelineData(currentPipelineStatus),
upstream_pipeline: mockPipelineData(upstreamStatus),
downstream_pipelines: [],
alert_count: alertCount,
}));
}
export const [mockOneProject] = mockProjectData(1);
...@@ -11363,9 +11363,6 @@ msgstr "" ...@@ -11363,9 +11363,6 @@ msgstr ""
msgid "Version" msgid "Version"
msgstr "" msgstr ""
msgid "View %{alerts}"
msgstr ""
msgid "View app" msgid "View app"
msgstr "" msgstr ""
......
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