[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) => {
|
||||
setFlyoutOpen(false);
|
||||
|
||||
switchToSuggestion(props.framePublicAPI, props.dispatch, {
|
||||
...selection,
|
||||
visualizationState: selection.getVisualizationState(),
|
||||
});
|
||||
switchToSuggestion(
|
||||
props.framePublicAPI,
|
||||
props.dispatch,
|
||||
{
|
||||
...selection,
|
||||
visualizationState: selection.getVisualizationState(),
|
||||
},
|
||||
'SWITCH_VISUALIZATION'
|
||||
);
|
||||
};
|
||||
|
||||
function getSelection(
|
||||
|
|
|
@ -34,6 +34,7 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config
|
|||
props.dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
newState,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
},
|
||||
[props.dispatch]
|
||||
|
|
|
@ -32,6 +32,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater,
|
||||
datasourceId: props.activeDatasource!,
|
||||
clearStagedPreview: true,
|
||||
});
|
||||
},
|
||||
[props.dispatch, props.activeDatasource]
|
||||
|
|
|
@ -1153,7 +1153,14 @@ describe('editor_frame', () => {
|
|||
.find('[data-test-subj="lnsSuggestion"]')
|
||||
.find(EuiPanel)
|
||||
.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 () => {
|
||||
|
@ -1196,7 +1203,7 @@ describe('editor_frame', () => {
|
|||
act(() => {
|
||||
instance
|
||||
.find('[data-test-subj="lnsSuggestion"]')
|
||||
.first()
|
||||
.at(2)
|
||||
.simulate('click');
|
||||
});
|
||||
|
||||
|
|
|
@ -84,6 +84,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
datasourceId: id,
|
||||
updater: newState,
|
||||
clearStagedPreview: true,
|
||||
});
|
||||
},
|
||||
layer
|
||||
|
@ -265,6 +266,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
visualizationMap={props.visualizationMap}
|
||||
dispatch={dispatch}
|
||||
ExpressionRenderer={props.ExpressionRenderer}
|
||||
stagedPreview={state.stagedPreview}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,14 +8,18 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EditorFrameProps } from '../editor_frame';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
|
||||
export interface EditorFrameState {
|
||||
persistedId?: string;
|
||||
title: string;
|
||||
export interface PreviewState {
|
||||
visualization: {
|
||||
activeId: string | null;
|
||||
state: unknown;
|
||||
};
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>;
|
||||
}
|
||||
|
||||
export interface EditorFrameState extends PreviewState {
|
||||
persistedId?: string;
|
||||
title: string;
|
||||
stagedPreview?: PreviewState;
|
||||
activeDatasourceId: string | null;
|
||||
}
|
||||
|
||||
|
@ -32,10 +36,12 @@ export type Action =
|
|||
type: 'UPDATE_DATASOURCE_STATE';
|
||||
updater: unknown | ((prevState: unknown) => unknown);
|
||||
datasourceId: string;
|
||||
clearStagedPreview?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_VISUALIZATION_STATE';
|
||||
newState: unknown;
|
||||
clearStagedPreview?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_LAYER';
|
||||
|
@ -59,6 +65,19 @@ export type Action =
|
|||
datasourceState: unknown;
|
||||
datasourceId: string;
|
||||
}
|
||||
| {
|
||||
type: 'SELECT_SUGGESTION';
|
||||
newVisualizationId: string;
|
||||
initialState: unknown;
|
||||
datasourceState: unknown;
|
||||
datasourceId: string;
|
||||
}
|
||||
| {
|
||||
type: 'ROLLBACK_SUGGESTION';
|
||||
}
|
||||
| {
|
||||
type: 'SUBMIT_SUGGESTION';
|
||||
}
|
||||
| {
|
||||
type: 'SWITCH_DATASOURCE';
|
||||
newDatasourceId: string;
|
||||
|
@ -176,6 +195,41 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
|
|||
activeId: action.newVisualizationId,
|
||||
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':
|
||||
return {
|
||||
|
@ -190,6 +244,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
|
|||
isLoading: false,
|
||||
},
|
||||
},
|
||||
stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview,
|
||||
};
|
||||
case 'UPDATE_VISUALIZATION_STATE':
|
||||
if (!state.visualization.activeId) {
|
||||
|
@ -201,6 +256,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
|
|||
...state.visualization,
|
||||
state: action.newState,
|
||||
},
|
||||
stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -144,14 +144,15 @@ export function switchToSuggestion(
|
|||
suggestion: Pick<
|
||||
Suggestion,
|
||||
'visualizationId' | 'visualizationState' | 'datasourceState' | 'datasourceId' | 'keptLayerIds'
|
||||
>
|
||||
>,
|
||||
type: 'SWITCH_VISUALIZATION' | 'SELECT_SUGGESTION' = 'SELECT_SUGGESTION'
|
||||
) {
|
||||
const action: Action = {
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
type,
|
||||
newVisualizationId: suggestion.visualizationId,
|
||||
initialState: suggestion.visualizationState,
|
||||
datasourceState: suggestion.datasourceState,
|
||||
datasourceId: suggestion.datasourceId,
|
||||
datasourceId: suggestion.datasourceId!,
|
||||
};
|
||||
dispatch(action);
|
||||
const layerIds = Object.keys(frame.datasourceLayers).filter(id => {
|
||||
|
|
|
@ -10,25 +10,20 @@
|
|||
}
|
||||
|
||||
.lnsSuggestionsPanel__title {
|
||||
margin: $euiSizeS 0 $euiSizeXS;
|
||||
margin-left: $euiSizeXS / 2;
|
||||
}
|
||||
|
||||
.lnsSuggestionsPanel__suggestions {
|
||||
@include euiScrollBar;
|
||||
@include lnsOverflowShadowHorizontal;
|
||||
padding-top: $euiSizeXS;
|
||||
overflow-x: auto;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
|
||||
// Padding / negative margins to make room for overflow shadow
|
||||
padding-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
|
||||
|
@ -39,8 +34,13 @@ $lnsSuggestionWidth: 150px;
|
|||
flex: 0 0 auto;
|
||||
width: $lnsSuggestionWidth !important;
|
||||
height: $lnsSuggestionHeight;
|
||||
// Allows the scrollbar to stay flush to window
|
||||
margin-bottom: $euiSize;
|
||||
margin-right: $euiSizeS;
|
||||
margin-left: $euiSizeXS / 2;
|
||||
margin-bottom: $euiSizeXS / 2;
|
||||
}
|
||||
|
||||
.lnsSuggestionPanel__button-isSelected {
|
||||
@include euiFocusRing;
|
||||
}
|
||||
|
||||
.lnsSidebar__suggestionIcon {
|
||||
|
@ -58,3 +58,15 @@ $lnsSuggestionWidth: 150px;
|
|||
pointer-events: none;
|
||||
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,
|
||||
createMockFramePublicAPI,
|
||||
} from '../mocks';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
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 { fromExpression } from '@kbn/interpreter/target/common';
|
||||
import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
jest.mock('./suggestion_helpers');
|
||||
|
||||
const getSuggestionsMock = getSuggestions as jest.Mock;
|
||||
|
||||
describe('suggestion_panel', () => {
|
||||
let mockVisualization: Visualization;
|
||||
let mockDatasource: DatasourceMock;
|
||||
|
@ -40,7 +42,7 @@ describe('suggestion_panel', () => {
|
|||
expressionRendererMock = createExpressionRendererMock();
|
||||
dispatchMock = jest.fn();
|
||||
|
||||
(getSuggestions as jest.Mock).mockReturnValue([
|
||||
getSuggestionsMock.mockReturnValue([
|
||||
{
|
||||
datasourceState: {},
|
||||
previewIcon: 'empty',
|
||||
|
@ -75,6 +77,7 @@ describe('suggestion_panel', () => {
|
|||
activeVisualizationId: 'vis',
|
||||
visualizationMap: {
|
||||
vis: mockVisualization,
|
||||
vis2: createMockVisualization(),
|
||||
},
|
||||
visualizationState: {},
|
||||
dispatch: dispatchMock,
|
||||
|
@ -84,27 +87,131 @@ describe('suggestion_panel', () => {
|
|||
});
|
||||
|
||||
it('should list passed in suggestions', () => {
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="lnsSuggestion"]')
|
||||
.find(EuiPanel)
|
||||
.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', () => {
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="lnsSuggestion"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('button[data-test-subj="lnsSuggestion"]')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
type: 'SELECT_SUGGESTION',
|
||||
initialState: suggestion1State,
|
||||
})
|
||||
);
|
||||
|
@ -113,12 +220,29 @@ describe('suggestion_panel', () => {
|
|||
it('should remove unused layers if suggestion is clicked', () => {
|
||||
defaultProps.frame.datasourceLayers.a = 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
|
||||
.find('[data-test-subj="lnsSuggestion"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('button[data-test-subj="lnsSuggestion"]')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[data-test-subj="lensSubmitSuggestion"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
});
|
||||
|
||||
expect(defaultProps.frame.removeLayers).toHaveBeenCalledWith(['b']);
|
||||
});
|
||||
|
@ -141,18 +265,18 @@ describe('suggestion_panel', () => {
|
|||
visualizationState: suggestion2State,
|
||||
visualizationId: 'vis',
|
||||
title: 'Suggestion2',
|
||||
previewExpression: 'test | expression',
|
||||
},
|
||||
] as Suggestion[]);
|
||||
|
||||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce(undefined);
|
||||
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression');
|
||||
mockDatasource.toExpression.mockReturnValue('datasource_expression');
|
||||
|
||||
mount(<SuggestionPanel {...defaultProps} />);
|
||||
mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||
|
||||
expect(expressionRendererMock).toHaveBeenCalledTimes(1);
|
||||
const passedExpression = fromExpression(
|
||||
(expressionRendererMock as jest.Mock).mock.calls[0][0].expression
|
||||
);
|
||||
const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression;
|
||||
|
||||
expect(passedExpression).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
|
@ -163,6 +287,7 @@ describe('suggestion_panel', () => {
|
|||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"filters": Array [],
|
||||
"query": Array [
|
||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||
],
|
||||
|
@ -212,7 +337,7 @@ describe('suggestion_panel', () => {
|
|||
|
||||
it('should render render icon if there is no preview expression', () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
(getSuggestions as jest.Mock).mockReturnValue([
|
||||
getSuggestionsMock.mockReturnValue([
|
||||
{
|
||||
datasourceState: {},
|
||||
previewIcon: 'visTable',
|
||||
|
@ -232,9 +357,15 @@ describe('suggestion_panel', () => {
|
|||
},
|
||||
] 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');
|
||||
|
||||
const wrapper = mount(<SuggestionPanel {...defaultProps} />);
|
||||
const wrapper = mount(<InnerSuggestionPanel {...defaultProps} />);
|
||||
|
||||
expect(wrapper.find(EuiIcon)).toHaveLength(1);
|
||||
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.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import _ from 'lodash';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiIcon, EuiTitle, EuiPanel, EuiIconTip, EuiToolTip } from '@elastic/eui';
|
||||
import { toExpression, Ast } from '@kbn/interpreter/common';
|
||||
import {
|
||||
EuiIcon,
|
||||
EuiTitle,
|
||||
EuiPanel,
|
||||
EuiIconTip,
|
||||
EuiToolTip,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { Ast } from '@kbn/interpreter/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Action } from './state_management';
|
||||
import { Datasource, Visualization, FramePublicAPI } from '../../types';
|
||||
import { getSuggestions, Suggestion, switchToSuggestion } from './suggestion_helpers';
|
||||
import classNames from 'classnames';
|
||||
import { Action, PreviewState } from './state_management';
|
||||
import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
|
||||
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
|
||||
import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public';
|
||||
import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers';
|
||||
import { debouncedComponent } from '../../debounced_component';
|
||||
|
@ -38,45 +49,48 @@ export interface SuggestionPanelProps {
|
|||
dispatch: (action: Action) => void;
|
||||
ExpressionRenderer: ExpressionRenderer;
|
||||
frame: FramePublicAPI;
|
||||
stagedPreview?: PreviewState;
|
||||
}
|
||||
|
||||
const SuggestionPreview = ({
|
||||
suggestion,
|
||||
dispatch,
|
||||
frame,
|
||||
previewExpression,
|
||||
preview,
|
||||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
selected,
|
||||
onSelect,
|
||||
showTitleAsLabel,
|
||||
}: {
|
||||
suggestion: Suggestion;
|
||||
dispatch: (action: Action) => void;
|
||||
frame: FramePublicAPI;
|
||||
onSelect: () => void;
|
||||
preview: {
|
||||
expression?: string | Ast;
|
||||
icon: string;
|
||||
title: string;
|
||||
};
|
||||
ExpressionRenderer: ExpressionRenderer;
|
||||
previewExpression?: string;
|
||||
selected: boolean;
|
||||
showTitleAsLabel?: boolean;
|
||||
}) => {
|
||||
const [expressionError, setExpressionError] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setExpressionError(false);
|
||||
}, [previewExpression]);
|
||||
|
||||
const clickHandler = () => {
|
||||
switchToSuggestion(frame, dispatch, suggestion);
|
||||
};
|
||||
}, [preview.expression]);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={suggestion.title}>
|
||||
<EuiToolTip content={preview.title}>
|
||||
<EuiPanelFixed
|
||||
className="lnsSuggestionPanel__button"
|
||||
className={classNames('lnsSuggestionPanel__button', {
|
||||
'lnsSuggestionPanel__button-isSelected': selected,
|
||||
})}
|
||||
paddingSize="none"
|
||||
data-test-subj="lnsSuggestion"
|
||||
onClick={clickHandler}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{expressionError ? (
|
||||
<div className="lnsSidebar__suggestionIcon">
|
||||
<EuiIconTip
|
||||
size="xxl"
|
||||
size="xl"
|
||||
color="danger"
|
||||
type="cross"
|
||||
type="alert"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.previewErrorLabel', {
|
||||
defaultMessage: 'Preview rendering failed',
|
||||
})}
|
||||
|
@ -85,10 +99,12 @@ const SuggestionPreview = ({
|
|||
})}
|
||||
/>
|
||||
</div>
|
||||
) : previewExpression ? (
|
||||
) : preview.expression ? (
|
||||
<ExpressionRendererComponent
|
||||
className="lnsSuggestionChartWrapper"
|
||||
expression={previewExpression}
|
||||
className={classNames('lnsSuggestionChartWrapper', {
|
||||
'lnsSuggestionChartWrapper--withLabel': showTitleAsLabel,
|
||||
})}
|
||||
expression={preview.expression}
|
||||
onRenderFailure={(e: unknown) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to render preview: `, e);
|
||||
|
@ -96,18 +112,23 @@ const SuggestionPreview = ({
|
|||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="lnsSidebar__suggestionIcon">
|
||||
<EuiIcon size="xxl" type={suggestion.previewIcon} />
|
||||
</div>
|
||||
<span className="lnsSidebar__suggestionIcon">
|
||||
<EuiIcon size="xxl" type={preview.icon} />
|
||||
</span>
|
||||
)}
|
||||
{showTitleAsLabel && (
|
||||
<span className="lnsSuggestionPanel__buttonLabel">{preview.title}</span>
|
||||
)}
|
||||
</EuiPanelFixed>
|
||||
</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,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
|
@ -117,70 +138,243 @@ function InnerSuggestionPanel({
|
|||
dispatch,
|
||||
frame,
|
||||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
stagedPreview,
|
||||
}: 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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suggestions = getSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
visualizationMap,
|
||||
activeVisualizationId,
|
||||
visualizationState,
|
||||
})
|
||||
.filter(suggestion => !suggestion.hide)
|
||||
.slice(0, MAX_SUGGESTIONS_DISPLAYED);
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
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 (
|
||||
<div className="lnsSuggestionsPanel">
|
||||
<EuiTitle className="lnsSuggestionsPanel__title" size="xxs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.editorFrame.suggestionPanelTitle"
|
||||
defaultMessage="Suggestions"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle className="lnsSuggestionsPanel__title" size="xxs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.editorFrame.suggestionPanelTitle"
|
||||
defaultMessage="Suggestions"
|
||||
/>
|
||||
</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">
|
||||
{suggestions.map((suggestion: Suggestion) => (
|
||||
{currentVisualizationId && (
|
||||
<SuggestionPreview
|
||||
suggestion={suggestion}
|
||||
dispatch={dispatch}
|
||||
frame={frame}
|
||||
preview={{
|
||||
expression: currentStateExpression
|
||||
? prependKibanaContext(currentStateExpression, expressionContext)
|
||||
: undefined,
|
||||
icon:
|
||||
visualizationMap[currentVisualizationId].getDescription(currentVisualizationState)
|
||||
.icon || 'empty',
|
||||
title: i18n.translate('xpack.lens.suggestions.currentVisLabel', {
|
||||
defaultMessage: 'Current',
|
||||
}),
|
||||
}}
|
||||
ExpressionRenderer={ExpressionRendererComponent}
|
||||
previewExpression={
|
||||
suggestion.previewExpression
|
||||
? preparePreviewExpression(
|
||||
suggestion.previewExpression,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
frame,
|
||||
suggestion.datasourceId,
|
||||
suggestion.datasourceState
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
key={`${suggestion.visualizationId}-${suggestion.title}`}
|
||||
onSelect={rollbackToCurrentVisualization}
|
||||
selected={lastSelectedSuggestion === -1}
|
||||
showTitleAsLabel
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
expression: string | Ast,
|
||||
visualizableState: VisualizableState,
|
||||
visualization: Visualization,
|
||||
datasourceMap: Record<string, Datasource<unknown, unknown>>,
|
||||
datasourceStates: Record<string, { isLoading: boolean; state: unknown }>,
|
||||
framePublicAPI: FramePublicAPI,
|
||||
suggestionDatasourceId?: string,
|
||||
suggestionDatasourceState?: unknown
|
||||
framePublicAPI: FramePublicAPI
|
||||
) {
|
||||
const suggestionDatasourceId = visualizableState.datasourceId;
|
||||
const suggestionDatasourceState = visualizableState.datasourceState;
|
||||
|
||||
const expression = getPreviewExpression(
|
||||
visualizableState,
|
||||
visualization,
|
||||
datasourceMap,
|
||||
framePublicAPI
|
||||
);
|
||||
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expressionWithDatasource = prependDatasourceExpression(
|
||||
expression,
|
||||
datasourceMap,
|
||||
|
@ -195,15 +389,5 @@ function preparePreviewExpression(
|
|||
: datasourceStates
|
||||
);
|
||||
|
||||
const expressionContext = {
|
||||
query: framePublicAPI.query,
|
||||
timeRange: {
|
||||
from: framePublicAPI.dateRange.fromDate,
|
||||
to: framePublicAPI.dateRange.toDate,
|
||||
},
|
||||
};
|
||||
|
||||
return expressionWithDatasource
|
||||
? toExpression(prependKibanaContext(expressionWithDatasource, expressionContext))
|
||||
: undefined;
|
||||
return expressionWithDatasource;
|
||||
}
|
||||
|
|
|
@ -76,7 +76,12 @@ export function InnerWorkspacePanel({
|
|||
|
||||
function onDrop() {
|
||||
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?) => ({})),
|
||||
renderConfigPanel: jest.fn(),
|
||||
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);
|
||||
});
|
||||
|
||||
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, {
|
||||
field: { name: 'dest', type: 'string', aggregatable: true, searchable: true },
|
||||
indexPatternId: '1',
|
||||
|
@ -686,7 +686,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
layers: {
|
||||
previousLayer: initialState.layers.previousLayer,
|
||||
currentLayer: expect.objectContaining({
|
||||
columnOrder: ['col1', 'newId', 'col2'],
|
||||
columnOrder: ['newId', 'col1', 'col2'],
|
||||
columns: {
|
||||
...initialState.layers.currentLayer.columns,
|
||||
newId: expect.objectContaining({
|
||||
|
|
|
@ -204,7 +204,7 @@ function addFieldAsBucketOperation(
|
|||
};
|
||||
let updatedColumnOrder: string[] = [];
|
||||
if (applicableBucketOperation === 'terms') {
|
||||
updatedColumnOrder = [...buckets, newColumnId, ...metrics];
|
||||
updatedColumnOrder = [newColumnId, ...buckets, ...metrics];
|
||||
} else {
|
||||
const oldDateHistogramColumn = layer.columnOrder.find(
|
||||
columnId => layer.columns[columnId].operationType === 'date_histogram'
|
||||
|
@ -392,16 +392,21 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId
|
|||
state,
|
||||
layerId,
|
||||
updatedLayer,
|
||||
label: i18n.translate('xpack.lens.indexpattern.suggestions.nestingChangeLabel', {
|
||||
defaultMessage: 'Nest within {operation}',
|
||||
values: {
|
||||
operation: layer.columns[secondBucket].label,
|
||||
},
|
||||
}),
|
||||
label: getNestedTitle([layer.columns[secondBucket], layer.columns[firstBucket]]),
|
||||
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(
|
||||
indexPattern: IndexPattern,
|
||||
layerId: string,
|
||||
|
|
|
@ -49,7 +49,7 @@ describe('AutoScale', () => {
|
|||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
<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
|
||||
style="transform:scale(0)"
|
||||
|
|
|
@ -76,6 +76,7 @@ export class AutoScale extends React.Component<Props, State> {
|
|||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
overflow: 'hidden',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -97,26 +97,6 @@ describe('metric_suggestions', () => {
|
|||
expect(rest).toHaveLength(0);
|
||||
expect(suggestion).toMatchInlineSnapshot(`
|
||||
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",
|
||||
"score": 0.5,
|
||||
"state": Object {
|
||||
|
|
|
@ -41,20 +41,6 @@ function getSuggestion(table: TableSuggestion): VisualizationSuggestion<State> {
|
|||
title,
|
||||
score: 0.5,
|
||||
previewIcon: 'visMetric',
|
||||
previewExpression: {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'lens_metric_chart',
|
||||
arguments: {
|
||||
title: [''],
|
||||
accessor: [col.columnId],
|
||||
mode: ['reduced'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
state: {
|
||||
layerId: table.layerId,
|
||||
accessor: col.columnId,
|
||||
|
|
|
@ -38,11 +38,11 @@ describe('metric_visualization', () => {
|
|||
|
||||
expect(initialState.accessor).toBeDefined();
|
||||
expect(initialState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"accessor": "test-id1",
|
||||
"layerId": "l42",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"accessor": "test-id1",
|
||||
"layerId": "l42",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('loads from persisted state', () => {
|
||||
|
@ -83,6 +83,9 @@ describe('metric_visualization', () => {
|
|||
"accessor": Array [
|
||||
"a",
|
||||
],
|
||||
"mode": Array [
|
||||
"full",
|
||||
],
|
||||
"title": Array [
|
||||
"shazm",
|
||||
],
|
||||
|
|
|
@ -8,12 +8,37 @@ import React from 'react';
|
|||
import { render } from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Ast } from '@kbn/interpreter/target/common';
|
||||
import { getSuggestions } from './metric_suggestions';
|
||||
import { MetricConfigPanel } from './metric_config_panel';
|
||||
import { Visualization } from '../types';
|
||||
import { Visualization, FramePublicAPI } from '../types';
|
||||
import { State, PersistableState } from './types';
|
||||
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> = {
|
||||
id: 'lnsMetric',
|
||||
|
||||
|
@ -57,22 +82,7 @@ export const metricVisualization: Visualization<State, PersistableState> = {
|
|||
domElement
|
||||
),
|
||||
|
||||
toExpression(state, frame) {
|
||||
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],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
toExpression,
|
||||
toPreviewExpression: (state: State, frame: FramePublicAPI) =>
|
||||
toExpression(state, frame, 'reduced'),
|
||||
};
|
||||
|
|
|
@ -270,11 +270,6 @@ export interface VisualizationSuggestion<T = unknown> {
|
|||
* The new state of the visualization if this suggestion is applied.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -323,6 +318,12 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
|
||||
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
|
||||
// rendering additional ways of using the data
|
||||
getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>;
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
.lnsChart {
|
||||
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) {
|
||||
if (!metadata) {
|
||||
return defaultScale;
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '@elastic/charts';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
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 { i18n } from '@kbn/i18n';
|
||||
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)) {
|
||||
const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar';
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" direction="column" alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText className="lnsChart__empty" textAlign="center" color="subdued" size="xs">
|
||||
<p>
|
||||
<EuiIcon type={icon} color="subdued" size="l" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyVisualization.noDataLabel"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyVisualization.noDataLabel"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
} from '../types';
|
||||
import { State, XYState } from './types';
|
||||
import { generateId } from '../id_generator';
|
||||
import { Ast } from '@kbn/interpreter/target/common';
|
||||
|
||||
jest.mock('../id_generator');
|
||||
|
||||
|
@ -65,6 +64,10 @@ describe('xy_suggestions', () => {
|
|||
}));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('ignores invalid combinations', () => {
|
||||
const unknownCol = () => {
|
||||
const str = strCol('foo');
|
||||
|
@ -114,17 +117,17 @@ describe('xy_suggestions', () => {
|
|||
|
||||
expect(rest).toHaveLength(0);
|
||||
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"seriesType": "area",
|
||||
"splitAccessor": "aaa",
|
||||
"x": "date",
|
||||
"y": Array [
|
||||
"bytes",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"seriesType": "area_stacked",
|
||||
"splitAccessor": "aaa",
|
||||
"x": "date",
|
||||
"y": Array [
|
||||
"bytes",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not suggest multiple splits', () => {
|
||||
|
@ -158,18 +161,18 @@ describe('xy_suggestions', () => {
|
|||
|
||||
expect(rest).toHaveLength(0);
|
||||
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"seriesType": "area",
|
||||
"splitAccessor": "product",
|
||||
"x": "date",
|
||||
"y": Array [
|
||||
"price",
|
||||
"quantity",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"seriesType": "area_stacked",
|
||||
"splitAccessor": "product",
|
||||
"x": "date",
|
||||
"y": Array [
|
||||
"price",
|
||||
"quantity",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('uses datasource provided title if available', () => {
|
||||
|
@ -254,7 +257,7 @@ describe('xy_suggestions', () => {
|
|||
state: currentState,
|
||||
});
|
||||
|
||||
expect(rest).toHaveLength(0);
|
||||
expect(rest).toHaveLength(1);
|
||||
expect(suggestion.state).toEqual({
|
||||
...currentState,
|
||||
preferredSeriesType: 'area',
|
||||
|
@ -290,7 +293,7 @@ describe('xy_suggestions', () => {
|
|||
state: currentState,
|
||||
});
|
||||
|
||||
expect(rest).toHaveLength(0);
|
||||
expect(rest).toHaveLength(1);
|
||||
expect(suggestion.state).toEqual({
|
||||
...currentState,
|
||||
isHorizontal: true,
|
||||
|
@ -298,6 +301,85 @@ describe('xy_suggestions', () => {
|
|||
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', () => {
|
||||
(generateId as jest.Mock).mockReturnValueOnce('ddd');
|
||||
const [suggestion] = getSuggestions({
|
||||
|
@ -310,17 +392,17 @@ describe('xy_suggestions', () => {
|
|||
});
|
||||
|
||||
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"seriesType": "bar",
|
||||
"splitAccessor": "ddd",
|
||||
"x": "quantity",
|
||||
"y": Array [
|
||||
"price",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"seriesType": "bar_stacked",
|
||||
"splitAccessor": "ddd",
|
||||
"x": "quantity",
|
||||
"y": Array [
|
||||
"price",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('handles unbucketed suggestions', () => {
|
||||
|
@ -345,36 +427,16 @@ describe('xy_suggestions', () => {
|
|||
});
|
||||
|
||||
expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"seriesType": "bar",
|
||||
"splitAccessor": "eee",
|
||||
"x": "mybool",
|
||||
"y": Array [
|
||||
"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();
|
||||
Array [
|
||||
Object {
|
||||
"seriesType": "bar_stacked",
|
||||
"splitAccessor": "eee",
|
||||
"x": "mybool",
|
||||
"y": Array [
|
||||
"num votes",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,12 +13,10 @@ import {
|
|||
VisualizationSuggestion,
|
||||
TableSuggestionColumn,
|
||||
TableSuggestion,
|
||||
OperationMetadata,
|
||||
TableChangeType,
|
||||
} from '../types';
|
||||
import { State, SeriesType, XYState } from './types';
|
||||
import { generateId } from '../id_generator';
|
||||
import { buildExpression } from './to_expression';
|
||||
|
||||
const columnSortOrder = {
|
||||
date: 0,
|
||||
|
@ -63,27 +61,24 @@ export function getSuggestions({
|
|||
return [];
|
||||
}
|
||||
|
||||
const suggestion = getSuggestionForColumns(table, state);
|
||||
const suggestions = getSuggestionForColumns(table, state);
|
||||
|
||||
if (suggestion) {
|
||||
return [suggestion];
|
||||
if (suggestions && suggestions instanceof Array) {
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
return [];
|
||||
return suggestions ? [suggestions] : [];
|
||||
}
|
||||
|
||||
function getSuggestionForColumns(
|
||||
table: TableSuggestion,
|
||||
currentState?: State
|
||||
): VisualizationSuggestion<State> | undefined {
|
||||
const [buckets, values] = partition(
|
||||
prioritizeColumns(table.columns),
|
||||
col => col.operation.isBucketed
|
||||
);
|
||||
): VisualizationSuggestion<State> | Array<VisualizationSuggestion<State>> | undefined {
|
||||
const [buckets, values] = partition(table.columns, col => col.operation.isBucketed);
|
||||
|
||||
if (buckets.length === 1 || buckets.length === 2) {
|
||||
const [x, splitBy] = buckets;
|
||||
return getSuggestion(
|
||||
const [x, splitBy] = getBucketMappings(table, currentState);
|
||||
return getSuggestionsForLayer(
|
||||
table.layerId,
|
||||
table.changeType,
|
||||
x,
|
||||
|
@ -93,8 +88,8 @@ function getSuggestionForColumns(
|
|||
table.label
|
||||
);
|
||||
} else if (buckets.length === 0) {
|
||||
const [x, ...yValues] = values;
|
||||
return getSuggestion(
|
||||
const [x, ...yValues] = prioritizeColumns(values);
|
||||
return getSuggestionsForLayer(
|
||||
table.layerId,
|
||||
table.changeType,
|
||||
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:
|
||||
// date, string, boolean, then number, in that priority. We then use this
|
||||
// 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,
|
||||
changeType: TableChangeType,
|
||||
xValue: TableSuggestionColumn,
|
||||
|
@ -123,7 +152,7 @@ function getSuggestion(
|
|||
splitBy?: TableSuggestionColumn,
|
||||
currentState?: State,
|
||||
tableLabel?: string
|
||||
): VisualizationSuggestion<State> {
|
||||
): VisualizationSuggestion<State> | Array<VisualizationSuggestion<State>> {
|
||||
const title = getSuggestionTitle(yValues, xValue, tableLabel);
|
||||
const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue, changeType);
|
||||
const isHorizontal = currentState ? currentState.isHorizontal : false;
|
||||
|
@ -146,30 +175,68 @@ function getSuggestion(
|
|||
return buildSuggestion(options);
|
||||
}
|
||||
|
||||
const sameStateSuggestions: Array<VisualizationSuggestion<State>> = [];
|
||||
|
||||
// if current state is using the same data, suggest same chart with different presentational configuration
|
||||
|
||||
if (xValue.operation.scale === 'ordinal') {
|
||||
// flip between horizontal/vertical for ordinal scales
|
||||
return buildSuggestion({
|
||||
...options,
|
||||
title: i18n.translate('xpack.lens.xySuggestions.flipTitle', { defaultMessage: 'Flip' }),
|
||||
isHorizontal: !options.isHorizontal,
|
||||
});
|
||||
sameStateSuggestions.push(
|
||||
buildSuggestion({
|
||||
...options,
|
||||
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
|
||||
const newSeriesType = flipSeriesType(seriesType);
|
||||
return 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',
|
||||
}),
|
||||
});
|
||||
// flip between stacked/unstacked
|
||||
sameStateSuggestions.push(
|
||||
buildSuggestion({
|
||||
...options,
|
||||
seriesType: toggleStackSeriesType(seriesType),
|
||||
title: seriesType.endsWith('stacked')
|
||||
? i18n.translate('xpack.lens.xySuggestions.unstackedChartTitle', {
|
||||
defaultMessage: 'Unstacked',
|
||||
})
|
||||
: i18n.translate('xpack.lens.xySuggestions.stackedChartTitle', {
|
||||
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) {
|
||||
|
@ -193,7 +260,7 @@ function getSeriesType(
|
|||
xValue: TableSuggestionColumn,
|
||||
changeType: TableChangeType
|
||||
): SeriesType {
|
||||
const defaultType = xValue.operation.dataType === 'date' ? 'area' : 'bar';
|
||||
const defaultType = xValue.operation.dataType === 'date' ? 'area_stacked' : 'bar_stacked';
|
||||
if (changeType === 'initial') {
|
||||
return defaultType;
|
||||
} else {
|
||||
|
@ -258,7 +325,7 @@ function buildSuggestion({
|
|||
xValue: TableSuggestionColumn;
|
||||
splitBy: TableSuggestionColumn | undefined;
|
||||
layerId: string;
|
||||
changeType: string;
|
||||
changeType: TableChangeType;
|
||||
}) {
|
||||
const newLayer = {
|
||||
...(getExistingLayer(currentState, layerId) || {}),
|
||||
|
@ -281,54 +348,25 @@ function buildSuggestion({
|
|||
|
||||
return {
|
||||
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: ((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3,
|
||||
score: getScore(yValues, splitBy, changeType),
|
||||
// don't advertise chart of same type but with less data
|
||||
hide: currentState && changeType === 'reduced',
|
||||
state,
|
||||
previewIcon: getIconForSeries(seriesType),
|
||||
previewExpression: buildPreviewExpression(state, layerId, xValue, yValues, splitBy),
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewExpression(
|
||||
state: XYState,
|
||||
layerId: string,
|
||||
xValue: TableSuggestionColumn,
|
||||
function getScore(
|
||||
yValues: TableSuggestionColumn[],
|
||||
splitBy: TableSuggestionColumn | undefined
|
||||
splitBy: TableSuggestionColumn | undefined,
|
||||
changeType: TableChangeType
|
||||
) {
|
||||
return buildExpression(
|
||||
{
|
||||
...state,
|
||||
// only show changed layer in preview and hide axes
|
||||
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) }
|
||||
);
|
||||
// Unchanged table suggestions half the score because the underlying data doesn't change
|
||||
const changeFactor = changeType === 'unchanged' ? 0.5 : 1;
|
||||
// chart with multiple y values and split series will have a score of 1, single y value and no split series reduce score
|
||||
return (((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3) * changeFactor;
|
||||
}
|
||||
|
||||
function getExistingLayer(currentState: XYState | undefined, layerId: string) {
|
||||
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 { Visualization } 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';
|
||||
|
||||
const defaultIcon = 'visBarVertical';
|
||||
|
@ -104,4 +104,5 @@ export const xyVisualization: Visualization<State, PersistableState> = {
|
|||
),
|
||||
|
||||
toExpression,
|
||||
toPreviewExpression,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue