diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-05 21:07:32 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-05 21:07:32 +0000 |
commit | 353c244f5c32ad3283e9ab6ff4b599f96a010b76 (patch) | |
tree | 20d70a7bb5c6ecee9386768d0dffcbedc0d1c605 /app/assets/javascripts/analytics | |
parent | b019ef7c4342be4c4febbb8e7afe2f6e68fc1013 (diff) | |
download | gitlab-ce-353c244f5c32ad3283e9ab6ff4b599f96a010b76.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/analytics')
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())); +}; |