Skip to content
Snippets Groups Projects
Commit 3c1e0fe9 authored by Tim Zallmann's avatar Tim Zallmann :speech_balloon: Committed by Robert Hunt
Browse files

Save visualizations to snowplow path

- Update the visualization save path
- Improve the designer UI consistency
- Improve the designer save error messages
- Move strings to constants
- Fix todo issue link
- Add ability to open dashboard in editing mode
- Change visualization "title" to "name"
parent 51e1ab06
No related branches found
No related tags found
1 merge request!114915Update visualization designer to save configs to file
Showing
with 626 additions and 44 deletions
......@@ -3,11 +3,11 @@ import axios from '~/lib/utils/axios_utils';
import service from '~/ide/services/';
import { s__, sprintf } from '~/locale';
const DASHBOARD_BRANCH = 'main';
export const DASHBOARD_BRANCH = 'main';
export const CUSTOM_DASHBOARDS_PATH = '.gitlab/dashboards/';
export const PRODUCT_ANALYTICS_VISUALIZATIONS_PATH =
'.gitlab/dashboards/product_analytics/visualizations/';
export const PRODUCT_ANALYTICS_VISUALIZATIONS_PATH = '.gitlab/analytics/dashboards/visualizations/';
export const CONFIGURATION_FILE_TYPE = '.yaml';
export const CREATE_FILE_ACTION = 'create';
export const UPDATE_FILE_ACTION = 'update';
......@@ -28,7 +28,7 @@ const getFileFromCustomDashboardProject = async (directory, fileId, projectInfo)
`${gon.relative_url_root}/${
projectInfo.fullPath
}/-/raw/${DASHBOARD_BRANCH}/${encodeURIComponent(
`${directory}${fileId}.yml`.replace(/^\//, ''),
`${directory}${fileId}${CONFIGURATION_FILE_TYPE}`.replace(/^\//, ''),
)}`,
{ params: { cb: Math.random() } },
);
......@@ -47,6 +47,28 @@ export async function getProductAnalyticsVisualization(visualizationId, projectI
);
}
export async function saveProductAnalyticsVisualization(
visualizationName,
visualizationCode,
projectInfo,
) {
const payload = {
branch: DASHBOARD_BRANCH,
commit_message: sprintf(s__('Analytics|Updating visualization %{visualizationName}'), {
visualizationName,
}),
actions: [
{
action: CREATE_FILE_ACTION,
file_path: `${PRODUCT_ANALYTICS_VISUALIZATIONS_PATH}${visualizationName}${CONFIGURATION_FILE_TYPE}`,
content: stringify(visualizationCode, null),
encoding: 'text',
},
],
};
return service.commit(projectInfo.fullPath, payload);
}
export async function getCustomDashboards(projectInfo) {
return getFileListFromCustomDashboardProject(CUSTOM_DASHBOARDS_PATH, projectInfo);
}
......@@ -71,7 +93,7 @@ export async function saveCustomDashboard({
actions: [
{
action,
file_path: `${CUSTOM_DASHBOARDS_PATH}${dashboardId}.yml`,
file_path: `${CUSTOM_DASHBOARDS_PATH}${dashboardId}${CONFIGURATION_FILE_TYPE}`,
content: stringify(dashboardObject, null),
encoding: 'text',
},
......
......@@ -110,7 +110,7 @@ export default {
},
apollo: {
// TODO: Add retrieval of visualizations for Snowplow
// https://gitlab.com/gitlab-org/gitlab/-/issues/411597
// https://gitlab.com/gitlab-org/gitlab/-/issues/414281
dashboard: {
query: getProductAnalyticsDashboardQuery,
variables() {
......
......@@ -2,13 +2,24 @@
import { QueryBuilder } from '@cubejs-client/vue';
import { GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import { slugify } from '~/lib/utils/text_utility';
import { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
import { createCubeJsApi } from 'ee/analytics/analytics_dashboards/data_sources/cube_analytics';
import { getPanelOptions } from 'ee/analytics/analytics_dashboards/utils/visualization_panel_options';
import { saveProductAnalyticsVisualization } from 'ee/analytics/analytics_dashboards/api/dashboards_api';
import { NEW_DASHBOARD_SLUG } from 'ee/vue_shared/components/customizable_dashboard/constants';
import {
PANEL_DISPLAY_TYPES,
I18N_DASHBOARD_LIST_VISUALIZATION_DESIGNER_CUBEJS_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_NAME_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_MEASURE_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_TYPE_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_ALREADY_EXISTS_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_SUCCESS,
} from '../constants';
import MeasureSelector from './visualization_designer/selectors/product_analytics/measure_selector.vue';
......@@ -26,6 +37,12 @@ export default {
VisualizationInspector,
VisualizationPreview,
},
inject: {
customDashboardsProject: {
type: Object,
default: null,
},
},
data() {
const query = {
limit: 100,
......@@ -38,11 +55,12 @@ export default {
measureType: '',
measureSubType: '',
},
cubeJsErrorAlert: null,
defaultTitle: '',
visualizationName: '',
selectedDisplayType: PANEL_DISPLAY_TYPES.DATA,
selectedVisualizationType: '',
hasTimeDimension: false,
isSaving: false,
alert: null,
};
},
computed: {
......@@ -56,7 +74,6 @@ export default {
return {
version: 1,
title: this.defaultTitle,
type: this.selectedVisualizationType,
data: {
type: 'cube_analytics',
......@@ -68,6 +85,14 @@ export default {
panelOptions() {
return getPanelOptions(this.selectedVisualizationType, this.hasTimeDimension);
},
saveButtonText() {
return this.$route?.params.dashboardid
? s__('Analytics|Save and add to Dashboard')
: s__('Analytics|Save new visualization');
},
},
beforeDestroy() {
this.alert?.dismiss();
},
mounted() {
// Needs to be dynamic as it can't be changed on the cube component
......@@ -83,15 +108,11 @@ export default {
methods: {
onQueryStatusChange({ error }) {
if (!error) {
this.cubeJsErrorAlert?.dismiss();
this.alert?.dismiss();
return;
}
this.cubeJsErrorAlert = createAlert({
message: I18N_DASHBOARD_LIST_VISUALIZATION_DESIGNER_CUBEJS_ERROR,
captureError: true,
error,
});
this.showAlert(I18N_DASHBOARD_LIST_VISUALIZATION_DESIGNER_CUBEJS_ERROR, error, true);
},
onVizStateChange(state) {
this.hasTimeDimension = Boolean(state.query.timeDimensions?.length);
......@@ -107,8 +128,90 @@ export default {
this.selectDisplayType(PANEL_DISPLAY_TYPES.VISUALIZATION);
this.selectedVisualizationType = newType;
},
addToDashboard() {
this.selectDisplayType(PANEL_DISPLAY_TYPES.CODE);
getSaveVisualizationValidationError() {
if (!this.visualizationName) {
return I18N_DASHBOARD_VISUALIZATION_DESIGNER_NAME_ERROR;
}
if (!this.selectedVisualizationType) {
return I18N_DASHBOARD_VISUALIZATION_DESIGNER_TYPE_ERROR;
}
if (!this.queryState.measureSubType) {
return I18N_DASHBOARD_VISUALIZATION_DESIGNER_MEASURE_ERROR;
}
return null;
},
async saveVisualization() {
const validationError = this.getSaveVisualizationValidationError();
if (validationError) {
this.showAlert(validationError);
return;
}
this.isSaving = true;
try {
const filename = slugify(this.visualizationName, '_');
const saveResult = await saveProductAnalyticsVisualization(
filename,
this.resultVisualization,
this.customDashboardsProject,
);
if (saveResult.status === HTTP_STATUS_CREATED) {
this.alert?.dismiss();
this.$toast.show(I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_SUCCESS);
if (this.$route?.params.dashboard) {
this.routeToDashboard(this.$route?.params.dashboard);
}
} else {
this.showAlert(
I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_ERROR,
new Error(
`Recieved an unexpected HTTP status while saving visualization: ${saveResult.status}`,
),
true,
);
}
} catch (error) {
const { message = '' } = error?.response?.data || {};
// eslint-disable-next-line @gitlab/require-i18n-strings
if (message === 'A file with this name already exists') {
this.showAlert(I18N_DASHBOARD_VISUALIZATION_DESIGNER_ALREADY_EXISTS_ERROR);
} else {
this.showAlert(
`${I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_ERROR} ${message}`.trimEnd(),
error,
true,
);
}
} finally {
this.isSaving = false;
}
},
routeToDashboard(dashboard) {
if (dashboard === NEW_DASHBOARD_SLUG) {
this.$router.push('/new');
} else {
this.$router.push({
name: 'dashboard-detail',
params: {
slug: dashboard,
editing: true,
},
});
}
},
showAlert(message, error = null, captureError = false) {
this.alert = createAlert({
message,
error,
captureError,
});
},
},
I18N_DASHBOARD_LIST_VISUALIZATION_DESIGNER_CUBEJS_ERROR,
......@@ -120,19 +223,24 @@ export default {
<div class="gl-display-flex gl-mb-4 gl-mt-4">
<div class="gl-flex-direction-column gl-flex-grow-1">
<input
v-model="defaultTitle"
v-model="visualizationName"
dir="auto"
type="text"
:placeholder="s__('Analytics|New Analytics Visualization Title')"
:aria-label="__('Title')"
:placeholder="s__('Analytics|New analytics visualization name')"
:aria-label="__('Name')"
class="form-control gl-border-gray-200"
data-testid="panel-title-tba"
/>
</div>
<div class="gl-ml-2">
<gl-button category="primary" @click="addToDashboard">{{
s__('Analytics|Add to Dashboard')
}}</gl-button>
<gl-button
:loading="isSaving"
category="primary"
variant="confirm"
data-testid="visualization-save-btn"
@click="saveVisualization"
>{{ saveButtonText }}</gl-button
>
</div>
</div>
<div id="js-query-builder-wrapper" class="gl-border-t">
......@@ -198,6 +306,7 @@ export default {
:loading="loading"
:result-set="resultSet ? resultSet : null"
:result-visualization="resultSet && isQueryPresent ? resultVisualization : null"
:title="visualizationName"
@selectedDisplayType="selectDisplayType"
/>
</div>
......
......@@ -51,6 +51,11 @@ export default {
required: false,
default: null,
},
title: {
type: String,
required: false,
default: '',
},
},
computed: {
dataTableResults() {
......@@ -114,7 +119,7 @@ export default {
data-testid="grid-stack-panel"
>
<div
class="grid-stack-item-content gl-shadow gl-rounded-base gl-p-4 gl-display-flex gl-flex-direction-column gl-bg-white"
class="grid-stack-item-content gl-shadow-sm gl-rounded-base gl-p-4 gl-display-flex gl-flex-direction-column gl-bg-white"
>
<strong class="gl-mb-2">{{ s__('Analytics|Resulting Data') }}</strong>
<div class="gl-overflow-y-auto gl-h-full">
......@@ -135,7 +140,7 @@ export default {
>
<panels-base
v-if="selectedVisualizationType"
:title="resultVisualization.title"
:title="title"
:visualization="resultVisualization"
:style="{ height: $options.PANEL_VISUALIZATION_HEIGHT }"
data-testid="preview-visualization"
......@@ -150,9 +155,12 @@ export default {
</div>
</div>
<div v-if="displayType === $options.PANEL_DISPLAY_TYPES.CODE" class="gl-m-4">
<div
v-if="displayType === $options.PANEL_DISPLAY_TYPES.CODE"
class="gl-bg-white gl-m-5 gl-p-4 gl-shadow-sm gl-rounded-base"
>
<pre
class="code highlight gl-display-flex gl-bg-white"
class="code highlight gl-display-flex gl-bg-transparent gl-border-none"
data-testid="preview-code"
><code>{{ resultVisualization }}</code></pre>
</div>
......
......@@ -17,6 +17,25 @@ export const I18N_DASHBOARD_LIST_VISUALIZATION_DESIGNER_CUBEJS_ERROR = s__(
'Analytics|An error occurred while loading data',
);
export const I18N_DASHBOARD_VISUALIZATION_DESIGNER_NAME_ERROR = s__(
'Analytics|Enter a visualization name',
);
export const I18N_DASHBOARD_VISUALIZATION_DESIGNER_MEASURE_ERROR = s__(
'Analytics|Select a measurement',
);
export const I18N_DASHBOARD_VISUALIZATION_DESIGNER_TYPE_ERROR = s__(
'Analytics|Select a visualization type',
);
export const I18N_DASHBOARD_VISUALIZATION_DESIGNER_ALREADY_EXISTS_ERROR = s__(
'Analytics|A visualization with that name already exists.',
);
export const I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_ERROR = s__(
'Analytics|Error while saving visualization.',
);
export const I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_SUCCESS = s__(
'Analytics|Visualization was saved successfully',
);
export const I18N_ALERT_NO_POINTER_TITLE = s__('Analytics|Custom dashboards');
export const I18N_ALERT_NO_POINTER_BUTTON = s__('Analytics|Configure Dashboard Project');
export const I18N_ALERT_NO_POINTER_DESCRIPTION = s__(
......
......@@ -14,3 +14,5 @@ export const CURSOR_GRABBING_CLASS = 'gl-cursor-grabbing!';
export const I18N_VISUALIZATION_SELECTOR_NEW = s__('ProductAnalytics|Create a visualization');
export const I18N_PRODUCT_ANALYTICS_TITLE = __('Product analytics');
export const NEW_DASHBOARD_SLUG = 'new';
......@@ -15,6 +15,7 @@ import {
GRIDSTACK_CELL_HEIGHT,
GRIDSTACK_MIN_ROW,
CURSOR_GRABBING_CLASS,
NEW_DASHBOARD_SLUG,
} from './constants';
import VisualizationSelector from './dashboard_editor/visualization_selector.vue';
import { filtersToQueryParams } from './utils';
......@@ -114,6 +115,14 @@ export default {
isNewDashboard(isNew) {
this.editing = isNew;
},
'$route.params.editing': {
handler(editing) {
if (editing !== undefined) {
this.editing = editing;
}
},
immediate: true,
},
},
async created() {
try {
......@@ -203,7 +212,8 @@ export default {
}
},
routeToVisualizationDesigner() {
this.$router.push({ name: 'visualization-designer' });
const dashboard = this.isNewDashboard ? NEW_DASHBOARD_SLUG : this.dashboard.slug;
this.$router.push({ name: 'visualization-designer', params: { dashboard } });
},
async saveEdit(submitEvent) {
submitEvent.preventDefault();
......
import { nextTick } from 'vue';
import { __setMockMetadata } from '@cubejs-client/core';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_CREATED, HTTP_STATUS_FORBIDDEN } from '~/lib/utils/http_status';
import { createAlert } from '~/alert';
import { saveProductAnalyticsVisualization } from 'ee/analytics/analytics_dashboards/api/dashboards_api';
import AnalyticsVisualizationDesigner from 'ee/analytics/analytics_dashboards/components/analytics_visualization_designer.vue';
import { mockMetaData } from '../mock_data';
import VisualizationInspector from 'ee/analytics/analytics_dashboards/components/visualization_designer/analytics_visualization_inspector.vue';
import {
I18N_DASHBOARD_VISUALIZATION_DESIGNER_NAME_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_MEASURE_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_TYPE_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_ALREADY_EXISTS_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_ERROR,
I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_SUCCESS,
I18N_DASHBOARD_LIST_VISUALIZATION_DESIGNER_CUBEJS_ERROR,
} from 'ee/analytics/analytics_dashboards/constants';
import { NEW_DASHBOARD_SLUG } from 'ee/vue_shared/components/customizable_dashboard/constants';
import { mockMetaData, TEST_CUSTOM_DASHBOARDS_PROJECT } from '../mock_data';
import { BuilderComponent, QueryBuilder } from '../stubs';
const mockAlertDismiss = jest.fn();
jest.mock('~/alert', () => ({
createAlert: jest.fn().mockImplementation(() => ({
dismiss: mockAlertDismiss,
})),
}));
jest.mock('ee/analytics/analytics_dashboards/api/dashboards_api');
const showToast = jest.fn();
const routerPush = jest.fn();
describe('AnalyticsVisualizationDesigner', () => {
let wrapper;
const findTitleInput = () => wrapper.findByTestId('panel-title-tba');
const findMeasureSelector = () => wrapper.findByTestId('panel-measure-selector');
const findDimensionSelector = () => wrapper.findByTestId('panel-dimension-selector');
const findSaveButton = () => wrapper.findByTestId('visualization-save-btn');
const findQueryBuilder = () => wrapper.findByTestId('query-builder');
const findVisualizationInspector = () => wrapper.findComponent(VisualizationInspector);
const setVisualizationTitle = (newTitle = '') => {
const textinput = findTitleInput();
textinput.setValue(newTitle);
textinput.trigger('input');
};
const setMeasurement = (type = '', subType = '') => {
findMeasureSelector().vm.$emit('measureSelected', type, subType);
};
const setVisualizationType = (type = '') => {
findVisualizationInspector().vm.$emit('selectVisualizationType', type);
};
const setAllRequiredFields = () => {
setVisualizationTitle('New Title');
setMeasurement('pageViews', 'all');
setVisualizationType('SingleStat');
};
const createWrapper = () => {
wrapper = shallowMountExtended(AnalyticsVisualizationDesigner);
const mockSaveVisualizationImplementation = async (responseCallback) => {
saveProductAnalyticsVisualization.mockImplementation(responseCallback);
await waitForPromises();
};
const createWrapper = (sourceDashboardSlug) => {
const mocks = {
$toast: {
show: showToast,
},
$route: {
params: {
dashboard: sourceDashboardSlug || '',
},
},
$router: {
push: routerPush,
},
};
wrapper = shallowMountExtended(AnalyticsVisualizationDesigner, {
stubs: {
RouterView: true,
BuilderComponent,
QueryBuilder,
},
mocks,
provide: {
customDashboardsProject: TEST_CUSTOM_DASHBOARDS_PROJECT,
},
});
};
describe('when mounted', () => {
......@@ -29,4 +114,181 @@ describe('AnalyticsVisualizationDesigner', () => {
expect(findDimensionSelector().exists()).toBe(false);
});
});
describe('query builder', () => {
beforeEach(() => {
__setMockMetadata(jest.fn().mockImplementation(() => mockMetaData));
createWrapper();
});
it('shows an alert when a query error occurs', () => {
const error = new Error();
findQueryBuilder().vm.$emit('queryStatus', { error });
expect(createAlert).toHaveBeenCalledWith({
message: I18N_DASHBOARD_LIST_VISUALIZATION_DESIGNER_CUBEJS_ERROR,
captureError: true,
error,
});
});
});
describe('when saving', () => {
beforeEach(() => {
__setMockMetadata(jest.fn().mockImplementation(() => mockMetaData));
createWrapper();
setAllRequiredFields();
});
it.each`
field | setter | errorMessage
${'title'} | ${setVisualizationTitle} | ${I18N_DASHBOARD_VISUALIZATION_DESIGNER_NAME_ERROR}
${'measurement'} | ${setMeasurement} | ${I18N_DASHBOARD_VISUALIZATION_DESIGNER_MEASURE_ERROR}
${'type'} | ${setVisualizationType} | ${I18N_DASHBOARD_VISUALIZATION_DESIGNER_TYPE_ERROR}
`(
'creates an alert when the $field is empty or not selected',
async ({ setter, errorMessage }) => {
setter();
await findSaveButton().vm.$emit('click');
expect(createAlert).toHaveBeenCalledWith({
message: errorMessage,
captureError: false,
error: null,
});
},
);
it('successfully', async () => {
await mockSaveVisualizationImplementation(() => ({ status: HTTP_STATUS_CREATED }));
await findSaveButton().vm.$emit('click');
expect(saveProductAnalyticsVisualization).toHaveBeenCalledWith(
'new_title',
{
data: {
query: { foo: 'bar' },
type: 'cube_analytics',
},
options: {},
type: 'SingleStat',
version: 1,
},
TEST_CUSTOM_DASHBOARDS_PROJECT,
);
await waitForPromises();
expect(showToast).toHaveBeenCalledWith(I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_SUCCESS);
});
it('dismisses the existing alert after successfully saving', async () => {
await setVisualizationTitle('');
await findSaveButton().vm.$emit('click');
await mockSaveVisualizationImplementation(() => ({ status: HTTP_STATUS_CREATED }));
await setAllRequiredFields();
await findSaveButton().vm.$emit('click');
await waitForPromises();
expect(mockAlertDismiss).toHaveBeenCalled();
});
it('and a error happens', async () => {
await mockSaveVisualizationImplementation(() => ({ status: HTTP_STATUS_FORBIDDEN }));
await findSaveButton().vm.$emit('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_ERROR,
error: new Error(
`Recieved an unexpected HTTP status while saving visualization: ${HTTP_STATUS_FORBIDDEN}`,
),
captureError: true,
});
});
it('and the server responds with "A file with this name already exists"', async () => {
const responseError = new Error();
responseError.response = {
data: { message: 'A file with this name already exists' },
};
mockSaveVisualizationImplementation(() => {
throw responseError;
});
await findSaveButton().vm.$emit('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: I18N_DASHBOARD_VISUALIZATION_DESIGNER_ALREADY_EXISTS_ERROR,
error: null,
captureError: false,
});
});
it('and an error is thrown', async () => {
const newError = new Error();
mockSaveVisualizationImplementation(() => {
throw newError;
});
await findSaveButton().vm.$emit('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
error: newError,
message: I18N_DASHBOARD_VISUALIZATION_DESIGNER_SAVE_ERROR,
captureError: true,
});
});
});
describe('beforeDestroy', () => {
beforeEach(() => {
__setMockMetadata(jest.fn().mockImplementation(() => mockMetaData));
createWrapper();
});
it('should dismiss the alert', async () => {
await findSaveButton().vm.$emit('click');
wrapper.destroy();
await nextTick();
expect(mockAlertDismiss).toHaveBeenCalled();
});
});
describe('when editing for dashboard', () => {
const setupSaveDashbboard = async (dashboard) => {
__setMockMetadata(jest.fn().mockImplementation(() => mockMetaData));
createWrapper(dashboard);
setAllRequiredFields();
await mockSaveVisualizationImplementation(() => ({ status: HTTP_STATUS_CREATED }));
await findSaveButton().vm.$emit('click');
await waitForPromises();
};
it('after save it will redirect for new dashboards', async () => {
await setupSaveDashbboard(NEW_DASHBOARD_SLUG);
expect(routerPush).toHaveBeenCalledWith('/new');
});
it('after save it will redirect for existing dashboards', async () => {
await setupSaveDashbboard('test-source-dashboard');
expect(routerPush).toHaveBeenCalledWith({
name: 'dashboard-detail',
params: {
slug: 'test-source-dashboard',
editing: true,
},
});
});
});
});
......@@ -5,6 +5,7 @@ import {
PANEL_VISUALIZATION_HEIGHT,
} from 'ee/analytics/analytics_dashboards/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_VISUALIZATION } from '../../mock_data';
describe('AnalyticsVisualizationPreview', () => {
let wrapper;
......@@ -15,6 +16,8 @@ describe('AnalyticsVisualizationPreview', () => {
const selectDisplayType = jest.fn();
const resultVisualization = TEST_VISUALIZATION();
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(AnalyticsVisualizationPreview, {
propsData: {
......@@ -24,7 +27,7 @@ describe('AnalyticsVisualizationPreview', () => {
isQueryPresent: false,
loading: false,
resultSet: { tableColumns: () => [], tablePivot: () => [] },
resultVisualization: {},
resultVisualization,
...props,
},
});
......@@ -95,6 +98,7 @@ describe('AnalyticsVisualizationPreview', () => {
describe('resultSet and visualization is selected', () => {
beforeEach(() => {
createWrapper({
title: 'Hello world',
isQueryPresent: true,
displayType: PANEL_DISPLAY_TYPES.VISUALIZATION,
selectedVisualizationType: 'LineChart',
......@@ -102,9 +106,13 @@ describe('AnalyticsVisualizationPreview', () => {
});
it('should render visualization', () => {
expect(wrapper.findByTestId('preview-visualization').attributes('style')).toBe(
`height: ${PANEL_VISUALIZATION_HEIGHT};`,
);
const preview = wrapper.findByTestId('preview-visualization');
expect(preview.attributes('style')).toBe(`height: ${PANEL_VISUALIZATION_HEIGHT};`);
expect(preview.props()).toMatchObject({
title: 'Hello world',
visualization: resultVisualization,
});
});
});
......
export const BuilderComponent = {
data() {
return {
resultSet: {
query: () => ({ foo: 'bar' }),
},
};
},
template: '<div><slot></slot></div>',
};
export const QueryBuilder = {
data() {
return {
loading: false,
filters: [],
measures: [],
dimensions: [],
timeDimensions: [],
setMeasures: () => {},
setFilters: () => {},
addFilters: () => {},
addDimensions: () => {},
removeDimensions: () => {},
setTimeDimensions: () => {},
removeTimeDimensions: () => {},
};
},
template: `
<builder-component>
<slot name="builder" v-bind="{measures, dimensions, timeDimensions, setTimeDimensions, removeTimeDimensions, removeDimensions, addDimensions, filters, setMeasures, setFilters, addFilters}"></slot>
<slot v-bind="{loading}"></slot>
</builder-component>
`,
};
......@@ -10,10 +10,13 @@ import {
saveCustomDashboard,
getProductAnalyticsVisualizationList,
getProductAnalyticsVisualization,
saveProductAnalyticsVisualization,
CUSTOM_DASHBOARDS_PATH,
PRODUCT_ANALYTICS_VISUALIZATIONS_PATH,
CREATE_FILE_ACTION,
UPDATE_FILE_ACTION,
CONFIGURATION_FILE_TYPE,
DASHBOARD_BRANCH,
} from 'ee/analytics/analytics_dashboards/api/dashboards_api';
import {
TEST_CUSTOM_DASHBOARDS_PROJECT,
......@@ -64,7 +67,9 @@ describe('AnalyticsDashboard', () => {
it('get a single dashboard', async () => {
const expectedUrl = `${dummyUrlRoot}/${
TEST_CUSTOM_DASHBOARDS_PROJECT.fullPath
}/-/raw/main/${encodeURIComponent(CUSTOM_DASHBOARDS_PATH + 'abc.yml'.replace(/^\//, ''))}`;
}/-/raw/main/${encodeURIComponent(
CUSTOM_DASHBOARDS_PATH + `abc${CONFIGURATION_FILE_TYPE}`.replace(/^\//, ''),
)}`;
mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, TEST_CUSTOM_DASHBOARD());
jest.spyOn(axios, 'get');
......@@ -95,12 +100,12 @@ describe('AnalyticsDashboard', () => {
});
const callPayload = {
branch: 'main',
branch: DASHBOARD_BRANCH,
commit_message: isNewFile ? 'Create dashboard abc' : 'Updating dashboard abc',
actions: [
{
action,
file_path: `${CUSTOM_DASHBOARDS_PATH}${dashboardId}.yml`,
file_path: `${CUSTOM_DASHBOARDS_PATH}${dashboardId}${CONFIGURATION_FILE_TYPE}`,
previous_path: undefined,
content: 'id: test\n',
encoding: 'text',
......@@ -144,7 +149,7 @@ describe('AnalyticsDashboard', () => {
const expectedUrl = `${dummyUrlRoot}/${
TEST_CUSTOM_DASHBOARDS_PROJECT.fullPath
}/-/raw/main/${encodeURIComponent(
PRODUCT_ANALYTICS_VISUALIZATIONS_PATH + 'abc.yml'.replace(/^\//, ''),
PRODUCT_ANALYTICS_VISUALIZATIONS_PATH + `abc${CONFIGURATION_FILE_TYPE}`.replace(/^\//, ''),
)}`;
mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, TEST_CUSTOM_DASHBOARD());
......@@ -155,4 +160,41 @@ describe('AnalyticsDashboard', () => {
});
});
});
describe('visualization save functions', () => {
beforeEach(() => {
jest.spyOn(service, 'commit').mockResolvedValue({ data: {} });
});
it('save a new visualization', async () => {
const visualizationName = 'abc';
const result = await saveProductAnalyticsVisualization(
visualizationName,
{ id: 'test' },
TEST_CUSTOM_DASHBOARDS_PROJECT,
);
const callPayload = {
branch: DASHBOARD_BRANCH,
commit_message: 'Updating visualization abc',
actions: [
{
action: 'create',
file_path: `${PRODUCT_ANALYTICS_VISUALIZATIONS_PATH}${visualizationName}${CONFIGURATION_FILE_TYPE}`,
content: 'id: test\n',
encoding: 'text',
},
],
start_sha: undefined,
};
expect(service.commit).toHaveBeenCalledWith(
TEST_CUSTOM_DASHBOARDS_PROJECT.fullPath,
callPayload,
);
expect(result).toEqual({ data: {} });
});
});
});
......@@ -11,6 +11,7 @@ import {
GRIDSTACK_CSS_HANDLE,
GRIDSTACK_CELL_HEIGHT,
GRIDSTACK_MIN_ROW,
NEW_DASHBOARD_SLUG,
} from 'ee/vue_shared/components/customizable_dashboard/constants';
import { loadCSSFile } from '~/lib/utils/css_utils';
import { createAlert } from '~/alert';
......@@ -55,7 +56,12 @@ describe('CustomizableDashboard', () => {
push: jest.fn(),
};
const createWrapper = (props = {}, loadedDashboard = dashboard, provide = {}) => {
const createWrapper = (
props = {},
loadedDashboard = dashboard,
provide = {},
routeParams = {},
) => {
const loadDashboard = { ...loadedDashboard };
loadDashboard.default = { ...loadDashboard };
......@@ -70,6 +76,9 @@ describe('CustomizableDashboard', () => {
},
mocks: {
$router,
$route: {
params: routeParams,
},
},
provide,
});
......@@ -236,6 +245,21 @@ describe('CustomizableDashboard', () => {
expect(findEditButton().exists()).toBe(true);
});
describe('when mounted with the $route.editing param', () => {
beforeEach(() => {
createWrapper(
{},
dashboard,
{ glFeatures: { combinedAnalyticsDashboardsEditor: true } },
{ editing: true },
);
});
it('opens the dashboard in edit mode', () => {
expect(findVisualizationSelector().exists()).toBe(true);
});
});
describe('when editing', () => {
beforeEach(() => {
findEditButton().vm.$emit('click');
......@@ -321,7 +345,12 @@ describe('CustomizableDashboard', () => {
it('routes to the designer when a "create" event is recieved', async () => {
await findVisualizationSelector().vm.$emit('create');
expect($router.push).toHaveBeenCalledWith({ name: 'visualization-designer' });
expect($router.push).toHaveBeenCalledWith({
name: 'visualization-designer',
params: {
dashboard: dashboard.slug,
},
});
});
});
});
......@@ -407,6 +436,17 @@ describe('CustomizableDashboard', () => {
expect(findCodeView().exists()).toBe(false);
});
it('routes to the designer with `dashboard: "new"` when a "create" event is recieved', async () => {
await findVisualizationSelector().vm.$emit('create');
expect($router.push).toHaveBeenCalledWith({
name: 'visualization-designer',
params: {
dashboard: NEW_DASHBOARD_SLUG,
},
});
});
});
describe('when saving while editing and the editor is enabled', () => {
......
......@@ -24,6 +24,7 @@ const cubeLineChart = {
export const dashboard = {
id: 'analytics_overview',
slug: 'analytics_overview',
title: 'Analytics Overview',
panels: [
{
......
......@@ -5239,7 +5239,7 @@ msgstr ""
msgid "Analytics"
msgstr ""
 
msgid "Analytics|Add to Dashboard"
msgid "Analytics|A visualization with that name already exists."
msgstr ""
 
msgid "Analytics|Add visualizations"
......@@ -5311,9 +5311,15 @@ msgstr ""
msgid "Analytics|Edit"
msgstr ""
 
msgid "Analytics|Enter a visualization name"
msgstr ""
msgid "Analytics|Error while saving dashboard"
msgstr ""
 
msgid "Analytics|Error while saving visualization."
msgstr ""
msgid "Analytics|Host"
msgstr ""
 
......@@ -5323,7 +5329,7 @@ msgstr ""
msgid "Analytics|Line Chart"
msgstr ""
 
msgid "Analytics|New Analytics Visualization Title"
msgid "Analytics|New analytics visualization name"
msgstr ""
 
msgid "Analytics|New dashboard"
......@@ -5362,6 +5368,18 @@ msgstr ""
msgid "Analytics|Save"
msgstr ""
 
msgid "Analytics|Save and add to Dashboard"
msgstr ""
msgid "Analytics|Save new visualization"
msgstr ""
msgid "Analytics|Select a measurement"
msgstr ""
msgid "Analytics|Select a visualization type"
msgstr ""
msgid "Analytics|Single Statistic"
msgstr ""
 
......@@ -5374,6 +5392,9 @@ msgstr ""
msgid "Analytics|Updating dashboard %{dashboardId}"
msgstr ""
 
msgid "Analytics|Updating visualization %{visualizationName}"
msgstr ""
msgid "Analytics|Users"
msgstr ""
 
......@@ -5392,6 +5413,9 @@ msgstr ""
msgid "Analytics|Visualization Type"
msgstr ""
 
msgid "Analytics|Visualization was saved successfully"
msgstr ""
msgid "Analyze your dependencies for known vulnerabilities."
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