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:
Diffstat (limited to 'app/assets/javascripts/cycle_analytics/components')
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue129
-rw-r--r--app/assets/javascripts/cycle_analytics/components/path_navigation.vue127
2 files changed, 181 insertions, 75 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 11a263015e4..8492f0b73e1 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -1,7 +1,8 @@
<script>
import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import { __ } from '~/locale';
import banner from './banner.vue';
import stageCodeComponent from './stage_code_component.vue';
@@ -29,6 +30,7 @@ export default {
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
'stage-nav-item': stageNavItem,
+ PathNavigation,
},
props: {
noDataSvgPath: {
@@ -52,21 +54,33 @@ export default {
'isEmptyStage',
'selectedStage',
'selectedStageEvents',
+ 'selectedStageError',
'stages',
'summary',
'startDate',
+ 'permissions',
]),
+ ...mapGetters(['pathNavigationData']),
displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
},
displayNotEnoughData() {
- const { selectedStage, isEmptyStage, isLoadingStage } = this;
- return selectedStage && isEmptyStage && !isLoadingStage;
+ return this.selectedStageReady && this.isEmptyStage;
},
displayNoAccess() {
- const { selectedStage } = this;
- return selectedStage && !selectedStage.isUserAllowed;
+ return this.selectedStageReady && !this.isUserAllowed(this.selectedStage.id);
+ },
+ selectedStageReady() {
+ return !this.isLoadingStage && this.selectedStage;
+ },
+ emptyStageTitle() {
+ return this.selectedStageError
+ ? this.selectedStageError
+ : __("We don't have enough data to show this stage.");
+ },
+ emptyStageText() {
+ return !this.selectedStageError ? this.selectedStage.emptyStageText : '';
},
},
methods: {
@@ -78,25 +92,18 @@ export default {
]),
handleDateSelect(startDate) {
this.setDateRange({ startDate });
- this.fetchCycleAnalyticsData();
},
- isActiveStage(stage) {
- return stage.slug === this.selectedStage.slug;
- },
- selectStage(stage) {
- if (this.selectedStage === stage) return;
-
+ onSelectStage(stage) {
this.setSelectedStage(stage);
- if (!stage.isUserAllowed) {
- return;
- }
-
- this.fetchStageData();
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
},
+ isUserAllowed(id) {
+ const { permissions } = this;
+ return Boolean(permissions?.[id]);
+ },
},
dayRangeOptions: [7, 30, 90],
i18n: {
@@ -106,9 +113,23 @@ export default {
</script>
<template>
<div class="cycle-analytics">
+ <path-navigation
+ v-if="selectedStageReady"
+ class="js-path-navigation gl-w-full gl-pb-2"
+ :loading="isLoading"
+ :stages="pathNavigationData"
+ :selected-stage="selectedStage"
+ :with-stage-counts="false"
+ @selected="onSelectStage"
+ />
<gl-loading-icon v-if="isLoading" size="lg" />
<div v-else class="wrapper">
- <div class="card">
+ <!--
+ We wont have access to the stage counts until we move to a default value stream
+ For now we can use the `withStageCounts` flag to ensure we don't display empty stage counts
+ Related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/326705
+ -->
+ <div class="card" data-testid="vsa-stage-overview-metrics">
<div class="card-header">{{ __('Recent Project Activity') }}</div>
<div class="d-flex justify-content-between">
<div v-for="item in summary" :key="item.title" class="gl-flex-grow-1 gl-text-center">
@@ -139,40 +160,12 @@ export default {
</div>
</div>
</div>
- <div class="stage-panel-container">
- <div class="card stage-panel">
+ <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>
- <li class="stage-header pl-5">
- <span class="stage-name font-weight-bold">{{
- s__('ProjectLifecycle|Stage')
- }}</span>
- <span
- class="has-tooltip"
- data-placement="top"
- :title="__('The phase of the development lifecycle.')"
- aria-hidden="true"
- >
- <gl-icon name="question-o" class="gl-text-gray-500" />
- </span>
- </li>
- <li class="median-header">
- <span class="stage-name font-weight-bold">{{ __('Median') }}</span>
- <span
- class="has-tooltip"
- data-placement="top"
- :title="
- __(
- 'The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.',
- )
- "
- aria-hidden="true"
- >
- <gl-icon name="question-o" class="gl-text-gray-500" />
- </span>
- </li>
- <li class="event-header pl-3">
+ <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>
@@ -187,7 +180,7 @@ export default {
<gl-icon name="question-o" class="gl-text-gray-500" />
</span>
</li>
- <li class="total-time-header pr-5 text-right">
+ <li>
<span class="stage-name font-weight-bold">{{ __('Time') }}</span>
<span
class="has-tooltip"
@@ -201,45 +194,31 @@ export default {
</ul>
</nav>
</div>
-
<div class="stage-panel-body">
- <nav class="stage-nav">
- <ul>
- <stage-nav-item
- v-for="stage in stages"
- :key="stage.title"
- :title="stage.title"
- :is-user-allowed="stage.isUserAllowed"
- :value="stage.value"
- :is-active="isActiveStage(stage)"
- @select="selectStage(stage)"
- />
- </ul>
- </nav>
- <section class="stage-events overflow-auto">
- <gl-loading-icon v-show="isLoadingStage" size="lg" />
- <template v-if="displayNoAccess">
+ <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>
- <template v-else>
- <template v-if="displayNotEnoughData">
+ <template v-else>
<gl-empty-state
+ v-if="displayNotEnoughData"
class="js-empty-state"
- :description="selectedStage.emptyStageText"
+ :description="emptyStageText"
:svg-path="noDataSvgPath"
- :title="__('We don\'t have enough data to show this stage.')"
+ :title="emptyStageTitle"
/>
- </template>
- <template v-if="displayStageEvents">
<component
:is="selectedStage.component"
+ v-if="displayStageEvents"
:stage="selectedStage"
:items="selectedStageEvents"
+ data-testid="stage-table-events"
/>
</template>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
new file mode 100644
index 00000000000..c1e33f73b13
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
@@ -0,0 +1,127 @@
+<script>
+import {
+ GlPath,
+ GlPopover,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
+import Tracking from '~/tracking';
+import { OVERVIEW_STAGE_ID } from '../constants';
+
+export default {
+ name: 'PathNavigation',
+ components: {
+ GlPath,
+ GlSkeletonLoading,
+ GlPopover,
+ },
+ directives: {
+ SafeHtml,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ stages: {
+ type: Array,
+ required: true,
+ },
+ selectedStage: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ withStageCounts: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ methods: {
+ showPopover({ id }) {
+ return id && id !== OVERVIEW_STAGE_ID;
+ },
+ hasStageCount({ stageCount = null }) {
+ return stageCount !== null;
+ },
+ onSelectStage($event) {
+ this.$emit('selected', $event);
+ this.track('click_path_navigation', {
+ extra: {
+ stage_id: $event.id,
+ },
+ });
+ },
+ },
+ popoverOptions: {
+ triggers: 'hover',
+ placement: 'bottom',
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loading v-if="loading" :lines="2" class="h-auto pt-2 pb-1" />
+ <gl-path v-else :key="selectedStage.id" :items="stages" @selected="onSelectStage">
+ <template #default="{ pathItem, pathId }">
+ <gl-popover
+ v-if="showPopover(pathItem)"
+ v-bind="$options.popoverOptions"
+ :target="pathId"
+ :css-classes="['stage-item-popover']"
+ data-testid="stage-item-popover"
+ >
+ <template #title>{{ pathItem.title }}</template>
+ <div class="gl-px-4">
+ <div class="gl-display-flex gl-justify-content-space-between">
+ <div class="gl-pr-4 gl-pb-4">
+ {{ s__('ValueStreamEvent|Stage time (median)') }}
+ </div>
+ <div class="gl-pb-4 gl-font-weight-bold">{{ pathItem.metric }}</div>
+ </div>
+ </div>
+ <div v-if="withStageCounts" class="gl-px-4">
+ <div class="gl-display-flex gl-justify-content-space-between">
+ <div class="gl-pr-4 gl-pb-4">
+ {{ s__('ValueStreamEvent|Items in stage') }}
+ </div>
+ <div class="gl-pb-4 gl-font-weight-bold">
+ <template v-if="hasStageCount(pathItem)">{{
+ n__('%d item', '%d items', pathItem.stageCount)
+ }}</template>
+ <template v-else>-</template>
+ </div>
+ </div>
+ </div>
+ <div class="gl-px-4 gl-pt-4 gl-border-t-1 gl-border-t-solid gl-border-gray-50">
+ <div
+ v-if="pathItem.startEventHtmlDescription"
+ class="gl-display-flex gl-flex-direction-row"
+ >
+ <div class="gl-display-flex gl-flex-direction-column gl-pr-4 gl-pb-4 metric-label">
+ {{ s__('ValueStreamEvent|Start') }}
+ </div>
+ <div
+ v-safe-html="pathItem.startEventHtmlDescription"
+ class="gl-display-flex gl-flex-direction-column gl-pb-4 stage-event-description"
+ ></div>
+ </div>
+ <div
+ v-if="pathItem.endEventHtmlDescription"
+ class="gl-display-flex gl-flex-direction-row"
+ >
+ <div class="gl-display-flex gl-flex-direction-column gl-pr-4 metric-label">
+ {{ s__('ValueStreamEvent|Stop') }}
+ </div>
+ <div
+ v-safe-html="pathItem.endEventHtmlDescription"
+ class="gl-display-flex gl-flex-direction-column stage-event-description"
+ ></div>
+ </div>
+ </div>
+ </gl-popover>
+ </template>
+ </gl-path>
+</template>