diff options
Diffstat (limited to 'app/assets/javascripts/cycle_analytics/components')
4 files changed, 270 insertions, 7 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue b/app/assets/javascripts/cycle_analytics/components/filter_bar.vue new file mode 100644 index 00000000000..5140b05e189 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/filter_bar.vue @@ -0,0 +1,142 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { __ } from '~/locale'; +import { + OPERATOR_IS_ONLY, + DEFAULT_NONE_ANY, +} from '~/vue_shared/components/filtered_search_bar/constants'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { + prepareTokens, + processFilters, + filterToQueryObject, +} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; +import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +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'; + +export default { + name: 'FilterBar', + components: { + FilteredSearchBar, + UrlSync, + }, + props: { + groupPath: { + type: String, + required: true, + }, + }, + computed: { + ...mapState('filters', { + selectedMilestone: (state) => state.milestones.selected, + selectedAuthor: (state) => state.authors.selected, + selectedLabelList: (state) => state.labels.selectedList, + selectedAssigneeList: (state) => state.assignees.selectedList, + milestonesData: (state) => state.milestones.data, + labelsData: (state) => state.labels.data, + authorsData: (state) => state.authors.data, + assigneesData: (state) => state.assignees.data, + }), + tokens() { + return [ + { + icon: 'clock', + title: __('Milestone'), + type: 'milestone', + token: MilestoneToken, + initialMilestones: this.milestonesData, + unique: true, + symbol: '%', + operators: OPERATOR_IS_ONLY, + fetchMilestones: this.fetchMilestones, + }, + { + icon: 'labels', + title: __('Label'), + type: 'labels', + token: LabelToken, + defaultLabels: DEFAULT_NONE_ANY, + initialLabels: this.labelsData, + unique: false, + symbol: '~', + operators: OPERATOR_IS_ONLY, + fetchLabels: this.fetchLabels, + }, + { + icon: 'pencil', + title: __('Author'), + type: 'author', + token: AuthorToken, + initialAuthors: this.authorsData, + unique: true, + operators: OPERATOR_IS_ONLY, + fetchAuthors: this.fetchAuthors, + }, + { + icon: 'user', + title: __('Assignees'), + type: 'assignees', + token: AuthorToken, + defaultAuthors: [], + initialAuthors: this.assigneesData, + unique: false, + operators: OPERATOR_IS_ONLY, + fetchAuthors: this.fetchAssignees, + }, + ]; + }, + query() { + return filterToQueryObject({ + milestone_title: this.selectedMilestone, + author_username: this.selectedAuthor, + label_name: this.selectedLabelList, + assignee_username: this.selectedAssigneeList, + }); + }, + }, + methods: { + ...mapActions('filters', [ + 'setFilters', + 'fetchMilestones', + 'fetchLabels', + 'fetchAuthors', + 'fetchAssignees', + ]), + initialFilterValue() { + return prepareTokens({ + milestone: this.selectedMilestone, + author: this.selectedAuthor, + assignees: this.selectedAssigneeList, + labels: this.selectedLabelList, + }); + }, + handleFilter(filters) { + const { labels, milestone, author, assignees } = processFilters(filters); + + this.setFilters({ + selectedAuthor: author ? author[0] : null, + selectedMilestone: milestone ? milestone[0] : null, + selectedAssigneeList: assignees || [], + selectedLabelList: labels || [], + }); + }, + }, +}; +</script> + +<template> + <div> + <filtered-search-bar + class="gl-flex-grow-1" + :namespace="groupPath" + recent-searches-storage-key="value-stream-analytics" + :search-input-placeholder="__('Filter results')" + :tokens="tokens" + :initial-filter-value="initialFilterValue()" + @onFilter="handleFilter" + /> + <url-sync :query="query" /> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue b/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue new file mode 100644 index 00000000000..b622b0441e2 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue @@ -0,0 +1,32 @@ +<script> +import { s__, n__, sprintf, formatNumber } from '~/locale'; + +export default { + props: { + stageCount: { + type: Number, + required: false, + default: null, + }, + }, + computed: { + formattedStageCount() { + if (!this.stageCount) { + return '-'; + } else if (this.stageCount > 1000) { + return sprintf(s__('ValueStreamAnalytics|%{stageCount}+ items'), { + stageCount: formatNumber(1000), + }); + } + + return sprintf(n__('%{count} item', '%{count} items', this.stageCount), { + count: formatNumber(this.stageCount), + }); + }, + }, +}; +</script> + +<template> + <span>{{ formattedStageCount }}</span> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue index c1e33f73b13..47fafc3b90c 100644 --- a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue +++ b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue @@ -7,6 +7,7 @@ import { } from '@gitlab/ui'; import Tracking from '~/tracking'; import { OVERVIEW_STAGE_ID } from '../constants'; +import FormattedStageCount from './formatted_stage_count.vue'; export default { name: 'PathNavigation', @@ -14,6 +15,7 @@ export default { GlPath, GlSkeletonLoading, GlPopover, + FormattedStageCount, }, directives: { SafeHtml, @@ -44,9 +46,6 @@ export default { 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', { @@ -88,10 +87,7 @@ export default { {{ 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> + <formatted-stage-count :stage-count="pathItem.stageCount" /> </div> </div> </div> diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue new file mode 100644 index 00000000000..6b1e537dc77 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue @@ -0,0 +1,93 @@ +<script> +import DateRange from '~/analytics/shared/components/daterange.vue'; +import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue'; +import { DATE_RANGE_LIMIT, PROJECTS_PER_PAGE } from '~/analytics/shared/constants'; +import FilterBar from './filter_bar.vue'; + +export default { + name: 'ValueStreamFilters', + components: { + DateRange, + ProjectsDropdownFilter, + FilterBar, + }, + props: { + selectedProjects: { + type: Array, + required: false, + default: () => [], + }, + hasProjectFilter: { + type: Boolean, + required: false, + default: true, + }, + hasDateRangeFilter: { + type: Boolean, + required: false, + default: true, + }, + groupId: { + type: Number, + required: true, + }, + groupPath: { + type: String, + required: true, + }, + startDate: { + type: Date, + required: false, + default: null, + }, + endDate: { + type: Date, + required: false, + default: null, + }, + }, + computed: { + projectsQueryParams() { + return { + first: PROJECTS_PER_PAGE, + includeSubgroups: true, + }; + }, + }, + multiProjectSelect: true, + maxDateRange: DATE_RANGE_LIMIT, +}; +</script> +<template> + <div class="gl-mt-3 gl-py-2 gl-px-3 bg-gray-light border-top border-bottom"> + <filter-bar + class="js-filter-bar filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none" + :group-path="groupPath" + /> + <div + v-if="hasDateRangeFilter || hasProjectFilter" + class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between" + > + <projects-dropdown-filter + v-if="hasProjectFilter" + :key="groupId" + class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0" + :group-id="groupId" + :group-namespace="groupPath" + :query-params="projectsQueryParams" + :multi-select="$options.multiProjectSelect" + :default-projects="selectedProjects" + @selected="$emit('selectProject', $event)" + /> + <date-range + v-if="hasDateRangeFilter" + :start-date="startDate" + :end-date="endDate" + :max-date-range="$options.maxDateRange" + :include-selected-date="true" + class="js-daterange-picker" + @change="$emit('setDateRange', $event)" + /> + </div> + </div> +</template> |