From 02fa476b78a51a9b39276c7625c4172cba44ca12 Mon Sep 17 00:00:00 2001 From: Rudy Crespo <rcrespo@gitlab.com> Date: Tue, 2 Apr 2024 17:59:40 -0400 Subject: [PATCH 1/2] Add top-level namespace to Usage Overview panel Adds the top-level namespace's full name, type, visibility level icon and avatar to the Usage Overview panel on the Value Stream Dashboard. Changelog: added EE: true --- .../visualizations/usage_overview.vue | 45 ++++++++++- .../data_sources/usage_overview.js | 23 +++++- .../queries/get_usage_overview.query.graphql | 3 + .../data_sources/usage_overview_spec.js | 17 +++- .../visualizations/usage_overview_spec.js | 81 +++++++++++++++---- .../analytics_dashboards/mock_data.js | 19 +++++ 6 files changed, 163 insertions(+), 25 deletions(-) diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/usage_overview.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/usage_overview.vue index f91494e8876c6f..f11c11be008d01 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/usage_overview.vue +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/usage_overview.vue @@ -1,17 +1,23 @@ <script> import { compact } from 'lodash'; +import { GlAvatar, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import dateFormat, { masks } from '~/lib/dateformat'; import SingleStat from './single_stat.vue'; export default { name: 'UsageOverview', + directives: { + GlTooltip: GlTooltipDirective, + }, components: { + GlAvatar, + GlIcon, SingleStat, }, props: { data: { - type: Array, + type: Object, required: true, }, options: { @@ -22,7 +28,7 @@ export default { }, computed: { recordedAt() { - const allRecordedAt = compact(this.data.map((metric) => metric.recordedAt)); + const allRecordedAt = compact(this.data.metrics.map((metric) => metric.recordedAt)); const [mostRecentRecordedAt] = allRecordedAt.sort().slice(-1); if (!mostRecentRecordedAt) return null; @@ -47,7 +53,40 @@ export default { <template> <div class="gl-display-flex gl-md-flex-direction-column gl-flex-direction-row gl-font-size-sm"> <div - v-for="metric in data" + v-if="data.namespace" + data-testid="usage-overview-namespace" + class="gl-display-flex gl-align-items-center gl-gap-3 gl-pr-9" + > + <gl-avatar + shape="rect" + :src="data.namespace.avatarUrl" + :size="48" + :entity-name="data.namespace.fullName" + :entity-id="data.namespace.id" + :fallback-on-error="true" + /> + + <div class="gl-line-height-20"> + <span + class="gl-display-block gl-mb-1 gl-font-base gl-font-weight-normal gl-text-gray-700" + >{{ data.namespace.namespaceType }}</span + > + <div class="gl-display-flex gl-align-items-center gl-gap-2"> + <span class="gl-font-size-h2 gl-font-weight-bold gl-text-gray-900 gl-truncate-end">{{ + data.namespace.fullName + }}</span> + <gl-icon + v-gl-tooltip + class="gl-text-secondary" + :name="data.namespace.visibilityLevelIcon" + :title="data.namespace.visibilityLevelTooltip" + /> + </div> + </div> + </div> + + <div + v-for="metric in data.metrics" :key="metric.identifier" class="gl-pr-9" :data-testid="`usage-overview-metric-${metric.identifier}`" 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 f4db43bac7ddb0..b6c2ea27f4f8b4 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 @@ -11,6 +11,9 @@ import { USAGE_OVERVIEW_IDENTIFIER_PIPELINES, } from '~/analytics/shared/constants'; import { toYmd } from '~/analytics/shared/utils'; +import { GROUP_VISIBILITY_TYPE, VISIBILITY_TYPE_ICON } from '~/visibility_level/constants'; +import { TYPENAME_GROUP } from '~/graphql_shared/constants'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { defaultClient } from '../graphql/client'; import getUsageOverviewQuery from '../graphql/queries/get_usage_overview.query.graphql'; @@ -23,6 +26,19 @@ const USAGE_OVERVIEW_IDENTIFIERS = [ USAGE_OVERVIEW_IDENTIFIER_PIPELINES, ]; +/** + * Takes the usage overview query response, extracts information + * about the top-level namespace and formats it for rendering. + */ +export const extractUsageNamespaceData = (data) => ({ + id: getIdFromGraphQLId(data?.id), + avatarUrl: data?.avatarUrl, + fullName: data?.fullName, + namespaceType: TYPENAME_GROUP, + visibilityLevelIcon: VISIBILITY_TYPE_ICON[data.visibility] ?? null, + visibilityLevelTooltip: GROUP_VISIBILITY_TYPE[data.visibility] ?? null, +}); + /** * Takes a usage metrics query response, extracts the values and * adds the additional metadata we need for rendering. @@ -106,10 +122,13 @@ export const fetch = async ({ }); if (!data.group) { - return usageOverviewNoData; + return { metrics: usageOverviewNoData }; } - return extractUsageMetrics(data.group); + return { + namespace: extractUsageNamespaceData(data.group), + metrics: extractUsageMetrics(data.group), + }; } catch { throw new Error(USAGE_OVERVIEW_NO_DATA_ERROR); } diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_usage_overview.query.graphql b/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_usage_overview.query.graphql index adbc5cf12a1a04..175bd10e497c2d 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_usage_overview.query.graphql +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_usage_overview.query.graphql @@ -11,6 +11,9 @@ query getUsageOverview( ) { group(fullPath: $fullPath) { id + avatarUrl + fullName + visibility groups: valueStreamDashboardUsageOverview( identifier: GROUPS timeframe: { start: $startDate, end: $endDate } 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 f6c6bff4c25c80..717aba907fd465 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 @@ -11,12 +11,15 @@ import { fetch, prepareQuery, extractUsageMetrics, + extractUsageNamespaceData, } from 'ee/analytics/analytics_dashboards/data_sources/usage_overview'; import { defaultClient } from 'ee/analytics/analytics_dashboards/graphql/client'; import { mockUsageMetricsQueryResponse, + mockUsageNamespaceData, mockUsageMetrics, mockUsageMetricsNoData, + mockUsageOverviewData, } from '../../mock_data'; describe('Usage overview Data Source', () => { @@ -52,6 +55,14 @@ describe('Usage overview Data Source', () => { }); }); + describe('extractUsageNamespaceData', () => { + it('returns the namespace data as expected', () => { + expect(extractUsageNamespaceData(mockGroupUsageMetricsQueryResponse)).toEqual( + mockUsageNamespaceData, + ); + }); + }); + describe('prepareQuery', () => { const queryIncludeKeys = [ 'includeGroups', @@ -148,7 +159,7 @@ describe('Usage overview Data Source', () => { obj = await fetch(params); - expect(obj).toMatchObject(mockUsageMetricsNoData); + expect(obj).toMatchObject({ metrics: mockUsageMetricsNoData }); }); describe('with an error', () => { @@ -172,8 +183,8 @@ describe('Usage overview Data Source', () => { obj = await fetch({ rootNamespace, queryOverrides: mockQuery }); }); - it('will fetch the usage metrics', () => { - expect(obj).toMatchObject(mockUsageMetrics); + it('will fetch the usage overview data', () => { + expect(obj).toMatchObject(mockUsageOverviewData); }); }); }); diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/usage_overview_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/usage_overview_spec.js index 3d0760f5bb9cc8..0c240a9711baa1 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/usage_overview_spec.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/usage_overview_spec.js @@ -1,11 +1,17 @@ +import { GlAvatar, GlIcon } from '@gitlab/ui'; import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import UsageOverview from 'ee/analytics/analytics_dashboards/components/visualizations/usage_overview.vue'; -import { mockUsageMetrics, mockUsageMetricsNoData } from '../../mock_data'; +import { mockUsageOverviewData, mockUsageMetrics, mockUsageMetricsNoData } from '../../mock_data'; -describe('Single Stat Visualization', () => { +describe('Usage Overview Visualization', () => { let wrapper; - const defaultProps = { data: mockUsageMetrics, options: {} }; + const defaultProps = { data: mockUsageOverviewData, options: {} }; + + const findNamespaceTile = () => wrapper.findByTestId('usage-overview-namespace'); + const findNamespaceAvatar = () => findNamespaceTile().findComponent(GlAvatar); + const findNamespaceVisibilityIcon = () => findNamespaceTile().findComponent(GlIcon); const findMetrics = () => wrapper.findAllComponents(GlSingleStat); @@ -16,6 +22,9 @@ describe('Single Stat Visualization', () => { const createWrapper = (props = defaultProps) => { wrapper = mountExtended(UsageOverview, { + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, propsData: { data: props.data, options: props.options, @@ -28,29 +37,67 @@ describe('Single Stat Visualization', () => { createWrapper(); }); - it('should render each metric', () => { - expect(findMetrics()).toHaveLength(mockUsageMetrics.length); - }); + describe('namespace', () => { + it("should render namespace's full name", () => { + expect(wrapper.findByText('GitLab Org').exists()).toBe(true); + }); + + it('should render namespace type', () => { + expect(wrapper.findByText('Group').exists()).toBe(true); + }); + + it('should render avatar', () => { + expect(findNamespaceAvatar().props()).toMatchObject({ + entityName: 'GitLab Org', + entityId: 225, + src: '/avatar.png', + shape: 'rect', + fallbackOnError: true, + size: 48, + }); + }); + + it('should render visibility level icon', () => { + const tooltip = getBinding(findNamespaceVisibilityIcon().element, 'gl-tooltip'); - it('should render each metric as a single stat', () => { - mockUsageMetrics.forEach(({ value, options }, idx) => { - expect(findMetricTitle(idx).text()).toBe(options.title); - expect(findMetricIcon(idx).props('name')).toBe(options.titleIcon); - expect(findMetricValue(idx).text()).toBe(String(value)); + expect(findNamespaceVisibilityIcon().exists()).toBe(true); + expect(tooltip).toBeDefined(); + expect(findNamespaceVisibilityIcon().props('name')).toBe('earth'); + expect(findNamespaceVisibilityIcon().attributes('title')).toBe( + 'Public - The group and any public projects can be viewed without any authentication.', + ); }); }); - it('emits `showTooltip` with the latest metric.recordedAt as the last updated time', () => { - expect(wrapper.emitted('showTooltip')).toHaveLength(1); - expect(wrapper.emitted('showTooltip')[0][0]).toEqual( - 'Statistics on top-level namespace usage. Usage data is a cumulative count, and updated monthly. Last updated: 2023-11-27 11:59 PM', - ); + describe('metrics', () => { + it('should render each metric', () => { + expect(findMetrics()).toHaveLength(mockUsageMetrics.length); + }); + + it('should render each metric as a single stat', () => { + mockUsageMetrics.forEach(({ value, options }, idx) => { + expect(findMetricTitle(idx).text()).toBe(options.title); + expect(findMetricIcon(idx).props('name')).toBe(options.titleIcon); + expect(findMetricValue(idx).text()).toBe(String(value)); + }); + }); + + it('emits `showTooltip` with the latest metric.recordedAt as the last updated time', () => { + expect(wrapper.emitted('showTooltip')).toHaveLength(1); + expect(wrapper.emitted('showTooltip')[0][0]).toEqual( + 'Statistics on top-level namespace usage. Usage data is a cumulative count, and updated monthly. Last updated: 2023-11-27 11:59 PM', + ); + }); }); }); describe('with no data', () => { beforeEach(() => { - createWrapper({ data: mockUsageMetricsNoData }); + createWrapper({ data: { metrics: mockUsageMetricsNoData } }); + }); + + it('should not render namespace tile', () => { + expect(findNamespaceTile().exists()).toBe(false); }); it('should render each metric', () => { diff --git a/ee/spec/frontend/analytics/analytics_dashboards/mock_data.js b/ee/spec/frontend/analytics/analytics_dashboards/mock_data.js index fd24bb8157ca49..5e635bc714e827 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/mock_data.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/mock_data.js @@ -591,6 +591,10 @@ export const mockMetaData = { export const mockUsageMetricsQueryResponse = { group: { + id: 'gid://gitlab/Group/225', + fullName: 'GitLab Org', + avatarUrl: '/avatar.png', + visibility: 'public', __typename: 'Group', groups: { __typename: 'ValueStreamDashboardCount', @@ -631,6 +635,16 @@ export const mockUsageMetricsQueryResponse = { }, }; +export const mockUsageNamespaceData = { + id: 225, + avatarUrl: '/avatar.png', + fullName: 'GitLab Org', + namespaceType: 'Group', + visibilityLevelIcon: 'earth', + visibilityLevelTooltip: + 'Public - The group and any public projects can be viewed without any authentication.', +}; + export const mockUsageMetrics = [ { identifier: 'groups', @@ -726,3 +740,8 @@ export const mockUsageMetricsNoData = [ options: { title: 'Pipelines', titleIcon: 'pipeline' }, }, ]; + +export const mockUsageOverviewData = { + namespace: mockUsageNamespaceData, + metrics: mockUsageMetrics, +}; -- GitLab From 138dfafb0ca69966b2d0f2458ad56f012e9f8252 Mon Sep 17 00:00:00 2001 From: Rudy Crespo <rcrespo@gitlab.com> Date: Thu, 4 Apr 2024 17:18:35 -0400 Subject: [PATCH 2/2] Fix tooltip's positioning --- .../components/visualizations/usage_overview.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/usage_overview.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/usage_overview.vue index f11c11be008d01..a7e28a8c3d58be 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/usage_overview.vue +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/usage_overview.vue @@ -76,7 +76,7 @@ export default { data.namespace.fullName }}</span> <gl-icon - v-gl-tooltip + v-gl-tooltip.viewport class="gl-text-secondary" :name="data.namespace.visibilityLevelIcon" :title="data.namespace.visibilityLevelTooltip" -- GitLab