mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[Lens] Metric trendlines design changes (#143781)
This commit is contained in:
parent
670fe25673
commit
15ef4a0bcc
12 changed files with 686 additions and 399 deletions
|
@ -1748,12 +1748,15 @@ describe('IndexPattern Data Source', () => {
|
||||||
currentIndexPatternId: '1',
|
currentIndexPatternId: '1',
|
||||||
};
|
};
|
||||||
expect(FormBasedDatasource.removeLayer(state, 'first')).toEqual({
|
expect(FormBasedDatasource.removeLayer(state, 'first')).toEqual({
|
||||||
...state,
|
removedLayerIds: ['first'],
|
||||||
layers: {
|
newState: {
|
||||||
second: {
|
...state,
|
||||||
indexPatternId: '2',
|
layers: {
|
||||||
columnOrder: [],
|
second: {
|
||||||
columns: {},
|
indexPatternId: '2',
|
||||||
|
columnOrder: [],
|
||||||
|
columns: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1777,8 +1780,72 @@ describe('IndexPattern Data Source', () => {
|
||||||
currentIndexPatternId: '1',
|
currentIndexPatternId: '1',
|
||||||
};
|
};
|
||||||
expect(FormBasedDatasource.removeLayer(state, 'first')).toEqual({
|
expect(FormBasedDatasource.removeLayer(state, 'first')).toEqual({
|
||||||
...state,
|
removedLayerIds: ['first', 'second'],
|
||||||
layers: {},
|
newState: {
|
||||||
|
...state,
|
||||||
|
layers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#clearLayer', () => {
|
||||||
|
it('should clear a layer', () => {
|
||||||
|
const state = {
|
||||||
|
layers: {
|
||||||
|
first: {
|
||||||
|
indexPatternId: '1',
|
||||||
|
columnOrder: ['some', 'order'],
|
||||||
|
columns: {
|
||||||
|
some: {} as GenericIndexPatternColumn,
|
||||||
|
columns: {} as GenericIndexPatternColumn,
|
||||||
|
},
|
||||||
|
linkToLayers: ['some-layer'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentIndexPatternId: '1',
|
||||||
|
};
|
||||||
|
expect(FormBasedDatasource.clearLayer(state, 'first')).toEqual({
|
||||||
|
removedLayerIds: [],
|
||||||
|
newState: {
|
||||||
|
...state,
|
||||||
|
layers: {
|
||||||
|
first: {
|
||||||
|
indexPatternId: '1',
|
||||||
|
columnOrder: [],
|
||||||
|
columns: {},
|
||||||
|
linkToLayers: ['some-layer'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove linked layers', () => {
|
||||||
|
const state = {
|
||||||
|
layers: {
|
||||||
|
first: {
|
||||||
|
indexPatternId: '1',
|
||||||
|
columnOrder: [],
|
||||||
|
columns: {},
|
||||||
|
},
|
||||||
|
second: {
|
||||||
|
indexPatternId: '2',
|
||||||
|
columnOrder: [],
|
||||||
|
columns: {},
|
||||||
|
linkToLayers: ['first'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentIndexPatternId: '1',
|
||||||
|
};
|
||||||
|
expect(FormBasedDatasource.clearLayer(state, 'first')).toEqual({
|
||||||
|
removedLayerIds: ['second'],
|
||||||
|
newState: {
|
||||||
|
...state,
|
||||||
|
layers: {
|
||||||
|
first: state.layers.first,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -220,27 +220,47 @@ export function getFormBasedDatasource({
|
||||||
removeLayer(state: FormBasedPrivateState, layerId: string) {
|
removeLayer(state: FormBasedPrivateState, layerId: string) {
|
||||||
const newLayers = { ...state.layers };
|
const newLayers = { ...state.layers };
|
||||||
delete newLayers[layerId];
|
delete newLayers[layerId];
|
||||||
|
const removedLayerIds: string[] = [layerId];
|
||||||
|
|
||||||
// delete layers linked to this layer
|
// delete layers linked to this layer
|
||||||
Object.keys(newLayers).forEach((id) => {
|
Object.keys(newLayers).forEach((id) => {
|
||||||
const linkedLayers = newLayers[id]?.linkToLayers;
|
const linkedLayers = newLayers[id]?.linkToLayers;
|
||||||
if (linkedLayers && linkedLayers.includes(layerId)) {
|
if (linkedLayers && linkedLayers.includes(layerId)) {
|
||||||
delete newLayers[id];
|
delete newLayers[id];
|
||||||
|
removedLayerIds.push(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
removedLayerIds,
|
||||||
layers: newLayers,
|
newState: {
|
||||||
|
...state,
|
||||||
|
layers: newLayers,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
clearLayer(state: FormBasedPrivateState, layerId: string) {
|
clearLayer(state: FormBasedPrivateState, layerId: string) {
|
||||||
|
const newLayers = { ...state.layers };
|
||||||
|
|
||||||
|
const removedLayerIds: string[] = [];
|
||||||
|
// delete layers linked to this layer
|
||||||
|
Object.keys(newLayers).forEach((id) => {
|
||||||
|
const linkedLayers = newLayers[id]?.linkToLayers;
|
||||||
|
if (linkedLayers && linkedLayers.includes(layerId)) {
|
||||||
|
delete newLayers[id];
|
||||||
|
removedLayerIds.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
removedLayerIds,
|
||||||
layers: {
|
newState: {
|
||||||
...state.layers,
|
...state,
|
||||||
[layerId]: blankLayer(state.currentIndexPatternId, state.layers[layerId].linkToLayers),
|
layers: {
|
||||||
|
...newLayers,
|
||||||
|
[layerId]: blankLayer(state.currentIndexPatternId, state.layers[layerId].linkToLayers),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -217,21 +217,24 @@ describe('IndexPattern Data Source', () => {
|
||||||
describe('#removeLayer', () => {
|
describe('#removeLayer', () => {
|
||||||
it('should remove a layer', () => {
|
it('should remove a layer', () => {
|
||||||
expect(TextBasedDatasource.removeLayer(baseState, 'a')).toEqual({
|
expect(TextBasedDatasource.removeLayer(baseState, 'a')).toEqual({
|
||||||
...baseState,
|
removedLayerIds: ['a'],
|
||||||
layers: {
|
newState: {
|
||||||
a: {
|
...baseState,
|
||||||
columns: [],
|
layers: {
|
||||||
allColumns: [
|
a: {
|
||||||
{
|
columns: [],
|
||||||
columnId: 'col1',
|
allColumns: [
|
||||||
fieldName: 'Test 1',
|
{
|
||||||
meta: {
|
columnId: 'col1',
|
||||||
type: 'number',
|
fieldName: 'Test 1',
|
||||||
|
meta: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
query: { sql: 'SELECT * FROM foo' },
|
||||||
query: { sql: 'SELECT * FROM foo' },
|
index: 'foo',
|
||||||
index: 'foo',
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -281,18 +281,24 @@ export function getTextBasedDatasource({
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
removedLayerIds: [layerId],
|
||||||
layers: newLayers,
|
newState: {
|
||||||
fieldList: state.fieldList,
|
...state,
|
||||||
|
layers: newLayers,
|
||||||
|
fieldList: state.fieldList,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
clearLayer(state: TextBasedPrivateState, layerId: string) {
|
clearLayer(state: TextBasedPrivateState, layerId: string) {
|
||||||
return {
|
return {
|
||||||
...state,
|
removedLayerIds: [],
|
||||||
layers: {
|
newState: {
|
||||||
...state.layers,
|
...state,
|
||||||
[layerId]: { ...state.layers[layerId], columns: [] },
|
layers: {
|
||||||
|
...state.layers,
|
||||||
|
[layerId]: { ...state.layers[layerId], columns: [] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -494,8 +494,8 @@ describe('chart_switch', () => {
|
||||||
|
|
||||||
switchTo('visB', instance);
|
switchTo('visB', instance);
|
||||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'a');
|
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'a');
|
||||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith(undefined, 'b');
|
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'b');
|
||||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith(undefined, 'c');
|
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'c');
|
||||||
expect(visualizationMap.visB.getSuggestions).toHaveBeenCalledWith(
|
expect(visualizationMap.visB.getSuggestions).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
keptLayerIds: ['a'],
|
keptLayerIds: ['a'],
|
||||||
|
|
|
@ -26,7 +26,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'testDatasource',
|
id: 'testDatasource',
|
||||||
clearLayer: jest.fn((state, _layerId) => state),
|
clearLayer: jest.fn((state, _layerId) => ({ newState: state, removedLayerIds: [] })),
|
||||||
getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn, _indexPatterns) => []),
|
getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn, _indexPatterns) => []),
|
||||||
getDatasourceSuggestionsForVisualizeField: jest.fn(
|
getDatasourceSuggestionsForVisualizeField: jest.fn(
|
||||||
(_state, _indexpatternId, _fieldName, _indexPatterns) => []
|
(_state, _indexpatternId, _fieldName, _indexPatterns) => []
|
||||||
|
@ -44,7 +44,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
||||||
renderLayerPanel: jest.fn(),
|
renderLayerPanel: jest.fn(),
|
||||||
toExpression: jest.fn((_frame, _state, _indexPatterns) => null),
|
toExpression: jest.fn((_frame, _state, _indexPatterns) => null),
|
||||||
insertLayer: jest.fn((_state, _newLayerId) => ({})),
|
insertLayer: jest.fn((_state, _newLayerId) => ({})),
|
||||||
removeLayer: jest.fn((_state, _layerId) => {}),
|
removeLayer: jest.fn((state, layerId) => ({ newState: state, removedLayerIds: [layerId] })),
|
||||||
cloneLayer: jest.fn((_state, _layerId, _newLayerId, getNewId) => {}),
|
cloneLayer: jest.fn((_state, _layerId, _newLayerId, getNewId) => {}),
|
||||||
removeColumn: jest.fn((props) => {}),
|
removeColumn: jest.fn((props) => {}),
|
||||||
getLayers: jest.fn((_state) => []),
|
getLayers: jest.fn((_state) => []),
|
||||||
|
|
|
@ -39,6 +39,7 @@ describe('lensSlice', () => {
|
||||||
let store: EnhancedStore<{ lens: LensAppState }>;
|
let store: EnhancedStore<{ lens: LensAppState }>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = makeLensStore({}).store;
|
store = makeLensStore({}).store;
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
const customQuery = { query: 'custom' } as Query;
|
const customQuery = { query: 'custom' } as Query;
|
||||||
|
|
||||||
|
@ -275,17 +276,21 @@ describe('lensSlice', () => {
|
||||||
return {
|
return {
|
||||||
id: datasourceId,
|
id: datasourceId,
|
||||||
getPublicAPI: () => ({
|
getPublicAPI: () => ({
|
||||||
datasourceId: 'testDatasource',
|
datasourceId,
|
||||||
getOperationForColumnId: jest.fn(),
|
getOperationForColumnId: jest.fn(),
|
||||||
getTableSpec: jest.fn(),
|
getTableSpec: jest.fn(),
|
||||||
}),
|
}),
|
||||||
getLayers: () => ['layer1'],
|
getLayers: () => ['layer1'],
|
||||||
clearLayer: (layerIds: unknown, layerId: string) =>
|
clearLayer: (layerIds: unknown, layerId: string) => ({
|
||||||
(layerIds as string[]).map((id: string) =>
|
removedLayerIds: [],
|
||||||
|
newState: (layerIds as string[]).map((id: string) =>
|
||||||
id === layerId ? `${datasourceId}_clear_${layerId}` : id
|
id === layerId ? `${datasourceId}_clear_${layerId}` : id
|
||||||
),
|
),
|
||||||
removeLayer: (layerIds: unknown, layerId: string) =>
|
}),
|
||||||
(layerIds as string[]).filter((id: string) => id !== layerId),
|
removeLayer: (layerIds: unknown, layerId: string) => ({
|
||||||
|
newState: (layerIds as string[]).filter((id: string) => id !== layerId),
|
||||||
|
removedLayerIds: [layerId],
|
||||||
|
}),
|
||||||
insertLayer: (layerIds: unknown, layerId: string, layersToLinkTo: string[]) => [
|
insertLayer: (layerIds: unknown, layerId: string, layersToLinkTo: string[]) => [
|
||||||
...(layerIds as string[]),
|
...(layerIds as string[]),
|
||||||
layerId,
|
layerId,
|
||||||
|
@ -317,8 +322,9 @@ describe('lensSlice', () => {
|
||||||
(layerIds as string[]).map((id: string) =>
|
(layerIds as string[]).map((id: string) =>
|
||||||
id === layerId ? `vis_clear_${layerId}` : id
|
id === layerId ? `vis_clear_${layerId}` : id
|
||||||
),
|
),
|
||||||
removeLayer: (layerIds: unknown, layerId: string) =>
|
removeLayer: jest.fn((layerIds: unknown, layerId: string) =>
|
||||||
(layerIds as string[]).filter((id: string) => id !== layerId),
|
(layerIds as string[]).filter((id: string) => id !== layerId)
|
||||||
|
),
|
||||||
getLayerIds: (layerIds: unknown) => layerIds as string[],
|
getLayerIds: (layerIds: unknown) => layerIds as string[],
|
||||||
getLayersToLinkTo: (state, newLayerId) => ['linked-layer-id'],
|
getLayersToLinkTo: (state, newLayerId) => ['linked-layer-id'],
|
||||||
appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
|
appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
|
||||||
|
@ -482,6 +488,54 @@ describe('lensSlice', () => {
|
||||||
expect(state.datasourceStates.testDatasource2.state).toEqual(['layer2']);
|
expect(state.datasourceStates.testDatasource2.state).toEqual(['layer2']);
|
||||||
expect(state.stagedPreview).not.toBeDefined();
|
expect(state.stagedPreview).not.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removeLayer: should remove all layers from visualization that were removed by datasource', () => {
|
||||||
|
const removedLayerId = 'other-removed-layer';
|
||||||
|
|
||||||
|
const testDatasource3 = testDatasource('testDatasource3');
|
||||||
|
testDatasource3.removeLayer = (layerIds: unknown, layerId: string) => ({
|
||||||
|
newState: (layerIds as string[]).filter((id: string) => id !== layerId),
|
||||||
|
removedLayerIds: [layerId, removedLayerId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const localStore = makeLensStore({
|
||||||
|
preloadedState: {
|
||||||
|
activeDatasourceId: 'testDatasource',
|
||||||
|
datasourceStates: {
|
||||||
|
...datasourceStates,
|
||||||
|
testDatasource3: {
|
||||||
|
isLoading: false,
|
||||||
|
state: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visualization: {
|
||||||
|
activeId: activeVisId,
|
||||||
|
state: ['layer1', 'layer2'],
|
||||||
|
},
|
||||||
|
stagedPreview: {
|
||||||
|
visualization: {
|
||||||
|
activeId: activeVisId,
|
||||||
|
state: ['layer1', 'layer2'],
|
||||||
|
},
|
||||||
|
datasourceStates,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
storeDeps: mockStoreDeps({
|
||||||
|
visualizationMap: visualizationMap as unknown as VisualizationMap,
|
||||||
|
datasourceMap: { ...datasourceMap, testDatasource3 } as unknown as DatasourceMap,
|
||||||
|
}),
|
||||||
|
}).store;
|
||||||
|
|
||||||
|
localStore.dispatch(
|
||||||
|
removeOrClearLayer({
|
||||||
|
visualizationId: 'testVis',
|
||||||
|
layerId: 'layer1',
|
||||||
|
layerIds: ['layer1', 'layer2'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(visualizationMap[activeVisId].removeLayer).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removing a dimension', () => {
|
describe('removing a dimension', () => {
|
||||||
|
@ -546,8 +600,6 @@ describe('lensSlice', () => {
|
||||||
datasourceMap: datasourceMap as unknown as DatasourceMap,
|
datasourceMap: datasourceMap as unknown as DatasourceMap,
|
||||||
}),
|
}),
|
||||||
}).store;
|
}).store;
|
||||||
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes a dimension', () => {
|
it('removes a dimension', () => {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit';
|
import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||||
import { mapValues } from 'lodash';
|
import { mapValues, uniq } from 'lodash';
|
||||||
import { Query } from '@kbn/es-query';
|
import { Query } from '@kbn/es-query';
|
||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
import { LensEmbeddableInput } from '..';
|
import { LensEmbeddableInput } from '..';
|
||||||
|
@ -400,16 +400,23 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
||||||
layerIds.length
|
layerIds.length
|
||||||
) === 'clear';
|
) === 'clear';
|
||||||
|
|
||||||
|
let removedLayerIds: string[] = [];
|
||||||
|
|
||||||
state.datasourceStates = mapValues(
|
state.datasourceStates = mapValues(
|
||||||
state.datasourceStates,
|
state.datasourceStates,
|
||||||
(datasourceState, datasourceId) => {
|
(datasourceState, datasourceId) => {
|
||||||
const datasource = datasourceMap[datasourceId!];
|
const datasource = datasourceMap[datasourceId!];
|
||||||
|
|
||||||
|
const { newState, removedLayerIds: removedLayerIdsForThisDatasource } = isOnlyLayer
|
||||||
|
? datasource.clearLayer(datasourceState.state, layerId)
|
||||||
|
: datasource.removeLayer(datasourceState.state, layerId);
|
||||||
|
|
||||||
|
removedLayerIds = [...removedLayerIds, ...removedLayerIdsForThisDatasource];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...datasourceState,
|
...datasourceState,
|
||||||
...(datasourceId === state.activeDatasourceId && {
|
...(datasourceId === state.activeDatasourceId && {
|
||||||
state: isOnlyLayer
|
state: newState,
|
||||||
? datasource.clearLayer(datasourceState.state, layerId)
|
|
||||||
: datasource.removeLayer(datasourceState.state, layerId),
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -419,10 +426,22 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
||||||
const currentDataViewsId = activeDataSource.getUsedDataView(
|
const currentDataViewsId = activeDataSource.getUsedDataView(
|
||||||
state.datasourceStates[state.activeDatasourceId!].state
|
state.datasourceStates[state.activeDatasourceId!].state
|
||||||
);
|
);
|
||||||
state.visualization.state =
|
|
||||||
isOnlyLayer || !activeVisualization.removeLayer
|
if (isOnlyLayer || !activeVisualization.removeLayer) {
|
||||||
? activeVisualization.clearLayer(state.visualization.state, layerId, currentDataViewsId)
|
state.visualization.state = activeVisualization.clearLayer(
|
||||||
: activeVisualization.removeLayer(state.visualization.state, layerId);
|
state.visualization.state,
|
||||||
|
layerId,
|
||||||
|
currentDataViewsId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uniq(removedLayerIds).forEach(
|
||||||
|
(removedId) =>
|
||||||
|
(state.visualization.state = activeVisualization.removeLayer?.(
|
||||||
|
state.visualization.state,
|
||||||
|
removedId
|
||||||
|
))
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[changeIndexPattern.type]: (
|
[changeIndexPattern.type]: (
|
||||||
state,
|
state,
|
||||||
|
@ -977,9 +996,12 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
||||||
);
|
);
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
if (layerDatasourceId) {
|
if (layerDatasourceId) {
|
||||||
state.datasourceStates[layerDatasourceId].state = datasourceMap[
|
const { newState } = datasourceMap[layerDatasourceId].removeLayer(
|
||||||
layerDatasourceId
|
current(state).datasourceStates[layerDatasourceId].state,
|
||||||
].removeLayer(current(state).datasourceStates[layerDatasourceId].state, layerId);
|
layerId
|
||||||
|
);
|
||||||
|
state.datasourceStates[layerDatasourceId].state = newState;
|
||||||
|
// TODO - call removeLayer for any extra (linked) layers removed by the datasource
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -265,8 +265,8 @@ export interface Datasource<T = unknown, P = unknown> {
|
||||||
|
|
||||||
insertLayer: (state: T, newLayerId: string, linkToLayers?: string[]) => T;
|
insertLayer: (state: T, newLayerId: string, linkToLayers?: string[]) => T;
|
||||||
createEmptyLayer: (indexPatternId: string) => T;
|
createEmptyLayer: (indexPatternId: string) => T;
|
||||||
removeLayer: (state: T, layerId: string) => T;
|
removeLayer: (state: T, layerId: string) => { newState: T; removedLayerIds: string[] };
|
||||||
clearLayer: (state: T, layerId: string) => T;
|
clearLayer: (state: T, layerId: string) => { newState: T; removedLayerIds: string[] };
|
||||||
cloneLayer: (
|
cloneLayer: (
|
||||||
state: T,
|
state: T,
|
||||||
layerId: string,
|
layerId: string,
|
||||||
|
|
|
@ -12,7 +12,11 @@ import { OperationDescriptor, VisualizationDimensionEditorProps } from '../../ty
|
||||||
import { CustomPaletteParams, PaletteOutput, PaletteRegistry } from '@kbn/coloring';
|
import { CustomPaletteParams, PaletteOutput, PaletteRegistry } from '@kbn/coloring';
|
||||||
|
|
||||||
import { MetricVisualizationState } from './visualization';
|
import { MetricVisualizationState } from './visualization';
|
||||||
import { DimensionEditor, SupportingVisType } from './dimension_editor';
|
import {
|
||||||
|
DimensionEditor,
|
||||||
|
DimensionEditorAdditionalSection,
|
||||||
|
SupportingVisType,
|
||||||
|
} from './dimension_editor';
|
||||||
import { HTMLAttributes, mount, ReactWrapper, shallow } from 'enzyme';
|
import { HTMLAttributes, mount, ReactWrapper, shallow } from 'enzyme';
|
||||||
import { CollapseSetting } from '../../shared_components/collapse_setting';
|
import { CollapseSetting } from '../../shared_components/collapse_setting';
|
||||||
import { EuiButtonGroup, EuiColorPicker, PropsOf } from '@elastic/eui';
|
import { EuiButtonGroup, EuiColorPicker, PropsOf } from '@elastic/eui';
|
||||||
|
@ -154,42 +158,6 @@ describe('dimension editor', () => {
|
||||||
this.colorPicker.props().onChange!(color, {} as EuiColorPickerOutput);
|
this.colorPicker.props().onChange!(color, {} as EuiColorPickerOutput);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private get supportingVisButtonGroup() {
|
|
||||||
return this._wrapper.find(
|
|
||||||
'EuiButtonGroup[data-test-subj="lnsMetric_supporting_visualization_buttons"]'
|
|
||||||
) as unknown as ReactWrapper<PropsOf<typeof EuiButtonGroup>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get currentSupportingVis() {
|
|
||||||
return this.supportingVisButtonGroup
|
|
||||||
.props()
|
|
||||||
.idSelected?.split('--')[1] as SupportingVisType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isDisabled(type: SupportingVisType) {
|
|
||||||
return this.supportingVisButtonGroup.props().options.find(({ id }) => id.includes(type))
|
|
||||||
?.isDisabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setSupportingVis(type: SupportingVisType) {
|
|
||||||
this.supportingVisButtonGroup.props().onChange(`some-id--${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get progressDirectionControl() {
|
|
||||||
return this._wrapper.find(
|
|
||||||
'EuiButtonGroup[data-test-subj="lnsMetric_progress_direction_buttons"]'
|
|
||||||
) as unknown as ReactWrapper<PropsOf<typeof EuiButtonGroup>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get progressDirectionShowing() {
|
|
||||||
return this.progressDirectionControl.exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public setProgressDirection(direction: LayoutDirection) {
|
|
||||||
this.progressDirectionControl.props().onChange(direction);
|
|
||||||
this._wrapper.update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockSetState = jest.fn();
|
const mockSetState = jest.fn();
|
||||||
|
@ -266,144 +234,6 @@ describe('dimension editor', () => {
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('supporting visualizations', () => {
|
|
||||||
const stateWOTrend = {
|
|
||||||
...metricAccessorState,
|
|
||||||
trendlineLayerId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('reflecting visualization state', () => {
|
|
||||||
it('should select the correct button', () => {
|
|
||||||
expect(
|
|
||||||
getHarnessWithState({ ...stateWOTrend, showBar: false, maxAccessor: undefined })
|
|
||||||
.currentSupportingVis
|
|
||||||
).toBe<SupportingVisType>('none');
|
|
||||||
expect(
|
|
||||||
getHarnessWithState({ ...stateWOTrend, showBar: true }).currentSupportingVis
|
|
||||||
).toBe<SupportingVisType>('bar');
|
|
||||||
expect(
|
|
||||||
getHarnessWithState(metricAccessorState).currentSupportingVis
|
|
||||||
).toBe<SupportingVisType>('trendline');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disable bar when no max dimension', () => {
|
|
||||||
expect(
|
|
||||||
getHarnessWithState({
|
|
||||||
...stateWOTrend,
|
|
||||||
showBar: false,
|
|
||||||
maxAccessor: 'something',
|
|
||||||
}).isDisabled('bar')
|
|
||||||
).toBeFalsy();
|
|
||||||
expect(
|
|
||||||
getHarnessWithState({
|
|
||||||
...stateWOTrend,
|
|
||||||
showBar: false,
|
|
||||||
maxAccessor: undefined,
|
|
||||||
}).isDisabled('bar')
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disable trendline when no default time field', () => {
|
|
||||||
expect(
|
|
||||||
getHarnessWithState(stateWOTrend, {
|
|
||||||
hasDefaultTimeField: () => false,
|
|
||||||
getOperationForColumnId: (id) => ({} as OperationDescriptor),
|
|
||||||
} as DatasourcePublicAPI).isDisabled('trendline')
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
getHarnessWithState(stateWOTrend, {
|
|
||||||
hasDefaultTimeField: () => true,
|
|
||||||
getOperationForColumnId: (id) => ({} as OperationDescriptor),
|
|
||||||
} as DatasourcePublicAPI).isDisabled('trendline')
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disable trendline when a metric dimension has a reduced time range', () => {
|
|
||||||
expect(
|
|
||||||
getHarnessWithState(stateWOTrend, {
|
|
||||||
hasDefaultTimeField: () => true,
|
|
||||||
getOperationForColumnId: (id) =>
|
|
||||||
({ hasReducedTimeRange: id === stateWOTrend.metricAccessor } as OperationDescriptor),
|
|
||||||
} as DatasourcePublicAPI).isDisabled('trendline')
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
getHarnessWithState(stateWOTrend, {
|
|
||||||
hasDefaultTimeField: () => true,
|
|
||||||
getOperationForColumnId: (id) =>
|
|
||||||
({
|
|
||||||
hasReducedTimeRange: id === stateWOTrend.secondaryMetricAccessor,
|
|
||||||
} as OperationDescriptor),
|
|
||||||
} as DatasourcePublicAPI).isDisabled('trendline')
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('responding to buttons', () => {
|
|
||||||
it('enables trendline', () => {
|
|
||||||
getHarnessWithState(stateWOTrend).setSupportingVis('trendline');
|
|
||||||
|
|
||||||
expect(mockSetState).toHaveBeenCalledWith({ ...stateWOTrend, showBar: false });
|
|
||||||
expect(props.addLayer).toHaveBeenCalledWith('metricTrendline');
|
|
||||||
|
|
||||||
expectCalledBefore(mockSetState, props.addLayer as jest.Mock);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('enables bar', () => {
|
|
||||||
getHarnessWithState(metricAccessorState).setSupportingVis('bar');
|
|
||||||
|
|
||||||
expect(mockSetState).toHaveBeenCalledWith({ ...metricAccessorState, showBar: true });
|
|
||||||
expect(props.removeLayer).toHaveBeenCalledWith(metricAccessorState.trendlineLayerId);
|
|
||||||
|
|
||||||
expectCalledBefore(mockSetState, props.removeLayer as jest.Mock);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('selects none from bar', () => {
|
|
||||||
getHarnessWithState(stateWOTrend).setSupportingVis('none');
|
|
||||||
|
|
||||||
expect(mockSetState).toHaveBeenCalledWith({ ...stateWOTrend, showBar: false });
|
|
||||||
expect(props.removeLayer).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('selects none from trendline', () => {
|
|
||||||
getHarnessWithState(metricAccessorState).setSupportingVis('none');
|
|
||||||
|
|
||||||
expect(mockSetState).toHaveBeenCalledWith({ ...metricAccessorState, showBar: false });
|
|
||||||
expect(props.removeLayer).toHaveBeenCalledWith(metricAccessorState.trendlineLayerId);
|
|
||||||
|
|
||||||
expectCalledBefore(mockSetState, props.removeLayer as jest.Mock);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('progress bar direction controls', () => {
|
|
||||||
it('hides direction controls if bar not showing', () => {
|
|
||||||
expect(
|
|
||||||
getHarnessWithState({ ...stateWOTrend, showBar: false }).progressDirectionShowing
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('toggles progress direction', () => {
|
|
||||||
const harness = getHarnessWithState(metricAccessorState);
|
|
||||||
|
|
||||||
expect(harness.progressDirectionShowing).toBeTruthy();
|
|
||||||
expect(harness.currentState.progressDirection).toBe('vertical');
|
|
||||||
|
|
||||||
harness.setProgressDirection('horizontal');
|
|
||||||
harness.setProgressDirection('vertical');
|
|
||||||
harness.setProgressDirection('horizontal');
|
|
||||||
|
|
||||||
expect(mockSetState).toHaveBeenCalledTimes(3);
|
|
||||||
expect(mockSetState.mock.calls.map((args) => args[0].progressDirection))
|
|
||||||
.toMatchInlineSnapshot(`
|
|
||||||
Array [
|
|
||||||
"horizontal",
|
|
||||||
"vertical",
|
|
||||||
"horizontal",
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('secondary metric dimension', () => {
|
describe('secondary metric dimension', () => {
|
||||||
|
@ -628,4 +458,235 @@ describe('dimension editor', () => {
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('additional section', () => {
|
||||||
|
const accessor = 'primary-metric-col-id';
|
||||||
|
const metricAccessorState = { ...fullState, metricAccessor: accessor };
|
||||||
|
|
||||||
|
class Harness {
|
||||||
|
public _wrapper;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
wrapper: ReactWrapper<HTMLAttributes, unknown, React.Component<{}, {}, unknown>>
|
||||||
|
) {
|
||||||
|
this._wrapper = wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get rootComponent() {
|
||||||
|
return this._wrapper.find(DimensionEditorAdditionalSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get currentState() {
|
||||||
|
return this.rootComponent.props().state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get supportingVisButtonGroup() {
|
||||||
|
return this._wrapper.find(
|
||||||
|
'EuiButtonGroup[data-test-subj="lnsMetric_supporting_visualization_buttons"]'
|
||||||
|
) as unknown as ReactWrapper<PropsOf<typeof EuiButtonGroup>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get currentSupportingVis() {
|
||||||
|
return this.supportingVisButtonGroup
|
||||||
|
.props()
|
||||||
|
.idSelected?.split('--')[1] as SupportingVisType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isDisabled(type: SupportingVisType) {
|
||||||
|
return this.supportingVisButtonGroup.props().options.find(({ id }) => id.includes(type))
|
||||||
|
?.isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSupportingVis(type: SupportingVisType) {
|
||||||
|
this.supportingVisButtonGroup.props().onChange(`some-id--${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get progressDirectionControl() {
|
||||||
|
return this._wrapper.find(
|
||||||
|
'EuiButtonGroup[data-test-subj="lnsMetric_progress_direction_buttons"]'
|
||||||
|
) as unknown as ReactWrapper<PropsOf<typeof EuiButtonGroup>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get progressDirectionShowing() {
|
||||||
|
return this.progressDirectionControl.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setProgressDirection(direction: LayoutDirection) {
|
||||||
|
this.progressDirectionControl.props().onChange(direction);
|
||||||
|
this._wrapper.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockSetState = jest.fn();
|
||||||
|
|
||||||
|
const getHarnessWithState = (state: MetricVisualizationState, datasource = props.datasource) =>
|
||||||
|
new Harness(
|
||||||
|
mountWithIntl(
|
||||||
|
<DimensionEditorAdditionalSection
|
||||||
|
{...props}
|
||||||
|
datasource={datasource}
|
||||||
|
state={state}
|
||||||
|
setState={mockSetState}
|
||||||
|
accessor={accessor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ name: 'secondary metric', accessor: metricAccessorState.secondaryMetricAccessor },
|
||||||
|
{ name: 'max', accessor: metricAccessorState.maxAccessor },
|
||||||
|
{ name: 'break down by', accessor: metricAccessorState.breakdownByAccessor },
|
||||||
|
])('doesnt show for the following dimension: %s', ({ accessor: testAccessor }) => {
|
||||||
|
expect(
|
||||||
|
shallow(
|
||||||
|
<DimensionEditorAdditionalSection
|
||||||
|
{...props}
|
||||||
|
state={metricAccessorState}
|
||||||
|
setState={mockSetState}
|
||||||
|
accessor={testAccessor}
|
||||||
|
/>
|
||||||
|
).isEmptyRender()
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supporting visualizations', () => {
|
||||||
|
const stateWOTrend = {
|
||||||
|
...metricAccessorState,
|
||||||
|
trendlineLayerId: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('reflecting visualization state', () => {
|
||||||
|
it('should select the correct button', () => {
|
||||||
|
expect(
|
||||||
|
getHarnessWithState({ ...stateWOTrend, showBar: false, maxAccessor: undefined })
|
||||||
|
.currentSupportingVis
|
||||||
|
).toBe<SupportingVisType>('none');
|
||||||
|
expect(
|
||||||
|
getHarnessWithState({ ...stateWOTrend, showBar: true }).currentSupportingVis
|
||||||
|
).toBe<SupportingVisType>('bar');
|
||||||
|
expect(
|
||||||
|
getHarnessWithState(metricAccessorState).currentSupportingVis
|
||||||
|
).toBe<SupportingVisType>('trendline');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable bar when no max dimension', () => {
|
||||||
|
expect(
|
||||||
|
getHarnessWithState({
|
||||||
|
...stateWOTrend,
|
||||||
|
showBar: false,
|
||||||
|
maxAccessor: 'something',
|
||||||
|
}).isDisabled('bar')
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
getHarnessWithState({
|
||||||
|
...stateWOTrend,
|
||||||
|
showBar: false,
|
||||||
|
maxAccessor: undefined,
|
||||||
|
}).isDisabled('bar')
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable trendline when no default time field', () => {
|
||||||
|
expect(
|
||||||
|
getHarnessWithState(stateWOTrend, {
|
||||||
|
hasDefaultTimeField: () => false,
|
||||||
|
getOperationForColumnId: (id) => ({} as OperationDescriptor),
|
||||||
|
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
getHarnessWithState(stateWOTrend, {
|
||||||
|
hasDefaultTimeField: () => true,
|
||||||
|
getOperationForColumnId: (id) => ({} as OperationDescriptor),
|
||||||
|
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable trendline when a metric dimension has a reduced time range', () => {
|
||||||
|
expect(
|
||||||
|
getHarnessWithState(stateWOTrend, {
|
||||||
|
hasDefaultTimeField: () => true,
|
||||||
|
getOperationForColumnId: (id) =>
|
||||||
|
({
|
||||||
|
hasReducedTimeRange: id === stateWOTrend.metricAccessor,
|
||||||
|
} as OperationDescriptor),
|
||||||
|
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
getHarnessWithState(stateWOTrend, {
|
||||||
|
hasDefaultTimeField: () => true,
|
||||||
|
getOperationForColumnId: (id) =>
|
||||||
|
({
|
||||||
|
hasReducedTimeRange: id === stateWOTrend.secondaryMetricAccessor,
|
||||||
|
} as OperationDescriptor),
|
||||||
|
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('responding to buttons', () => {
|
||||||
|
it('enables trendline', () => {
|
||||||
|
getHarnessWithState(stateWOTrend).setSupportingVis('trendline');
|
||||||
|
|
||||||
|
expect(mockSetState).toHaveBeenCalledWith({ ...stateWOTrend, showBar: false });
|
||||||
|
expect(props.addLayer).toHaveBeenCalledWith('metricTrendline');
|
||||||
|
|
||||||
|
expectCalledBefore(mockSetState, props.addLayer as jest.Mock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables bar', () => {
|
||||||
|
getHarnessWithState(metricAccessorState).setSupportingVis('bar');
|
||||||
|
|
||||||
|
expect(mockSetState).toHaveBeenCalledWith({ ...metricAccessorState, showBar: true });
|
||||||
|
expect(props.removeLayer).toHaveBeenCalledWith(metricAccessorState.trendlineLayerId);
|
||||||
|
|
||||||
|
expectCalledBefore(mockSetState, props.removeLayer as jest.Mock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects none from bar', () => {
|
||||||
|
getHarnessWithState(stateWOTrend).setSupportingVis('none');
|
||||||
|
|
||||||
|
expect(mockSetState).toHaveBeenCalledWith({ ...stateWOTrend, showBar: false });
|
||||||
|
expect(props.removeLayer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects none from trendline', () => {
|
||||||
|
getHarnessWithState(metricAccessorState).setSupportingVis('none');
|
||||||
|
|
||||||
|
expect(mockSetState).toHaveBeenCalledWith({ ...metricAccessorState, showBar: false });
|
||||||
|
expect(props.removeLayer).toHaveBeenCalledWith(metricAccessorState.trendlineLayerId);
|
||||||
|
|
||||||
|
expectCalledBefore(mockSetState, props.removeLayer as jest.Mock);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('progress bar direction controls', () => {
|
||||||
|
it('hides direction controls if bar not showing', () => {
|
||||||
|
expect(
|
||||||
|
getHarnessWithState({ ...stateWOTrend, showBar: false }).progressDirectionShowing
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles progress direction', () => {
|
||||||
|
const harness = getHarnessWithState(metricAccessorState);
|
||||||
|
|
||||||
|
expect(harness.progressDirectionShowing).toBeTruthy();
|
||||||
|
expect(harness.currentState.progressDirection).toBe('vertical');
|
||||||
|
|
||||||
|
harness.setProgressDirection('horizontal');
|
||||||
|
harness.setProgressDirection('vertical');
|
||||||
|
harness.setProgressDirection('horizontal');
|
||||||
|
|
||||||
|
expect(mockSetState).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockSetState.mock.calls.map((args) => args[0].progressDirection))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"horizontal",
|
||||||
|
"vertical",
|
||||||
|
"horizontal",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {
|
||||||
EuiColorPicker,
|
EuiColorPicker,
|
||||||
euiPaletteColorBlind,
|
euiPaletteColorBlind,
|
||||||
EuiSpacer,
|
EuiSpacer,
|
||||||
|
EuiText,
|
||||||
|
useEuiTheme,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { LayoutDirection } from '@elastic/charts';
|
import { LayoutDirection } from '@elastic/charts';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
@ -30,6 +32,7 @@ import {
|
||||||
} from '@kbn/coloring';
|
} from '@kbn/coloring';
|
||||||
import { getDataBoundsForPalette } from '@kbn/expression-metric-vis-plugin/public';
|
import { getDataBoundsForPalette } from '@kbn/expression-metric-vis-plugin/public';
|
||||||
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils';
|
import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils';
|
||||||
import {
|
import {
|
||||||
applyPaletteParams,
|
applyPaletteParams,
|
||||||
|
@ -263,170 +266,8 @@ function PrimaryMetricEditor(props: SubProps) {
|
||||||
|
|
||||||
const togglePalette = () => setIsPaletteOpen(!isPaletteOpen);
|
const togglePalette = () => setIsPaletteOpen(!isPaletteOpen);
|
||||||
|
|
||||||
const supportingVisLabel = i18n.translate('xpack.lens.metric.supportingVis.label', {
|
|
||||||
defaultMessage: 'Supporting visualization',
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasDefaultTimeField = props.datasource?.hasDefaultTimeField();
|
|
||||||
const metricHasReducedTimeRange = Boolean(
|
|
||||||
state.metricAccessor &&
|
|
||||||
props.datasource?.getOperationForColumnId(state.metricAccessor)?.hasReducedTimeRange
|
|
||||||
);
|
|
||||||
const secondaryMetricHasReducedTimeRange = Boolean(
|
|
||||||
state.secondaryMetricAccessor &&
|
|
||||||
props.datasource?.getOperationForColumnId(state.secondaryMetricAccessor)?.hasReducedTimeRange
|
|
||||||
);
|
|
||||||
|
|
||||||
const supportingVisHelpTexts: string[] = [];
|
|
||||||
|
|
||||||
const supportsTrendline =
|
|
||||||
hasDefaultTimeField && !metricHasReducedTimeRange && !secondaryMetricHasReducedTimeRange;
|
|
||||||
|
|
||||||
if (!supportsTrendline) {
|
|
||||||
supportingVisHelpTexts.push(
|
|
||||||
!hasDefaultTimeField
|
|
||||||
? i18n.translate('xpack.lens.metric.supportingVis.needDefaultTimeField', {
|
|
||||||
defaultMessage: 'Use a data view with a default time field to enable trend lines.',
|
|
||||||
})
|
|
||||||
: metricHasReducedTimeRange
|
|
||||||
? i18n.translate('xpack.lens.metric.supportingVis.metricHasReducedTimeRange', {
|
|
||||||
defaultMessage:
|
|
||||||
'Remove the reduced time range on this dimension to enable trend lines.',
|
|
||||||
})
|
|
||||||
: secondaryMetricHasReducedTimeRange
|
|
||||||
? i18n.translate('xpack.lens.metric.supportingVis.secondaryMetricHasReducedTimeRange', {
|
|
||||||
defaultMessage:
|
|
||||||
'Remove the reduced time range on the secondary metric dimension to enable trend lines.',
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.maxAccessor) {
|
|
||||||
supportingVisHelpTexts.push(
|
|
||||||
i18n.translate('xpack.lens.metric.summportingVis.needMaxDimension', {
|
|
||||||
defaultMessage: 'Add a maximum dimension to enable the progress bar.',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonIdPrefix = `${idPrefix}--`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EuiFormRow
|
|
||||||
display="columnCompressed"
|
|
||||||
fullWidth
|
|
||||||
label={supportingVisLabel}
|
|
||||||
helpText={supportingVisHelpTexts.map((text) => (
|
|
||||||
<div>{text}</div>
|
|
||||||
))}
|
|
||||||
>
|
|
||||||
<EuiButtonGroup
|
|
||||||
isFullWidth
|
|
||||||
buttonSize="compressed"
|
|
||||||
legend={supportingVisLabel}
|
|
||||||
data-test-subj="lnsMetric_supporting_visualization_buttons"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
id: `${buttonIdPrefix}none`,
|
|
||||||
label: i18n.translate('xpack.lens.metric.supportingVisualization.none', {
|
|
||||||
defaultMessage: 'None',
|
|
||||||
}),
|
|
||||||
'data-test-subj': 'lnsMetric_supporting_visualization_none',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `${buttonIdPrefix}trendline`,
|
|
||||||
label: i18n.translate('xpack.lens.metric.supportingVisualization.trendline', {
|
|
||||||
defaultMessage: 'Trend line',
|
|
||||||
}),
|
|
||||||
isDisabled: !supportsTrendline,
|
|
||||||
'data-test-subj': 'lnsMetric_supporting_visualization_trendline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `${buttonIdPrefix}bar`,
|
|
||||||
label: i18n.translate('xpack.lens.metric.supportingVisualization.bar', {
|
|
||||||
defaultMessage: 'Bar',
|
|
||||||
}),
|
|
||||||
isDisabled: !state.maxAccessor,
|
|
||||||
'data-test-subj': 'lnsMetric_supporting_visualization_bar',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
idSelected={`${buttonIdPrefix}${
|
|
||||||
state.trendlineLayerId ? 'trendline' : showingBar(state) ? 'bar' : 'none'
|
|
||||||
}`}
|
|
||||||
onChange={(id) => {
|
|
||||||
const supportingVisualizationType = id.split('--')[1] as SupportingVisType;
|
|
||||||
|
|
||||||
switch (supportingVisualizationType) {
|
|
||||||
case 'trendline':
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
showBar: false,
|
|
||||||
});
|
|
||||||
props.addLayer('metricTrendline');
|
|
||||||
break;
|
|
||||||
case 'bar':
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
showBar: true,
|
|
||||||
});
|
|
||||||
if (state.trendlineLayerId) props.removeLayer(state.trendlineLayerId);
|
|
||||||
break;
|
|
||||||
case 'none':
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
showBar: false,
|
|
||||||
});
|
|
||||||
if (state.trendlineLayerId) props.removeLayer(state.trendlineLayerId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
{showingBar(state) && (
|
|
||||||
<EuiFormRow
|
|
||||||
label={i18n.translate('xpack.lens.metric.progressDirectionLabel', {
|
|
||||||
defaultMessage: 'Bar direction',
|
|
||||||
})}
|
|
||||||
fullWidth
|
|
||||||
display="columnCompressed"
|
|
||||||
>
|
|
||||||
<EuiButtonGroup
|
|
||||||
isFullWidth
|
|
||||||
buttonSize="compressed"
|
|
||||||
legend={i18n.translate('xpack.lens.metric.progressDirectionLabel', {
|
|
||||||
defaultMessage: 'Bar direction',
|
|
||||||
})}
|
|
||||||
data-test-subj="lnsMetric_progress_direction_buttons"
|
|
||||||
name="alignment"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
id: `${idPrefix}vertical`,
|
|
||||||
label: i18n.translate('xpack.lens.metric.progressDirection.vertical', {
|
|
||||||
defaultMessage: 'Vertical',
|
|
||||||
}),
|
|
||||||
'data-test-subj': 'lnsMetric_progress_bar_vertical',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `${idPrefix}horizontal`,
|
|
||||||
label: i18n.translate('xpack.lens.metric.progressDirection.horizontal', {
|
|
||||||
defaultMessage: 'Horizontal',
|
|
||||||
}),
|
|
||||||
'data-test-subj': 'lnsMetric_progress_bar_horizontal',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
idSelected={`${idPrefix}${state.progressDirection ?? 'vertical'}`}
|
|
||||||
onChange={(id) => {
|
|
||||||
const newDirection = id.replace(idPrefix, '') as LayoutDirection;
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
progressDirection: newDirection,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
)}
|
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
display="columnCompressed"
|
display="columnCompressed"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@ -580,3 +421,203 @@ function StaticColorControls({ state, setState }: Pick<Props, 'state' | 'setStat
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DimensionEditorAdditionalSection({
|
||||||
|
state,
|
||||||
|
datasource,
|
||||||
|
setState,
|
||||||
|
addLayer,
|
||||||
|
removeLayer,
|
||||||
|
accessor,
|
||||||
|
}: VisualizationDimensionEditorProps<MetricVisualizationState>) {
|
||||||
|
const { euiTheme } = useEuiTheme();
|
||||||
|
|
||||||
|
if (accessor !== state.metricAccessor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idPrefix = htmlIdGenerator()();
|
||||||
|
|
||||||
|
const hasDefaultTimeField = datasource?.hasDefaultTimeField();
|
||||||
|
const metricHasReducedTimeRange = Boolean(
|
||||||
|
state.metricAccessor &&
|
||||||
|
datasource?.getOperationForColumnId(state.metricAccessor)?.hasReducedTimeRange
|
||||||
|
);
|
||||||
|
const secondaryMetricHasReducedTimeRange = Boolean(
|
||||||
|
state.secondaryMetricAccessor &&
|
||||||
|
datasource?.getOperationForColumnId(state.secondaryMetricAccessor)?.hasReducedTimeRange
|
||||||
|
);
|
||||||
|
|
||||||
|
const supportingVisHelpTexts: string[] = [];
|
||||||
|
|
||||||
|
const supportsTrendline =
|
||||||
|
hasDefaultTimeField && !metricHasReducedTimeRange && !secondaryMetricHasReducedTimeRange;
|
||||||
|
|
||||||
|
if (!supportsTrendline) {
|
||||||
|
supportingVisHelpTexts.push(
|
||||||
|
!hasDefaultTimeField
|
||||||
|
? i18n.translate('xpack.lens.metric.supportingVis.needDefaultTimeField', {
|
||||||
|
defaultMessage:
|
||||||
|
'Line visualizations require use of a data view with a default time field.',
|
||||||
|
})
|
||||||
|
: metricHasReducedTimeRange
|
||||||
|
? i18n.translate('xpack.lens.metric.supportingVis.metricHasReducedTimeRange', {
|
||||||
|
defaultMessage:
|
||||||
|
'Line visualizations cannot be used when a reduced time range is applied to the primary metric.',
|
||||||
|
})
|
||||||
|
: secondaryMetricHasReducedTimeRange
|
||||||
|
? i18n.translate('xpack.lens.metric.supportingVis.secondaryMetricHasReducedTimeRange', {
|
||||||
|
defaultMessage:
|
||||||
|
'Line visualizations cannot be used when a reduced time range is applied to the secondary metric.',
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.maxAccessor) {
|
||||||
|
supportingVisHelpTexts.push(
|
||||||
|
i18n.translate('xpack.lens.metric.summportingVis.needMaxDimension', {
|
||||||
|
defaultMessage: 'Bar visualizations require a maximum value to be defined.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonIdPrefix = `${idPrefix}--`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lnsIndexPatternDimensionEditor--padded lnsIndexPatternDimensionEditor--collapseNext">
|
||||||
|
<EuiText
|
||||||
|
size="s"
|
||||||
|
css={css`
|
||||||
|
margin-bottom: ${euiTheme.size.base};
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
{i18n.translate('xpack.lens.metric.supportingVis.label', {
|
||||||
|
defaultMessage: 'Supporting visualization',
|
||||||
|
})}
|
||||||
|
</h4>
|
||||||
|
</EuiText>
|
||||||
|
|
||||||
|
<>
|
||||||
|
<EuiFormRow
|
||||||
|
display="columnCompressed"
|
||||||
|
fullWidth
|
||||||
|
label={i18n.translate('xpack.lens.metric.supportingVis.type', {
|
||||||
|
defaultMessage: 'Type',
|
||||||
|
})}
|
||||||
|
helpText={supportingVisHelpTexts.map((text) => (
|
||||||
|
<p>{text}</p>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<EuiButtonGroup
|
||||||
|
isFullWidth
|
||||||
|
buttonSize="compressed"
|
||||||
|
legend={i18n.translate('xpack.lens.metric.supportingVis.type', {
|
||||||
|
defaultMessage: 'Type',
|
||||||
|
})}
|
||||||
|
data-test-subj="lnsMetric_supporting_visualization_buttons"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
id: `${buttonIdPrefix}none`,
|
||||||
|
label: i18n.translate('xpack.lens.metric.supportingVisualization.none', {
|
||||||
|
defaultMessage: 'None',
|
||||||
|
}),
|
||||||
|
'data-test-subj': 'lnsMetric_supporting_visualization_none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${buttonIdPrefix}trendline`,
|
||||||
|
label: i18n.translate('xpack.lens.metric.supportingVisualization.trendline', {
|
||||||
|
defaultMessage: 'Line',
|
||||||
|
}),
|
||||||
|
isDisabled: !supportsTrendline,
|
||||||
|
'data-test-subj': 'lnsMetric_supporting_visualization_trendline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${buttonIdPrefix}bar`,
|
||||||
|
label: i18n.translate('xpack.lens.metric.supportingVisualization.bar', {
|
||||||
|
defaultMessage: 'Bar',
|
||||||
|
}),
|
||||||
|
isDisabled: !state.maxAccessor,
|
||||||
|
'data-test-subj': 'lnsMetric_supporting_visualization_bar',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
idSelected={`${buttonIdPrefix}${
|
||||||
|
state.trendlineLayerId ? 'trendline' : showingBar(state) ? 'bar' : 'none'
|
||||||
|
}`}
|
||||||
|
onChange={(id) => {
|
||||||
|
const supportingVisualizationType = id.split('--')[1] as SupportingVisType;
|
||||||
|
|
||||||
|
switch (supportingVisualizationType) {
|
||||||
|
case 'trendline':
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
showBar: false,
|
||||||
|
});
|
||||||
|
addLayer('metricTrendline');
|
||||||
|
break;
|
||||||
|
case 'bar':
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
showBar: true,
|
||||||
|
});
|
||||||
|
if (state.trendlineLayerId) removeLayer(state.trendlineLayerId);
|
||||||
|
break;
|
||||||
|
case 'none':
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
showBar: false,
|
||||||
|
});
|
||||||
|
if (state.trendlineLayerId) removeLayer(state.trendlineLayerId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
{showingBar(state) && (
|
||||||
|
<EuiFormRow
|
||||||
|
label={i18n.translate('xpack.lens.metric.progressDirectionLabel', {
|
||||||
|
defaultMessage: 'Bar orientation',
|
||||||
|
})}
|
||||||
|
fullWidth
|
||||||
|
display="columnCompressed"
|
||||||
|
>
|
||||||
|
<EuiButtonGroup
|
||||||
|
isFullWidth
|
||||||
|
buttonSize="compressed"
|
||||||
|
legend={i18n.translate('xpack.lens.metric.progressDirectionLabel', {
|
||||||
|
defaultMessage: 'Bar orientation',
|
||||||
|
})}
|
||||||
|
data-test-subj="lnsMetric_progress_direction_buttons"
|
||||||
|
name="alignment"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
id: `${idPrefix}vertical`,
|
||||||
|
label: i18n.translate('xpack.lens.metric.progressDirection.vertical', {
|
||||||
|
defaultMessage: 'Vertical',
|
||||||
|
}),
|
||||||
|
'data-test-subj': 'lnsMetric_progress_bar_vertical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${idPrefix}horizontal`,
|
||||||
|
label: i18n.translate('xpack.lens.metric.progressDirection.horizontal', {
|
||||||
|
defaultMessage: 'Horizontal',
|
||||||
|
}),
|
||||||
|
'data-test-subj': 'lnsMetric_progress_bar_horizontal',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
idSelected={`${idPrefix}${state.progressDirection ?? 'vertical'}`}
|
||||||
|
onChange={(id) => {
|
||||||
|
const newDirection = id.replace(idPrefix, '') as LayoutDirection;
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
progressDirection: newDirection,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
Suggestion,
|
Suggestion,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { GROUP_ID, LENS_METRIC_ID } from './constants';
|
import { GROUP_ID, LENS_METRIC_ID } from './constants';
|
||||||
import { DimensionEditor } from './dimension_editor';
|
import { DimensionEditor, DimensionEditorAdditionalSection } from './dimension_editor';
|
||||||
import { Toolbar } from './toolbar';
|
import { Toolbar } from './toolbar';
|
||||||
import { generateId } from '../../id_generator';
|
import { generateId } from '../../id_generator';
|
||||||
import { FormatSelectorOptions } from '../../datasources/form_based/dimension_panel/format_selector';
|
import { FormatSelectorOptions } from '../../datasources/form_based/dimension_panel/format_selector';
|
||||||
|
@ -454,6 +454,10 @@ export const getMetricVisualization = ({
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getRemoveOperation(state, layerId) {
|
||||||
|
return layerId === state.trendlineLayerId ? 'remove' : 'clear';
|
||||||
|
},
|
||||||
|
|
||||||
getLayersToLinkTo(state, newLayerId: string): string[] {
|
getLayersToLinkTo(state, newLayerId: string): string[] {
|
||||||
return newLayerId === state.trendlineLayerId ? [state.layerId] : [];
|
return newLayerId === state.trendlineLayerId ? [state.layerId] : [];
|
||||||
},
|
},
|
||||||
|
@ -617,6 +621,17 @@ export const getMetricVisualization = ({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderDimensionEditorAdditionalSection(domElement, props) {
|
||||||
|
render(
|
||||||
|
<KibanaThemeProvider theme$={theme.theme$}>
|
||||||
|
<I18nProvider>
|
||||||
|
<DimensionEditorAdditionalSection {...props} />
|
||||||
|
</I18nProvider>
|
||||||
|
</KibanaThemeProvider>,
|
||||||
|
domElement
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
getErrorMessages(state) {
|
getErrorMessages(state) {
|
||||||
// Is it possible to break it?
|
// Is it possible to break it?
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue