diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 12:08:43 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 12:08:43 +0300 |
commit | f5f1f221ba08228dbbdd7080509028a7cac2fce2 (patch) | |
tree | 7a95ad0d16829f719c429276a8ed4ddaa097392a /app/assets/javascripts/cycle_analytics | |
parent | 1ad2f1981f05721d92d04c490cfc0f234737fec1 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/cycle_analytics')
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> + · + </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">·</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 = []; |