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/analytics')
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue3
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue1
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/constants.js1
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue190
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js58
5 files changed, 138 insertions, 115 deletions
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue b/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue
index 133513d6c21..33d6eb139f7 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue
@@ -22,6 +22,7 @@ import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_t
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
+import { MAX_LABELS } from '../constants';
export default {
name: 'FilterBar',
@@ -70,6 +71,7 @@ export default {
symbol: '~',
operators: OPERATORS_IS,
fetchLabels: this.fetchLabels,
+ maxSuggestions: MAX_LABELS,
},
{
icon: 'pencil',
@@ -146,6 +148,7 @@ export default {
:search-input-placeholder="__('Filter results')"
:tokens="tokens"
:initial-filter-value="initialFilterValue()"
+ terms-as-tokens
@onFilter="handleFilter"
/>
<url-sync :query="query" />
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
index b9d1c4b0fe0..0de62013a63 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
@@ -82,6 +82,7 @@ export default {
<div>
<projects-dropdown-filter
v-if="hasProjectFilter"
+ toggle-classes="gl-max-w-26"
class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
:group-namespace="groupPath"
:query-params="projectsQueryParams"
diff --git a/app/assets/javascripts/analytics/cycle_analytics/constants.js b/app/assets/javascripts/analytics/cycle_analytics/constants.js
index bea562fb18c..c14f3cfc6c9 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/constants.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/constants.js
@@ -43,3 +43,4 @@ export const METRICS_REQUESTS = [
export const MILESTONES_ENDPOINT = '/-/milestones.json';
export const LABELS_ENDPOINT = '/-/labels.json';
+export const MAX_LABELS = 100;
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
index 98193de4a12..f881c924ae5 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -1,14 +1,5 @@
<script>
-import {
- GlIcon,
- GlLoadingIcon,
- GlAvatar,
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlSearchBoxByType,
- GlTruncate,
-} from '@gitlab/ui';
+import { GlButton, GlIcon, GlAvatar, GlCollapsibleListbox, GlTruncate } from '@gitlab/ui';
import { debounce } from 'lodash';
import { filterBySearchTerm } from '~/analytics/shared/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -18,17 +9,15 @@ import { n__, s__, __ } from '~/locale';
import getProjects from '../graphql/projects.query.graphql';
const sortByProjectName = (projects = []) => projects.sort((a, b) => a.name.localeCompare(b.name));
+const mapItemToListboxFormat = (item) => ({ ...item, value: item.id, text: item.name });
export default {
name: 'ProjectsDropdownFilter',
components: {
+ GlButton,
GlIcon,
- GlLoadingIcon,
GlAvatar,
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
GlTruncate,
},
props: {
@@ -61,6 +50,11 @@ export default {
required: false,
default: false,
},
+ toggleClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -94,6 +88,9 @@ export default {
selectedProjectIds() {
return this.selectedProjects.map((p) => p.id);
},
+ selectedListBoxItems() {
+ return this.multiSelect ? this.selectedProjectIds : this.selectedProjectIds[0];
+ },
hasSelectedProjects() {
return Boolean(this.selectedProjects.length);
},
@@ -110,6 +107,28 @@ export default {
unselectedItems() {
return this.availableProjects.filter(({ id }) => !this.selectedProjectIds.includes(id));
},
+ selectedGroupOptions() {
+ return this.selectedItems.map(mapItemToListboxFormat);
+ },
+ unSelectedGroupOptions() {
+ return this.unselectedItems.map(mapItemToListboxFormat);
+ },
+ listBoxItems() {
+ if (this.selectedGroupOptions.length === 0) {
+ return this.unSelectedGroupOptions;
+ }
+
+ return [
+ {
+ text: __('Selected'),
+ options: this.selectedGroupOptions,
+ },
+ {
+ text: __('Unselected'),
+ options: this.unSelectedGroupOptions,
+ },
+ ];
+ },
},
watch: {
searchTerm() {
@@ -129,32 +148,29 @@ export default {
search: debounce(function debouncedSearch() {
this.fetchData();
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
- getSelectedProjects(selectedProject, isSelected) {
- return isSelected
- ? this.selectedProjects.concat([selectedProject])
- : this.selectedProjects.filter((project) => project.id !== selectedProject.id);
- },
singleSelectedProject(selectedObj, isMarking) {
return isMarking ? [selectedObj] : [];
},
- setSelectedProjects(project) {
+ setSelectedProjects(payload) {
this.selectedProjects = this.multiSelect
- ? this.getSelectedProjects(project, !this.isProjectSelected(project))
- : this.singleSelectedProject(project, !this.isProjectSelected(project));
+ ? payload
+ : this.singleSelectedProject(payload, !this.isProjectSelected(payload));
},
- onClick(project) {
+ onClick(projectId) {
+ const project = this.availableProjects.find(({ id }) => id === projectId);
this.setSelectedProjects(project);
this.handleUpdatedSelectedProjects();
},
- onMultiSelectClick(project) {
- this.setSelectedProjects(project);
+ onMultiSelectClick(projectIds) {
+ const projects = this.availableProjects.filter(({ id }) => projectIds.includes(id));
+ this.setSelectedProjects(projects);
this.isDirty = true;
},
- onSelected(project) {
+ onSelected(payload) {
if (this.multiSelect) {
- this.onMultiSelectClick(project);
+ this.onMultiSelectClick(payload);
} else {
- this.onClick(project);
+ this.onClick(payload);
}
},
onHide() {
@@ -201,97 +217,67 @@ export default {
getEntityId(project) {
return getIdFromGraphQLId(project.id);
},
+ setSearchTerm(val) {
+ this.searchTerm = val;
+ },
},
AVATAR_SHAPE_OPTION_RECT,
};
</script>
<template>
- <gl-dropdown
+ <gl-collapsible-listbox
ref="projectsDropdown"
- class="dropdown dropdown-projects"
- toggle-class="gl-shadow-none gl-mb-0"
+ :header-text="__('Projects')"
+ :items="listBoxItems"
+ :reset-button-label="__('Clear All')"
:loading="loadingDefaultProjects"
- :show-clear-all="hasSelectedProjects"
- show-highlighted-items-title
- highlighted-items-title-class="gl-p-3"
- block
- @clear-all.stop="onClearAll"
- @hide="onHide"
+ :multiple="multiSelect"
+ :no-results-text="__('No matching results')"
+ :selected="selectedListBoxItems"
+ :searching="loading"
+ searchable
+ @hidden="onHide"
+ @reset="onClearAll"
+ @search="setSearchTerm"
+ @select="onSelected"
>
- <template #button-content>
- <gl-loading-icon v-if="loadingDefaultProjects" class="gl-mr-2 gl-flex-shrink-0" />
- <gl-avatar
- v-if="isOnlyOneProjectSelected"
- :src="selectedProjects[0].avatarUrl"
- :entity-id="getEntityId(selectedProjects[0])"
- :entity-name="selectedProjects[0].name"
- :size="16"
- :shape="$options.AVATAR_SHAPE_OPTION_RECT"
- :alt="selectedProjects[0].name"
- class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2 gl-flex-shrink-0"
- />
- <gl-truncate :text="selectedProjectsLabel" class="gl-min-w-0 gl-flex-grow-1" />
- <gl-icon class="gl-ml-2 gl-flex-shrink-0" name="chevron-down" />
- </template>
- <template #header>
- <gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
- <gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search')" />
- </template>
- <template #highlighted-items>
- <gl-dropdown-item
- v-for="project in selectedItems"
- :key="project.id"
- is-check-item
- :is-checked="isProjectSelected(project)"
- @click.native.capture.stop="onSelected(project)"
+ <template #toggle>
+ <gl-button
+ button-text-classes="gl-w-full gl-justify-content-space-between gl-display-flex gl-shadow-none gl-mb-0"
+ :class="['dropdown-projects', toggleClasses]"
>
- <div class="gl-display-flex">
- <gl-avatar
- class="gl-mr-2 gl-vertical-align-middle"
- :alt="project.name"
- :size="16"
- :entity-id="getEntityId(project)"
- :entity-name="project.name"
- :src="project.avatarUrl"
- :shape="$options.AVATAR_SHAPE_OPTION_RECT"
- />
- <div>
- <div data-testid="project-name">{{ project.name }}</div>
- <div class="gl-text-gray-500" data-testid="project-full-path">
- {{ project.fullPath }}
- </div>
- </div>
- </div>
- </gl-dropdown-item>
+ <gl-avatar
+ v-if="isOnlyOneProjectSelected"
+ :src="selectedProjects[0].avatarUrl"
+ :entity-id="getEntityId(selectedProjects[0])"
+ :entity-name="selectedProjects[0].name"
+ :size="16"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :alt="selectedProjects[0].name"
+ class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2 gl-flex-shrink-0"
+ />
+ <gl-truncate :text="selectedProjectsLabel" class="gl-min-w-0 gl-flex-grow-1" />
+ <gl-icon class="gl-ml-2 gl-flex-shrink-0" name="chevron-down" />
+ </gl-button>
</template>
- <gl-dropdown-item
- v-for="project in unselectedItems"
- :key="project.id"
- @click.native.capture.stop="onSelected(project)"
- >
+ <template #list-item="{ item }">
<div class="gl-display-flex">
<gl-avatar
- class="gl-mr-2 vertical-align-middle"
- :alt="project.name"
+ class="gl-mr-2 gl-vertical-align-middle"
+ :alt="item.name"
:size="16"
- :entity-id="getEntityId(project)"
- :entity-name="project.name"
- :src="project.avatarUrl"
+ :entity-id="getEntityId(item)"
+ :entity-name="item.name"
+ :src="item.avatarUrl"
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
/>
<div>
- <div data-testid="project-name" data-qa-selector="project_name">{{ project.name }}</div>
+ <div data-testid="project-name" data-qa-selector="project_name">{{ item.name }}</div>
<div class="gl-text-gray-500" data-testid="project-full-path">
- {{ project.fullPath }}
+ {{ item.fullPath }}
</div>
</div>
</div>
- </gl-dropdown-item>
- <gl-dropdown-item v-show="noResultsAvailable" class="gl-pointer-events-none text-secondary">{{
- __('No matching results')
- }}</gl-dropdown-item>
- <gl-dropdown-item v-if="loading">
- <gl-loading-icon size="lg" />
- </gl-dropdown-item>
- </gl-dropdown>
+ </template>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index c98cf90f406..25699c17b10 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -1,4 +1,5 @@
-import { masks } from '~/lib/dateformat';
+import dateFormat, { masks } from '~/lib/dateformat';
+import { nDaysBefore, getStartOfDay } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -13,12 +14,19 @@ export const dateFormats = {
month: 'mmmm',
};
+const startOfToday = getStartOfDay(new Date(), { utc: true });
+const last180Days = nDaysBefore(startOfToday, DATE_RANGE_LIMIT, { utc: true });
+const formatDateParam = (d) => dateFormat(d, dateFormats.isoDate, true);
+
export const METRIC_POPOVER_LABEL = s__('ValueStreamAnalytics|View details');
-export const KEY_METRICS = {
+export const ISSUES_COMPLETED_TYPE = 'issues_completed';
+
+export const FLOW_METRICS = {
LEAD_TIME: 'lead_time',
CYCLE_TIME: 'cycle_time',
ISSUES: 'issues',
+ ISSUES_COMPLETED: ISSUES_COMPLETED_TYPE,
COMMITS: 'commits',
DEPLOYS: 'deploys',
};
@@ -33,7 +41,7 @@ export const DORA_METRICS = {
const VSA_FLOW_METRICS_GROUP = {
key: 'key_metrics',
title: s__('ValueStreamAnalytics|Key metrics'),
- keys: Object.values(KEY_METRICS),
+ keys: Object.values(FLOW_METRICS),
};
export const VSA_METRICS_GROUPS = [VSA_FLOW_METRICS_GROUP];
@@ -46,6 +54,12 @@ export const VULNERABILITY_METRICS = {
HIGH: VULNERABILITY_HIGH_TYPE,
};
+export const MERGE_REQUEST_THROUGHPUT_TYPE = 'merge_request_throughput';
+
+export const MERGE_REQUEST_METRICS = {
+ THROUGHPUT: MERGE_REQUEST_THROUGHPUT_TYPE,
+};
+
export const METRIC_TOOLTIPS = {
[DORA_METRICS.DEPLOYMENT_FREQUENCY]: {
description: s__(
@@ -79,7 +93,7 @@ export const METRIC_TOOLTIPS = {
projectLink: '-/pipelines/charts?chart=change-failure-rate',
docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'change-failure-rate' }),
},
- [KEY_METRICS.LEAD_TIME]: {
+ [FLOW_METRICS.LEAD_TIME]: {
description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'),
groupLink: '-/analytics/value_stream_analytics',
projectLink: '-/value_stream_analytics',
@@ -87,7 +101,7 @@ export const METRIC_TOOLTIPS = {
anchor: 'view-the-lead-time-and-cycle-time-for-issues',
}),
},
- [KEY_METRICS.CYCLE_TIME]: {
+ [FLOW_METRICS.CYCLE_TIME]: {
description: s__(
"ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.",
),
@@ -97,13 +111,21 @@ export const METRIC_TOOLTIPS = {
anchor: 'view-the-lead-time-and-cycle-time-for-issues',
}),
},
- [KEY_METRICS.ISSUES]: {
+ [FLOW_METRICS.ISSUES]: {
description: s__('ValueStreamAnalytics|Number of new issues created.'),
groupLink: '-/issues_analytics',
projectLink: '-/analytics/issues_analytics',
docsLink: helpPagePath('user/analytics/issue_analytics'),
},
- [KEY_METRICS.DEPLOYS]: {
+ [FLOW_METRICS.ISSUES_COMPLETED]: {
+ description: s__('ValueStreamAnalytics|Number of issues closed by month.'),
+ groupLink: '-/analytics/value_stream_analytics',
+ projectLink: '-/value_stream_analytics',
+ docsLink: helpPagePath('user/analytics/value_streams_dashboard', {
+ anchor: 'dashboard-metrics-and-drill-down-reports',
+ }),
+ },
+ [FLOW_METRICS.DEPLOYS]: {
description: s__('ValueStreamAnalytics|Total number of deploys to production.'),
groupLink: '-/analytics/productivity_analytics',
projectLink: '-/analytics/merge_request_analytics',
@@ -111,15 +133,25 @@ export const METRIC_TOOLTIPS = {
},
[VULNERABILITY_METRICS.CRITICAL]: {
description: s__('ValueStreamAnalytics|Critical vulnerabilities over time.'),
- groupLink: '-/security/vulnerabilities',
- projectLink: '-/security/vulnerability_report',
- docsLink: helpPagePath('user/application_security/vulnerability_report/index'),
+ groupLink: '-/security/vulnerabilities?severity=CRITICAL',
+ projectLink: '-/security/vulnerability_report?severity=CRITICAL',
+ docsLink: helpPagePath('user/application_security/vulnerabilities/severities.html'),
},
[VULNERABILITY_METRICS.HIGH]: {
description: s__('ValueStreamAnalytics|High vulnerabilities over time.'),
- groupLink: '-/security/vulnerabilities',
- projectLink: '-/security/vulnerability_report',
- docsLink: helpPagePath('user/application_security/vulnerability_report/index'),
+ groupLink: '-/security/vulnerabilities?severity=HIGH',
+ projectLink: '-/security/vulnerability_report?severity=HIGH',
+ docsLink: helpPagePath('user/application_security/vulnerabilities/severities.html'),
+ },
+ [MERGE_REQUEST_METRICS.THROUGHPUT]: {
+ description: s__('ValueStreamAnalytics|The number of merge requests merged by month.'),
+ groupLink: '-/analytics/productivity_analytics',
+ projectLink: `-/analytics/merge_request_analytics?start_date=${formatDateParam(
+ last180Days,
+ )}&end_date=${formatDateParam(startOfToday)}`,
+ docsLink: helpPagePath('user/analytics/merge_request_analytics', {
+ anchor: 'view-the-number-of-merge-requests-in-a-date-range',
+ }),
},
};