Skip to content
Snippets Groups Projects
Commit 956003d5 authored by Elwyn Benson's avatar Elwyn Benson :two: Committed by Robert Hunt
Browse files

Add empty state for customizable dashboard panels

This will help make it clear there is no data instead of just showing
a blank / empty panel.

Changelog: changed
MR: !115266
EE: true
parent 8a56e084
No related branches found
No related tags found
3 merge requests!118700Remove refactor_vulnerability_filters feature flag,!116602Draft: Resolve "Remove the possibility to set redis_slot in known_events",!115266Add empty state to panels
......@@ -44,10 +44,10 @@ const convertToSingleValue = (resultSet, query) => {
const [row] = resultSet.rawData();
if (!row) {
return undefined;
return 0;
}
return row[measure ?? DEFAULT_COUNT_KEY] ?? Object.values(row)[0] ?? undefined;
return row[measure ?? DEFAULT_COUNT_KEY] ?? Object.values(row)[0] ?? 0;
};
const buildDateRangeFilter = (query, queryOverrides, { startDate, endDate }) => {
......
import { s__ } from '~/locale';
export const GRIDSTACK_MARGIN = 10;
export const GRIDSTACK_CSS_HANDLE = '.grid-stack-item-handle';
export const GRIDSTACK_CELL_HEIGHT = '120px';
export const GRIDSTACK_MIN_ROW = 1;
export const I18N_PANEL_EMPTY_STATE_MESSAGE = s__(
'Analytics|No results match your query or filter',
);
......@@ -2,6 +2,8 @@
import { GlLoadingIcon } from '@gitlab/ui';
import dataSources from 'ee/analytics/analytics_dashboards/data_sources';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { isEmptyPanelData } from 'ee/vue_shared/components/customizable_dashboard/utils';
import { I18N_PANEL_EMPTY_STATE_MESSAGE } from './constants';
export default {
name: 'AnalyticsDashboardPanel',
......@@ -46,6 +48,11 @@ export default {
loading: true,
};
},
computed: {
showEmptyState() {
return !this.error && isEmptyPanelData(this.visualization.type, this.data);
},
},
watch: {
visualization: {
handler: 'fetchData',
......@@ -78,6 +85,7 @@ export default {
}
},
},
I18N_PANEL_EMPTY_STATE_MESSAGE,
};
</script>
......@@ -94,8 +102,16 @@ export default {
>
<strong>{{ title }}</strong>
</tooltip-on-truncate>
<div class="gl-overflow-y-auto gl-h-full" :class="{ 'gl--flex-center': loading }">
<div
class="gl-overflow-y-auto gl-h-full"
:class="{ 'gl--flex-center': loading || showEmptyState }"
>
<gl-loading-icon v-if="loading" size="lg" />
<div v-else-if="showEmptyState" class="gl-text-center gl-text-secondary">
{{ $options.I18N_PANEL_EMPTY_STATE_MESSAGE }}
</div>
<component
:is="visualization.type"
v-else-if="!error"
......
import isEmpty from 'lodash/isEmpty';
import { queryToObject } from '~/lib/utils/url_utility';
import { formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { ISO_SHORT_FORMAT } from '~/vue_shared/constants';
......@@ -51,3 +52,12 @@ export const filtersToQueryParams = ({ dateRangeOption, startDate, endDate }) =>
endDate: customDateRange ? formatDate(endDate, ISO_SHORT_FORMAT) : null,
});
};
export const isEmptyPanelData = (visualizationType, data) => {
if (visualizationType === 'SingleStat') {
// SingleStat visualizations currently do not show an empty state, and instead show a default "0" value
// This will be revisited: https://gitlab.com/gitlab-org/gitlab/-/issues/398792
return false;
}
return isEmpty(data);
};
......@@ -5,6 +5,7 @@ import PanelsBase from 'ee/vue_shared/components/customizable_dashboard/panels_b
import dataSources from 'ee/analytics/analytics_dashboards/data_sources';
import waitForPromises from 'helpers/wait_for_promises';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { I18N_PANEL_EMPTY_STATE_MESSAGE } from 'ee/vue_shared/components/customizable_dashboard/constants';
import { dashboard } from './mock_data';
jest.mock('ee/analytics/analytics_dashboards/data_sources', () => ({
......@@ -70,22 +71,45 @@ describe('PanelsBase', () => {
});
describe('when the data has been fetched', () => {
const mockData = [{ name: 'foo' }];
describe('and there is data', () => {
const mockData = [{ name: 'foo' }];
beforeEach(() => {
jest.spyOn(dataSources.cube_analytics(), 'fetch').mockReturnValue(mockData);
createWrapper();
return waitForPromises();
});
beforeEach(() => {
jest.spyOn(dataSources.cube_analytics(), 'fetch').mockReturnValue(mockData);
createWrapper();
return waitForPromises();
});
it('should not render the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
it('should not render the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should not render the empty state', () => {
expect(wrapper.text()).not.toContain(I18N_PANEL_EMPTY_STATE_MESSAGE);
});
it('should render the visualization with the fetched data', () => {
expect(findVisualization().props()).toMatchObject({
data: mockData,
options: panelConfig.visualization.options,
});
});
});
it('should render the visualization with the fetched data', () => {
expect(findVisualization().props()).toMatchObject({
data: mockData,
options: panelConfig.visualization.options,
describe('and there is no data', () => {
beforeEach(() => {
jest.spyOn(dataSources.cube_analytics(), 'fetch').mockReturnValue(undefined);
createWrapper();
return waitForPromises();
});
it('should not render the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should render the empty state', () => {
const text = wrapper.text();
expect(text).toContain(I18N_PANEL_EMPTY_STATE_MESSAGE);
});
});
});
......@@ -103,6 +127,10 @@ describe('PanelsBase', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should not render the empty state', () => {
expect(wrapper.text()).not.toContain(I18N_PANEL_EMPTY_STATE_MESSAGE);
});
it('should not render the visualization', () => {
expect(findVisualization().exists()).toBe(false);
});
......
import {
getDateRangeOption,
dateRangeOptionToFilter,
buildDefaultDashboardFilters,
dateRangeOptionToFilter,
filtersToQueryParams,
getDateRangeOption,
isEmptyPanelData,
} from 'ee/vue_shared/components/customizable_dashboard/utils';
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
import {
DATE_RANGE_OPTIONS,
CUSTOM_DATE_RANGE_KEY,
DATE_RANGE_OPTIONS,
DEFAULT_SELECTED_OPTION_INDEX,
} from 'ee/vue_shared/components/customizable_dashboard/filters/constants';
import { mockDateRangeFilterChangePayload } from './mock_data';
......@@ -99,3 +100,19 @@ describe('filtersToQueryParams', () => {
});
});
});
describe('isEmptyPanelData', () => {
it.each`
visualizationType | value | expected
${'SingleStat'} | ${[]} | ${false}
${'SingleStat'} | ${1} | ${false}
${'LineChart'} | ${[]} | ${true}
${'LineChart'} | ${[1]} | ${false}
`(
'returns $expected for visualization "$visualizationType" with value "$value"',
async ({ visualizationType, value, expected }) => {
const result = isEmptyPanelData(visualizationType, value);
expect(result).toBe(expected);
},
);
});
......@@ -4834,6 +4834,9 @@ msgstr ""
msgid "Analytics|No dashboard matches the specified URL path."
msgstr ""
 
msgid "Analytics|No results match your query or filter"
msgstr ""
msgid "Analytics|OS"
msgstr ""
 
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment