Commit 11a05b6f authored by Scott Hampton's avatar Scott Hampton

Remove the pipeline_dashboard feature flag

Discarding old files, and moving new dashboard files to the forefront.
parent 48063e7b
......@@ -12,21 +12,11 @@ export default {
required: false,
default: 0,
},
lastAlert: {
type: Object,
required: false,
default: null,
},
alertPath: {
type: String,
required: false,
default: null,
},
},
computed: {
alertClasses() {
return {
'text-success': this.count <= 0,
'text-tertiary': this.count <= 0,
'text-warning': this.count > 0,
};
},
......@@ -36,17 +26,6 @@ export default {
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() {
return n__('Alert', 'Alerts', this.count);
},
......@@ -55,17 +34,14 @@ export default {
</script>
<template>
<div class="row">
<div class="col-12 d-flex align-items-center">
<icon :class="alertClasses" name="warning" />
<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 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>
</template>
......@@ -2,13 +2,11 @@
import { mapState, mapActions } from 'vuex';
import { GlLoadingIcon, GlDashboardSkeleton } from '@gitlab/ui';
import DashboardProject from './project.vue';
import NewDashboardProject from './new_project.vue';
import ProjectSearch from './project_search.vue';
export default {
components: {
DashboardProject,
NewDashboardProject,
ProjectSearch,
GlLoadingIcon,
GlDashboardSkeleton,
......@@ -36,15 +34,6 @@ export default {
addIsDisabled() {
return !this.projectTokens.length;
},
showNewPipelineDashboard() {
return gon && gon.features && gon.features.pipelineDashboard;
},
dashboardClasses() {
return {
'm-0': !this.showNewPipelineDashboard,
'dashboard-cards': this.showNewPipelineDashboard,
};
},
},
created() {
this.setProjectEndpoints({
......@@ -85,21 +74,10 @@ export default {
</div>
</div>
<div class="prepend-top-default">
<div v-if="projects.length" :class="dashboardClasses" class="row prepend-top-default">
<template v-if="showNewPipelineDashboard">
<div v-for="project in projects" :key="project.id" class="col-12 col-md-6 col-xl-4 px-2">
<new-dashboard-project :project="project" />
</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 v-if="projects.length" class="row prepend-top-default dashboard-cards">
<div v-for="project in projects" :key="project.id" class="col-12 col-md-6 col-xl-4 px-2">
<dashboard-project :project="project" />
</div>
</div>
<div v-else-if="!isLoadingProjects" class="row prepend-top-20 text-center">
<div class="col-12 d-flex justify-content-center svg-content">
......@@ -125,8 +103,7 @@ export default {
</a>
</div>
</div>
<gl-dashboard-skeleton v-else-if="showNewPipelineDashboard" />
<gl-loading-icon v-else :size="2" class="prepend-top-20" />
<gl-dashboard-skeleton v-else />
</div>
</div>
</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>
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 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 DashboardAlerts from './alerts.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 {
components: {
Icon,
Commit,
DashboardAlerts,
ProjectHeader,
UserAvatarLink,
Commit,
Alerts,
ProjectPipeline,
GlTooltip,
Icon,
},
mixins: [timeago],
mixins: [timeagoMixin],
props: {
project: {
type: Object,
required: true,
},
},
tooltips: {
timeAgo: __('Finished'),
triggerer: __('Triggerer'),
},
computed: {
author() {
return this.hasDeployment && this.project.last_deployment.user
? {
avatar_url: this.project.last_deployment.user.avatar_url,
path: this.project.last_deployment.user.web_url,
username: this.project.last_deployment.user.username,
}
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.hasDeployment && this.project.last_deployment.ref
return this.lastPipeline && !_.isEmpty(this.lastPipeline.ref)
? {
name: this.project.last_deployment.ref.name,
ref_url: this.project.last_deployment.ref.ref_path,
...this.lastPipeline.ref,
ref_url: this.lastPipeline.ref.path,
}
: null;
: {};
},
finishedTime() {
return (
this.lastPipeline && this.lastPipeline.details && this.lastPipeline.details.finished_at
);
},
hasDeployment() {
return this.project.last_deployment !== null;
finishedTimeTitle() {
return this.tooltipTitle(this.finishedTime);
},
lastDeployed() {
return this.hasDeployment ? this.timeFormated(this.project.last_deployment.created_at) : null;
shouldShowTimeAgo() {
return (
this.lastPipeline &&
this.lastPipeline.details &&
this.lastPipeline.details.status &&
this.lastPipeline.details.status.group !== STATUS_RUNNING &&
this.finishedTime
);
},
},
methods: {
......@@ -50,41 +94,70 @@ export default {
},
};
</script>
<template>
<div class="card">
<project-header :project="project" class="card-header" @remove="removeProject" />
<div class="card-body">
<div class="row">
<div class="col-6 col-sm-4 col-md-6 col-lg-4 pr-1">
<dashboard-alerts
:count="project.alert_count"
:last-alert="project.last_alert"
:alert-path="project.alert_path"
<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>
<template v-if="project.last_deployment">
<div class="col-6 col-sm-4 col-md-6 col-lg-4 px-1">
<commit
:commit-ref="commitRef"
:short-sha="project.last_deployment.commit.short_id"
:commit-url="project.last_deployment.commit.commit_url"
:title="project.last_deployment.commit.title"
:author="author"
:tag="project.last_deployment.tag"
<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>
<div
class="js-project-container col-12 col-sm-4 col-md-12 col-lg-4 pl-1 d-flex justify-content-end"
>
<div class="d-flex align-items-end justify-content-end">
<div class="prepend-top-default text-secondary d-flex align-items-center flex-wrap">
<icon name="calendar" class="append-right-4 js-dashboard-project-calendar-icon" />
{{ lastDeployed }}
</div>
</div>
</div>
</template>
<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>
......
<script>
import Icon from '~/vue_shared/components/icon.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 {
components: {
Icon,
ProjectAvatar,
GlButton,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -16,6 +18,25 @@ export default {
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() {
......@@ -26,27 +47,33 @@ export default {
</script>
<template>
<div class="project-header d-flex align-items-center">
<project-avatar :project="project" :size="20" class="flex-shrink-0" />
<div class="flex-grow-1">
<a class="js-project-link cgray" :href="project.web_url">
<span class="js-name-with-namespace bold"> {{ project.name_with_namespace }} </span>
</a>
<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">
<div
<gl-button
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"
:title="__('More actions')"
>
<icon name="ellipsis_v" class="text-secondary" />
</div>
</gl-button>
<ul class="dropdown-menu dropdown-menu-right">
<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>
</button>
</gl-button>
</li>
</ul>
</div>
......
......@@ -3,10 +3,6 @@
class OperationsController < ApplicationController
before_action :authorize_read_operations_dashboard!
before_action do
push_frontend_feature_flag(:pipeline_dashboard)
end
respond_to :json, only: [:list]
POLLING_INTERVAL = 120_000
......
---
title: Removing pipeline dashboard feature flag
merge_request: 10302
author:
type: added
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { shallowMount, createLocalVue } from '@vue/test-utils';
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', () => {
const AlertsComponent = Vue.extend(Alerts);
const mockPath = 'https://mock-alert_path/';
const mount = (props = {}) => mountComponentWithStore(AlertsComponent, { props });
let vm;
const AlertsComponent = localVue.extend(Alerts);
let wrapper;
beforeEach(() => {
vm = mount();
});
const mount = (propsData = {}) => shallowMount(AlertsComponent, { propsData, sync: false });
afterEach(() => {
if (vm.$destroy) {
vm.$destroy();
}
wrapper.destroy();
});
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', () => {
vm = mount({ count: 1 });
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({
wrapper = mount({
count: 1,
alertPath: mockPath,
lastAlert: mockAlert,
});
const lastAlert = vm.$el.querySelector('.js-last-alert');
const innerText = removeWhitespace(lastAlert.innerText).trim();
expect(innerText).toBe(alertMessage);
});
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');
expect(wrapper.element.querySelector('.js-alert-count').innerText.trim()).toBe('1 Alert');
});
describe('wrapped components', () => {
describe('icon', () => {
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', () => {
});
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', () => {
......@@ -62,7 +64,7 @@ describe('project header component', () => {
});
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([
{ name: 'remove', args: [mockOneProject.remove_path] },
......
import { mount, createLocalVue } from '@vue/test-utils';
import ProjectPipeline from 'ee/operations/components/dashboard/project_pipeline.vue';
import { mockPipelineData } from '../../new_mock_data';
import { mockPipelineData } from '../../mock_data';
const localVue = createLocalVue();
......
......@@ -19,9 +19,7 @@ describe('project component', () => {
sync: false,
store,
localVue,
propsData: {
project: mockOneProject,
},
propsData: { project: mockOneProject },
});
});
......@@ -39,18 +37,10 @@ describe('project component', () => {
});
describe('alerts', () => {
let alert;
beforeEach(() => {
alert = wrapper.find(Alerts);
});
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('lastAlert')).toEqual(mockOneProject.last_alert);
expect(alert.props('count')).toBe(mockOneProject.alert_count);
});
});
......@@ -66,38 +56,38 @@ describe('project component', () => {
});
it('binds short_id to shortSha', () => {
expect(commit.props().shortSha).toBe(
wrapper.props().project.last_deployment.commit.short_id,
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_deployment.commit.commit_url,
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_deployment.commit.title);
expect(commit.props('title')).toBe(wrapper.props().project.last_pipeline.commit.title);
});
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', () => {
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', () => {
it('renders calendar icon', () => {
expect(wrapper.contains('.js-dashboard-project-calendar-icon')).toBe(true);
describe('deploy finished at', () => {
it('renders clock icon', () => {
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 container = wrapper.element.querySelector('.js-project-container');
const container = wrapper.element.querySelector('.js-dashboard-project-time-ago');
expect(container.innerText.trim()).toBe(timeago);
});
......
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',
......@@ -14,52 +18,104 @@ export const mockText = {
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,
deployTimeStamp = `${new Date(Date.now() - 86400000).getTime()}`,
alertCount = 1,
isTag = false,
currentPipelineStatus = 'success',
upstreamStatus = 'success',
alertCount = 0,
) {
return Array(projectCount)
.fill(null)
.map((_, index) => ({
id: index,
name: 'mock-name',
name_with_namespace: 'mock-namespace / mock-name',
path: 'mock-path',
path_with_namespace: 'mock-path_with-namespace',
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,
last_deployment: {
created_at: deployTimeStamp,
commit: {
short_id: 'mock-short_id',
title: 'mock-title',
commit_url: 'https://mock-commit_url/',
},
tag: isTag,
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/',
},
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,
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);
......@@ -11189,9 +11189,6 @@ msgstr ""
msgid "Version"
msgstr ""
msgid "View %{alerts}"
msgstr ""
msgid "View app"
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