[Lens] Transient suggestions (#44234)
* [lens] Initial Commit (#35627)
* [visualization editor] Initial Commit
* [lens] Add more complete initial state
* [lens] Fix type issues
* [lens] Remove feature control
* [lens] Bring back feature control and add tests
* [lens] Update plugin structure and naming per comments
* replace any usage by safe casting
* [lens] Respond to review comments
* [lens] Remove unused EditorFrameState type
* [lens] Initial state for IndexPatternDatasource (#36052)
* [lens] Add first tests to indexpattern data source
* Respond to review comments
* Fix type definitions
* [lens] Editor frame initializes datasources and visualizations (#36060)
* [lens] Editor frame initializes datasources and visualizations
* Respond to review comments
* Fix build issues
* Fix state management issue
* [lens][draft] Lens/drag drop (#36268)
Add basic drag / drop component to Lens
* remove local package (#36456)
* [lens] Native renderer (#36165)
* Add nativerenderer component
* Use native renderer in app and editor frame
* [Lens] No explicit any (#36515)
* [Lens] Implement basic editor frame state handling (#36443)
* [lens] Load index patterns and render in data panel (#36463)
* [lens] Editor frame initializes datasources and visualizations
* Respond to review comments
* Fix build issues
* remove local package
* [lens] Load index patterns into data source
* Redo types for Index Pattern Datasource
* Fix one more type
* Respond to review comments
* [draft] Lens/line chart renderer (#36827)
Expression logic for the Lens xy chart.
* [lens] Index pattern data panel (initial) (#37015)
* [lens] Index pattern switcher
* Respond to review comments
* [Lens] Editor state 2 (#36513)
* [lens] Dimension panel that generates columns (#37117)
* [lens] Dimension panel that generates columns
* Update from review comments
* [lens] Generate esdocs queries from index pattern (#37361)
* [lens] Generate esdocs queries from index pattern
* Remove unused code
* Update yarn.lock from yarn kbn bootstrap
* [Lens] Add basic Lens xy chart suggestions (#37030)
Basic xy chart suggestions
* [Lens] Expression rendering (#37648)
* [Lens] Expression handling (#37876)
* [Lens] Lens/xy config panel (#37391)
Basic xy chart configuration panel
* [Lens] Xy expression building (#37967)
* [Lens] Initialize visualization with datasource api (#38142)
* [lens] Dimension panel lets users select operations and fields individually (#37573)
* [lens] Dimension panel lets users select operations and fields individually
* Split files and add tests
* Fix dimension labeling and add clear button
* Support more aggregations, aggregation nesting, rollups, and clearing
* Fix esaggs expression
* Increase top-level test coverage of dimension panel
* Update from review comments
* [Lens] Rename columns (#38278)
* [Lens] Lens/index pattern drag drop (#37711)
* Basic xy chart suggestions
* Re-apply XY config panel after force merge
* Initial integration of lens drag and drop
* Tweak naming, remove irellevant comment
* Tweaks per Wylie's feedback
* Add xy chart internationalization
Tweak types per Joe's feedback
* Update xy chart i18n implementation
* Fix i18n id
* Add drop tests to the lens index pattern
* improve tests
* [lens] Only allow aggregated dimensions (#38820)
* [lens] Only allow aggregated dimensions
* [lens] Index pattern suggest on drop
* Fully remove value
* Revert "[lens] Index pattern suggest on drop"
This reverts commit 604c6ed68c
.
* Fix type errors
* [lens] Suggest on drop (#38848)
* [lens] Index pattern suggest on drop
* Add test for suggestion without date field
* fix merge
* [Lens] Parameter configurations and new dimension config flow (#38863)
* fix eslint failure
* [lens] Fix build by updating saved objects and i18n (#39391)
* [lens] Update location of saved objects code
* Update internatationalization
* Remove added file
* Lens basic metric visualization
* [lens] Fix arguments to esaggs using booleans (#39462)
* [lens] Datatable visualization plugin (#39390)
* [lens] Datatable visualization plugin
* Fix merge issues and add tests
* Update from review
* Fix file locations
* Fix merge issues, localize expression help text
* Add auto-scaling to the lens metric visualization
* Fix unit tests broken by autoscale
* Move autoscale to the new Lens folder
* [lens] Use first suggestion when switching visualizations (#39377)
* [lens] Label each Y axis with its operation label (#39461)
* [lens] Label each Y axis with its operation label
* Remove comment
* Add link to chart issue
* [Lens] Suggestion preview rendering (#39576)
* [Lens] Popover configs (#39565)
* [Lens] Basic layouting (#39587)
* remove datasource public API in suggestions (#39772)
* [Lens] Basic save / load (#39257)
Add basic routing, save, and load to Lens
* [lens] Fix lint error
* [lens] Use node scripts/eslint.js --fix to fix errors
* [lens] Include link to lens from Visualize (#40542)
* [lens] Support stacking in xy visualization (#40546)
* [lens] Support stacking in xy visualization
* Use chart type switcher for stacked and horizontal xy charts
* Clean up remaining isStacked code
* Fix type error
* [Lens] Add xy split series support (#39726)
* Add split series to lens xy chart
* [lens] Lens Filter Ratio (#40196)
* WIP filter ratio
* Fix test issues
* Pass dependencies through plugin like new platform
* Pass props into filter ratio popover editor
* Provide mocks to filter_ratio popover test
* Add another test
* Clean up to prepare for review
* Clean up unnecessary changes
* Respond to review comments
* Fix tests
* [Lens] Terms order direction (#39884)
* fix types
* [Lens] Data panel styling and optimizations (#40787)
Style the data panel (mostly Joe Reuter's doing). Optimize a bunch of the Lens stack.
* Add metric preview icon
* Fix metric vis tests
* Fix metric plugin imports
* Use the operation label as the metric title
* [Lens] Optimize dimension panel flow (#41114)
* [Lens] re-introduce no-explicit-any (#41454)
* [Lens] No results marker (#41450)
* [lens] Support layers for visualizing results of multiple queries (#41290)
* [lens] WIP add support for layers
* [lens] WIP switch to nested tables
* Get basic layering to work
* Load multiple tables and render in one chart
* Fix priority ordering
* Reduce quantity of linting errors
* Ensure that new xy layer state has a split column
* Make the "add" y / split the trailing accessor
* Various fixes for datasource public API and implementation
* Unify datasource deletion and accessor removal
* Fix broken scss
* Fix xy visualization TypeScript errors?
* Build basic suggestions
* Restore save/load and fix typescript bugs
* simplify init routine
* fix save tests
* fix persistence tests
* fix state management tests
* Ensure the data table is aligned to the top
* Add layer support to Lens datatable
* Give xy chart a default layer initially
* Allow deletion of layers in xy charts
* xy: Make split accessor singular
Remove commented code blocks
* Change expression type for lens_merge_tables
* Fix XY chart rendering expression
* Fix type errors relating to `layerId` in table suggestions
* Pass around tables for suggestions with associated layerIds
* fix tests in workspace panel
* fix editor_frame tests
* Fix xy tests, skip inapplicable tests
that will be implemented in a separate PR
* add some tests for multiple datasources and layers
* Suggest that split series comes before X axis in XY chart
* Get datatable suggestion working
* Adjust how xy axis labels are computed
* Datasource suggestions should handle layers and have tests
* Fix linting in XY chart and remove commented code
* Update snapshots from earlier change
* Fix linting errors
* More cleanup
* Remove commented code
* Test the multi-column editor
* XY Visualization does not need to track datasourceId
* Fix various comments
* Remove unused xy prop
Add datasource header to datatable config
* Use operation labels for XY chart
* Adding and removing layers is reflected in the datasource
* rewrote datasource state init
* clean up editor_frame frame api implementation
* clean up editor frame
* [Lens] Embeddable (#41361)
* [lens] Move XY chart config into popover and fix layering (#41927)
* [lens] Move XY chart config into popover and fix layering
* Fix tests
* Update style
* Change wrapping of layer settings popover
* [Lens] Fix bugs in date_histogram and filter ratio (#42046)
* [Lens] Performance improvements (#41784)
* fix type error
* switch default size of terms operation to 3 (#42334)
* [lens] Improve suggestions for split series (#42052)
* [lens] Add chart switcher (#42093)
* solve merge conflicts
* fix test case
* [Lens] Allow only current visualization on field drop in workspace (#42344)
* [Lens] Remove indexpattern id on column (#42429)
* [lens] Implement app-level filtering and time picker (#42031)
* [lens] Implement app-level filtering and time picker
* More integration with filter bar
* Clean up test code and type errors
* Add frame level tests for syncing with app
* Add test coverage for app logic
* Simplify state management from app down
* Fix import errors
* Clarify whether properties are ids or titles for index pattern
* pass new saved object by ref
* add dirty state checking
* Fix tests
* [Lens] Add some tests around document handling in dimension panel (#42670)
* [Lens] Terms operation boolean support (#42817)
* [lens] Minor UX/UI improvements in Lens (#42852)
* Make dimension popover toggle when clicking button
* Without suggestions hide suggestion panel
* Add missing translations (#42921)
* [Lens] Config panel design (#42980)
* Fix up design of config panel
Does not include config popover
* Add metric suggestions, fix tests
* Remove a couple of non-null assertions (#43013)
* Remove a couple of non-null assertions
* Remove orphaned import
* [Lens] Switch indexpattern manually (#42599)
* [Lens] Update frame to put suggestions at the bottom (#42997)
* Back out suggestion changes, in lieu
of Joe's work
* fix type errors
* switch indexpattern on layer if there is only a single empty one (#43079)
* [Lens] Suggest reduced versions of current data table (#42537)
* [Lens] Field formatter support (#38874)
* Fix bugs
* Fix metric autoscale logic
* Register metric as an embeddable
* Fix metric autoscale flicker
* Render mini metric in suggestions panel
* Cache the metric filterOperations function
* fix auto scaling edge cases
* [Lens] Add bucket nesting editor to indexpattern (#42869)
* Modify auto-scale to handle resize events
* use format hints in metric vis
* start cleaning up suggestions
* [Lens] Remove unnecessary fields and indexing from mappings (#43285)
* Tweak metric to only scale down so far, and
scale both the number and the label.
* Fix lens metric tests
* [Lens] Xy scale type (#42142)
* start adding more suggestions
* remove unused imports
* work on suggestions
* work more on suggestions
* work more on suggestions
* work more on suggestions
* [lens] Allow updater function to be used for updating state (#43373)
* [Lens] Lens metric visualization (#39364)
* clean up tests and add new ones
* remove isMetric
* area as default on time dimension
* fix bug in area chart for time
* Fix axis rotation (#43792)
* remove title form layer
* [Lens] Auto date histogram (#43775)
* Add auto date histogram
* Improve documentation and cleanup
* Add tests
* Change test name
* handle state in app
* fix isMetric usages
* fix integration tests
* fix type errors
* fix date handling on submit
* add new suggestion types
* fix test
* do not suggest single tables
* remove unused import
* [Lens] Fix query bar integration (#43865)
* switch order of appending new string column
* resolve merge conflicts
* [Lens] Clean up operations code (#43784)
* fix merge conflicts
* poc implementation
* highlight currently active suggestion and provide button to submit current choice
* [Lens] Functional tests (#44279)
Foundational layer for lens functional tests. Lens-specific page objects are not in this PR.
* [Lens] Add Lens visualizations to Visualize list (#43398)
* [Lens] Suggestion improvements (#43688)
* fix bugs
* [lens] Calculate existence of fields in datasource (#44422)
* [lens] Calculate existence of fields in datasource
* Fix route registration
* Add page object and use existence in functional test
* Simplify layout of filters for index pattern
* Respond to review feedback
* Update class names
* Use new URL constant
* Fix usage of base path
* Fix lint errors
* [Lens ] Preview metric (#43755)
* format filter ratio as percentage (#44625)
* [Lens] Remove datasource suggestion id (#44495)
* [Lens] Make breadcrumbs look and feel like Visualize (#44258)
* [lens] Fix breakage from app-arch movements (#44720)
* Design cleanup
* PR review comments
* fix tests
* small cleanup
* remove unused import
* [lens] Fix type error in test from merge
* [lens] Fix registration of embeddable (#45171)
* keep references stable if table is just extended and add tests
* changed label for stack/unstack
* fix test
* [Lens] Functional tests (#44814)
Basic functional tests for Lens, by no means comprehensive. This is more of a smokescreen test of some normal use cases.
* [lens] Add Lens to CODEOWNERS (#45296)
* [lens] Fix visualization alias registration
* [lens] Fix usage of EUI after typescript upgrade (#45404)
* [lens] Fix usage of EUI after typescript upgrade
* Use local fix instead of workaround
* fix bug and address reviews
* Fix frame tests
This commit is contained in:
parent
e507264ad8
commit
921e356a4b
|
@ -77,10 +77,15 @@ export function ChartSwitch(props: Props) {
|
||||||
const commitSelection = (selection: VisualizationSelection) => {
|
const commitSelection = (selection: VisualizationSelection) => {
|
||||||
setFlyoutOpen(false);
|
setFlyoutOpen(false);
|
||||||
|
|
||||||
switchToSuggestion(props.framePublicAPI, props.dispatch, {
|
switchToSuggestion(
|
||||||
...selection,
|
props.framePublicAPI,
|
||||||
visualizationState: selection.getVisualizationState(),
|
props.dispatch,
|
||||||
});
|
{
|
||||||
|
...selection,
|
||||||
|
visualizationState: selection.getVisualizationState(),
|
||||||
|
},
|
||||||
|
'SWITCH_VISUALIZATION'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSelection(
|
function getSelection(
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config
|
||||||
props.dispatch({
|
props.dispatch({
|
||||||
type: 'UPDATE_VISUALIZATION_STATE',
|
type: 'UPDATE_VISUALIZATION_STATE',
|
||||||
newState,
|
newState,
|
||||||
|
clearStagedPreview: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[props.dispatch]
|
[props.dispatch]
|
||||||
|
|
|
@ -32,6 +32,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
||||||
type: 'UPDATE_DATASOURCE_STATE',
|
type: 'UPDATE_DATASOURCE_STATE',
|
||||||
updater,
|
updater,
|
||||||
datasourceId: props.activeDatasource!,
|
datasourceId: props.activeDatasource!,
|
||||||
|
clearStagedPreview: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[props.dispatch, props.activeDatasource]
|
[props.dispatch, props.activeDatasource]
|
||||||
|
|
|
@ -1153,7 +1153,14 @@ describe('editor_frame', () => {
|
||||||
.find('[data-test-subj="lnsSuggestion"]')
|
.find('[data-test-subj="lnsSuggestion"]')
|
||||||
.find(EuiPanel)
|
.find(EuiPanel)
|
||||||
.map(el => el.parents(EuiToolTip).prop('content'))
|
.map(el => el.parents(EuiToolTip).prop('content'))
|
||||||
).toEqual(['Suggestion1', 'Suggestion2', 'Suggestion3', 'Suggestion4', 'Suggestion5']);
|
).toEqual([
|
||||||
|
'Current',
|
||||||
|
'Suggestion1',
|
||||||
|
'Suggestion2',
|
||||||
|
'Suggestion3',
|
||||||
|
'Suggestion4',
|
||||||
|
'Suggestion5',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should switch to suggested visualization', async () => {
|
it('should switch to suggested visualization', async () => {
|
||||||
|
@ -1196,7 +1203,7 @@ describe('editor_frame', () => {
|
||||||
act(() => {
|
act(() => {
|
||||||
instance
|
instance
|
||||||
.find('[data-test-subj="lnsSuggestion"]')
|
.find('[data-test-subj="lnsSuggestion"]')
|
||||||
.first()
|
.at(2)
|
||||||
.simulate('click');
|
.simulate('click');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
||||||
type: 'UPDATE_DATASOURCE_STATE',
|
type: 'UPDATE_DATASOURCE_STATE',
|
||||||
datasourceId: id,
|
datasourceId: id,
|
||||||
updater: newState,
|
updater: newState,
|
||||||
|
clearStagedPreview: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
layer
|
layer
|
||||||
|
@ -265,6 +266,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
||||||
visualizationMap={props.visualizationMap}
|
visualizationMap={props.visualizationMap}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
ExpressionRenderer={props.ExpressionRenderer}
|
ExpressionRenderer={props.ExpressionRenderer}
|
||||||
|
stagedPreview={state.stagedPreview}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,18 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { EditorFrameProps } from '../editor_frame';
|
import { EditorFrameProps } from '../editor_frame';
|
||||||
import { Document } from '../../persistence/saved_object_store';
|
import { Document } from '../../persistence/saved_object_store';
|
||||||
|
|
||||||
export interface EditorFrameState {
|
export interface PreviewState {
|
||||||
persistedId?: string;
|
|
||||||
title: string;
|
|
||||||
visualization: {
|
visualization: {
|
||||||
activeId: string | null;
|
activeId: string | null;
|
||||||
state: unknown;
|
state: unknown;
|
||||||
};
|
};
|
||||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>;
|
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorFrameState extends PreviewState {
|
||||||
|
persistedId?: string;
|
||||||
|
title: string;
|
||||||
|
stagedPreview?: PreviewState;
|
||||||
activeDatasourceId: string | null;
|
activeDatasourceId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,10 +36,12 @@ export type Action =
|
||||||
type: 'UPDATE_DATASOURCE_STATE';
|
type: 'UPDATE_DATASOURCE_STATE';
|
||||||
updater: unknown | ((prevState: unknown) => unknown);
|
updater: unknown | ((prevState: unknown) => unknown);
|
||||||
datasourceId: string;
|
datasourceId: string;
|
||||||
|
clearStagedPreview?: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'UPDATE_VISUALIZATION_STATE';
|
type: 'UPDATE_VISUALIZATION_STATE';
|
||||||
newState: unknown;
|
newState: unknown;
|
||||||
|
clearStagedPreview?: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'UPDATE_LAYER';
|
type: 'UPDATE_LAYER';
|
||||||
|
@ -59,6 +65,19 @@ export type Action =
|
||||||
datasourceState: unknown;
|
datasourceState: unknown;
|
||||||
datasourceId: string;
|
datasourceId: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'SELECT_SUGGESTION';
|
||||||
|
newVisualizationId: string;
|
||||||
|
initialState: unknown;
|
||||||
|
datasourceState: unknown;
|
||||||
|
datasourceId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'ROLLBACK_SUGGESTION';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SUBMIT_SUGGESTION';
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'SWITCH_DATASOURCE';
|
type: 'SWITCH_DATASOURCE';
|
||||||
newDatasourceId: string;
|
newDatasourceId: string;
|
||||||
|
@ -176,6 +195,41 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
|
||||||
activeId: action.newVisualizationId,
|
activeId: action.newVisualizationId,
|
||||||
state: action.initialState,
|
state: action.initialState,
|
||||||
},
|
},
|
||||||
|
stagedPreview: undefined,
|
||||||
|
};
|
||||||
|
case 'SELECT_SUGGESTION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
datasourceStates:
|
||||||
|
'datasourceId' in action && action.datasourceId
|
||||||
|
? {
|
||||||
|
...state.datasourceStates,
|
||||||
|
[action.datasourceId]: {
|
||||||
|
...state.datasourceStates[action.datasourceId],
|
||||||
|
state: action.datasourceState,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: state.datasourceStates,
|
||||||
|
visualization: {
|
||||||
|
...state.visualization,
|
||||||
|
activeId: action.newVisualizationId,
|
||||||
|
state: action.initialState,
|
||||||
|
},
|
||||||
|
stagedPreview: state.stagedPreview || {
|
||||||
|
datasourceStates: state.datasourceStates,
|
||||||
|
visualization: state.visualization,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'ROLLBACK_SUGGESTION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...(state.stagedPreview || {}),
|
||||||
|
stagedPreview: undefined,
|
||||||
|
};
|
||||||
|
case 'SUBMIT_SUGGESTION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
stagedPreview: undefined,
|
||||||
};
|
};
|
||||||
case 'UPDATE_DATASOURCE_STATE':
|
case 'UPDATE_DATASOURCE_STATE':
|
||||||
return {
|
return {
|
||||||
|
@ -190,6 +244,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview,
|
||||||
};
|
};
|
||||||
case 'UPDATE_VISUALIZATION_STATE':
|
case 'UPDATE_VISUALIZATION_STATE':
|
||||||
if (!state.visualization.activeId) {
|
if (!state.visualization.activeId) {
|
||||||
|
@ -201,6 +256,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
|
||||||
...state.visualization,
|
...state.visualization,
|
||||||
state: action.newState,
|
state: action.newState,
|
||||||
},
|
},
|
||||||
|
stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -144,14 +144,15 @@ export function switchToSuggestion(
|
||||||
suggestion: Pick<
|
suggestion: Pick<
|
||||||
Suggestion,
|
Suggestion,
|
||||||
'visualizationId' | 'visualizationState' | 'datasourceState' | 'datasourceId' | 'keptLayerIds'
|
'visualizationId' | 'visualizationState' | 'datasourceState' | 'datasourceId' | 'keptLayerIds'
|
||||||
>
|
>,
|
||||||
|
type: 'SWITCH_VISUALIZATION' | 'SELECT_SUGGESTION' = 'SELECT_SUGGESTION'
|
||||||
) {
|
) {
|
||||||
const action: Action = {
|
const action: Action = {
|
||||||
type: 'SWITCH_VISUALIZATION',
|
type,
|
||||||
newVisualizationId: suggestion.visualizationId,
|
newVisualizationId: suggestion.visualizationId,
|
||||||
initialState: suggestion.visualizationState,
|
initialState: suggestion.visualizationState,
|
||||||
datasourceState: suggestion.datasourceState,
|
datasourceState: suggestion.datasourceState,
|
||||||
datasourceId: suggestion.datasourceId,
|
datasourceId: suggestion.datasourceId!,
|
||||||
};
|
};
|
||||||
dispatch(action);
|
dispatch(action);
|
||||||
const layerIds = Object.keys(frame.datasourceLayers).filter(id => {
|
const layerIds = Object.keys(frame.datasourceLayers).filter(id => {
|
||||||
|
|
|
@ -10,25 +10,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnsSuggestionsPanel__title {
|
.lnsSuggestionsPanel__title {
|
||||||
margin: $euiSizeS 0 $euiSizeXS;
|
margin-left: $euiSizeXS / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnsSuggestionsPanel__suggestions {
|
.lnsSuggestionsPanel__suggestions {
|
||||||
@include euiScrollBar;
|
@include euiScrollBar;
|
||||||
@include lnsOverflowShadowHorizontal;
|
@include lnsOverflowShadowHorizontal;
|
||||||
padding-top: $euiSizeXS;
|
padding-top: $euiSizeXS;
|
||||||
overflow-x: auto;
|
overflow-x: scroll;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
// Padding / negative margins to make room for overflow shadow
|
// Padding / negative margins to make room for overflow shadow
|
||||||
padding-left: $euiSizeXS;
|
padding-left: $euiSizeXS;
|
||||||
margin-left: -$euiSizeXS;
|
margin-left: -$euiSizeXS;
|
||||||
|
|
||||||
// Add margin to the next of the same type
|
|
||||||
> * + * {
|
|
||||||
margin-left: $euiSizeS;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// These sizes also match canvas' page thumbnails for consistency
|
// These sizes also match canvas' page thumbnails for consistency
|
||||||
|
@ -39,8 +34,13 @@ $lnsSuggestionWidth: 150px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: $lnsSuggestionWidth !important;
|
width: $lnsSuggestionWidth !important;
|
||||||
height: $lnsSuggestionHeight;
|
height: $lnsSuggestionHeight;
|
||||||
// Allows the scrollbar to stay flush to window
|
margin-right: $euiSizeS;
|
||||||
margin-bottom: $euiSize;
|
margin-left: $euiSizeXS / 2;
|
||||||
|
margin-bottom: $euiSizeXS / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnsSuggestionPanel__button-isSelected {
|
||||||
|
@include euiFocusRing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnsSidebar__suggestionIcon {
|
.lnsSidebar__suggestionIcon {
|
||||||
|
@ -58,3 +58,15 @@ $lnsSuggestionWidth: 150px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
margin: 0 $euiSizeS;
|
margin: 0 $euiSizeS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lnsSuggestionChartWrapper--withLabel {
|
||||||
|
height: $lnsSuggestionHeight - $euiSizeL;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnsSuggestionPanel__buttonLabel {
|
||||||
|
@include euiFontSizeXS;
|
||||||
|
display: block;
|
||||||
|
font-weight: $euiFontWeightBold;
|
||||||
|
text-align: center;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -14,14 +14,16 @@ import {
|
||||||
DatasourceMock,
|
DatasourceMock,
|
||||||
createMockFramePublicAPI,
|
createMockFramePublicAPI,
|
||||||
} from '../mocks';
|
} from '../mocks';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public';
|
import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public';
|
||||||
import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel';
|
import { InnerSuggestionPanel, SuggestionPanelProps } from './suggestion_panel';
|
||||||
import { getSuggestions, Suggestion } from './suggestion_helpers';
|
import { getSuggestions, Suggestion } from './suggestion_helpers';
|
||||||
import { fromExpression } from '@kbn/interpreter/target/common';
|
|
||||||
import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui';
|
import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||||
|
|
||||||
jest.mock('./suggestion_helpers');
|
jest.mock('./suggestion_helpers');
|
||||||
|
|
||||||
|
const getSuggestionsMock = getSuggestions as jest.Mock;
|
||||||
|
|
||||||
describe('suggestion_panel', () => {
|
describe('suggestion_panel', () => {
|
||||||
let mockVisualization: Visualization;
|
let mockVisualization: Visualization;
|
||||||
let mockDatasource: DatasourceMock;
|
let mockDatasource: DatasourceMock;
|
||||||
|
@ -40,7 +42,7 @@ describe('suggestion_panel', () => {
|
||||||
expressionRendererMock = createExpressionRendererMock();
|
expressionRendererMock = createExpressionRendererMock();
|
||||||
dispatchMock = jest.fn();
|
dispatchMock = jest.fn();
|
||||||
|
|
||||||
(getSuggestions as jest.Mock).mockReturnValue([
|
getSuggestionsMock.mockReturnValue([
|
||||||
{
|
{
|
||||||
datasourceState: {},
|
datasourceState: {},
|
||||||
previewIcon: 'empty',
|
previewIcon: 'empty',
|
||||||
|
@ -75,6 +77,7 @@ describe('suggestion_panel', () => {
|
||||||
activeVisualizationId: 'vis',
|
activeVisualizationId: 'vis',
|
||||||
visualizationMap: {
|
visualizationMap: {
|
||||||
vis: mockVisualization,
|
vis: mockVisualization,
|
||||||
|
vis2: createMockVisualization(),
|
||||||
},
|
},
|
||||||
visualizationState: {},
|
visualizationState: {},
|
||||||
dispatch: dispatchMock,
|
dispatch: dispatchMock,
|
||||||
|
@ -84,27 +87,131 @@ describe('suggestion_panel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list passed in suggestions', () => {
|
it('should list passed in suggestions', () => {
|
||||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('[data-test-subj="lnsSuggestion"]')
|
.find('[data-test-subj="lnsSuggestion"]')
|
||||||
.find(EuiPanel)
|
.find(EuiPanel)
|
||||||
.map(el => el.parents(EuiToolTip).prop('content'))
|
.map(el => el.parents(EuiToolTip).prop('content'))
|
||||||
).toEqual(['Suggestion1', 'Suggestion2']);
|
).toEqual(['Current', 'Suggestion1', 'Suggestion2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('uncommitted suggestions', () => {
|
||||||
|
let suggestionState: Pick<
|
||||||
|
SuggestionPanelProps,
|
||||||
|
'datasourceStates' | 'activeVisualizationId' | 'visualizationState'
|
||||||
|
>;
|
||||||
|
let stagedPreview: SuggestionPanelProps['stagedPreview'];
|
||||||
|
beforeEach(() => {
|
||||||
|
suggestionState = {
|
||||||
|
datasourceStates: {
|
||||||
|
mock: {
|
||||||
|
isLoading: false,
|
||||||
|
state: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activeVisualizationId: 'vis2',
|
||||||
|
visualizationState: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
stagedPreview = {
|
||||||
|
datasourceStates: defaultProps.datasourceStates,
|
||||||
|
visualization: {
|
||||||
|
state: defaultProps.visualizationState,
|
||||||
|
activeId: defaultProps.activeVisualizationId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update suggestions if current state is moved to staged preview', () => {
|
||||||
|
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||||
|
getSuggestionsMock.mockClear();
|
||||||
|
wrapper.setProps({
|
||||||
|
stagedPreview,
|
||||||
|
...suggestionState,
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(getSuggestionsMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update suggestions if staged preview is removed', () => {
|
||||||
|
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||||
|
getSuggestionsMock.mockClear();
|
||||||
|
wrapper.setProps({
|
||||||
|
stagedPreview,
|
||||||
|
...suggestionState,
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
wrapper.setProps({
|
||||||
|
stagedPreview: undefined,
|
||||||
|
...suggestionState,
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(getSuggestionsMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should highlight currently active suggestion', () => {
|
||||||
|
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[data-test-subj="lnsSuggestion"]')
|
||||||
|
.at(2)
|
||||||
|
.simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('[data-test-subj="lnsSuggestion"]')
|
||||||
|
.at(2)
|
||||||
|
.prop('className')
|
||||||
|
).toContain('lnsSuggestionPanel__button-isSelected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rollback suggestion if current panel is clicked', () => {
|
||||||
|
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[data-test-subj="lnsSuggestion"]')
|
||||||
|
.at(2)
|
||||||
|
.simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[data-test-subj="lnsSuggestion"]')
|
||||||
|
.at(0)
|
||||||
|
.simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(dispatchMock).toHaveBeenCalledWith({
|
||||||
|
type: 'ROLLBACK_SUGGESTION',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch visualization switch action if suggestion is clicked', () => {
|
it('should dispatch visualization switch action if suggestion is clicked', () => {
|
||||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||||
|
|
||||||
wrapper
|
act(() => {
|
||||||
.find('[data-test-subj="lnsSuggestion"]')
|
wrapper
|
||||||
.first()
|
.find('button[data-test-subj="lnsSuggestion"]')
|
||||||
.simulate('click');
|
.at(1)
|
||||||
|
.simulate('click');
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
expect(dispatchMock).toHaveBeenCalledWith(
|
expect(dispatchMock).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: 'SWITCH_VISUALIZATION',
|
type: 'SELECT_SUGGESTION',
|
||||||
initialState: suggestion1State,
|
initialState: suggestion1State,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -113,12 +220,29 @@ describe('suggestion_panel', () => {
|
||||||
it('should remove unused layers if suggestion is clicked', () => {
|
it('should remove unused layers if suggestion is clicked', () => {
|
||||||
defaultProps.frame.datasourceLayers.a = mockDatasource.publicAPIMock;
|
defaultProps.frame.datasourceLayers.a = mockDatasource.publicAPIMock;
|
||||||
defaultProps.frame.datasourceLayers.b = mockDatasource.publicAPIMock;
|
defaultProps.frame.datasourceLayers.b = mockDatasource.publicAPIMock;
|
||||||
const wrapper = mount(<SuggestionPanel {...defaultProps} activeVisualizationId="vis2" />);
|
const wrapper = mount(
|
||||||
|
<InnerSuggestionPanel
|
||||||
|
{...defaultProps}
|
||||||
|
stagedPreview={{ visualization: { state: {}, activeId: 'vis' }, datasourceStates: {} }}
|
||||||
|
activeVisualizationId="vis2"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
wrapper
|
act(() => {
|
||||||
.find('[data-test-subj="lnsSuggestion"]')
|
wrapper
|
||||||
.first()
|
.find('button[data-test-subj="lnsSuggestion"]')
|
||||||
.simulate('click');
|
.at(1)
|
||||||
|
.simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[data-test-subj="lensSubmitSuggestion"]')
|
||||||
|
.first()
|
||||||
|
.simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
expect(defaultProps.frame.removeLayers).toHaveBeenCalledWith(['b']);
|
expect(defaultProps.frame.removeLayers).toHaveBeenCalledWith(['b']);
|
||||||
});
|
});
|
||||||
|
@ -141,18 +265,18 @@ describe('suggestion_panel', () => {
|
||||||
visualizationState: suggestion2State,
|
visualizationState: suggestion2State,
|
||||||
visualizationId: 'vis',
|
visualizationId: 'vis',
|
||||||
title: 'Suggestion2',
|
title: 'Suggestion2',
|
||||||
previewExpression: 'test | expression',
|
|
||||||
},
|
},
|
||||||
] as Suggestion[]);
|
] as Suggestion[]);
|
||||||
|
|
||||||
|
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce(undefined);
|
||||||
|
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression');
|
||||||
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
||||||
|
|
||||||
mount(<SuggestionPanel {...defaultProps} />);
|
mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||||
|
|
||||||
expect(expressionRendererMock).toHaveBeenCalledTimes(1);
|
expect(expressionRendererMock).toHaveBeenCalledTimes(1);
|
||||||
const passedExpression = fromExpression(
|
const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression;
|
||||||
(expressionRendererMock as jest.Mock).mock.calls[0][0].expression
|
|
||||||
);
|
|
||||||
expect(passedExpression).toMatchInlineSnapshot(`
|
expect(passedExpression).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"chain": Array [
|
"chain": Array [
|
||||||
|
@ -163,6 +287,7 @@ describe('suggestion_panel', () => {
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"arguments": Object {
|
"arguments": Object {
|
||||||
|
"filters": Array [],
|
||||||
"query": Array [
|
"query": Array [
|
||||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||||
],
|
],
|
||||||
|
@ -212,7 +337,7 @@ describe('suggestion_panel', () => {
|
||||||
|
|
||||||
it('should render render icon if there is no preview expression', () => {
|
it('should render render icon if there is no preview expression', () => {
|
||||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||||
(getSuggestions as jest.Mock).mockReturnValue([
|
getSuggestionsMock.mockReturnValue([
|
||||||
{
|
{
|
||||||
datasourceState: {},
|
datasourceState: {},
|
||||||
previewIcon: 'visTable',
|
previewIcon: 'visTable',
|
||||||
|
@ -232,9 +357,15 @@ describe('suggestion_panel', () => {
|
||||||
},
|
},
|
||||||
] as Suggestion[]);
|
] as Suggestion[]);
|
||||||
|
|
||||||
|
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce(undefined);
|
||||||
|
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression');
|
||||||
|
|
||||||
|
// this call will go to the currently active visualization
|
||||||
|
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('current | preview');
|
||||||
|
|
||||||
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
||||||
|
|
||||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||||
|
|
||||||
expect(wrapper.find(EuiIcon)).toHaveLength(1);
|
expect(wrapper.find(EuiIcon)).toHaveLength(1);
|
||||||
expect(wrapper.find(EuiIcon).prop('type')).toEqual('visTable');
|
expect(wrapper.find(EuiIcon).prop('type')).toEqual('visTable');
|
||||||
|
|
|
@ -4,14 +4,25 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import _ from 'lodash';
|
||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { EuiIcon, EuiTitle, EuiPanel, EuiIconTip, EuiToolTip } from '@elastic/eui';
|
import {
|
||||||
import { toExpression, Ast } from '@kbn/interpreter/common';
|
EuiIcon,
|
||||||
|
EuiTitle,
|
||||||
|
EuiPanel,
|
||||||
|
EuiIconTip,
|
||||||
|
EuiToolTip,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { Ast } from '@kbn/interpreter/common';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { Action } from './state_management';
|
import classNames from 'classnames';
|
||||||
import { Datasource, Visualization, FramePublicAPI } from '../../types';
|
import { Action, PreviewState } from './state_management';
|
||||||
import { getSuggestions, Suggestion, switchToSuggestion } from './suggestion_helpers';
|
import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
|
||||||
|
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
|
||||||
import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public';
|
import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public';
|
||||||
import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers';
|
import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers';
|
||||||
import { debouncedComponent } from '../../debounced_component';
|
import { debouncedComponent } from '../../debounced_component';
|
||||||
|
@ -38,45 +49,48 @@ export interface SuggestionPanelProps {
|
||||||
dispatch: (action: Action) => void;
|
dispatch: (action: Action) => void;
|
||||||
ExpressionRenderer: ExpressionRenderer;
|
ExpressionRenderer: ExpressionRenderer;
|
||||||
frame: FramePublicAPI;
|
frame: FramePublicAPI;
|
||||||
|
stagedPreview?: PreviewState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SuggestionPreview = ({
|
const SuggestionPreview = ({
|
||||||
suggestion,
|
preview,
|
||||||
dispatch,
|
|
||||||
frame,
|
|
||||||
previewExpression,
|
|
||||||
ExpressionRenderer: ExpressionRendererComponent,
|
ExpressionRenderer: ExpressionRendererComponent,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
showTitleAsLabel,
|
||||||
}: {
|
}: {
|
||||||
suggestion: Suggestion;
|
onSelect: () => void;
|
||||||
dispatch: (action: Action) => void;
|
preview: {
|
||||||
frame: FramePublicAPI;
|
expression?: string | Ast;
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
ExpressionRenderer: ExpressionRenderer;
|
ExpressionRenderer: ExpressionRenderer;
|
||||||
previewExpression?: string;
|
selected: boolean;
|
||||||
|
showTitleAsLabel?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [expressionError, setExpressionError] = useState<boolean>(false);
|
const [expressionError, setExpressionError] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExpressionError(false);
|
setExpressionError(false);
|
||||||
}, [previewExpression]);
|
}, [preview.expression]);
|
||||||
|
|
||||||
const clickHandler = () => {
|
|
||||||
switchToSuggestion(frame, dispatch, suggestion);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiToolTip content={suggestion.title}>
|
<EuiToolTip content={preview.title}>
|
||||||
<EuiPanelFixed
|
<EuiPanelFixed
|
||||||
className="lnsSuggestionPanel__button"
|
className={classNames('lnsSuggestionPanel__button', {
|
||||||
|
'lnsSuggestionPanel__button-isSelected': selected,
|
||||||
|
})}
|
||||||
paddingSize="none"
|
paddingSize="none"
|
||||||
data-test-subj="lnsSuggestion"
|
data-test-subj="lnsSuggestion"
|
||||||
onClick={clickHandler}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
{expressionError ? (
|
{expressionError ? (
|
||||||
<div className="lnsSidebar__suggestionIcon">
|
<div className="lnsSidebar__suggestionIcon">
|
||||||
<EuiIconTip
|
<EuiIconTip
|
||||||
size="xxl"
|
size="xl"
|
||||||
color="danger"
|
color="danger"
|
||||||
type="cross"
|
type="alert"
|
||||||
aria-label={i18n.translate('xpack.lens.editorFrame.previewErrorLabel', {
|
aria-label={i18n.translate('xpack.lens.editorFrame.previewErrorLabel', {
|
||||||
defaultMessage: 'Preview rendering failed',
|
defaultMessage: 'Preview rendering failed',
|
||||||
})}
|
})}
|
||||||
|
@ -85,10 +99,12 @@ const SuggestionPreview = ({
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : previewExpression ? (
|
) : preview.expression ? (
|
||||||
<ExpressionRendererComponent
|
<ExpressionRendererComponent
|
||||||
className="lnsSuggestionChartWrapper"
|
className={classNames('lnsSuggestionChartWrapper', {
|
||||||
expression={previewExpression}
|
'lnsSuggestionChartWrapper--withLabel': showTitleAsLabel,
|
||||||
|
})}
|
||||||
|
expression={preview.expression}
|
||||||
onRenderFailure={(e: unknown) => {
|
onRenderFailure={(e: unknown) => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(`Failed to render preview: `, e);
|
console.error(`Failed to render preview: `, e);
|
||||||
|
@ -96,18 +112,23 @@ const SuggestionPreview = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="lnsSidebar__suggestionIcon">
|
<span className="lnsSidebar__suggestionIcon">
|
||||||
<EuiIcon size="xxl" type={suggestion.previewIcon} />
|
<EuiIcon size="xxl" type={preview.icon} />
|
||||||
</div>
|
</span>
|
||||||
|
)}
|
||||||
|
{showTitleAsLabel && (
|
||||||
|
<span className="lnsSuggestionPanel__buttonLabel">{preview.title}</span>
|
||||||
)}
|
)}
|
||||||
</EuiPanelFixed>
|
</EuiPanelFixed>
|
||||||
</EuiToolTip>
|
</EuiToolTip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SuggestionPanel = debouncedComponent(InnerSuggestionPanel, 2000);
|
// TODO this little debounce value is just here to showcase the feature better,
|
||||||
|
// will be fixed in suggestion performance PR
|
||||||
|
export const SuggestionPanel = debouncedComponent(InnerSuggestionPanel, 200);
|
||||||
|
|
||||||
function InnerSuggestionPanel({
|
export function InnerSuggestionPanel({
|
||||||
activeDatasourceId,
|
activeDatasourceId,
|
||||||
datasourceMap,
|
datasourceMap,
|
||||||
datasourceStates,
|
datasourceStates,
|
||||||
|
@ -117,70 +138,243 @@ function InnerSuggestionPanel({
|
||||||
dispatch,
|
dispatch,
|
||||||
frame,
|
frame,
|
||||||
ExpressionRenderer: ExpressionRendererComponent,
|
ExpressionRenderer: ExpressionRendererComponent,
|
||||||
|
stagedPreview,
|
||||||
}: SuggestionPanelProps) {
|
}: SuggestionPanelProps) {
|
||||||
|
const currentDatasourceStates = stagedPreview ? stagedPreview.datasourceStates : datasourceStates;
|
||||||
|
const currentVisualizationState = stagedPreview
|
||||||
|
? stagedPreview.visualization.state
|
||||||
|
: visualizationState;
|
||||||
|
const currentVisualizationId = stagedPreview
|
||||||
|
? stagedPreview.visualization.activeId
|
||||||
|
: activeVisualizationId;
|
||||||
|
|
||||||
|
const { suggestions, currentStateExpression } = useMemo(() => {
|
||||||
|
const newSuggestions = getSuggestions({
|
||||||
|
datasourceMap,
|
||||||
|
datasourceStates: currentDatasourceStates,
|
||||||
|
visualizationMap,
|
||||||
|
activeVisualizationId: currentVisualizationId,
|
||||||
|
visualizationState: currentVisualizationState,
|
||||||
|
})
|
||||||
|
.map(suggestion => ({
|
||||||
|
...suggestion,
|
||||||
|
previewExpression: preparePreviewExpression(
|
||||||
|
suggestion,
|
||||||
|
visualizationMap[suggestion.visualizationId],
|
||||||
|
datasourceMap,
|
||||||
|
currentDatasourceStates,
|
||||||
|
frame
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(suggestion => !suggestion.hide)
|
||||||
|
.slice(0, MAX_SUGGESTIONS_DISPLAYED);
|
||||||
|
|
||||||
|
const newStateExpression =
|
||||||
|
currentVisualizationState && currentVisualizationId
|
||||||
|
? preparePreviewExpression(
|
||||||
|
{ visualizationState: currentVisualizationState },
|
||||||
|
visualizationMap[currentVisualizationId],
|
||||||
|
datasourceMap,
|
||||||
|
currentDatasourceStates,
|
||||||
|
frame
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return { suggestions: newSuggestions, currentStateExpression: newStateExpression };
|
||||||
|
}, [
|
||||||
|
currentDatasourceStates,
|
||||||
|
currentVisualizationState,
|
||||||
|
currentVisualizationId,
|
||||||
|
datasourceMap,
|
||||||
|
visualizationMap,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState<number>(-1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if the staged preview is overwritten by a suggestion,
|
||||||
|
// reset the selected index to "current visualization" because
|
||||||
|
// we are not in transient suggestion state anymore
|
||||||
|
if (!stagedPreview && lastSelectedSuggestion !== -1) {
|
||||||
|
setLastSelectedSuggestion(-1);
|
||||||
|
}
|
||||||
|
}, [stagedPreview]);
|
||||||
|
|
||||||
if (!activeDatasourceId) {
|
if (!activeDatasourceId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestions = getSuggestions({
|
|
||||||
datasourceMap,
|
|
||||||
datasourceStates,
|
|
||||||
visualizationMap,
|
|
||||||
activeVisualizationId,
|
|
||||||
visualizationState,
|
|
||||||
})
|
|
||||||
.filter(suggestion => !suggestion.hide)
|
|
||||||
.slice(0, MAX_SUGGESTIONS_DISPLAYED);
|
|
||||||
|
|
||||||
if (suggestions.length === 0) {
|
if (suggestions.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rollbackToCurrentVisualization() {
|
||||||
|
if (lastSelectedSuggestion !== -1) {
|
||||||
|
setLastSelectedSuggestion(-1);
|
||||||
|
dispatch({
|
||||||
|
type: 'ROLLBACK_SUGGESTION',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressionContext = {
|
||||||
|
query: frame.query,
|
||||||
|
timeRange: {
|
||||||
|
from: frame.dateRange.fromDate,
|
||||||
|
to: frame.dateRange.toDate,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lnsSuggestionsPanel">
|
<div className="lnsSuggestionsPanel">
|
||||||
<EuiTitle className="lnsSuggestionsPanel__title" size="xxs">
|
<EuiFlexGroup alignItems="center">
|
||||||
<h3>
|
<EuiFlexItem>
|
||||||
<FormattedMessage
|
<EuiTitle className="lnsSuggestionsPanel__title" size="xxs">
|
||||||
id="xpack.lens.editorFrame.suggestionPanelTitle"
|
<h3>
|
||||||
defaultMessage="Suggestions"
|
<FormattedMessage
|
||||||
/>
|
id="xpack.lens.editorFrame.suggestionPanelTitle"
|
||||||
</h3>
|
defaultMessage="Suggestions"
|
||||||
</EuiTitle>
|
/>
|
||||||
|
</h3>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiFlexItem>
|
||||||
|
{stagedPreview && (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonEmpty
|
||||||
|
data-test-subj="lensSubmitSuggestion"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SUBMIT_SUGGESTION',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.translate('xpack.lens.sugegstion.confirmSuggestionLabel', {
|
||||||
|
defaultMessage: 'Confirm and reload suggestions',
|
||||||
|
})}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
|
||||||
<div className="lnsSuggestionsPanel__suggestions">
|
<div className="lnsSuggestionsPanel__suggestions">
|
||||||
{suggestions.map((suggestion: Suggestion) => (
|
{currentVisualizationId && (
|
||||||
<SuggestionPreview
|
<SuggestionPreview
|
||||||
suggestion={suggestion}
|
preview={{
|
||||||
dispatch={dispatch}
|
expression: currentStateExpression
|
||||||
frame={frame}
|
? prependKibanaContext(currentStateExpression, expressionContext)
|
||||||
|
: undefined,
|
||||||
|
icon:
|
||||||
|
visualizationMap[currentVisualizationId].getDescription(currentVisualizationState)
|
||||||
|
.icon || 'empty',
|
||||||
|
title: i18n.translate('xpack.lens.suggestions.currentVisLabel', {
|
||||||
|
defaultMessage: 'Current',
|
||||||
|
}),
|
||||||
|
}}
|
||||||
ExpressionRenderer={ExpressionRendererComponent}
|
ExpressionRenderer={ExpressionRendererComponent}
|
||||||
previewExpression={
|
onSelect={rollbackToCurrentVisualization}
|
||||||
suggestion.previewExpression
|
selected={lastSelectedSuggestion === -1}
|
||||||
? preparePreviewExpression(
|
showTitleAsLabel
|
||||||
suggestion.previewExpression,
|
|
||||||
datasourceMap,
|
|
||||||
datasourceStates,
|
|
||||||
frame,
|
|
||||||
suggestion.datasourceId,
|
|
||||||
suggestion.datasourceState
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
key={`${suggestion.visualizationId}-${suggestion.title}`}
|
|
||||||
/>
|
/>
|
||||||
))}
|
)}
|
||||||
|
{suggestions.map((suggestion, index) => {
|
||||||
|
return (
|
||||||
|
<SuggestionPreview
|
||||||
|
preview={{
|
||||||
|
expression: suggestion.previewExpression
|
||||||
|
? prependKibanaContext(suggestion.previewExpression, expressionContext)
|
||||||
|
: undefined,
|
||||||
|
icon: suggestion.previewIcon,
|
||||||
|
title: suggestion.title,
|
||||||
|
}}
|
||||||
|
ExpressionRenderer={ExpressionRendererComponent}
|
||||||
|
key={index}
|
||||||
|
onSelect={() => {
|
||||||
|
if (lastSelectedSuggestion === index) {
|
||||||
|
rollbackToCurrentVisualization();
|
||||||
|
} else {
|
||||||
|
setLastSelectedSuggestion(index);
|
||||||
|
switchToSuggestion(frame, dispatch, suggestion);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
selected={index === lastSelectedSuggestion}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VisualizableState {
|
||||||
|
visualizationState: unknown;
|
||||||
|
datasourceState?: unknown;
|
||||||
|
datasourceId?: string;
|
||||||
|
keptLayerIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviewExpression(
|
||||||
|
visualizableState: VisualizableState,
|
||||||
|
visualization: Visualization,
|
||||||
|
datasources: Record<string, Datasource>,
|
||||||
|
frame: FramePublicAPI
|
||||||
|
) {
|
||||||
|
if (!visualization.toPreviewExpression) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestionFrameApi: FramePublicAPI = {
|
||||||
|
...frame,
|
||||||
|
datasourceLayers: { ...frame.datasourceLayers },
|
||||||
|
};
|
||||||
|
|
||||||
|
// use current frame api and patch apis for changed datasource layers
|
||||||
|
if (
|
||||||
|
visualizableState.keptLayerIds &&
|
||||||
|
visualizableState.datasourceId &&
|
||||||
|
visualizableState.datasourceState
|
||||||
|
) {
|
||||||
|
const datasource = datasources[visualizableState.datasourceId];
|
||||||
|
const datasourceState = visualizableState.datasourceState;
|
||||||
|
const updatedLayerApis: Record<string, DatasourcePublicAPI> = _.pick(
|
||||||
|
frame.datasourceLayers,
|
||||||
|
visualizableState.keptLayerIds
|
||||||
|
);
|
||||||
|
const changedLayers = datasource.getLayers(visualizableState.datasourceState);
|
||||||
|
changedLayers.forEach(layerId => {
|
||||||
|
if (updatedLayerApis[layerId]) {
|
||||||
|
updatedLayerApis[layerId] = datasource.getPublicAPI(datasourceState, () => {}, layerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return visualization.toPreviewExpression(
|
||||||
|
visualizableState.visualizationState,
|
||||||
|
suggestionFrameApi
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function preparePreviewExpression(
|
function preparePreviewExpression(
|
||||||
expression: string | Ast,
|
visualizableState: VisualizableState,
|
||||||
|
visualization: Visualization,
|
||||||
datasourceMap: Record<string, Datasource<unknown, unknown>>,
|
datasourceMap: Record<string, Datasource<unknown, unknown>>,
|
||||||
datasourceStates: Record<string, { isLoading: boolean; state: unknown }>,
|
datasourceStates: Record<string, { isLoading: boolean; state: unknown }>,
|
||||||
framePublicAPI: FramePublicAPI,
|
framePublicAPI: FramePublicAPI
|
||||||
suggestionDatasourceId?: string,
|
|
||||||
suggestionDatasourceState?: unknown
|
|
||||||
) {
|
) {
|
||||||
|
const suggestionDatasourceId = visualizableState.datasourceId;
|
||||||
|
const suggestionDatasourceState = visualizableState.datasourceState;
|
||||||
|
|
||||||
|
const expression = getPreviewExpression(
|
||||||
|
visualizableState,
|
||||||
|
visualization,
|
||||||
|
datasourceMap,
|
||||||
|
framePublicAPI
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!expression) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const expressionWithDatasource = prependDatasourceExpression(
|
const expressionWithDatasource = prependDatasourceExpression(
|
||||||
expression,
|
expression,
|
||||||
datasourceMap,
|
datasourceMap,
|
||||||
|
@ -195,15 +389,5 @@ function preparePreviewExpression(
|
||||||
: datasourceStates
|
: datasourceStates
|
||||||
);
|
);
|
||||||
|
|
||||||
const expressionContext = {
|
return expressionWithDatasource;
|
||||||
query: framePublicAPI.query,
|
|
||||||
timeRange: {
|
|
||||||
from: framePublicAPI.dateRange.fromDate,
|
|
||||||
to: framePublicAPI.dateRange.toDate,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return expressionWithDatasource
|
|
||||||
? toExpression(prependKibanaContext(expressionWithDatasource, expressionContext))
|
|
||||||
: undefined;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,12 @@ export function InnerWorkspacePanel({
|
||||||
|
|
||||||
function onDrop() {
|
function onDrop() {
|
||||||
if (suggestionForDraggedField) {
|
if (suggestionForDraggedField) {
|
||||||
switchToSuggestion(framePublicAPI, dispatch, suggestionForDraggedField);
|
switchToSuggestion(
|
||||||
|
framePublicAPI,
|
||||||
|
dispatch,
|
||||||
|
suggestionForDraggedField,
|
||||||
|
'SWITCH_VISUALIZATION'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ export function createMockVisualization(): jest.Mocked<Visualization> {
|
||||||
initialize: jest.fn((_frame, _state?) => ({})),
|
initialize: jest.fn((_frame, _state?) => ({})),
|
||||||
renderConfigPanel: jest.fn(),
|
renderConfigPanel: jest.fn(),
|
||||||
toExpression: jest.fn((_state, _frame) => null),
|
toExpression: jest.fn((_state, _frame) => null),
|
||||||
|
toPreviewExpression: jest.fn((_state, _frame) => null),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -674,7 +674,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
||||||
expect(suggestions).toHaveLength(0);
|
expect(suggestions).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('appends a terms column after the last existing bucket column on string field', () => {
|
it('prepends a terms column on string field', () => {
|
||||||
const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, {
|
const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, {
|
||||||
field: { name: 'dest', type: 'string', aggregatable: true, searchable: true },
|
field: { name: 'dest', type: 'string', aggregatable: true, searchable: true },
|
||||||
indexPatternId: '1',
|
indexPatternId: '1',
|
||||||
|
@ -686,7 +686,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
||||||
layers: {
|
layers: {
|
||||||
previousLayer: initialState.layers.previousLayer,
|
previousLayer: initialState.layers.previousLayer,
|
||||||
currentLayer: expect.objectContaining({
|
currentLayer: expect.objectContaining({
|
||||||
columnOrder: ['col1', 'newId', 'col2'],
|
columnOrder: ['newId', 'col1', 'col2'],
|
||||||
columns: {
|
columns: {
|
||||||
...initialState.layers.currentLayer.columns,
|
...initialState.layers.currentLayer.columns,
|
||||||
newId: expect.objectContaining({
|
newId: expect.objectContaining({
|
||||||
|
|
|
@ -204,7 +204,7 @@ function addFieldAsBucketOperation(
|
||||||
};
|
};
|
||||||
let updatedColumnOrder: string[] = [];
|
let updatedColumnOrder: string[] = [];
|
||||||
if (applicableBucketOperation === 'terms') {
|
if (applicableBucketOperation === 'terms') {
|
||||||
updatedColumnOrder = [...buckets, newColumnId, ...metrics];
|
updatedColumnOrder = [newColumnId, ...buckets, ...metrics];
|
||||||
} else {
|
} else {
|
||||||
const oldDateHistogramColumn = layer.columnOrder.find(
|
const oldDateHistogramColumn = layer.columnOrder.find(
|
||||||
columnId => layer.columns[columnId].operationType === 'date_histogram'
|
columnId => layer.columns[columnId].operationType === 'date_histogram'
|
||||||
|
@ -392,16 +392,21 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId
|
||||||
state,
|
state,
|
||||||
layerId,
|
layerId,
|
||||||
updatedLayer,
|
updatedLayer,
|
||||||
label: i18n.translate('xpack.lens.indexpattern.suggestions.nestingChangeLabel', {
|
label: getNestedTitle([layer.columns[secondBucket], layer.columns[firstBucket]]),
|
||||||
defaultMessage: 'Nest within {operation}',
|
|
||||||
values: {
|
|
||||||
operation: layer.columns[secondBucket].label,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
changeType: 'extended',
|
changeType: 'extended',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNestedTitle([outerBucket, innerBucket]: IndexPatternColumn[]) {
|
||||||
|
return i18n.translate('xpack.lens.indexpattern.suggestions.nestingChangeLabel', {
|
||||||
|
defaultMessage: '{innerOperation} per each {outerOperation}',
|
||||||
|
values: {
|
||||||
|
innerOperation: innerBucket.label,
|
||||||
|
outerOperation: hasField(outerBucket) ? outerBucket.sourceField : outerBucket.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createAlternativeMetricSuggestions(
|
function createAlternativeMetricSuggestions(
|
||||||
indexPattern: IndexPattern,
|
indexPattern: IndexPattern,
|
||||||
layerId: string,
|
layerId: string,
|
||||||
|
|
|
@ -49,7 +49,7 @@ describe('AutoScale', () => {
|
||||||
)
|
)
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
<div
|
<div
|
||||||
style="display:flex;justify-content:center;align-items:center;max-width:100%;max-height:100%;overflow:hidden"
|
style="display:flex;justify-content:center;align-items:center;max-width:100%;max-height:100%;overflow:hidden;line-height:1.5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style="transform:scale(0)"
|
style="transform:scale(0)"
|
||||||
|
|
|
@ -76,6 +76,7 @@ export class AutoScale extends React.Component<Props, State> {
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
lineHeight: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -97,26 +97,6 @@ describe('metric_suggestions', () => {
|
||||||
expect(rest).toHaveLength(0);
|
expect(rest).toHaveLength(0);
|
||||||
expect(suggestion).toMatchInlineSnapshot(`
|
expect(suggestion).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"previewExpression": Object {
|
|
||||||
"chain": Array [
|
|
||||||
Object {
|
|
||||||
"arguments": Object {
|
|
||||||
"accessor": Array [
|
|
||||||
"bytes",
|
|
||||||
],
|
|
||||||
"mode": Array [
|
|
||||||
"reduced",
|
|
||||||
],
|
|
||||||
"title": Array [
|
|
||||||
"",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"function": "lens_metric_chart",
|
|
||||||
"type": "function",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"type": "expression",
|
|
||||||
},
|
|
||||||
"previewIcon": "visMetric",
|
"previewIcon": "visMetric",
|
||||||
"score": 0.5,
|
"score": 0.5,
|
||||||
"state": Object {
|
"state": Object {
|
||||||
|
|
|
@ -41,20 +41,6 @@ function getSuggestion(table: TableSuggestion): VisualizationSuggestion<State> {
|
||||||
title,
|
title,
|
||||||
score: 0.5,
|
score: 0.5,
|
||||||
previewIcon: 'visMetric',
|
previewIcon: 'visMetric',
|
||||||
previewExpression: {
|
|
||||||
type: 'expression',
|
|
||||||
chain: [
|
|
||||||
{
|
|
||||||
type: 'function',
|
|
||||||
function: 'lens_metric_chart',
|
|
||||||
arguments: {
|
|
||||||
title: [''],
|
|
||||||
accessor: [col.columnId],
|
|
||||||
mode: ['reduced'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
state: {
|
state: {
|
||||||
layerId: table.layerId,
|
layerId: table.layerId,
|
||||||
accessor: col.columnId,
|
accessor: col.columnId,
|
||||||
|
|
|
@ -38,11 +38,11 @@ describe('metric_visualization', () => {
|
||||||
|
|
||||||
expect(initialState.accessor).toBeDefined();
|
expect(initialState.accessor).toBeDefined();
|
||||||
expect(initialState).toMatchInlineSnapshot(`
|
expect(initialState).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"accessor": "test-id1",
|
"accessor": "test-id1",
|
||||||
"layerId": "l42",
|
"layerId": "l42",
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads from persisted state', () => {
|
it('loads from persisted state', () => {
|
||||||
|
@ -83,6 +83,9 @@ describe('metric_visualization', () => {
|
||||||
"accessor": Array [
|
"accessor": Array [
|
||||||
"a",
|
"a",
|
||||||
],
|
],
|
||||||
|
"mode": Array [
|
||||||
|
"full",
|
||||||
|
],
|
||||||
"title": Array [
|
"title": Array [
|
||||||
"shazm",
|
"shazm",
|
||||||
],
|
],
|
||||||
|
|
|
@ -8,12 +8,37 @@ import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { I18nProvider } from '@kbn/i18n/react';
|
import { I18nProvider } from '@kbn/i18n/react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { Ast } from '@kbn/interpreter/target/common';
|
||||||
import { getSuggestions } from './metric_suggestions';
|
import { getSuggestions } from './metric_suggestions';
|
||||||
import { MetricConfigPanel } from './metric_config_panel';
|
import { MetricConfigPanel } from './metric_config_panel';
|
||||||
import { Visualization } from '../types';
|
import { Visualization, FramePublicAPI } from '../types';
|
||||||
import { State, PersistableState } from './types';
|
import { State, PersistableState } from './types';
|
||||||
import { generateId } from '../id_generator';
|
import { generateId } from '../id_generator';
|
||||||
|
|
||||||
|
const toExpression = (
|
||||||
|
state: State,
|
||||||
|
frame: FramePublicAPI,
|
||||||
|
mode: 'reduced' | 'full' = 'full'
|
||||||
|
): Ast => {
|
||||||
|
const [datasource] = Object.values(frame.datasourceLayers);
|
||||||
|
const operation = datasource && datasource.getOperationForColumnId(state.accessor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'expression',
|
||||||
|
chain: [
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: 'lens_metric_chart',
|
||||||
|
arguments: {
|
||||||
|
title: [(operation && operation.label) || ''],
|
||||||
|
accessor: [state.accessor],
|
||||||
|
mode: [mode],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const metricVisualization: Visualization<State, PersistableState> = {
|
export const metricVisualization: Visualization<State, PersistableState> = {
|
||||||
id: 'lnsMetric',
|
id: 'lnsMetric',
|
||||||
|
|
||||||
|
@ -57,22 +82,7 @@ export const metricVisualization: Visualization<State, PersistableState> = {
|
||||||
domElement
|
domElement
|
||||||
),
|
),
|
||||||
|
|
||||||
toExpression(state, frame) {
|
toExpression,
|
||||||
const [datasource] = Object.values(frame.datasourceLayers);
|
toPreviewExpression: (state: State, frame: FramePublicAPI) =>
|
||||||
const operation = datasource && datasource.getOperationForColumnId(state.accessor);
|
toExpression(state, frame, 'reduced'),
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'expression',
|
|
||||||
chain: [
|
|
||||||
{
|
|
||||||
type: 'function',
|
|
||||||
function: 'lens_metric_chart',
|
|
||||||
arguments: {
|
|
||||||
title: [(operation && operation.label) || ''],
|
|
||||||
accessor: [state.accessor],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -270,11 +270,6 @@ export interface VisualizationSuggestion<T = unknown> {
|
||||||
* The new state of the visualization if this suggestion is applied.
|
* The new state of the visualization if this suggestion is applied.
|
||||||
*/
|
*/
|
||||||
state: T;
|
state: T;
|
||||||
/**
|
|
||||||
* The expression of the preview of the chart rendered if the suggestion is advertised to the user.
|
|
||||||
* If there is no expression provided, the preview icon is used.
|
|
||||||
*/
|
|
||||||
previewExpression?: Ast | string;
|
|
||||||
/**
|
/**
|
||||||
* An EUI icon type shown instead of the preview expression.
|
* An EUI icon type shown instead of the preview expression.
|
||||||
*/
|
*/
|
||||||
|
@ -323,6 +318,12 @@ export interface Visualization<T = unknown, P = unknown> {
|
||||||
|
|
||||||
toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null;
|
toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epression to render a preview version of the chart in very constraint space.
|
||||||
|
* If there is no expression provided, the preview icon is used.
|
||||||
|
*/
|
||||||
|
toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null;
|
||||||
|
|
||||||
// The frame will call this function on all visualizations when the table changes, or when
|
// The frame will call this function on all visualizations when the table changes, or when
|
||||||
// rendering additional ways of using the data
|
// rendering additional ways of using the data
|
||||||
getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>;
|
getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>;
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
.lnsChart {
|
.lnsChart {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lnsChart__empty {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
|
@ -76,6 +76,21 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null =>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function toPreviewExpression(state: State, frame: FramePublicAPI) {
|
||||||
|
return toExpression(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
layers: state.layers.map(layer => ({ ...layer, hide: true })),
|
||||||
|
// hide legend for preview
|
||||||
|
legend: {
|
||||||
|
...state.legend,
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
frame
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getScaleType(metadata: OperationMetadata | null, defaultScale: ScaleType) {
|
export function getScaleType(metadata: OperationMetadata | null, defaultScale: ScaleType) {
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
return defaultScale;
|
return defaultScale;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
} from '@elastic/charts';
|
} from '@elastic/charts';
|
||||||
import { I18nProvider } from '@kbn/i18n/react';
|
import { I18nProvider } from '@kbn/i18n/react';
|
||||||
import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types';
|
import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, IconType } from '@elastic/eui';
|
import { EuiIcon, EuiText, IconType } from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities';
|
import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities';
|
||||||
|
@ -129,19 +129,17 @@ export function XYChart({
|
||||||
if (Object.values(data.tables).every(table => table.rows.length === 0)) {
|
if (Object.values(data.tables).every(table => table.rows.length === 0)) {
|
||||||
const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar';
|
const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar';
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup gutterSize="s" direction="column" alignItems="center" justifyContent="center">
|
<EuiText className="lnsChart__empty" textAlign="center" color="subdued" size="xs">
|
||||||
<EuiFlexItem>
|
<p>
|
||||||
<EuiIcon type={icon} color="subdued" size="l" />
|
<EuiIcon type={icon} color="subdued" size="l" />
|
||||||
</EuiFlexItem>
|
</p>
|
||||||
<EuiFlexItem>
|
<p>
|
||||||
<EuiText color="subdued" size="xs">
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id="xpack.lens.xyVisualization.noDataLabel"
|
||||||
id="xpack.lens.xyVisualization.noDataLabel"
|
defaultMessage="No results found"
|
||||||
defaultMessage="No results found"
|
/>
|
||||||
/>
|
</p>
|
||||||
</EuiText>
|
</EuiText>
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { State, XYState } from './types';
|
import { State, XYState } from './types';
|
||||||
import { generateId } from '../id_generator';
|
import { generateId } from '../id_generator';
|
||||||
import { Ast } from '@kbn/interpreter/target/common';
|
|
||||||
|
|
||||||
jest.mock('../id_generator');
|
jest.mock('../id_generator');
|
||||||
|
|
||||||
|
@ -65,6 +64,10 @@ describe('xy_suggestions', () => {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
test('ignores invalid combinations', () => {
|
test('ignores invalid combinations', () => {
|
||||||
const unknownCol = () => {
|
const unknownCol = () => {
|
||||||
const str = strCol('foo');
|
const str = strCol('foo');
|
||||||
|
@ -114,17 +117,17 @@ describe('xy_suggestions', () => {
|
||||||
|
|
||||||
expect(rest).toHaveLength(0);
|
expect(rest).toHaveLength(0);
|
||||||
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"seriesType": "area",
|
"seriesType": "area_stacked",
|
||||||
"splitAccessor": "aaa",
|
"splitAccessor": "aaa",
|
||||||
"x": "date",
|
"x": "date",
|
||||||
"y": Array [
|
"y": Array [
|
||||||
"bytes",
|
"bytes",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not suggest multiple splits', () => {
|
test('does not suggest multiple splits', () => {
|
||||||
|
@ -158,18 +161,18 @@ describe('xy_suggestions', () => {
|
||||||
|
|
||||||
expect(rest).toHaveLength(0);
|
expect(rest).toHaveLength(0);
|
||||||
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"seriesType": "area",
|
"seriesType": "area_stacked",
|
||||||
"splitAccessor": "product",
|
"splitAccessor": "product",
|
||||||
"x": "date",
|
"x": "date",
|
||||||
"y": Array [
|
"y": Array [
|
||||||
"price",
|
"price",
|
||||||
"quantity",
|
"quantity",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uses datasource provided title if available', () => {
|
test('uses datasource provided title if available', () => {
|
||||||
|
@ -254,7 +257,7 @@ describe('xy_suggestions', () => {
|
||||||
state: currentState,
|
state: currentState,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rest).toHaveLength(0);
|
expect(rest).toHaveLength(1);
|
||||||
expect(suggestion.state).toEqual({
|
expect(suggestion.state).toEqual({
|
||||||
...currentState,
|
...currentState,
|
||||||
preferredSeriesType: 'area',
|
preferredSeriesType: 'area',
|
||||||
|
@ -290,7 +293,7 @@ describe('xy_suggestions', () => {
|
||||||
state: currentState,
|
state: currentState,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rest).toHaveLength(0);
|
expect(rest).toHaveLength(1);
|
||||||
expect(suggestion.state).toEqual({
|
expect(suggestion.state).toEqual({
|
||||||
...currentState,
|
...currentState,
|
||||||
isHorizontal: true,
|
isHorizontal: true,
|
||||||
|
@ -298,6 +301,85 @@ describe('xy_suggestions', () => {
|
||||||
expect(suggestion.title).toEqual('Flip');
|
expect(suggestion.title).toEqual('Flip');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('suggests a stacked chart for unchanged table and unstacked chart', () => {
|
||||||
|
(generateId as jest.Mock).mockReturnValueOnce('dummyCol');
|
||||||
|
(generateId as jest.Mock).mockReturnValueOnce('dummyCol');
|
||||||
|
const currentState: XYState = {
|
||||||
|
isHorizontal: false,
|
||||||
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
|
preferredSeriesType: 'bar',
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
accessors: ['price', 'quantity'],
|
||||||
|
layerId: 'first',
|
||||||
|
seriesType: 'bar',
|
||||||
|
splitAccessor: 'dummyCol',
|
||||||
|
xAccessor: 'product',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const suggestion = getSuggestions({
|
||||||
|
table: {
|
||||||
|
isMultiRow: true,
|
||||||
|
columns: [numCol('price'), numCol('quantity'), strCol('product')],
|
||||||
|
layerId: 'first',
|
||||||
|
changeType: 'unchanged',
|
||||||
|
},
|
||||||
|
state: currentState,
|
||||||
|
})[1];
|
||||||
|
|
||||||
|
expect(suggestion.state).toEqual({
|
||||||
|
...currentState,
|
||||||
|
preferredSeriesType: 'bar_stacked',
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
...currentState.layers[0],
|
||||||
|
seriesType: 'bar_stacked',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(suggestion.title).toEqual('Stacked');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps column to dimension mappings on extended tables', () => {
|
||||||
|
(generateId as jest.Mock).mockReturnValueOnce('dummyCol');
|
||||||
|
const currentState: XYState = {
|
||||||
|
isHorizontal: false,
|
||||||
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
|
preferredSeriesType: 'bar',
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
accessors: ['price', 'quantity'],
|
||||||
|
layerId: 'first',
|
||||||
|
seriesType: 'bar',
|
||||||
|
splitAccessor: 'dummyCol',
|
||||||
|
xAccessor: 'product',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const [suggestion, ...rest] = getSuggestions({
|
||||||
|
table: {
|
||||||
|
isMultiRow: true,
|
||||||
|
columns: [numCol('price'), numCol('quantity'), strCol('product'), strCol('category')],
|
||||||
|
layerId: 'first',
|
||||||
|
changeType: 'extended',
|
||||||
|
},
|
||||||
|
state: currentState,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rest).toHaveLength(0);
|
||||||
|
expect(suggestion.state).toEqual({
|
||||||
|
...currentState,
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
...currentState.layers[0],
|
||||||
|
xAccessor: 'product',
|
||||||
|
splitAccessor: 'category',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('handles two numeric values', () => {
|
test('handles two numeric values', () => {
|
||||||
(generateId as jest.Mock).mockReturnValueOnce('ddd');
|
(generateId as jest.Mock).mockReturnValueOnce('ddd');
|
||||||
const [suggestion] = getSuggestions({
|
const [suggestion] = getSuggestions({
|
||||||
|
@ -310,17 +392,17 @@ describe('xy_suggestions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"seriesType": "bar",
|
"seriesType": "bar_stacked",
|
||||||
"splitAccessor": "ddd",
|
"splitAccessor": "ddd",
|
||||||
"x": "quantity",
|
"x": "quantity",
|
||||||
"y": Array [
|
"y": Array [
|
||||||
"price",
|
"price",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles unbucketed suggestions', () => {
|
test('handles unbucketed suggestions', () => {
|
||||||
|
@ -345,36 +427,16 @@ describe('xy_suggestions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"seriesType": "bar",
|
"seriesType": "bar_stacked",
|
||||||
"splitAccessor": "eee",
|
"splitAccessor": "eee",
|
||||||
"x": "mybool",
|
"x": "mybool",
|
||||||
"y": Array [
|
"y": Array [
|
||||||
"num votes",
|
"num votes",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
|
||||||
|
|
||||||
test('adds a preview expression with disabled axes and legend', () => {
|
|
||||||
const [suggestion] = getSuggestions({
|
|
||||||
table: {
|
|
||||||
isMultiRow: true,
|
|
||||||
columns: [numCol('bytes'), dateCol('date')],
|
|
||||||
layerId: 'first',
|
|
||||||
changeType: 'unchanged',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const expression = suggestion.previewExpression! as Ast;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
(expression.chain[0].arguments.legend[0] as Ast).chain[0].arguments.isVisible[0]
|
|
||||||
).toBeFalsy();
|
|
||||||
expect(
|
|
||||||
(expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.hide[0]
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,12 +13,10 @@ import {
|
||||||
VisualizationSuggestion,
|
VisualizationSuggestion,
|
||||||
TableSuggestionColumn,
|
TableSuggestionColumn,
|
||||||
TableSuggestion,
|
TableSuggestion,
|
||||||
OperationMetadata,
|
|
||||||
TableChangeType,
|
TableChangeType,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { State, SeriesType, XYState } from './types';
|
import { State, SeriesType, XYState } from './types';
|
||||||
import { generateId } from '../id_generator';
|
import { generateId } from '../id_generator';
|
||||||
import { buildExpression } from './to_expression';
|
|
||||||
|
|
||||||
const columnSortOrder = {
|
const columnSortOrder = {
|
||||||
date: 0,
|
date: 0,
|
||||||
|
@ -63,27 +61,24 @@ export function getSuggestions({
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestion = getSuggestionForColumns(table, state);
|
const suggestions = getSuggestionForColumns(table, state);
|
||||||
|
|
||||||
if (suggestion) {
|
if (suggestions && suggestions instanceof Array) {
|
||||||
return [suggestion];
|
return suggestions;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return suggestions ? [suggestions] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSuggestionForColumns(
|
function getSuggestionForColumns(
|
||||||
table: TableSuggestion,
|
table: TableSuggestion,
|
||||||
currentState?: State
|
currentState?: State
|
||||||
): VisualizationSuggestion<State> | undefined {
|
): VisualizationSuggestion<State> | Array<VisualizationSuggestion<State>> | undefined {
|
||||||
const [buckets, values] = partition(
|
const [buckets, values] = partition(table.columns, col => col.operation.isBucketed);
|
||||||
prioritizeColumns(table.columns),
|
|
||||||
col => col.operation.isBucketed
|
|
||||||
);
|
|
||||||
|
|
||||||
if (buckets.length === 1 || buckets.length === 2) {
|
if (buckets.length === 1 || buckets.length === 2) {
|
||||||
const [x, splitBy] = buckets;
|
const [x, splitBy] = getBucketMappings(table, currentState);
|
||||||
return getSuggestion(
|
return getSuggestionsForLayer(
|
||||||
table.layerId,
|
table.layerId,
|
||||||
table.changeType,
|
table.changeType,
|
||||||
x,
|
x,
|
||||||
|
@ -93,8 +88,8 @@ function getSuggestionForColumns(
|
||||||
table.label
|
table.label
|
||||||
);
|
);
|
||||||
} else if (buckets.length === 0) {
|
} else if (buckets.length === 0) {
|
||||||
const [x, ...yValues] = values;
|
const [x, ...yValues] = prioritizeColumns(values);
|
||||||
return getSuggestion(
|
return getSuggestionsForLayer(
|
||||||
table.layerId,
|
table.layerId,
|
||||||
table.changeType,
|
table.changeType,
|
||||||
x,
|
x,
|
||||||
|
@ -106,6 +101,40 @@ function getSuggestionForColumns(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBucketMappings(table: TableSuggestion, currentState?: State) {
|
||||||
|
const currentLayer =
|
||||||
|
currentState && currentState.layers.find(({ layerId }) => layerId === table.layerId);
|
||||||
|
|
||||||
|
const buckets = table.columns.filter(col => col.operation.isBucketed);
|
||||||
|
// reverse the buckets before prioritization to always use the most inner
|
||||||
|
// bucket of the highest-prioritized group as x value (don't use nested
|
||||||
|
// buckets as split series)
|
||||||
|
const prioritizedBuckets = prioritizeColumns(buckets.reverse());
|
||||||
|
|
||||||
|
if (!currentLayer || table.changeType === 'initial') {
|
||||||
|
return prioritizedBuckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if existing table is just modified, try to map buckets to the current dimensions
|
||||||
|
const currentXColumnIndex = prioritizedBuckets.findIndex(
|
||||||
|
({ columnId }) => columnId === currentLayer.xAccessor
|
||||||
|
);
|
||||||
|
if (currentXColumnIndex) {
|
||||||
|
const [x] = prioritizedBuckets.splice(currentXColumnIndex, 1);
|
||||||
|
prioritizedBuckets.unshift(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSplitColumnIndex = prioritizedBuckets.findIndex(
|
||||||
|
({ columnId }) => columnId === currentLayer.splitAccessor
|
||||||
|
);
|
||||||
|
if (currentSplitColumnIndex) {
|
||||||
|
const [splitBy] = prioritizedBuckets.splice(currentSplitColumnIndex, 1);
|
||||||
|
prioritizedBuckets.push(splitBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prioritizedBuckets;
|
||||||
|
}
|
||||||
|
|
||||||
// This shuffles columns around so that the left-most column defualts to:
|
// This shuffles columns around so that the left-most column defualts to:
|
||||||
// date, string, boolean, then number, in that priority. We then use this
|
// date, string, boolean, then number, in that priority. We then use this
|
||||||
// order to pluck out the x column, and the split / stack column.
|
// order to pluck out the x column, and the split / stack column.
|
||||||
|
@ -115,7 +144,7 @@ function prioritizeColumns(columns: TableSuggestionColumn[]) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSuggestion(
|
function getSuggestionsForLayer(
|
||||||
layerId: string,
|
layerId: string,
|
||||||
changeType: TableChangeType,
|
changeType: TableChangeType,
|
||||||
xValue: TableSuggestionColumn,
|
xValue: TableSuggestionColumn,
|
||||||
|
@ -123,7 +152,7 @@ function getSuggestion(
|
||||||
splitBy?: TableSuggestionColumn,
|
splitBy?: TableSuggestionColumn,
|
||||||
currentState?: State,
|
currentState?: State,
|
||||||
tableLabel?: string
|
tableLabel?: string
|
||||||
): VisualizationSuggestion<State> {
|
): VisualizationSuggestion<State> | Array<VisualizationSuggestion<State>> {
|
||||||
const title = getSuggestionTitle(yValues, xValue, tableLabel);
|
const title = getSuggestionTitle(yValues, xValue, tableLabel);
|
||||||
const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue, changeType);
|
const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue, changeType);
|
||||||
const isHorizontal = currentState ? currentState.isHorizontal : false;
|
const isHorizontal = currentState ? currentState.isHorizontal : false;
|
||||||
|
@ -146,30 +175,68 @@ function getSuggestion(
|
||||||
return buildSuggestion(options);
|
return buildSuggestion(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sameStateSuggestions: Array<VisualizationSuggestion<State>> = [];
|
||||||
|
|
||||||
// if current state is using the same data, suggest same chart with different presentational configuration
|
// if current state is using the same data, suggest same chart with different presentational configuration
|
||||||
|
|
||||||
if (xValue.operation.scale === 'ordinal') {
|
if (xValue.operation.scale === 'ordinal') {
|
||||||
// flip between horizontal/vertical for ordinal scales
|
// flip between horizontal/vertical for ordinal scales
|
||||||
return buildSuggestion({
|
sameStateSuggestions.push(
|
||||||
...options,
|
buildSuggestion({
|
||||||
title: i18n.translate('xpack.lens.xySuggestions.flipTitle', { defaultMessage: 'Flip' }),
|
...options,
|
||||||
isHorizontal: !options.isHorizontal,
|
title: i18n.translate('xpack.lens.xySuggestions.flipTitle', { defaultMessage: 'Flip' }),
|
||||||
});
|
isHorizontal: !options.isHorizontal,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// change chart type for interval or ratio scales on x axis
|
||||||
|
const newSeriesType = flipSeriesType(seriesType);
|
||||||
|
sameStateSuggestions.push(
|
||||||
|
buildSuggestion({
|
||||||
|
...options,
|
||||||
|
seriesType: newSeriesType,
|
||||||
|
title: newSeriesType.startsWith('area')
|
||||||
|
? i18n.translate('xpack.lens.xySuggestions.areaChartTitle', {
|
||||||
|
defaultMessage: 'Area chart',
|
||||||
|
})
|
||||||
|
: i18n.translate('xpack.lens.xySuggestions.barChartTitle', {
|
||||||
|
defaultMessage: 'Bar chart',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// change chart type for interval or ratio scales on x axis
|
// flip between stacked/unstacked
|
||||||
const newSeriesType = flipSeriesType(seriesType);
|
sameStateSuggestions.push(
|
||||||
return buildSuggestion({
|
buildSuggestion({
|
||||||
...options,
|
...options,
|
||||||
seriesType: newSeriesType,
|
seriesType: toggleStackSeriesType(seriesType),
|
||||||
title: newSeriesType.startsWith('area')
|
title: seriesType.endsWith('stacked')
|
||||||
? i18n.translate('xpack.lens.xySuggestions.areaChartTitle', {
|
? i18n.translate('xpack.lens.xySuggestions.unstackedChartTitle', {
|
||||||
defaultMessage: 'Area chart',
|
defaultMessage: 'Unstacked',
|
||||||
})
|
})
|
||||||
: i18n.translate('xpack.lens.xySuggestions.barChartTitle', {
|
: i18n.translate('xpack.lens.xySuggestions.stackedChartTitle', {
|
||||||
defaultMessage: 'Bar chart',
|
defaultMessage: 'Stacked',
|
||||||
}),
|
}),
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return sameStateSuggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStackSeriesType(oldSeriesType: SeriesType) {
|
||||||
|
switch (oldSeriesType) {
|
||||||
|
case 'area':
|
||||||
|
return 'area_stacked';
|
||||||
|
case 'area_stacked':
|
||||||
|
return 'area';
|
||||||
|
case 'bar':
|
||||||
|
return 'bar_stacked';
|
||||||
|
case 'bar_stacked':
|
||||||
|
return 'bar';
|
||||||
|
default:
|
||||||
|
return oldSeriesType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flipSeriesType(oldSeriesType: SeriesType) {
|
function flipSeriesType(oldSeriesType: SeriesType) {
|
||||||
|
@ -193,7 +260,7 @@ function getSeriesType(
|
||||||
xValue: TableSuggestionColumn,
|
xValue: TableSuggestionColumn,
|
||||||
changeType: TableChangeType
|
changeType: TableChangeType
|
||||||
): SeriesType {
|
): SeriesType {
|
||||||
const defaultType = xValue.operation.dataType === 'date' ? 'area' : 'bar';
|
const defaultType = xValue.operation.dataType === 'date' ? 'area_stacked' : 'bar_stacked';
|
||||||
if (changeType === 'initial') {
|
if (changeType === 'initial') {
|
||||||
return defaultType;
|
return defaultType;
|
||||||
} else {
|
} else {
|
||||||
|
@ -258,7 +325,7 @@ function buildSuggestion({
|
||||||
xValue: TableSuggestionColumn;
|
xValue: TableSuggestionColumn;
|
||||||
splitBy: TableSuggestionColumn | undefined;
|
splitBy: TableSuggestionColumn | undefined;
|
||||||
layerId: string;
|
layerId: string;
|
||||||
changeType: string;
|
changeType: TableChangeType;
|
||||||
}) {
|
}) {
|
||||||
const newLayer = {
|
const newLayer = {
|
||||||
...(getExistingLayer(currentState, layerId) || {}),
|
...(getExistingLayer(currentState, layerId) || {}),
|
||||||
|
@ -281,54 +348,25 @@ function buildSuggestion({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
// chart with multiple y values and split series will have a score of 1, single y value and no split series reduce score
|
score: getScore(yValues, splitBy, changeType),
|
||||||
score: ((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3,
|
|
||||||
// don't advertise chart of same type but with less data
|
// don't advertise chart of same type but with less data
|
||||||
hide: currentState && changeType === 'reduced',
|
hide: currentState && changeType === 'reduced',
|
||||||
state,
|
state,
|
||||||
previewIcon: getIconForSeries(seriesType),
|
previewIcon: getIconForSeries(seriesType),
|
||||||
previewExpression: buildPreviewExpression(state, layerId, xValue, yValues, splitBy),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPreviewExpression(
|
function getScore(
|
||||||
state: XYState,
|
|
||||||
layerId: string,
|
|
||||||
xValue: TableSuggestionColumn,
|
|
||||||
yValues: TableSuggestionColumn[],
|
yValues: TableSuggestionColumn[],
|
||||||
splitBy: TableSuggestionColumn | undefined
|
splitBy: TableSuggestionColumn | undefined,
|
||||||
|
changeType: TableChangeType
|
||||||
) {
|
) {
|
||||||
return buildExpression(
|
// Unchanged table suggestions half the score because the underlying data doesn't change
|
||||||
{
|
const changeFactor = changeType === 'unchanged' ? 0.5 : 1;
|
||||||
...state,
|
// chart with multiple y values and split series will have a score of 1, single y value and no split series reduce score
|
||||||
// only show changed layer in preview and hide axes
|
return (((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3) * changeFactor;
|
||||||
layers: state.layers
|
|
||||||
.filter(layer => layer.layerId === layerId)
|
|
||||||
.map(layer => ({ ...layer, hide: true })),
|
|
||||||
// hide legend for preview
|
|
||||||
legend: {
|
|
||||||
...state.legend,
|
|
||||||
isVisible: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ [layerId]: collectColumnMetaData(xValue, yValues, splitBy) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExistingLayer(currentState: XYState | undefined, layerId: string) {
|
function getExistingLayer(currentState: XYState | undefined, layerId: string) {
|
||||||
return currentState && currentState.layers.find(layer => layer.layerId === layerId);
|
return currentState && currentState.layers.find(layer => layer.layerId === layerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectColumnMetaData(
|
|
||||||
xValue: TableSuggestionColumn,
|
|
||||||
yValues: TableSuggestionColumn[],
|
|
||||||
splitBy: TableSuggestionColumn | undefined
|
|
||||||
) {
|
|
||||||
const metadata: Record<string, OperationMetadata> = {};
|
|
||||||
[xValue, ...yValues, splitBy].forEach(col => {
|
|
||||||
if (col) {
|
|
||||||
metadata[col.columnId] = col.operation;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { getSuggestions } from './xy_suggestions';
|
||||||
import { XYConfigPanel } from './xy_config_panel';
|
import { XYConfigPanel } from './xy_config_panel';
|
||||||
import { Visualization } from '../types';
|
import { Visualization } from '../types';
|
||||||
import { State, PersistableState, SeriesType, visualizationTypes } from './types';
|
import { State, PersistableState, SeriesType, visualizationTypes } from './types';
|
||||||
import { toExpression } from './to_expression';
|
import { toExpression, toPreviewExpression } from './to_expression';
|
||||||
import { generateId } from '../id_generator';
|
import { generateId } from '../id_generator';
|
||||||
|
|
||||||
const defaultIcon = 'visBarVertical';
|
const defaultIcon = 'visBarVertical';
|
||||||
|
@ -104,4 +104,5 @@ export const xyVisualization: Visualization<State, PersistableState> = {
|
||||||
),
|
),
|
||||||
|
|
||||||
toExpression,
|
toExpression,
|
||||||
|
toPreviewExpression,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue