diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/dora_chart.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/dora_chart.vue index 71fe3210173dec40229b9db6c39dba7a18888619..3c9f65eb83a3da77ee3af23938004bb821066001 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/dora_chart.vue +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/dora_chart.vue @@ -1,12 +1,12 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; -import ComparisonChart from 'ee/analytics/dashboards/components/comparison_chart.vue'; +import FilterableComparisonChart from 'ee/analytics/dashboards/components/filterable_comparison_chart.vue'; import GroupOrProjectProvider from 'ee/analytics/dashboards/components/group_or_project_provider.vue'; export default { name: 'DoraChart', components: { - ComparisonChart, + FilterableComparisonChart, GlLoadingIcon, GroupOrProjectProvider, }, @@ -22,23 +22,38 @@ export default { default: () => ({}), }, }, + computed: { + filters() { + const { filters: { labels = [], excludeMetrics = [] } = {} } = this.data; + return { + labels, + excludeMetrics, + }; + }, + }, + methods: { + webUrl(group, project, isProject) { + return isProject ? project.webUrl : group.webUrl; + }, + }, }; </script> <template> <group-or-project-provider - #default="{ isProject, isNamespaceLoading }" + #default="{ isProject, isNamespaceLoading, group, project }" :full-path="data.namespace" > <div v-if="isNamespaceLoading" class="gl--flex-center gl-h-full"> <gl-loading-icon size="lg" /> </div> - <comparison-chart + <filterable-comparison-chart v-else - :request-path="data.namespace" + :namespace="data.namespace" + :filters="filters" :is-project="isProject" - :exclude-metrics="data.excludeMetrics" - :filter-labels="data.filterLabels" + :is-loading="isNamespaceLoading" + :web-url="webUrl(group, project, isProject)" @set-errors="$emit('set-errors', $event)" /> </group-or-project-provider> diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/usage_overview.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/usage_overview.js index 4a67cdbe766636c308583e9711fe6ffb81284805..1caae2f47c3a0f5856360810f7978aba32e6e37b 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/usage_overview.js +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/usage_overview.js @@ -84,11 +84,10 @@ export const prepareQuery = (queryKeysToInclude = []) => { */ export const fetch = async ({ rootNamespace: { requestPath: fullPath }, - query: { include = [] }, + queryOverrides: { filters: { include = USAGE_OVERVIEW_IDENTIFIERS } = {} } = {}, }) => { const variableOverrides = prepareQuery(include); const { startDate, endDate } = USAGE_OVERVIEW_DEFAULT_DATE_RANGE; - try { const { data = {} } = await defaultClient.query({ query: getUsageOverviewQuery, diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/value_stream.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/value_stream.js index 1635ed4bc19cffaa070b7a652d37f3b4729928f5..3510ed8727168d200834819de788b06eb5484a83 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/value_stream.js +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/value_stream.js @@ -1,4 +1,5 @@ import { sprintf, s__ } from '~/locale'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; const I18N_VSD_DORA_METRICS_PANEL_TITLE = s__('DORA4Metrics|Metrics comparison for %{name}'); @@ -7,10 +8,13 @@ const generatePanelTitle = ({ namespace: { name } }) => { }; export const fetch = ({ title, namespace, query, queryOverrides = {} }) => { - return { - namespace, - title: title || generatePanelTitle({ namespace }), - ...query, - ...queryOverrides, - }; + return convertObjectPropsToCamelCase( + { + namespace, + title: title || generatePanelTitle({ namespace }), + ...query, + ...queryOverrides, + }, + { deep: true }, + ); }; diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js index 77290952210332d0210339db99633bb930783007..dfd28c2d103beb0d1635db1abb5e2a46e7e912cb 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js @@ -44,6 +44,7 @@ export default () => { rootNamespaceFullPath, dataSourceClickhouse, aiGenerateCubeQueryEnabled, + topicsExploreProjectsPath, } = el.dataset; const analyticsDashboardPointer = buildAnalyticsDashboardPointer(analyticsDashboardPointerJSON); @@ -122,6 +123,7 @@ export default () => { rootNamespaceFullPath, dataSourceClickhouse: parseBoolean(dataSourceClickhouse), currentUserId: window.gon?.current_user_id, + topicsExploreProjectsPath, }, render(h) { return h(DashboardsApp); diff --git a/ee/app/assets/javascripts/analytics/dashboards/components/dora_performers_score_chart.vue b/ee/app/assets/javascripts/analytics/dashboards/components/dora_performers_score_chart.vue index cc4c83b12e46614e85f77b7dcc236380f1cf551e..51b397778fa2dd40b9f1f0315abfc4906a7fa1a4 100644 --- a/ee/app/assets/javascripts/analytics/dashboards/components/dora_performers_score_chart.vue +++ b/ee/app/assets/javascripts/analytics/dashboards/components/dora_performers_score_chart.vue @@ -131,7 +131,8 @@ export default { ); }, filterProjectTopics() { - return validateProjectTopics(this.data?.filter_project_topics || []); + const { filters: { projectTopics = [] } = {} } = this.data; + return validateProjectTopics(projectTopics); }, hasFilterProjectTopics() { return this.filterProjectTopics.length > 0; diff --git a/ee/app/assets/javascripts/analytics/dashboards/components/filterable_comparison_chart.vue b/ee/app/assets/javascripts/analytics/dashboards/components/filterable_comparison_chart.vue new file mode 100644 index 0000000000000000000000000000000000000000..df39ee0e9dcd5592ad74213bdce33169b08d026a --- /dev/null +++ b/ee/app/assets/javascripts/analytics/dashboards/components/filterable_comparison_chart.vue @@ -0,0 +1,120 @@ +<script> +import { uniq, flatten, uniqBy } from 'lodash'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import { sprintf } from '~/locale'; +import filterLabelsQueryBuilder, { LABEL_PREFIX } from '../graphql/filter_labels_query_builder'; +import { DASHBOARD_LABELS_LOAD_ERROR, METRICS_WITHOUT_LABEL_FILTERING } from '../constants'; +import ComparisonChart from './comparison_chart.vue'; +import ComparisonChartLabels from './comparison_chart_labels.vue'; + +export default { + name: 'FilterableComparisonChart', + components: { + ComparisonChart, + ComparisonChartLabels, + GlSkeletonLoader, + }, + props: { + namespace: { + type: String, + required: true, + }, + webUrl: { + type: String, + required: true, + }, + filters: { + type: Object, + required: false, + default: () => ({}), + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + isProject: { + type: Boolean, + required: false, + default: false, + }, + }, + apollo: { + filterLabelsResults: { + query() { + return filterLabelsQueryBuilder(this.filterLabelsQuery, this.isProject); + }, + variables() { + return { + fullPath: this.namespace, + }; + }, + skip() { + return !this.hasFilterLabelsQuery || !this.namespace; + }, + update(data) { + const labels = Object.entries(data.namespace || {}) + .filter(([key]) => key.includes(LABEL_PREFIX)) + .map(([, { nodes }]) => nodes); + return uniqBy(flatten(labels), ({ id }) => id); + }, + error() { + this.hasLabelErrors = true; + const labels = this.filterLabelsQuery.join(', '); + this.$emit('set-errors', { errors: [sprintf(DASHBOARD_LABELS_LOAD_ERROR, { labels })] }); + }, + }, + }, + data() { + return { + filterLabelsResults: [], + hasLabelErrors: false, + }; + }, + computed: { + loading() { + return this.isLoading || this.$apollo.queries.filterLabelsResults.loading; + }, + filterLabelsQuery() { + return this.filters?.labels; + }, + hasFilterLabelsQuery() { + return this.filterLabelsQuery.length; + }, + hasFilterLabels() { + return this.filterLabelsResults.length > 0; + }, + filterLabelNames() { + return this.filterLabelsResults.map(({ title }) => title); + }, + excludeMetrics() { + let metrics = this.filters?.excludeMetrics; + if (this.hasFilterLabels) { + metrics = [...metrics, ...METRICS_WITHOUT_LABEL_FILTERING]; + } + return uniq(metrics); + }, + }, +}; +</script> +<template> + <div v-if="loading"> + <gl-skeleton-loader :lines="1" /> + </div> + <div v-else> + <div class="gl-text-right gl-py-2"> + <comparison-chart-labels + v-if="filterLabelsResults.length" + :labels="filterLabelsResults" + :web-url="webUrl" + /> + </div> + <comparison-chart + v-if="!hasLabelErrors" + :request-path="namespace" + :is-project="isProject" + :exclude-metrics="excludeMetrics" + :filter-labels="filterLabelNames" + /> + </div> +</template> diff --git a/ee/app/views/groups/analytics/dashboards/index.html.haml b/ee/app/views/groups/analytics/dashboards/index.html.haml index f5bdd595661735b72e085a039ef54bd49257c6fb..051a8e33efe1dc3cdc5eab16fdcd19260ab78550 100644 --- a/ee/app/views/groups/analytics/dashboards/index.html.haml +++ b/ee/app/views/groups/analytics/dashboards/index.html.haml @@ -1,4 +1,4 @@ - page_title s_('Analytics|Analytics dashboards') - breadcrumb_title s_("Analytics|Analytics dashboards") -#js-analytics-dashboards-list-app{ data: analytics_dashboards_list_app_data(@group).merge({ available_visualizations: @available_visualizations&.to_json, data_source_clickhouse: @data_source_clickhouse.to_s }) } +#js-analytics-dashboards-list-app{ data: analytics_dashboards_list_app_data(@group).merge({ available_visualizations: @available_visualizations&.to_json, data_source_clickhouse: @data_source_clickhouse.to_s, topics_explore_projects_path: topics_explore_projects_path }) } diff --git a/ee/app/views/groups/analytics/dashboards/value_streams_dashboard.html.haml b/ee/app/views/groups/analytics/dashboards/value_streams_dashboard.html.haml index f962174871d2930b971cae9b0de8cec2e1f32c68..e9aaa22da76f698863cbe24a2582c89618d9efb6 100644 --- a/ee/app/views/groups/analytics/dashboards/value_streams_dashboard.html.haml +++ b/ee/app/views/groups/analytics/dashboards/value_streams_dashboard.html.haml @@ -5,6 +5,6 @@ - breadcrumb_title s_('Analytics|Analytics dashboards') - if Feature.enabled?(:group_analytics_dashboard_dynamic_vsd, @group) - #js-analytics-dashboards-list-app{ data: analytics_dashboards_list_app_data(@group).merge({ available_visualizations: @available_visualizations&.to_json, data_source_clickhouse: @data_source_clickhouse.to_s }) } + #js-analytics-dashboards-list-app{ data: analytics_dashboards_list_app_data(@group).merge({ available_visualizations: @available_visualizations&.to_json, data_source_clickhouse: @data_source_clickhouse.to_s, topics_explore_projects_path: topics_explore_projects_path }) } - else #js-analytics-dashboards-app{ data: { full_path: @group.full_path, namespaces: @namespaces.to_json, pointer_project: @pointer_project&.to_json, available_visualizations: @available_visualizations&.to_json, topics_explore_projects_path: topics_explore_projects_path, data_source_clickhouse: @data_source_clickhouse.to_s } } diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/usage_overview_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/usage_overview_spec.js index 49c1217c955c70a55c87d04d585bb5d19fb55df9..c832d8322fe343718364a7dd51270981001516f0 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/usage_overview_spec.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/usage_overview_spec.js @@ -23,7 +23,7 @@ describe('Usage overview Data Source', () => { const rootNamespace = { name: 'cool namespace', requestPath: 'some-group-path' }; const queryKeys = [USAGE_OVERVIEW_IDENTIFIER_GROUPS, USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS]; - const mockQuery = { include: queryKeys }; + const mockQuery = { filters: { include: queryKeys } }; const { group: mockGroupUsageMetricsQueryResponse } = mockUsageMetricsQueryResponse; const identifiers = [ USAGE_OVERVIEW_IDENTIFIER_GROUPS, @@ -91,7 +91,7 @@ describe('Usage overview Data Source', () => { `('$label queries the top level group', async ({ namespace }) => { jest.spyOn(defaultClient, 'query').mockResolvedValue({ data: {} }); - obj = await fetch({ rootNamespace, namespace, query: mockQuery }); + obj = await fetch({ rootNamespace, namespace, queryOverrides: mockQuery }); expect(defaultClient.query).toHaveBeenCalledWith( expect.objectContaining({ @@ -114,7 +114,7 @@ describe('Usage overview Data Source', () => { obj = await fetch({ rootNamespace, - query: { include: [USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS] }, + queryOverrides: { filters: { include: [USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS] } }, }); expect(defaultClient.query).toHaveBeenCalledWith( @@ -135,8 +135,8 @@ describe('Usage overview Data Source', () => { it.each` label | data | params - ${'with no data'} | ${{}} | ${{ rootNamespace, query: mockQuery }} - ${'with no namespace.requestPath'} | ${mockUsageMetricsQueryResponse} | ${{ rootNamespace: {}, query: mockQuery }} + ${'with no data'} | ${{}} | ${{ rootNamespace, queryOverrides: mockQuery }} + ${'with no namespace.requestPath'} | ${mockUsageMetricsQueryResponse} | ${{ rootNamespace: {}, queryOverrides: mockQuery }} `('$label returns the no data object', async ({ params }) => { jest.spyOn(defaultClient, 'query').mockResolvedValue({ data: {} }); @@ -149,7 +149,7 @@ describe('Usage overview Data Source', () => { beforeEach(() => { jest.spyOn(defaultClient, 'query').mockRejectedValue(); - obj = fetch({ rootNamespace, query: mockQuery }); + obj = fetch({ rootNamespace, queryOverrides: mockQuery }); }); it('returns the no data object', async () => { @@ -163,7 +163,7 @@ describe('Usage overview Data Source', () => { .spyOn(defaultClient, 'query') .mockResolvedValue({ data: mockUsageMetricsQueryResponse }); - obj = await fetch({ rootNamespace, query: mockQuery }); + obj = await fetch({ rootNamespace, queryOverrides: mockQuery }); }); it('will fetch the usage metrics', () => { diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/value_stream_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/value_stream_spec.js index 3dd8768550be18395b762481cac1828b2c0817bf..aa8577480f71a1a9416f85549a3c8e76bd04b351 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/value_stream_spec.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/value_stream_spec.js @@ -3,9 +3,9 @@ import { fetch } from 'ee/analytics/analytics_dashboards/data_sources/value_stre describe('Value Stream Data Source', () => { let obj; - const query = { exclude_metrics: [] }; - const queryOverrides = { exclude_metrics: ['some metric'] }; - const namespace = { name: 'cool namespace' }; + const query = { filters: { exclude_metrics: [] } }; + const queryOverrides = { filters: { excludeMetrics: ['some metric'] } }; + const namespace = 'cool namespace'; const title = 'fake title'; describe('fetch', () => { @@ -14,20 +14,13 @@ describe('Value Stream Data Source', () => { expect(obj.namespace).toBe(namespace); expect(obj.title).toBe(title); - expect(obj).toMatchObject({ exclude_metrics: [] }); - }); - - it('generates a default title from the namespace if there is none', () => { - obj = fetch({ namespace }); - - expect(obj.namespace).toBe(namespace); - expect(obj.title).toBe('Metrics comparison for cool namespace'); + expect(obj).toMatchObject({ filters: { excludeMetrics: [] } }); }); it('applies the queryOverrides over any relevant query parameters', () => { obj = fetch({ namespace, query, queryOverrides }); - expect(obj).not.toMatchObject({ exclude_metrics: [] }); + expect(obj).not.toMatchObject({ filters: { excludeMetrics: [] } }); expect(obj).toMatchObject(queryOverrides); }); }); diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/dora_chart_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/dora_chart_spec.js index cf87bf4f858d4d9e188bd52b8e8324de56daba0e..9b53c41ce7b7fb72f24bce768ccddaf9c87c3704 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/dora_chart_spec.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/dora_chart_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import DoraChart from 'ee/analytics/analytics_dashboards/components/visualizations/dora_chart.vue'; -import ComparisonChart from 'ee/analytics/dashboards/components/comparison_chart.vue'; +import FilterableComparisonChart from 'ee/analytics/dashboards/components/filterable_comparison_chart.vue'; import GroupOrProjectProvider from 'ee/analytics/dashboards/components/group_or_project_provider.vue'; import GetGroupOrProjectQuery from 'ee/analytics/dashboards/graphql/get_group_or_project.query.graphql'; import { mockGroup } from 'ee_jest/analytics/dashboards/mock_data'; @@ -23,11 +23,13 @@ describe('DoraChart Visualization', () => { const defaultData = { namespace, - excludeMetrics, - filterLabels, + filters: { + excludeMetrics, + labels: filterLabels, + }, }; - const findChart = () => wrapper.findComponent(ComparisonChart); + const findChart = () => wrapper.findComponent(FilterableComparisonChart); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const createWrapper = (props = {}) => { @@ -83,9 +85,12 @@ describe('DoraChart Visualization', () => { it('renders the comparison chart component', () => { expect(findChart().props()).toMatchObject({ - excludeMetrics, - filterLabels, - requestPath: 'some/fake/path', + namespace, + filters: { + excludeMetrics, + labels: filterLabels, + }, + webUrl: 'gdk.test/groups/group-10', }); }); diff --git a/ee/spec/frontend/analytics/dashboards/components/dora_performers_score_chart_spec.js b/ee/spec/frontend/analytics/dashboards/components/dora_performers_score_chart_spec.js index dcb8f6d392ef24c7da77d7c434764bf5fb2d1371..497ebd19629a4a159538831ab9a54f565b68c572 100644 --- a/ee/spec/frontend/analytics/dashboards/components/dora_performers_score_chart_spec.js +++ b/ee/spec/frontend/analytics/dashboards/components/dora_performers_score_chart_spec.js @@ -201,7 +201,9 @@ describe('DoraPerformersScoreChart', () => { it('renders the filter badges when provided', async () => { const topics = ['one', 'two']; - await createWrapper({ props: { data: { ...mockData, filter_project_topics: topics } } }); + await createWrapper({ + props: { data: { ...mockData, filters: { projectTopics: topics } } }, + }); expect(findFilterBadges().exists()).toBe(true); expect(findFilterBadges().props('topics')).toEqual(topics); }); @@ -213,7 +215,9 @@ describe('DoraPerformersScoreChart', () => { it('filters out invalid project topics', async () => { const topics = ['one', 'two\n']; - await createWrapper({ props: { data: { ...mockData, filter_project_topics: topics } } }); + await createWrapper({ + props: { data: { ...mockData, filters: { projectTopics: topics } } }, + }); expect(findFilterBadges().exists()).toBe(true); expect(findFilterBadges().props('topics')).toEqual(['one']); }); diff --git a/ee/spec/frontend/analytics/dashboards/components/filterable_comparison_chart_spec.js b/ee/spec/frontend/analytics/dashboards/components/filterable_comparison_chart_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..43300455259c9dc90f73a90c98fea96ee0e75c6c --- /dev/null +++ b/ee/spec/frontend/analytics/dashboards/components/filterable_comparison_chart_spec.js @@ -0,0 +1,234 @@ +import VueApollo from 'vue-apollo'; +import Vue from 'vue'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import FilterableComparisonChart from 'ee/analytics/dashboards/components/filterable_comparison_chart.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { METRICS_WITHOUT_LABEL_FILTERING } from 'ee/analytics/dashboards/constants'; +import ComparisonChartLabels from 'ee/analytics/dashboards/components/comparison_chart_labels.vue'; +import ComparisonChart from 'ee/analytics/dashboards/components/comparison_chart.vue'; +import filterLabelsQueryBuilder from 'ee/analytics/dashboards/graphql/filter_labels_query_builder'; +import { mockFilterLabelsResponse } from '../helpers'; + +Vue.use(VueApollo); + +describe('FilterableComparisonChart', () => { + let wrapper; + + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findComparisonChart = () => wrapper.findComponent(ComparisonChart); + const findComparisonChartLabels = () => wrapper.findComponent(ComparisonChartLabels); + + const excludeMetrics = ['cycle_time']; + const labels = ['test::one', 'test::two']; + + const groupWebUrl = 'group/web/url'; + const projectWebUrl = 'project/web/url'; + + const groupNamespace = 'group-namespace'; + const projectNamespace = 'group-namespace/project'; + + const createWrapper = async ({ + namespace = groupNamespace, + isProject = false, + webUrl = groupWebUrl, + isLoading = false, + filters = {}, + filterLabelsResolver = null, + } = {}) => { + const { labels: filterLabels = [] } = filters; + const apolloProvider = createMockApollo([ + [ + filterLabelsQueryBuilder(filterLabels, isProject), + filterLabelsResolver || + jest.fn().mockResolvedValue({ data: mockFilterLabelsResponse(filterLabels) }), + ], + ]); + + wrapper = shallowMountExtended(FilterableComparisonChart, { + apolloProvider, + propsData: { + namespace, + isProject, + webUrl, + isLoading, + filters: { + labels: filterLabels, + excludeMetrics: [], + ...filters, + }, + }, + }); + + await waitForPromises(); + }; + + describe('default', () => { + beforeEach(async () => { + await createWrapper(); + }); + + it('does not render the skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(false); + }); + + it('does not emit an error', () => { + expect(wrapper.emitted('set-errors')).toBeUndefined(); + }); + + it('does not render the chart labels', () => { + expect(findComparisonChartLabels().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findComparisonChart().props()).toEqual({ + excludeMetrics: [], + filterLabels: [], + isProject: false, + requestPath: 'group-namespace', + }); + }); + }); + + describe('with filters', () => { + describe('error loading filters', () => { + beforeEach(async () => { + await createWrapper({ + filters: { labels }, + filterLabelsResolver: jest.fn().mockRejectedValue(), + }); + }); + + it('emits the `set-errors` event', () => { + expect(wrapper.emitted('set-errors')[0]).toEqual([ + { errors: ['Failed to load labels matching the filter: test::one, test::two'] }, + ]); + }); + + it('does not render the chart labels', () => { + expect(findComparisonChartLabels().exists()).toBe(false); + }); + }); + + describe('labels', () => { + beforeEach(async () => { + await createWrapper({ filters: { labels } }); + }); + + it('does not render the skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findComparisonChart().props()).toEqual({ + excludeMetrics: METRICS_WITHOUT_LABEL_FILTERING, + filterLabels: labels, + isProject: false, + requestPath: 'group-namespace', + }); + }); + + it('renders the chart labels', () => { + expect(findComparisonChartLabels().props()).toEqual({ + webUrl: groupWebUrl, + labels: [ + { + color: '#FFFFFF', + id: 'test::one', + title: 'test::one', + }, + { + color: '#FFFFFF', + id: 'test::two', + title: 'test::two', + }, + ], + }); + }); + + describe('with duplicate labels', () => { + beforeEach(async () => { + await createWrapper({ filters: { labels: [...labels, ...labels] } }); + }); + + it('removes duplicates result', () => { + expect(findComparisonChart().props('filterLabels').length).toBe(2); + }); + }); + }); + + describe('excludeMetrics', () => { + beforeEach(() => { + createWrapper({ filters: { excludeMetrics } }); + }); + + it('does not render the chart labels', () => { + expect(findComparisonChartLabels().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findComparisonChart().props()).toEqual({ + excludeMetrics: ['cycle_time'], + filterLabels: [], + isProject: false, + requestPath: 'group-namespace', + }); + }); + }); + + describe('with excludeMetrics and labels', () => { + beforeEach(async () => { + await createWrapper({ filters: { excludeMetrics, labels } }); + }); + + it('will exclude incompatible metrics', () => { + expect(findComparisonChart().props()).toEqual( + expect.objectContaining({ + filterLabels: labels, + excludeMetrics: ['cycle_time', ...METRICS_WITHOUT_LABEL_FILTERING], + isProject: false, + requestPath: 'group-namespace', + }), + ); + }); + }); + }); + + describe('while loading', () => { + beforeEach(() => { + createWrapper({ isLoading: true }); + }); + + it('renders the skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('does not render the chart', () => { + expect(findComparisonChart().exists()).toBe(false); + }); + + it('does not render the chart labels', () => { + expect(findComparisonChartLabels().exists()).toBe(false); + }); + + it('does not emit an error', () => { + expect(wrapper.emitted('set-errors')).toBeUndefined(); + }); + }); + + describe('with a project', () => { + beforeEach(async () => { + await createWrapper({ webUrl: projectWebUrl, isProject: true, namespace: projectNamespace }); + }); + + it('renders the chart for project', () => { + expect(findComparisonChart().props()).toEqual({ + excludeMetrics: [], + filterLabels: [], + isProject: true, + requestPath: 'group-namespace/project', + }); + }); + }); +}); diff --git a/ee/spec/frontend/analytics/dashboards/helpers.js b/ee/spec/frontend/analytics/dashboards/helpers.js index aa6616ffa1f1f22f09a160cc3a03a1da92df1e85..9b44c3a905d58c96ad847b96a7d1bd9337aae583 100644 --- a/ee/spec/frontend/analytics/dashboards/helpers.js +++ b/ee/spec/frontend/analytics/dashboards/helpers.js @@ -100,8 +100,8 @@ export const mockGraphqlContributorCountResponse = ( }, }); -export const mockFilterLabelsResponse = (mockLabels) => ({ - namespace: mockLabels.reduce( +export const mockFilterLabelsResponse = (mockLabels = []) => ({ + namespace: mockLabels?.reduce( (acc, label, index) => Object.assign(acc, { [`label_${index}`]: { nodes: [{ id: label, title: label, color: '#FFFFFF' }] }, diff --git a/ee/spec/requests/groups/analytics/dashboards_controller_spec.rb b/ee/spec/requests/groups/analytics/dashboards_controller_spec.rb index 39017cbef62e65c7bafb05e9d02cdd65ff972aac..b08d146653a3df51d9c26402d5b1815633148986 100644 --- a/ee/spec/requests/groups/analytics/dashboards_controller_spec.rb +++ b/ee/spec/requests/groups/analytics/dashboards_controller_spec.rb @@ -91,6 +91,14 @@ def build_dashboard_path(path, namespaces) expect(js_list_app_attributes).to include('data-data-source-clickhouse') end + it 'passes topics-explore-projects-path to data attributes' do + request + + expect(response).to be_successful + + expect(js_list_app_attributes).to include('data-topics-explore-projects-path') + end + context 'when project_id outside of the group hierarchy was set' do it 'does not pass the project pointer' do project_outside_the_hierarchy = create(:project)