diff --git a/ee/app/assets/javascripts/metrics/details/metrics_details.vue b/ee/app/assets/javascripts/metrics/details/metrics_details.vue index 6e3bf8e42986c2e237dd1ef6b7a3000e2e62bc98..5f4d55a416b22c306ce9b849375e43e348ee5fc8 100644 --- a/ee/app/assets/javascripts/metrics/details/metrics_details.vue +++ b/ee/app/assets/javascripts/metrics/details/metrics_details.vue @@ -1,5 +1,6 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; +import EMPTY_CHART_SVG from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg?url'; import { s__ } from '~/locale'; import { createAlert } from '~/alert'; import { visitUrl, isSafeURL } from '~/lib/utils/url_utility'; @@ -11,10 +12,12 @@ export default { 'ObservabilityMetrics|Error: Failed to load metrics details. Try reloading the page.', ), metricType: s__('ObservabilityMetrics|Type'), + noData: s__('ObservabilityMetrics|No data found for the selected metric.'), }, components: { GlLoadingIcon, MetricsChart, + GlEmptyState, }, props: { observabilityClient: { @@ -37,20 +40,17 @@ export default { }, data() { return { - metricData: null, + metricData: [], loading: false, }; }, computed: { header() { - if (this.metricData.length > 0) { - return { - title: this.metricData[0].name, - description: this.metricData[0].description, - type: this.metricData[0].type, - }; - } - return null; + return { + title: this.metricId, + type: this.metricType, + description: this.metricData[0]?.description, + }; }, }, created() { @@ -99,6 +99,7 @@ export default { visitUrl(this.metricsIndexUrl); }, }, + EMPTY_CHART_SVG, }; </script> @@ -107,8 +108,8 @@ export default { <gl-loading-icon size="lg" /> </div> - <div v-else-if="metricData" data-testid="metric-details" class="gl-m-7"> - <div v-if="header" data-testid="metric-header"> + <div v-else data-testid="metric-details" class="gl-m-7"> + <div data-testid="metric-header"> <h1 class="gl-font-size-h1 gl-my-0" data-testid="metric-title">{{ header.title }}</h1> <p class="gl-my-0" data-testid="metric-type"> <strong>{{ $options.i18n.metricType }}: </strong>{{ header.type }} @@ -116,6 +117,13 @@ export default { <p class="gl-my-0" data-testid="metric-description">{{ header.description }}</p> </div> - <metrics-chart :metric-data="metricData" /> + <div class="gl-my-6"> + <metrics-chart v-if="metricData.length > 0" :metric-data="metricData" /> + <gl-empty-state v-else :svg-path="$options.EMPTY_CHART_SVG"> + <template #title> + <p class="gl-font-lg">{{ $options.i18n.noData }}</p> + </template> + </gl-empty-state> + </div> </div> </template> diff --git a/ee/spec/frontend/metrics/details/metrics_details_spec.js b/ee/spec/frontend/metrics/details/metrics_details_spec.js index b0051d0a3e97164b07dcc5fd64f3d668b8703695..23f3f1cce6c91b1c393ab5b5dfef9424efa00ccd 100644 --- a/ee/spec/frontend/metrics/details/metrics_details_spec.js +++ b/ee/spec/frontend/metrics/details/metrics_details_spec.js @@ -1,4 +1,4 @@ -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; import MetricsDetails from 'ee/metrics/details/metrics_details.vue'; import { createMockClient } from 'helpers/mock_observability_client'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -20,7 +20,14 @@ describe('MetricsDetails', () => { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findMetricDetails = () => wrapper.findComponentByTestId('metric-details'); - const findHeader = () => wrapper.findComponentByTestId('metric-header'); + + const findHeader = () => findMetricDetails().find(`[data-testid="metric-header"]`); + const findHeaderTitle = () => findHeader().find(`[data-testid="metric-title"]`); + const findHeaderType = () => findHeader().find(`[data-testid="metric-type"]`); + const findHeaderDescription = () => findHeader().find(`[data-testid="metric-description"]`); + + const findChart = () => findMetricDetails().findComponent(MetricsChart); + const findEmptyState = () => findMetricDetails().findComponent(GlEmptyState); const props = { metricId: METRIC_ID, @@ -48,6 +55,7 @@ describe('MetricsDetails', () => { mountComponent(); expect(findLoadingIcon().exists()).toBe(true); + expect(findMetricDetails().exists()).toBe(false); expect(observabilityClientMock.isObservabilityEnabled).toHaveBeenCalled(); }); @@ -90,25 +98,26 @@ describe('MetricsDetails', () => { expect(chart.props('metricData')).toEqual(mockMetricData); }); - describe('header', () => { - it('renders the details header', () => { - const header = findHeader(); - expect(header.exists()).toBe(true); - expect(header.find(`[data-testid="metric-title"]`).text()).toBe( - 'container_cpu_usage_seconds_total', - ); - expect(header.find(`[data-testid="metric-description"]`).text()).toBe( - 'System disk operations', - ); - expect(header.find(`[data-testid="metric-type"]`).text()).toBe('Type:\u00a0Gauge'); - }); + it('renders the details header', () => { + expect(findHeader().exists()).toBe(true); + expect(findHeaderTitle().text()).toBe(METRIC_ID); + expect(findHeaderType().text()).toBe(`Type:\u00a0${METRIC_TYPE}`); + expect(findHeaderDescription().text()).toBe('System disk operations'); + }); - it('does not render the header if the metric data is empty', () => { + describe('with no data', () => { + beforeEach(async () => { observabilityClientMock.fetchMetric.mockResolvedValueOnce([]); - mountComponent(); - - expect(findHeader().exists()).toBe(false); + await mountComponent(); + }); + it('only renders the title and type headers', () => { + expect(findHeaderTitle().text()).toBe(METRIC_ID); + expect(findHeaderType().text()).toBe(`Type:\u00a0${METRIC_TYPE}`); + expect(findHeaderDescription().text()).toBe(''); + }); + it('renders the empty state', () => { + expect(findEmptyState().exists()).toBe(true); }); }); }); @@ -122,23 +131,36 @@ describe('MetricsDetails', () => { it('redirects to metricsIndexUrl', () => { expect(visitUrl).toHaveBeenCalledWith(props.metricsIndexUrl); }); - - it('does not render the metrics details', () => { - expect(findMetricDetails().exists()).toBe(false); - }); }); describe('error handling', () => { - it('if isObservabilityEnabled fails, it renders an alert and empty page', async () => { + beforeEach(async () => { observabilityClientMock.isObservabilityEnabled.mockRejectedValueOnce('error'); await mountComponent(); + }); - expect(createAlert).toHaveBeenCalledWith({ - message: 'Error: Failed to load metrics details. Try reloading the page.', + describe.each([ + ['isObservabilityEnabled', () => observabilityClientMock.isObservabilityEnabled], + ['fetchMetric', () => observabilityClientMock.fetchMetric], + ])('when %s fails', (_, mockFn) => { + beforeEach(async () => { + mockFn().mockRejectedValueOnce('error'); + await mountComponent(); + }); + it('renders an alert', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: 'Error: Failed to load metrics details. Try reloading the page.', + }); + }); + + it('only renders the empty state and header', () => { + expect(findMetricDetails().exists()).toBe(true); + expect(findEmptyState().exists()).toBe(true); + expect(findLoadingIcon().exists()).toBe(false); + expect(findHeader().exists()).toBe(true); + expect(findChart().exists()).toBe(false); }); - expect(findLoadingIcon().exists()).toBe(false); - expect(findMetricDetails().exists()).toBe(false); }); }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9f2ddbd3853bf517ff91b2c278ad7b545ccb2a77..0fd50bcc35b32251a3aa5518290275f5f5447101 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -33008,6 +33008,9 @@ msgstr "" msgid "ObservabilityMetrics|Name" msgstr "" +msgid "ObservabilityMetrics|No data found for the selected metric." +msgstr "" + msgid "ObservabilityMetrics|Search metrics starting with..." msgstr ""