Commit 7d1b05db authored by Mark Florian's avatar Mark Florian

Merge branch '198626-operations-dashboard-empty-state' into 'master'

Updates the operations empty state

Closes #219763

See merge request gitlab-org/gitlab!32536
parents 43efc7a4 1084710e
<script>
import { mapState, mapActions } from 'vuex';
import { GlModal, GlModalDirective, GlDeprecatedButton, GlDashboardSkeleton } from '@gitlab/ui';
import {
GlDashboardSkeleton,
GlButton,
GlEmptyState,
GlLink,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import VueDraggable from 'vuedraggable';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import DashboardProject from './project.vue';
......@@ -8,9 +15,11 @@ import DashboardProject from './project.vue';
export default {
components: {
DashboardProject,
GlModal,
GlDashboardSkeleton,
GlDeprecatedButton,
GlButton,
GlEmptyState,
GlLink,
GlModal,
ProjectSelector,
VueDraggable,
},
......@@ -131,13 +140,15 @@ export default {
<h1 class="js-dashboard-title page-title text-nowrap flex-fill">
{{ s__('OperationsDashboard|Operations Dashboard') }}
</h1>
<gl-deprecated-button
<gl-button
v-if="projects.length"
v-gl-modal="$options.modalId"
class="js-add-projects-button btn btn-success"
variant="success"
category="primary"
data-testid="add-projects-button"
>
{{ s__('OperationsDashboard|Add projects') }}
</gl-deprecated-button>
</gl-button>
</div>
<div class="prepend-top-default">
<vue-draggable
......@@ -150,34 +161,35 @@ export default {
<dashboard-project :project="project" />
</div>
</vue-draggable>
<div v-else-if="!isLoadingProjects" class="row prepend-top-20 text-center">
<div class="col-12 d-flex justify-content-center svg-content">
<img :src="emptyDashboardSvgPath" class="js-empty-state-svg col-12 prepend-top-20" />
</div>
<h4 class="js-title col-12 prepend-top-20">
{{ s__('OperationsDashboard|Add a project to the dashboard') }}
</h4>
<div class="col-12 d-flex justify-content-center">
<span class="js-sub-title mw-460 text-tertiary text-left">
{{
s__(`OperationsDashboard|The operations dashboard provides a summary of each project's
operational health, including pipeline and alert statuses.`)
}}
<a :href="emptyDashboardHelpPath" class="js-documentation-link">
{{ s__('OperationsDashboard|More information') }}
</a>
</span>
</div>
<div class="col-12">
<gl-deprecated-button
<gl-dashboard-skeleton v-else-if="isLoadingProjects" />
<gl-empty-state
v-else
:title="s__(`OperationsDashboard|Add a project to the dashboard`)"
:svg-path="emptyDashboardSvgPath"
>
<template #description>
{{
s__(
`OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses.`,
)
}}
<gl-link :href="emptyDashboardHelpPath" data-testid="documentation-link">{{
s__('OperationsDashboard|More information')
}}</gl-link
>.
</template>
<template #actions>
<gl-button
v-gl-modal="$options.modalId"
class="js-add-projects-button btn btn-success prepend-top-default append-bottom-default"
variant="success"
data-testid="add-projects-button"
>
{{ s__('OperationsDashboard|Add projects') }}
</gl-deprecated-button>
</div>
</div>
<gl-dashboard-skeleton v-else />
</gl-button>
</template>
</gl-empty-state>
</div>
</div>
</template>
import MockAdapter from 'axios-mock-adapter';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlEmptyState } from '@gitlab/ui';
import Project from 'ee/operations/components/dashboard/project.vue';
import Dashboard from 'ee/operations/components/dashboard/dashboard.vue';
import createStore from 'ee/vue_shared/dashboards/store';
import waitForPromises from 'helpers/wait_for_promises';
import { trimText } from 'helpers/text_helper';
import axios from '~/lib/utils/axios_utils';
import { mockProjectData, mockText } from '../../mock_data';
......@@ -19,18 +19,26 @@ describe('dashboard component', () => {
let wrapper;
let mockAxios;
const mountComponent = () =>
const emptyDashboardHelpPath = '/help/user/operations_dashboard/index.html';
const emptyDashboardSvgPath = '/assets/illustrations/operations-dashboard_empty.svg';
const mountComponent = ({ stubs = {}, state = {} } = {}) =>
mount(Dashboard, {
store,
localVue,
propsData: {
addPath: mockAddEndpoint,
listPath: mockListEndpoint,
emptyDashboardSvgPath: '/assets/illustrations/operations-dashboard_empty.svg',
emptyDashboardHelpPath: '/help/user/operations_dashboard/index.html',
emptyDashboardSvgPath,
emptyDashboardHelpPath,
},
state,
stubs,
});
const findEmptyState = () => wrapper.find(GlEmptyState);
const findAddProjectButton = () => wrapper.find('[data-testid=add-projects-button]');
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: mockProjectData(1) });
......@@ -52,17 +60,11 @@ describe('dashboard component', () => {
let button;
beforeEach(() => {
button = wrapper.element.querySelector('.js-add-projects-button');
button = findAddProjectButton();
});
it('renders add projects text', () => {
expect(button.innerText.trim()).toBe(mockText.ADD_PROJECTS);
});
it('renders the projects modal', () => {
button.click();
expect(wrapper.element.querySelector('.add-projects-modal')).toBeDefined();
expect(button.text()).toBe(mockText.ADD_PROJECTS);
});
describe('when a project is added', () => {
......@@ -204,42 +206,28 @@ describe('dashboard component', () => {
});
});
describe('empty state', () => {
describe('when no projects have been added', () => {
beforeEach(() => {
store.state.projects = [];
mockAxios.reset();
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: [] });
wrapper = mountComponent();
});
it('renders empty state svg after requesting projects with no results', () => {
const svgSrc = wrapper.element.querySelector('.js-empty-state-svg').src;
expect(svgSrc).toMatch(mockText.EMPTY_SVG_SOURCE);
});
it('renders title', () => {
expect(wrapper.element.querySelector('.js-title').innerText.trim()).toBe(
mockText.EMPTY_TITLE,
);
store.state.isLoadingProjects = false;
});
it('renders sub-title', () => {
expect(trimText(wrapper.element.querySelector('.js-sub-title').innerText)).toBe(
mockText.EMPTY_SUBTITLE,
);
it('should render the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('renders link to documentation', () => {
const link = wrapper.element.querySelector('.js-documentation-link');
it('should link to the documentation', () => {
const link = findEmptyState().find('[data-testid="documentation-link"]');
expect(link.innerText.trim()).toBe('More information');
expect(link.exists()).toBe(true);
expect(link.attributes().href).toEqual(emptyDashboardHelpPath);
});
it('links to documentation', () => {
const link = wrapper.element.querySelector('.js-documentation-link');
it('should render the add projects button', () => {
const button = findAddProjectButton();
expect(link.href).toMatch(wrapper.props().emptyDashboardHelpPath);
expect(button.exists()).toBe(true);
expect(button.text()).toEqual('Add projects');
});
});
});
......
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