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-06 00:07:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-06 00:07:32 +0300
commit353c244f5c32ad3283e9ab6ff4b599f96a010b76 (patch)
tree20d70a7bb5c6ecee9386768d0dffcbedc0d1c605 /app/assets/javascripts/analytics
parentb019ef7c4342be4c4febbb8e7afe2f6e68fc1013 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/analytics')
-rw-r--r--app/assets/javascripts/analytics/shared/components/daterange.vue121
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue217
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js3
-rw-r--r--app/assets/javascripts/analytics/shared/graphql/projects.query.graphql22
-rw-r--r--app/assets/javascripts/analytics/shared/utils.js4
5 files changed, 367 insertions, 0 deletions
diff --git a/app/assets/javascripts/analytics/shared/components/daterange.vue b/app/assets/javascripts/analytics/shared/components/daterange.vue
new file mode 100644
index 00000000000..d6250e0efad
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/components/daterange.vue
@@ -0,0 +1,121 @@
+<script>
+import { GlDaterangePicker, GlSprintf, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { getDayDifference } from '~/lib/utils/datetime_utility';
+import { __, sprintf } from '~/locale';
+import { OFFSET_DATE_BY_ONE } from '../constants';
+
+export default {
+ components: {
+ GlDaterangePicker,
+ GlSprintf,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ show: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ startDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ endDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ minDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ maxDate: {
+ type: Date,
+ required: false,
+ default() {
+ return new Date();
+ },
+ },
+ maxDateRange: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ includeSelectedDate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ maxDateRangeTooltip: sprintf(
+ __(
+ 'Showing data for workflow items created in this date range. Date range cannot exceed %{maxDateRange} days.',
+ ),
+ {
+ maxDateRange: this.maxDateRange,
+ },
+ ),
+ };
+ },
+ computed: {
+ dateRange: {
+ get() {
+ return { startDate: this.startDate, endDate: this.endDate };
+ },
+ set({ startDate, endDate }) {
+ this.$emit('change', { startDate, endDate });
+ },
+ },
+ numberOfDays() {
+ const dayDifference = getDayDifference(this.startDate, this.endDate);
+ return this.includeSelectedDate ? dayDifference + OFFSET_DATE_BY_ONE : dayDifference;
+ },
+ },
+};
+</script>
+<template>
+ <div
+ v-if="show"
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row align-items-lg-center justify-content-lg-end"
+ >
+ <gl-daterange-picker
+ v-model="dateRange"
+ class="d-flex flex-column flex-lg-row"
+ :default-start-date="startDate"
+ :default-end-date="endDate"
+ :default-min-date="minDate"
+ :max-date-range="maxDateRange"
+ :default-max-date="maxDate"
+ :same-day-selection="includeSelectedDate"
+ theme="animate-picker"
+ start-picker-class="js-daterange-picker-from gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-lg-align-items-center gl-lg-mr-3 gl-mb-2 gl-lg-mb-0"
+ end-picker-class="js-daterange-picker-to d-flex flex-column flex-lg-row align-items-lg-center"
+ label-class="gl-mb-2 gl-lg-mb-0"
+ />
+ <div
+ v-if="maxDateRange"
+ class="daterange-indicator d-flex flex-row flex-lg-row align-items-flex-start align-items-lg-center"
+ >
+ <span class="number-of-days pl-2 pr-1">
+ <gl-sprintf :message="n__('1 day', '%d days', numberOfDays)">
+ <template #numberOfDays>{{ numberOfDays }}</template>
+ </gl-sprintf>
+ </span>
+ <gl-icon
+ v-gl-tooltip
+ data-testid="helper-icon"
+ :title="maxDateRangeTooltip"
+ name="question"
+ :size="14"
+ class="text-secondary"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
new file mode 100644
index 00000000000..d58033b36c7
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -0,0 +1,217 @@
+<script>
+import {
+ GlIcon,
+ GlLoadingIcon,
+ GlAvatar,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { filterBySearchTerm } from '~/analytics/shared/utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { n__, s__, __ } from '~/locale';
+import getProjects from '../graphql/projects.query.graphql';
+
+export default {
+ name: 'ProjectsDropdownFilter',
+ components: {
+ GlIcon,
+ GlLoadingIcon,
+ GlAvatar,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ },
+ props: {
+ groupId: {
+ type: Number,
+ required: true,
+ },
+ groupNamespace: {
+ type: String,
+ required: true,
+ },
+ multiSelect: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: s__('CycleAnalytics|project dropdown filter'),
+ },
+ queryParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ defaultProjects: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ loading: true,
+ projects: [],
+ selectedProjects: this.defaultProjects || [],
+ searchTerm: '',
+ };
+ },
+ computed: {
+ selectedProjectsLabel() {
+ if (this.selectedProjects.length === 1) {
+ return this.selectedProjects[0].name;
+ } else if (this.selectedProjects.length > 1) {
+ return n__(
+ 'CycleAnalytics|Project selected',
+ 'CycleAnalytics|%d projects selected',
+ this.selectedProjects.length,
+ );
+ }
+
+ return this.selectedProjectsPlaceholder;
+ },
+ selectedProjectsPlaceholder() {
+ return this.multiSelect ? __('Select projects') : __('Select a project');
+ },
+ isOnlyOneProjectSelected() {
+ return this.selectedProjects.length === 1;
+ },
+ selectedProjectIds() {
+ return this.selectedProjects.map((p) => p.id);
+ },
+ availableProjects() {
+ return filterBySearchTerm(this.projects, this.searchTerm);
+ },
+ noResultsAvailable() {
+ const { loading, availableProjects } = this;
+ return !loading && !availableProjects.length;
+ },
+ },
+ watch: {
+ searchTerm() {
+ this.search();
+ },
+ },
+ mounted() {
+ this.search();
+ },
+ methods: {
+ search: debounce(function debouncedSearch() {
+ this.fetchData();
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
+ getSelectedProjects(selectedProject, isMarking) {
+ return isMarking
+ ? this.selectedProjects.concat([selectedProject])
+ : this.selectedProjects.filter((project) => project.id !== selectedProject.id);
+ },
+ singleSelectedProject(selectedObj, isMarking) {
+ return isMarking ? [selectedObj] : [];
+ },
+ setSelectedProjects(selectedObj, isMarking) {
+ this.selectedProjects = this.multiSelect
+ ? this.getSelectedProjects(selectedObj, isMarking)
+ : this.singleSelectedProject(selectedObj, isMarking);
+ },
+ onClick({ project, isSelected }) {
+ this.setSelectedProjects(project, !isSelected);
+ this.$emit('selected', this.selectedProjects);
+ },
+ fetchData() {
+ this.loading = true;
+
+ return this.$apollo
+ .query({
+ query: getProjects,
+ variables: {
+ groupFullPath: this.groupNamespace,
+ search: this.searchTerm,
+ ...this.queryParams,
+ },
+ })
+ .then((response) => {
+ const {
+ data: {
+ group: {
+ projects: { nodes },
+ },
+ },
+ } = response;
+
+ this.loading = false;
+ this.projects = nodes;
+ });
+ },
+ isProjectSelected(id) {
+ return this.selectedProjects ? this.selectedProjectIds.includes(id) : false;
+ },
+ getEntityId(project) {
+ return getIdFromGraphQLId(project.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ ref="projectsDropdown"
+ class="dropdown dropdown-projects"
+ toggle-class="gl-shadow-none"
+ >
+ <template #button-content>
+ <div class="gl-display-flex gl-flex-grow-1">
+ <gl-avatar
+ v-if="isOnlyOneProjectSelected"
+ :src="selectedProjects[0].avatarUrl"
+ :entity-id="getEntityId(selectedProjects[0])"
+ :entity-name="selectedProjects[0].name"
+ :size="16"
+ shape="rect"
+ :alt="selectedProjects[0].name"
+ class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2"
+ />
+ {{ selectedProjectsLabel }}
+ </div>
+ <gl-icon class="gl-ml-2" name="chevron-down" />
+ </template>
+ <gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
+ <gl-search-box-by-type v-model.trim="searchTerm" />
+
+ <gl-dropdown-item
+ v-for="project in availableProjects"
+ :key="project.id"
+ :is-check-item="true"
+ :is-checked="isProjectSelected(project.id)"
+ @click.prevent="onClick({ project, isSelected: isProjectSelected(project.id) })"
+ >
+ <div class="gl-display-flex">
+ <gl-avatar
+ class="gl-mr-2 vertical-align-middle"
+ :alt="project.name"
+ :size="16"
+ :entity-id="getEntityId(project)"
+ :entity-name="project.name"
+ :src="project.avatarUrl"
+ shape="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-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>
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
new file mode 100644
index 00000000000..4eba7a29e2c
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -0,0 +1,3 @@
+export const DATE_RANGE_LIMIT = 180;
+export const OFFSET_DATE_BY_ONE = 1;
+export const PROJECTS_PER_PAGE = 50;
diff --git a/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
new file mode 100644
index 00000000000..63e95d6804c
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
@@ -0,0 +1,22 @@
+query getGroupProjects(
+ $groupFullPath: ID!
+ $search: String!
+ $first: Int!
+ $includeSubgroups: Boolean = false
+) {
+ group(fullPath: $groupFullPath) {
+ projects(
+ search: $search
+ first: $first
+ includeSubgroups: $includeSubgroups
+ sort: SIMILARITY
+ ) {
+ nodes {
+ id
+ name
+ avatarUrl
+ fullPath
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js
new file mode 100644
index 00000000000..84189b675f2
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/utils.js
@@ -0,0 +1,4 @@
+export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
+ if (!searchTerm?.length) return data;
+ return data.filter((item) => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase()));
+};