mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -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',
|
||||
};
|
||||
expect(FormBasedDatasource.removeLayer(state, 'first')).toEqual({
|
||||
...state,
|
||||
layers: {
|
||||
second: {
|
||||
indexPatternId: '2',
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
removedLayerIds: ['first'],
|
||||
newState: {
|
||||
...state,
|
||||
layers: {
|
||||
second: {
|
||||
indexPatternId: '2',
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1777,8 +1780,72 @@ describe('IndexPattern Data Source', () => {
|
|||
currentIndexPatternId: '1',
|
||||
};
|
||||
expect(FormBasedDatasource.removeLayer(state, 'first')).toEqual({
|
||||
...state,
|
||||
layers: {},
|
||||
removedLayerIds: ['first', 'second'],
|
||||
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) {
|
||||
const newLayers = { ...state.layers };
|
||||
delete newLayers[layerId];
|
||||
const removedLayerIds: string[] = [layerId];
|
||||
|
||||
// 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 {
|
||||
...state,
|
||||
layers: newLayers,
|
||||
removedLayerIds,
|
||||
newState: {
|
||||
...state,
|
||||
layers: newLayers,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
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 {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: blankLayer(state.currentIndexPatternId, state.layers[layerId].linkToLayers),
|
||||
removedLayerIds,
|
||||
newState: {
|
||||
...state,
|
||||
layers: {
|
||||
...newLayers,
|
||||
[layerId]: blankLayer(state.currentIndexPatternId, state.layers[layerId].linkToLayers),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -217,21 +217,24 @@ describe('IndexPattern Data Source', () => {
|
|||
describe('#removeLayer', () => {
|
||||
it('should remove a layer', () => {
|
||||
expect(TextBasedDatasource.removeLayer(baseState, 'a')).toEqual({
|
||||
...baseState,
|
||||
layers: {
|
||||
a: {
|
||||
columns: [],
|
||||
allColumns: [
|
||||
{
|
||||
columnId: 'col1',
|
||||
fieldName: 'Test 1',
|
||||
meta: {
|
||||
type: 'number',
|
||||
removedLayerIds: ['a'],
|
||||
newState: {
|
||||
...baseState,
|
||||
layers: {
|
||||
a: {
|
||||
columns: [],
|
||||
allColumns: [
|
||||
{
|
||||
columnId: 'col1',
|
||||
fieldName: 'Test 1',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
query: { sql: 'SELECT * FROM foo' },
|
||||
index: 'foo',
|
||||
],
|
||||
query: { sql: 'SELECT * FROM foo' },
|
||||
index: 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -281,18 +281,24 @@ export function getTextBasedDatasource({
|
|||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
layers: newLayers,
|
||||
fieldList: state.fieldList,
|
||||
removedLayerIds: [layerId],
|
||||
newState: {
|
||||
...state,
|
||||
layers: newLayers,
|
||||
fieldList: state.fieldList,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
clearLayer(state: TextBasedPrivateState, layerId: string) {
|
||||
return {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: { ...state.layers[layerId], columns: [] },
|
||||
removedLayerIds: [],
|
||||
newState: {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: { ...state.layers[layerId], columns: [] },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -494,8 +494,8 @@ describe('chart_switch', () => {
|
|||
|
||||
switchTo('visB', instance);
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'a');
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith(undefined, 'b');
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith(undefined, 'c');
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'b');
|
||||
expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'c');
|
||||
expect(visualizationMap.visB.getSuggestions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
keptLayerIds: ['a'],
|
||||
|
|
|
@ -26,7 +26,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
|
||||
return {
|
||||
id: 'testDatasource',
|
||||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
clearLayer: jest.fn((state, _layerId) => ({ newState: state, removedLayerIds: [] })),
|
||||
getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn, _indexPatterns) => []),
|
||||
getDatasourceSuggestionsForVisualizeField: jest.fn(
|
||||
(_state, _indexpatternId, _fieldName, _indexPatterns) => []
|
||||
|
@ -44,7 +44,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
renderLayerPanel: jest.fn(),
|
||||
toExpression: jest.fn((_frame, _state, _indexPatterns) => null),
|
||||
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) => {}),
|
||||
removeColumn: jest.fn((props) => {}),
|
||||
getLayers: jest.fn((_state) => []),
|
||||
|
|
|
@ -39,6 +39,7 @@ describe('lensSlice', () => {
|
|||
let store: EnhancedStore<{ lens: LensAppState }>;
|
||||
beforeEach(() => {
|
||||
store = makeLensStore({}).store;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const customQuery = { query: 'custom' } as Query;
|
||||
|
||||
|
@ -275,17 +276,21 @@ describe('lensSlice', () => {
|
|||
return {
|
||||
id: datasourceId,
|
||||
getPublicAPI: () => ({
|
||||
datasourceId: 'testDatasource',
|
||||
datasourceId,
|
||||
getOperationForColumnId: jest.fn(),
|
||||
getTableSpec: jest.fn(),
|
||||
}),
|
||||
getLayers: () => ['layer1'],
|
||||
clearLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).map((id: string) =>
|
||||
clearLayer: (layerIds: unknown, layerId: string) => ({
|
||||
removedLayerIds: [],
|
||||
newState: (layerIds as string[]).map((id: string) =>
|
||||
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[]) => [
|
||||
...(layerIds as string[]),
|
||||
layerId,
|
||||
|
@ -317,8 +322,9 @@ describe('lensSlice', () => {
|
|||
(layerIds as string[]).map((id: string) =>
|
||||
id === layerId ? `vis_clear_${layerId}` : id
|
||||
),
|
||||
removeLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).filter((id: string) => id !== layerId),
|
||||
removeLayer: jest.fn((layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).filter((id: string) => id !== layerId)
|
||||
),
|
||||
getLayerIds: (layerIds: unknown) => layerIds as string[],
|
||||
getLayersToLinkTo: (state, newLayerId) => ['linked-layer-id'],
|
||||
appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
|
||||
|
@ -482,6 +488,54 @@ describe('lensSlice', () => {
|
|||
expect(state.datasourceStates.testDatasource2.state).toEqual(['layer2']);
|
||||
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', () => {
|
||||
|
@ -546,8 +600,6 @@ describe('lensSlice', () => {
|
|||
datasourceMap: datasourceMap as unknown as DatasourceMap,
|
||||
}),
|
||||
}).store;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('removes a dimension', () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { mapValues } from 'lodash';
|
||||
import { mapValues, uniq } from 'lodash';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { History } from 'history';
|
||||
import { LensEmbeddableInput } from '..';
|
||||
|
@ -400,16 +400,23 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
layerIds.length
|
||||
) === 'clear';
|
||||
|
||||
let removedLayerIds: string[] = [];
|
||||
|
||||
state.datasourceStates = mapValues(
|
||||
state.datasourceStates,
|
||||
(datasourceState, datasourceId) => {
|
||||
const datasource = datasourceMap[datasourceId!];
|
||||
|
||||
const { newState, removedLayerIds: removedLayerIdsForThisDatasource } = isOnlyLayer
|
||||
? datasource.clearLayer(datasourceState.state, layerId)
|
||||
: datasource.removeLayer(datasourceState.state, layerId);
|
||||
|
||||
removedLayerIds = [...removedLayerIds, ...removedLayerIdsForThisDatasource];
|
||||
|
||||
return {
|
||||
...datasourceState,
|
||||
...(datasourceId === state.activeDatasourceId && {
|
||||
state: isOnlyLayer
|
||||
? datasource.clearLayer(datasourceState.state, layerId)
|
||||
: datasource.removeLayer(datasourceState.state, layerId),
|
||||
state: newState,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -419,10 +426,22 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
const currentDataViewsId = activeDataSource.getUsedDataView(
|
||||
state.datasourceStates[state.activeDatasourceId!].state
|
||||
);
|
||||
state.visualization.state =
|
||||
isOnlyLayer || !activeVisualization.removeLayer
|
||||
? activeVisualization.clearLayer(state.visualization.state, layerId, currentDataViewsId)
|
||||
: activeVisualization.removeLayer(state.visualization.state, layerId);
|
||||
|
||||
if (isOnlyLayer || !activeVisualization.removeLayer) {
|
||||
state.visualization.state = activeVisualization.clearLayer(
|
||||
state.visualization.state,
|
||||
layerId,
|
||||
currentDataViewsId
|
||||
);
|
||||
}
|
||||
|
||||
uniq(removedLayerIds).forEach(
|
||||
(removedId) =>
|
||||
(state.visualization.state = activeVisualization.removeLayer?.(
|
||||
state.visualization.state,
|
||||
removedId
|
||||
))
|
||||
);
|
||||
},
|
||||
[changeIndexPattern.type]: (
|
||||
state,
|
||||
|
@ -977,9 +996,12 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
);
|
||||
}) ?? [];
|
||||
if (layerDatasourceId) {
|
||||
state.datasourceStates[layerDatasourceId].state = datasourceMap[
|
||||
layerDatasourceId
|
||||
].removeLayer(current(state).datasourceStates[layerDatasourceId].state, layerId);
|
||||
const { newState } = datasourceMap[layerDatasourceId].removeLayer(
|
||||
current(state).datasourceStates[layerDatasourceId].state,
|
||||
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;
|
||||
createEmptyLayer: (indexPatternId: string) => T;
|
||||
removeLayer: (state: T, layerId: string) => T;
|
||||
clearLayer: (state: T, layerId: string) => T;
|
||||
removeLayer: (state: T, layerId: string) => { newState: T; removedLayerIds: string[] };
|
||||
clearLayer: (state: T, layerId: string) => { newState: T; removedLayerIds: string[] };
|
||||
cloneLayer: (
|
||||
state: T,
|
||||
layerId: string,
|
||||
|
|
|
@ -12,7 +12,11 @@ import { OperationDescriptor, VisualizationDimensionEditorProps } from '../../ty
|
|||
import { CustomPaletteParams, PaletteOutput, PaletteRegistry } from '@kbn/coloring';
|
||||
|
||||
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 { CollapseSetting } from '../../shared_components/collapse_setting';
|
||||
import { EuiButtonGroup, EuiColorPicker, PropsOf } from '@elastic/eui';
|
||||
|
@ -154,42 +158,6 @@ describe('dimension editor', () => {
|
|||
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();
|
||||
|
@ -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', () => {
|
||||
|
@ -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,
|
||||
euiPaletteColorBlind,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { LayoutDirection } from '@elastic/charts';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
@ -30,6 +32,7 @@ import {
|
|||
} from '@kbn/coloring';
|
||||
import { getDataBoundsForPalette } from '@kbn/expression-metric-vis-plugin/public';
|
||||
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { css } from '@emotion/react';
|
||||
import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils';
|
||||
import {
|
||||
applyPaletteParams,
|
||||
|
@ -263,170 +266,8 @@ function PrimaryMetricEditor(props: SubProps) {
|
|||
|
||||
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 (
|
||||
<>
|
||||
<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
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
|
@ -580,3 +421,203 @@ function StaticColorControls({ state, setState }: Pick<Props, 'state' | 'setStat
|
|||
</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,
|
||||
} from '../../types';
|
||||
import { GROUP_ID, LENS_METRIC_ID } from './constants';
|
||||
import { DimensionEditor } from './dimension_editor';
|
||||
import { DimensionEditor, DimensionEditorAdditionalSection } from './dimension_editor';
|
||||
import { Toolbar } from './toolbar';
|
||||
import { generateId } from '../../id_generator';
|
||||
import { FormatSelectorOptions } from '../../datasources/form_based/dimension_panel/format_selector';
|
||||
|
@ -454,6 +454,10 @@ export const getMetricVisualization = ({
|
|||
return newState;
|
||||
},
|
||||
|
||||
getRemoveOperation(state, layerId) {
|
||||
return layerId === state.trendlineLayerId ? 'remove' : 'clear';
|
||||
},
|
||||
|
||||
getLayersToLinkTo(state, newLayerId: string): string[] {
|
||||
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) {
|
||||
// Is it possible to break it?
|
||||
return undefined;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue