Commit 3bc07e26 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch 'feat-add-new-vsa-stage-table-component' into 'master'

Add new stage table component

See merge request gitlab-org/gitlab!58738
parents 6e6e7fd3 a8336ef2
<script>
import { GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
import { NOT_ENOUGH_DATA_ERROR } from '../constants';
import TotalTime from './total_time_component.vue';
export default {
name: 'StageTableNew',
components: {
GlEmptyState,
GlIcon,
GlLink,
GlLoadingIcon,
GlTable,
TotalTime,
},
props: {
currentStage: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
stageEvents: {
type: Array,
required: true,
},
noDataSvgPath: {
type: String,
required: true,
},
emptyStateMessage: {
type: String,
required: false,
default: '',
},
},
computed: {
isEmptyStage() {
return !this.stageEvents.length;
},
emptyStateTitle() {
const { emptyStateMessage } = this;
return emptyStateMessage || NOT_ENOUGH_DATA_ERROR;
},
withBuildStatus() {
const { currentStage } = this;
return !currentStage.custom && currentStage.name.toLowerCase().trim() === 'test';
},
},
methods: {
isMrLink(url = '') {
return url.includes('/merge_request');
},
itemTitle(item) {
return item.title || item.name;
},
},
fields: [
{ key: 'issues', label: __('Issues'), thClass: 'gl-w-half' },
{ key: 'time', label: __('Time'), thClass: 'gl-w-half' },
],
};
</script>
<template>
<div data-testid="vsa-stage-table">
<gl-loading-icon v-if="isLoading" class="gl-mt-4" size="md" />
<gl-empty-state v-else-if="isEmptyStage" :title="emptyStateTitle" :svg-path="noDataSvgPath" />
<gl-table
v-else
head-variant="white"
stacked="lg"
thead-class="border-bottom"
show-empty
:fields="$options.fields"
:items="stageEvents"
:empty-text="emptyStateMessage"
>
<template #cell(issues)="{ item }">
<div data-testid="vsa-stage-event">
<div v-if="item.id" data-testid="vsa-stage-content">
<p class="gl-m-0">
<template v-if="withBuildStatus">
<span
class="icon-build-status gl-vertical-align-middle gl-text-green-500"
data-testid="vsa-stage-event-build-status"
>
<gl-icon name="status_success" :size="14" />
</span>
<gl-link
class="gl-text-black-normal item-build-name"
data-testid="vsa-stage-event-build-name"
:href="item.url"
>
{{ item.name }}
</gl-link>
&middot;
</template>
<gl-link class="gl-text-black-normal pipeline-id" :href="item.url"
>#{{ item.id }}</gl-link
>
<gl-icon :size="16" name="fork" />
<gl-link
v-if="item.branch"
:href="item.branch.url"
class="gl-text-black-normal ref-name"
>{{ item.branch.name }}</gl-link
>
<span class="icon-branch gl-text-gray-400">
<gl-icon name="commit" :size="14" />
</span>
<gl-link
class="commit-sha"
:href="item.commitUrl"
data-testid="vsa-stage-event-build-sha"
>{{ item.shortSha }}</gl-link
>
</p>
<p class="gl-m-0">
<span v-if="withBuildStatus" data-testid="vsa-stage-event-build-status-date">
<gl-link class="gl-text-black-normal issue-date" :href="item.url">{{
item.date
}}</gl-link>
</span>
<span v-else data-testid="vsa-stage-event-build-author-and-date">
<gl-link class="gl-text-black-normal build-date" :href="item.url">{{
item.date
}}</gl-link>
{{ s__('ByAuthor|by') }}
<gl-link
class="gl-text-black-normal issue-author-link"
:href="item.author.webUrl"
>{{ item.author.name }}</gl-link
>
</span>
</p>
</div>
<div v-else data-testid="vsa-stage-content">
<h5 class="gl-font-weight-bold gl-my-1" data-testid="vsa-stage-event-title">
<gl-link class="gl-text-black-normal" :href="item.url">{{ itemTitle(item) }}</gl-link>
</h5>
<p class="gl-m-0">
<template v-if="isMrLink(item.url)">
<gl-link class="gl-text-black-normal" :href="item.url">!{{ item.iid }}</gl-link>
</template>
<template v-else>
<gl-link class="gl-text-black-normal" :href="item.url">#{{ item.iid }}</gl-link>
</template>
<span class="gl-font-lg">&middot;</span>
<span data-testid="vsa-stage-event-date">
{{ s__('OpenedNDaysAgo|Opened') }}
<gl-link class="gl-text-black-normal" :href="item.url">{{
item.createdAt
}}</gl-link>
</span>
<span data-testid="vsa-stage-event-author">
{{ s__('ByAuthor|by') }}
<gl-link class="gl-text-black-normal" :href="item.author.webUrl">{{
item.author.name
}}</gl-link>
</span>
</p>
</div>
</div>
</template>
<template #cell(time)="{ item }">
<total-time :time="item.totalTime" data-testid="vsa-stage-event-time" />
</template>
</gl-table>
</div>
</template>
...@@ -46,7 +46,7 @@ export default { ...@@ -46,7 +46,7 @@ export default {
<template> <template>
<span class="total-time"> <span class="total-time">
<template v-if="hasData"> <template v-if="hasData">
{{ calculatedTime.duration }} <span> {{ calculatedTime.units }} </span> {{ calculatedTime.duration }} <span>{{ calculatedTime.units }}</span>
</template> </template>
<template v-else> -- </template> <template v-else> -- </template>
</span> </span>
......
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
export const PROJECTS_PER_PAGE = 50; export const PROJECTS_PER_PAGE = 50;
...@@ -69,3 +69,7 @@ export const OVERVIEW_STAGE_CONFIG = { ...@@ -69,3 +69,7 @@ export const OVERVIEW_STAGE_CONFIG = {
title: __('Overview'), title: __('Overview'),
icon: 'home', icon: 'home',
}; };
export const NOT_ENOUGH_DATA_ERROR = s__(
"ValueStreamAnalyticsStage|We don't have enough data to show this stage.",
);
...@@ -4,25 +4,25 @@ exports[`TotalTimeComponent with a blank object to render -- 1`] = `"<span class ...@@ -4,25 +4,25 @@ exports[`TotalTimeComponent with a blank object to render -- 1`] = `"<span class
exports[`TotalTimeComponent with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = ` exports[`TotalTimeComponent with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = `
"<span class=\\"total-time\\"> "<span class=\\"total-time\\">
3 <span> days </span></span>" 3 <span>days</span></span>"
`; `;
exports[`TotalTimeComponent with a valid time object with {"hours": 7, "mins": 20, "seconds": 10} 1`] = ` exports[`TotalTimeComponent with a valid time object with {"hours": 7, "mins": 20, "seconds": 10} 1`] = `
"<span class=\\"total-time\\"> "<span class=\\"total-time\\">
7 <span> hrs </span></span>" 7 <span>hrs</span></span>"
`; `;
exports[`TotalTimeComponent with a valid time object with {"hours": 23, "mins": 10} 1`] = ` exports[`TotalTimeComponent with a valid time object with {"hours": 23, "mins": 10} 1`] = `
"<span class=\\"total-time\\"> "<span class=\\"total-time\\">
23 <span> hrs </span></span>" 23 <span>hrs</span></span>"
`; `;
exports[`TotalTimeComponent with a valid time object with {"mins": 47, "seconds": 3} 1`] = ` exports[`TotalTimeComponent with a valid time object with {"mins": 47, "seconds": 3} 1`] = `
"<span class=\\"total-time\\"> "<span class=\\"total-time\\">
47 <span> mins </span></span>" 47 <span>mins</span></span>"
`; `;
exports[`TotalTimeComponent with a valid time object with {"seconds": 35} 1`] = ` exports[`TotalTimeComponent with a valid time object with {"seconds": 35} 1`] = `
"<span class=\\"total-time\\"> "<span class=\\"total-time\\">
35 <span> s </span></span>" 35 <span>s</span></span>"
`; `;
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import StageTableNew from 'ee/analytics/cycle_analytics/components/stage_table_new.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
stagingEvents,
stagingStage,
issueEvents,
issueStage,
testEvents,
testStage,
} from '../mock_data';
let wrapper = null;
const noDataSvgPath = 'path/to/no/data';
const emptyStateMessage = 'Too much data';
const notEnoughDataError = "We don't have enough data to show this stage.";
const [firstIssueEvent] = issueEvents;
const [firstStagingEvent] = stagingEvents;
const [firstTestEvent] = testEvents;
const findStageEvents = () => wrapper.findAllByTestId('vsa-stage-event');
const findStageEventTitle = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-title');
function createComponent(props = {}, shallow = false) {
const func = shallow ? shallowMount : mount;
return extendedWrapper(
func(StageTableNew, {
propsData: {
isLoading: false,
stageEvents: issueEvents,
noDataSvgPath,
currentStage: issueStage,
...props,
},
stubs: {
GlLoadingIcon,
GlEmptyState,
},
}),
);
}
describe('StageTable', () => {
afterEach(() => {
wrapper.destroy();
});
describe('is loaded with data', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('will render the correct events', () => {
const evs = findStageEvents();
expect(evs).toHaveLength(issueEvents.length);
const titles = evs.wrappers.map((ev) => findStageEventTitle(ev).text());
issueEvents.forEach((ev, index) => {
expect(titles[index]).toBe(ev.title);
});
});
it('will not display the default data message', () => {
expect(wrapper.html()).not.toContain(notEnoughDataError);
});
});
describe('default event', () => {
beforeEach(() => {
wrapper = createComponent({
stageEvents: [{ ...firstIssueEvent }],
currentStage: { ...issueStage, custom: false },
});
});
it('will render the event title', () => {
expect(wrapper.findByTestId('vsa-stage-event-title').text()).toBe(firstIssueEvent.title);
});
it('does not render the fork icon', () => {
expect(wrapper.findByTestId('fork-icon').exists()).toBe(false);
});
it('does not render the branch icon', () => {
expect(wrapper.findByTestId('commit-icon').exists()).toBe(false);
});
it('will render the total time', () => {
expect(wrapper.findByTestId('vsa-stage-event-time').text()).toBe('2 days');
});
it('will render the author', () => {
expect(wrapper.findByTestId('vsa-stage-event-author').text()).toContain(
firstIssueEvent.author.name,
);
});
it('will render the created at date', () => {
expect(wrapper.findByTestId('vsa-stage-event-date').text()).toContain(
firstIssueEvent.createdAt,
);
});
});
describe('staging event', () => {
beforeEach(() => {
wrapper = createComponent({
stageEvents: [{ ...firstStagingEvent }],
currentStage: { ...stagingStage, custom: false },
});
});
it('will not render the event title', () => {
expect(wrapper.findByTestId('vsa-stage-event-title').exists()).toBe(false);
});
it('will render the fork icon', () => {
expect(wrapper.findByTestId('fork-icon').exists()).toBe(true);
});
it('will render the branch icon', () => {
expect(wrapper.findByTestId('commit-icon').exists()).toBe(true);
});
it('will render the total time', () => {
expect(wrapper.findByTestId('vsa-stage-event-time').text()).toBe('2 mins');
});
it('will render the build shortSha', () => {
expect(wrapper.findByTestId('vsa-stage-event-build-sha').text()).toBe(
firstStagingEvent.shortSha,
);
});
it('will render the author and date', () => {
const content = wrapper.findByTestId('vsa-stage-event-build-author-and-date').text();
expect(content).toContain(firstStagingEvent.author.name);
expect(content).toContain(firstStagingEvent.date);
});
});
describe('test event', () => {
beforeEach(() => {
wrapper = createComponent({
stageEvents: [{ ...firstTestEvent }],
currentStage: { ...testStage, custom: false },
});
});
it('will not render the event title', () => {
expect(wrapper.findByTestId('vsa-stage-event-title').exists()).toBe(false);
});
it('will render the fork icon', () => {
expect(wrapper.findByTestId('fork-icon').exists()).toBe(true);
});
it('will render the branch icon', () => {
expect(wrapper.findByTestId('commit-icon').exists()).toBe(true);
});
it('will render the total time', () => {
expect(wrapper.findByTestId('vsa-stage-event-time').text()).toBe('2 mins');
});
it('will render the build shortSha', () => {
expect(wrapper.findByTestId('vsa-stage-event-build-sha').text()).toBe(
firstTestEvent.shortSha,
);
});
it('will render the build pipeline success icon', () => {
expect(wrapper.findByTestId('status_success-icon').exists()).toBe(true);
});
it('will render the build date', () => {
const content = wrapper.findByTestId('vsa-stage-event-build-status-date').text();
expect(content).toContain(firstTestEvent.date);
});
it('will render the build event name', () => {
expect(wrapper.findByTestId('vsa-stage-event-build-name').text()).toContain(
firstTestEvent.name,
);
});
});
it('isLoading = true', () => {
wrapper = createComponent({ isLoading: true }, true);
expect(wrapper.find(GlLoadingIcon).exists()).toEqual(true);
});
describe('with no stageEvents', () => {
beforeEach(() => {
wrapper = createComponent({ stageEvents: [] });
});
it('will render the empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
});
it('will display the default no data message', () => {
expect(wrapper.html()).toContain(notEnoughDataError);
});
});
describe('emptyStateMessage set', () => {
beforeEach(() => {
wrapper = createComponent({ stageEvents: [], emptyStateMessage });
});
it('will display the custom message', () => {
expect(wrapper.html()).not.toContain(notEnoughDataError);
expect(wrapper.html()).toContain(emptyStateMessage);
});
});
});
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