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