Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 12:08:43 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 12:08:43 +0300
commitf5f1f221ba08228dbbdd7080509028a7cac2fce2 (patch)
tree7a95ad0d16829f719c429276a8ed4ddaa097392a /app/assets/javascripts/cycle_analytics
parent1ad2f1981f05721d92d04c490cfc0f234737fec1 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/cycle_analytics')
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue107
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_table.vue305
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.vue50
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js15
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js8
5 files changed, 381 insertions, 104 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 8492f0b73e1..e637bd0d819 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -1,16 +1,10 @@
<script>
-import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
+import StageTable from '~/cycle_analytics/components/stage_table.vue';
import { __ } from '~/locale';
-import banner from './banner.vue';
-import stageCodeComponent from './stage_code_component.vue';
-import stageComponent from './stage_component.vue';
-import stageNavItem from './stage_nav_item.vue';
-import stageReviewComponent from './stage_review_component.vue';
-import stageStagingComponent from './stage_staging_component.vue';
-import stageTestComponent from './stage_test_component.vue';
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
@@ -18,19 +12,10 @@ export default {
name: 'CycleAnalytics',
components: {
GlIcon,
- GlEmptyState,
GlLoadingIcon,
GlSprintf,
- banner,
- 'stage-issue-component': stageComponent,
- 'stage-plan-component': stageComponent,
- 'stage-code-component': stageCodeComponent,
- 'stage-test-component': stageTestComponent,
- 'stage-review-component': stageReviewComponent,
- 'stage-staging-component': stageStagingComponent,
- 'stage-production-component': stageComponent,
- 'stage-nav-item': stageNavItem,
PathNavigation,
+ StageTable,
},
props: {
noDataSvgPath: {
@@ -75,12 +60,20 @@ export default {
return !this.isLoadingStage && this.selectedStage;
},
emptyStageTitle() {
+ if (this.displayNoAccess) {
+ return __('You need permission.');
+ }
return this.selectedStageError
? this.selectedStageError
: __("We don't have enough data to show this stage.");
},
emptyStageText() {
- return !this.selectedStageError ? this.selectedStage.emptyStageText : '';
+ if (this.displayNoAccess) {
+ return __('Want to see the data? Please ask an administrator for access.');
+ }
+ return !this.selectedStageError && this.selectedStage?.emptyStageText
+ ? this.selectedStage?.emptyStageText
+ : '';
},
},
methods: {
@@ -160,72 +153,16 @@ export default {
</div>
</div>
</div>
- <div class="stage-panel-container" data-testid="vsa-stage-table">
- <div class="card stage-panel gl-px-5">
- <div class="card-header border-bottom-0">
- <nav class="col-headers">
- <ul class="gl-display-flex gl-justify-content-space-between gl-list-style-none">
- <li>
- <span v-if="selectedStage" class="stage-name font-weight-bold">{{
- selectedStage.legend ? __(selectedStage.legend) : __('Related Issues')
- }}</span>
- <span
- class="has-tooltip"
- data-placement="top"
- :title="
- __('The collection of events added to the data gathered for that stage.')
- "
- aria-hidden="true"
- >
- <gl-icon name="question-o" class="gl-text-gray-500" />
- </span>
- </li>
- <li>
- <span class="stage-name font-weight-bold">{{ __('Time') }}</span>
- <span
- class="has-tooltip"
- data-placement="top"
- :title="__('The time taken by each data entry gathered by that stage.')"
- aria-hidden="true"
- >
- <gl-icon name="question-o" class="gl-text-gray-500" />
- </span>
- </li>
- </ul>
- </nav>
- </div>
- <div class="stage-panel-body">
- <section class="stage-events gl-overflow-auto gl-w-full">
- <gl-loading-icon v-if="isLoadingStage" size="lg" />
- <template v-else>
- <gl-empty-state
- v-if="displayNoAccess"
- class="js-empty-state"
- :title="__('You need permission.')"
- :svg-path="noAccessSvgPath"
- :description="__('Want to see the data? Please ask an administrator for access.')"
- />
- <template v-else>
- <gl-empty-state
- v-if="displayNotEnoughData"
- class="js-empty-state"
- :description="emptyStageText"
- :svg-path="noDataSvgPath"
- :title="emptyStageTitle"
- />
- <component
- :is="selectedStage.component"
- v-if="displayStageEvents"
- :stage="selectedStage"
- :items="selectedStageEvents"
- data-testid="stage-table-events"
- />
- </template>
- </template>
- </section>
- </div>
- </div>
- </div>
+ <stage-table
+ :is-loading="isLoading || isLoadingStage"
+ :stage-events="selectedStageEvents"
+ :selected-stage="selectedStage"
+ :stage-count="null"
+ :empty-state-title="emptyStageTitle"
+ :empty-state-message="emptyStageText"
+ :no-data-svg-path="noDataSvgPath"
+ :pagination="null"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
new file mode 100644
index 00000000000..2e225d90f9c
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
@@ -0,0 +1,305 @@
+<script>
+import {
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlPagination,
+ GlTable,
+ GlBadge,
+} from '@gitlab/ui';
+import FormattedStageCount from '~/cycle_analytics/components/formatted_stage_count.vue';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
+import {
+ NOT_ENOUGH_DATA_ERROR,
+ PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
+ PAGINATION_SORT_DIRECTION_ASC,
+ PAGINATION_SORT_DIRECTION_DESC,
+ STAGE_TITLE_STAGING,
+ STAGE_TITLE_TEST,
+} from '../constants';
+import TotalTime from './total_time_component.vue';
+
+const DEFAULT_WORKFLOW_TITLE_PROPERTIES = {
+ thClass: 'gl-w-half',
+ key: PAGINATION_SORT_FIELD_END_EVENT,
+ sortable: true,
+};
+const WORKFLOW_COLUMN_TITLES = {
+ issues: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Issues') },
+ jobs: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Jobs') },
+ deployments: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Deployments') },
+ mergeRequests: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Merge requests') },
+};
+
+export default {
+ name: 'StageTable',
+ components: {
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlPagination,
+ GlTable,
+ GlBadge,
+ TotalTime,
+ FormattedStageCount,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ selectedStage: {
+ type: Object,
+ required: false,
+ default: () => ({ custom: false }),
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ stageEvents: {
+ type: Array,
+ required: true,
+ },
+ stageCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ noDataSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyStateTitle: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ emptyStateMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ pagination: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ if (this.pagination) {
+ const {
+ pagination: { sort, direction },
+ } = this;
+ return {
+ sort,
+ direction,
+ sortDesc: direction === PAGINATION_SORT_DIRECTION_DESC,
+ };
+ }
+ return { sort: null, direction: null, sortDesc: null };
+ },
+ computed: {
+ isEmptyStage() {
+ return !this.stageEvents.length;
+ },
+ emptyStateTitleText() {
+ return this.emptyStateTitle || NOT_ENOUGH_DATA_ERROR;
+ },
+ isDefaultTestStage() {
+ const { selectedStage } = this;
+ return (
+ !selectedStage.custom && selectedStage.title?.toLowerCase().trim() === STAGE_TITLE_TEST
+ );
+ },
+ isDefaultStagingStage() {
+ const { selectedStage } = this;
+ return (
+ !selectedStage.custom && selectedStage.title?.toLowerCase().trim() === STAGE_TITLE_STAGING
+ );
+ },
+ isMergeRequestStage() {
+ const [firstEvent] = this.stageEvents;
+ return this.isMrLink(firstEvent.url);
+ },
+ workflowTitle() {
+ if (this.isDefaultTestStage) {
+ return WORKFLOW_COLUMN_TITLES.jobs;
+ } else if (this.isDefaultStagingStage) {
+ return WORKFLOW_COLUMN_TITLES.deployments;
+ } else if (this.isMergeRequestStage) {
+ return WORKFLOW_COLUMN_TITLES.mergeRequests;
+ }
+ return WORKFLOW_COLUMN_TITLES.issues;
+ },
+ fields() {
+ return [
+ this.workflowTitle,
+ {
+ key: PAGINATION_SORT_FIELD_DURATION,
+ label: __('Time'),
+ thClass: 'gl-w-half',
+ sortable: true,
+ },
+ ];
+ },
+ prevPage() {
+ return Math.max(this.pagination.page - 1, 0);
+ },
+ nextPage() {
+ return this.pagination.hasNextPage ? this.pagination.page + 1 : null;
+ },
+ },
+ methods: {
+ isMrLink(url = '') {
+ return url.includes('/merge_request');
+ },
+ itemId({ url, iid }) {
+ return this.isMrLink(url) ? `!${iid}` : `#${iid}`;
+ },
+ itemTitle(item) {
+ return item.title || item.name;
+ },
+ onSelectPage(page) {
+ const { sort, direction } = this.pagination;
+ this.track('click_button', { label: 'pagination' });
+ this.$emit('handleUpdatePagination', { sort, direction, page });
+ },
+ onSort({ sortBy, sortDesc }) {
+ const direction = sortDesc ? PAGINATION_SORT_DIRECTION_DESC : PAGINATION_SORT_DIRECTION_ASC;
+ this.sort = sortBy;
+ this.sortDesc = sortDesc;
+ this.$emit('handleUpdatePagination', { sort: sortBy, direction });
+ this.track('click_button', { label: `sort_${sortBy}_${direction}` });
+ },
+ },
+};
+</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="emptyStateTitleText"
+ :description="emptyStateMessage"
+ :svg-path="noDataSvgPath"
+ />
+ <gl-table
+ v-else
+ head-variant="white"
+ stacked="lg"
+ thead-class="border-bottom"
+ show-empty
+ :sort-by.sync="sort"
+ :sort-direction.sync="direction"
+ :sort-desc.sync="sortDesc"
+ :fields="fields"
+ :items="stageEvents"
+ :empty-text="emptyStateMessage"
+ @sort-changed="onSort"
+ >
+ <template v-if="stageCount" #head(end_event)="data">
+ <span>{{ data.label }}</span
+ ><gl-badge class="gl-ml-2" size="sm"
+ ><formatted-stage-count :stage-count="stageCount"
+ /></gl-badge>
+ </template>
+ <template #cell(end_event)="{ 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="isDefaultTestStage">
+ <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="isDefaultTestStage" 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">
+ <gl-link class="gl-text-black-normal" :href="item.url">{{ itemId(item) }}</gl-link>
+ <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(duration)="{ item }">
+ <total-time :time="item.totalTime" data-testid="vsa-stage-event-time" />
+ </template>
+ </gl-table>
+ <gl-pagination
+ v-if="pagination && !isLoading && !isEmptyStage"
+ :value="pagination.page"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-mt-3"
+ data-testid="vsa-stage-pagination"
+ @input="onSelectPage"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
index f52438ca2cb..a5a90a56974 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
@@ -1,4 +1,6 @@
<script>
+import { n__, s__ } from '~/locale';
+
export default {
props: {
time: {
@@ -11,24 +13,48 @@ export default {
hasData() {
return Object.keys(this.time).length;
},
+ calculatedTime() {
+ const {
+ time: { days = null, mins = null, hours = null, seconds = null },
+ } = this;
+
+ if (days) {
+ return {
+ duration: days,
+ units: n__('day', 'days', days),
+ };
+ }
+
+ if (hours) {
+ return {
+ duration: hours,
+ units: n__('Time|hr', 'Time|hrs', hours),
+ };
+ }
+
+ if (mins && !days) {
+ return {
+ duration: mins,
+ units: n__('Time|min', 'Time|mins', mins),
+ };
+ }
+
+ if ((seconds && this.hasData === 1) || seconds === 0) {
+ return {
+ duration: seconds,
+ units: s__('Time|s'),
+ };
+ }
+
+ return { duration: null, units: null };
+ },
},
};
</script>
<template>
<span class="total-time">
<template v-if="hasData">
- <template v-if="time.days">
- {{ time.days }} <span> {{ n__('day', 'days', time.days) }} </span>
- </template>
- <template v-if="time.hours">
- {{ time.hours }} <span> {{ n__('Time|hr', 'Time|hrs', time.hours) }} </span>
- </template>
- <template v-if="time.mins && !time.days">
- {{ time.mins }} <span> {{ n__('Time|min', 'Time|mins', time.mins) }} </span>
- </template>
- <template v-if="(time.seconds && hasData === 1) || time.seconds === 0">
- {{ time.seconds }} <span> {{ s__('Time|s') }} </span>
- </template>
+ {{ calculatedTime.duration }} <span>{{ calculatedTime.units }}</span>
</template>
<template v-else> -- </template>
</span>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index 97f502326e5..755977f87df 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -1,3 +1,5 @@
+import { s__ } from '~/locale';
+
export const DEFAULT_DAYS_IN_PAST = 30;
export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
@@ -7,3 +9,16 @@ export const DEFAULT_VALUE_STREAM = {
slug: 'default',
name: 'default',
};
+
+export const NOT_ENOUGH_DATA_ERROR = s__(
+ "ValueStreamAnalyticsStage|We don't have enough data to show this stage.",
+);
+
+export const PAGINATION_TYPE = 'keyset';
+export const PAGINATION_SORT_FIELD_END_EVENT = 'end_event';
+export const PAGINATION_SORT_FIELD_DURATION = 'duration';
+export const PAGINATION_SORT_DIRECTION_DESC = 'desc';
+export const PAGINATION_SORT_DIRECTION_ASC = 'asc';
+
+export const STAGE_TITLE_STAGING = 'staging';
+export const STAGE_TITLE_TEST = 'test';
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js
index a8b7a607b66..118d5174fd0 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutations.js
@@ -47,13 +47,7 @@ export default {
state.stages = [];
},
[types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS](state, { stages = [] }) {
- state.stages = stages.map((s) => ({
- ...convertObjectPropsToCamelCase(s, { deep: true }),
- // NOTE: we set the component type here to match the current behaviour
- // this can be removed when we migrate to the update stage table
- // https://gitlab.com/gitlab-org/gitlab/-/issues/326704
- component: `stage-${s.id}-component`,
- }));
+ state.stages = stages.map((s) => convertObjectPropsToCamelCase(s, { deep: true }));
},
[types.RECEIVE_VALUE_STREAM_STAGES_ERROR](state) {
state.stages = [];