Commit 203c355d authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Simon Knox

Remove legacy stage table components

In https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66189 we
replaced the project VSA stage table with the shared table used
for group VSA, we can now remove all the old project level
table components
parent edab8275
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default {
components: {
userAvatarImage,
limitWarning,
totalTime,
},
props: {
items: {
type: Array,
default: () => [],
required: false,
},
stage: {
type: Object,
default: () => ({}),
required: false,
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="(mergeRequest, i) in items" :key="i" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-request-title">
<a :href="mergeRequest.url"> {{ mergeRequest.title }} </a>
</h5>
<a :href="mergeRequest.url" class="issue-link"> !{{ mergeRequest.iid }} </a> &middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date"> {{ mergeRequest.createdAt }} </a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">
{{ mergeRequest.author.name }}
</a>
</span>
</div>
<div class="item-time"><total-time :time="mergeRequest.totalTime" /></div>
</li>
</ul>
</div>
</template>
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default {
components: {
userAvatarImage,
limitWarning,
totalTime,
},
props: {
items: {
type: Array,
default: () => [],
required: false,
},
stage: {
type: Object,
default: () => ({}),
required: false,
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="(issue, i) in items" :key="i" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl" />
<h5 class="item-title issue-title">
<a :href="issue.url" class="issue-title"> {{ issue.title }} </a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> &middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link"> {{ issue.author.name }} </a>
</span>
</div>
<div class="item-time"><total-time :time="issue.totalTime" /></div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'StageNavItem',
props: {
isDefaultStage: {
type: Boolean,
default: false,
required: false,
},
isActive: {
type: Boolean,
default: false,
required: false,
},
isUserAllowed: {
type: Boolean,
required: true,
},
title: {
type: String,
required: true,
},
value: {
type: String,
default: '',
required: false,
},
},
computed: {
hasValue() {
return this.value && this.value.length > 0;
},
},
};
</script>
<template>
<li @click="$emit('select')">
<div
:class="{ active: isActive }"
class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
>
<div
class="stage-nav-item-cell stage-name w-50 pr-2"
:class="{ 'font-weight-bold': isActive }"
>
{{ title }}
</div>
<div class="stage-nav-item-cell stage-median w-50">
<template v-if="isUserAllowed">
<span v-if="hasValue">{{ value }}</span>
<span v-else class="stage-empty">{{ __('Not enough data') }}</span>
</template>
<template v-else>
<span class="not-available">{{ __('Not available') }}</span>
</template>
</div>
</div>
</li>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default {
components: {
userAvatarImage,
totalTime,
limitWarning,
GlIcon,
},
props: {
items: {
type: Array,
default: () => [],
required: false,
},
stage: {
type: Object,
default: () => ({}),
required: false,
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="(mergeRequest, i) in items" :key="i" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-request-title">
<a :href="mergeRequest.url"> {{ mergeRequest.title }} </a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> &middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{
mergeRequest.author.name
}}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<gl-icon name="cancel" class="gl-vertical-align-text-bottom" />
{{ __('CLOSED') }}
</span>
</template>
<template v-else>
<span v-if="mergeRequest.branch" class="merge-request-branch">
<gl-icon :size="16" name="fork" />
<a :href="mergeRequest.branch.url"> {{ mergeRequest.branch.name }} </a>
</span>
</template>
</div>
<div class="item-time"><total-time :time="mergeRequest.totalTime" /></div>
</li>
</ul>
</div>
</template>
<script>
import { GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default {
components: {
userAvatarImage,
totalTime,
limitWarning,
GlIcon,
},
directives: {
SafeHtml,
},
props: {
items: {
type: Array,
default: () => [],
required: false,
},
stage: {
type: Object,
default: () => ({}),
required: false,
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="(build, i) in items" :key="i" class="stage-event-item item-build-component">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl" />
<h5 class="item-title">
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
<gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
<span class="icon-branch gl-text-gray-400">
<gl-icon name="commit" :size="14" />
</span>
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
</h5>
<span>
<a :href="build.url" class="build-date"> {{ build.date }} </a> {{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link"> {{ build.author.name }} </a>
</span>
</div>
<div class="item-time"><total-time :time="build.totalTime" /></div>
</li>
</ul>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default {
components: {
totalTime,
limitWarning,
GlIcon,
},
props: {
items: {
type: Array,
default: () => [],
required: false,
},
stage: {
type: Object,
default: () => ({}),
required: false,
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="(build, i) in items" :key="i" class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status gl-text-green-500">
<gl-icon name="status_success" :size="14" />
</span>
<a :href="build.url" class="item-build-name"> {{ build.name }} </a> &middot;
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
<gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
<span class="icon-branch gl-text-gray-400">
<gl-icon name="commit" :size="14" />
</span>
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
</h5>
<span>
<a :href="build.url" class="issue-date"> {{ build.date }} </a>
</span>
</div>
<div class="item-time"><total-time :time="build.totalTime" /></div>
</li>
</ul>
</div>
</template>
export default {
issue: {
created_at: '',
url: '',
iid: '',
title: '',
total_time: {},
author: {
avatar_url: '',
id: '',
name: '',
web_url: '',
},
},
plan: {
title: '',
commit_url: '',
short_sha: '',
total_time: {},
author: {
name: '',
id: '',
avatar_url: '',
web_url: '',
},
},
code: {
title: '',
iid: '',
created_at: '',
url: '',
total_time: {},
author: {
name: '',
id: '',
avatar_url: '',
web_url: '',
},
},
test: {
name: '',
id: '',
date: '',
url: '',
short_sha: '',
commit_url: '',
total_time: {},
branch: {
name: '',
url: '',
},
},
review: {
title: '',
iid: '',
created_at: '',
url: '',
state: '',
total_time: {},
author: {
name: '',
id: '',
avatar_url: '',
web_url: '',
},
},
staging: {
id: '',
short_sha: '',
date: '',
url: '',
commit_url: '',
total_time: {},
author: {
name: '',
id: '',
avatar_url: '',
web_url: '',
},
branch: {
name: '',
url: '',
},
},
production: {
title: '',
created_at: '',
url: '',
iid: '',
total_time: {},
author: {
name: '',
id: '',
avatar_url: '',
web_url: '',
},
},
};
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
import { import { decorateData, formatMedianValues, calculateFormattedDayInPast } from '../utils';
decorateData,
decorateEvents,
formatMedianValues,
calculateFormattedDayInPast,
} from '../utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
...@@ -82,10 +77,11 @@ export default { ...@@ -82,10 +77,11 @@ export default {
state.hasError = false; state.hasError = false;
}, },
[types.RECEIVE_STAGE_DATA_SUCCESS](state, { events = [] }) { [types.RECEIVE_STAGE_DATA_SUCCESS](state, { events = [] }) {
const { selectedStage } = state;
state.isLoadingStage = false; state.isLoadingStage = false;
state.isEmptyStage = !events.length; state.isEmptyStage = !events.length;
state.selectedStageEvents = decorateEvents(events, selectedStage); state.selectedStageEvents = events.map((ev) =>
convertObjectPropsToCamelCase(ev, { deep: true }),
);
state.hasError = false; state.hasError = false;
}, },
[types.RECEIVE_STAGE_DATA_ERROR](state, error) { [types.RECEIVE_STAGE_DATA_ERROR](state, error) {
......
...@@ -2,27 +2,10 @@ import dateFormat from 'dateformat'; ...@@ -2,27 +2,10 @@ import dateFormat from 'dateformat';
import { unescape } from 'lodash'; import { unescape } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants'; import { dateFormats } from '~/analytics/shared/constants';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { roundToNearestHalf } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility'; import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility'; import { parseSeconds } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '../locale'; import { s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
/**
* These `decorate` methods will be removed when me migrate to the
* new table layout https://gitlab.com/gitlab-org/gitlab/-/issues/326704
*/
const mapToEvent = (event, stage) => {
return convertObjectPropsToCamelCase(
{
...DEFAULT_EVENT_OBJECTS[stage.slug],
...event,
},
{ deep: true },
);
};
export const decorateEvents = (events, stage) => events.map((event) => mapToEvent(event, stage));
const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' }); const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' });
const mapToMedians = ({ name: id, value }) => ({ id, value }); const mapToMedians = ({ name: id, value }) => ({ id, value });
......
...@@ -22378,9 +22378,6 @@ msgstr "" ...@@ -22378,9 +22378,6 @@ msgstr ""
msgid "Not confidential" msgid "Not confidential"
msgstr "" msgstr ""
msgid "Not enough data"
msgstr ""
msgid "Not found." msgid "Not found."
msgstr "" msgstr ""
......
import { mount, shallowMount } from '@vue/test-utils';
import StageNavItem from '~/cycle_analytics/components/stage_nav_item.vue';
describe('StageNavItem', () => {
let wrapper = null;
const title = 'Cool stage';
const value = '1 day';
function createComponent(props, shallow = true) {
const func = shallow ? shallowMount : mount;
return func(StageNavItem, {
propsData: {
isActive: false,
isUserAllowed: false,
isDefaultStage: true,
title,
value,
...props,
},
});
}
function hasStageName() {
const stageName = wrapper.find('.stage-name');
expect(stageName.exists()).toBe(true);
expect(stageName.text()).toEqual(title);
}
it('renders stage name', () => {
wrapper = createComponent({ isUserAllowed: true });
hasStageName();
wrapper.destroy();
});
describe('User has access', () => {
describe('with a value', () => {
beforeEach(() => {
wrapper = createComponent({ isUserAllowed: true });
});
afterEach(() => {
wrapper.destroy();
});
it('renders the value for median value', () => {
expect(wrapper.find('.stage-empty').exists()).toBe(false);
expect(wrapper.find('.not-available').exists()).toBe(false);
expect(wrapper.find('.stage-median').text()).toEqual(value);
});
});
describe('without a value', () => {
beforeEach(() => {
wrapper = createComponent({ isUserAllowed: true, value: null });
});
afterEach(() => {
wrapper.destroy();
});
it('has the stage-empty class', () => {
expect(wrapper.find('.stage-empty').exists()).toBe(true);
});
it('renders Not enough data for the median value', () => {
expect(wrapper.find('.stage-median').text()).toEqual('Not enough data');
});
});
});
describe('is active', () => {
beforeEach(() => {
wrapper = createComponent({ isActive: true }, false);
});
afterEach(() => {
wrapper.destroy();
});
it('has the active class', () => {
expect(wrapper.find('.stage-nav-item').classes('active')).toBe(true);
});
});
describe('is not active', () => {
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('emits the `select` event when clicked', () => {
expect(wrapper.emitted().select).toBeUndefined();
wrapper.trigger('click');
return wrapper.vm.$nextTick(() => {
expect(wrapper.emitted().select.length).toBe(1);
});
});
});
describe('User does not have access', () => {
beforeEach(() => {
wrapper = createComponent({ isUserAllowed: false }, false);
});
afterEach(() => {
wrapper.destroy();
});
it('renders stage name', () => {
hasStageName();
});
it('has class not-available', () => {
expect(wrapper.find('.stage-empty').exists()).toBe(false);
expect(wrapper.find('.not-available').exists()).toBe(true);
});
it('renders Not available for the median value', () => {
expect(wrapper.find('.stage-median').text()).toBe('Not available');
});
it('does not render options menu', () => {
expect(wrapper.find('[data-testid="more-actions-toggle"]').exists()).toBe(false);
});
});
describe('User can edit stages', () => {
beforeEach(() => {
wrapper = createComponent({ isUserAllowed: true }, false);
});
afterEach(() => {
wrapper.destroy();
});
it('renders stage name', () => {
hasStageName();
});
it('does not render options menu', () => {
expect(wrapper.find('[data-testid="more-actions-toggle"]').exists()).toBe(false);
});
it('can not edit the stage', () => {
expect(wrapper.text()).not.toContain('Edit stage');
});
it('can not remove the stage', () => {
expect(wrapper.text()).not.toContain('Remove stage');
});
it('can not hide the stage', () => {
expect(wrapper.text()).not.toContain('Hide stage');
});
});
});
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import { import {
decorateEvents,
decorateData, decorateData,
transformStagesForPathNavigation, transformStagesForPathNavigation,
timeSummaryForPathNavigation, timeSummaryForPathNavigation,
...@@ -13,7 +12,6 @@ import { ...@@ -13,7 +12,6 @@ import {
selectedStage, selectedStage,
rawData, rawData,
convertedData, convertedData,
rawEvents,
allowedStages, allowedStages,
stageMedians, stageMedians,
pathNavIssueMetric, pathNavIssueMetric,
...@@ -21,34 +19,6 @@ import { ...@@ -21,34 +19,6 @@ import {
} from './mock_data'; } from './mock_data';
describe('Value stream analytics utils', () => { describe('Value stream analytics utils', () => {
describe('decorateEvents', () => {
const [result] = decorateEvents(rawEvents, selectedStage);
const eventKeys = Object.keys(result);
const authorKeys = Object.keys(result.author);
it('will return the same number of events', () => {
expect(decorateEvents(rawEvents, selectedStage).length).toBe(rawEvents.length);
});
it('will set all the required event fields', () => {
['totalTime', 'author', 'createdAt', 'shortSha', 'commitUrl'].forEach((key) => {
expect(eventKeys).toContain(key);
});
['webUrl', 'avatarUrl'].forEach((key) => {
expect(authorKeys).toContain(key);
});
});
it('will remove unused fields', () => {
['total_time', 'created_at', 'short_sha', 'commit_url'].forEach((key) => {
expect(eventKeys).not.toContain(key);
});
['web_url', 'avatar_url'].forEach((key) => {
expect(authorKeys).not.toContain(key);
});
});
});
describe('decorateData', () => { describe('decorateData', () => {
const result = decorateData(rawData); const result = decorateData(rawData);
it('returns the summary data', () => { it('returns the summary data', () => {
......
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