mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens] Introduces new chart switcher (#91844)
Co-authored-by: Wylie Conlon <wylieconlon@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
657c273866
commit
03cc5cc0c2
19 changed files with 346 additions and 155 deletions
|
@ -10,7 +10,7 @@ import { render } from 'react-dom';
|
|||
import { Ast } from '@kbn/interpreter/common';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type {
|
||||
import {
|
||||
SuggestionRequest,
|
||||
Visualization,
|
||||
VisualizationSuggestion,
|
||||
|
@ -37,6 +37,10 @@ export interface DatatableVisualizationState {
|
|||
sorting?: SortingState;
|
||||
}
|
||||
|
||||
const visualizationLabel = i18n.translate('xpack.lens.datatable.label', {
|
||||
defaultMessage: 'Table',
|
||||
});
|
||||
|
||||
export const datatableVisualization: Visualization<DatatableVisualizationState> = {
|
||||
id: 'lnsDatatable',
|
||||
|
||||
|
@ -44,8 +48,9 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
|
|||
{
|
||||
id: 'lnsDatatable',
|
||||
icon: LensIconChartDatatable,
|
||||
label: i18n.translate('xpack.lens.datatable.label', {
|
||||
defaultMessage: 'Data table',
|
||||
label: visualizationLabel,
|
||||
groupLabel: i18n.translate('xpack.lens.datatable.groupLabel', {
|
||||
defaultMessage: 'Tabular and single value',
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
@ -68,9 +73,7 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
|
|||
getDescription() {
|
||||
return {
|
||||
icon: LensIconChartDatatable,
|
||||
label: i18n.translate('xpack.lens.datatable.label', {
|
||||
defaultMessage: 'Data table',
|
||||
}),
|
||||
label: visualizationLabel,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ describe('ConfigPanel', () => {
|
|||
icon: 'empty',
|
||||
id: 'testVis',
|
||||
label: 'TEST1',
|
||||
groupLabel: 'testVisGroup',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -85,6 +86,7 @@ describe('ConfigPanel', () => {
|
|||
icon: 'empty',
|
||||
id: 'testVis2',
|
||||
label: 'TEST2',
|
||||
groupLabel: 'testVis2Group',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -82,6 +82,7 @@ describe('LayerPanel', () => {
|
|||
icon: 'empty',
|
||||
id: 'testVis',
|
||||
label: 'TEST1',
|
||||
groupLabel: 'testVisGroup',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -94,6 +95,7 @@ describe('LayerPanel', () => {
|
|||
icon: 'empty',
|
||||
id: 'testVis2',
|
||||
label: 'TEST2',
|
||||
groupLabel: 'testVis2Group',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -7,6 +7,19 @@
|
|||
|
||||
import React, { ReactElement } from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
|
||||
// Tests are executed in a jsdom environment who does not have sizing methods,
|
||||
// thus the AutoSizer will always compute a 0x0 size space
|
||||
// Mock the AutoSizer inside EuiSelectable (Chart Switch) and return some dimensions > 0
|
||||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
return function (props: {
|
||||
children: (dimensions: { width: number; height: number }) => React.ReactNode;
|
||||
}) {
|
||||
const { children, ...otherProps } = props;
|
||||
return <div {...otherProps}>{children({ width: 100, height: 100 })}</div>;
|
||||
};
|
||||
});
|
||||
|
||||
import { EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import { mountWithIntl as mount } from '@kbn/test/jest';
|
||||
import { EditorFrame } from './editor_frame';
|
||||
|
@ -83,6 +96,7 @@ describe('editor_frame', () => {
|
|||
icon: 'empty',
|
||||
id: 'testVis',
|
||||
label: 'TEST1',
|
||||
groupLabel: 'testVisGroup',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -94,6 +108,7 @@ describe('editor_frame', () => {
|
|||
icon: 'empty',
|
||||
id: 'testVis2',
|
||||
label: 'TEST2',
|
||||
groupLabel: 'testVis2Group',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1372,6 +1387,7 @@ describe('editor_frame', () => {
|
|||
icon: 'empty',
|
||||
id: 'testVis3',
|
||||
label: 'TEST3',
|
||||
groupLabel: 'testVis3Group',
|
||||
},
|
||||
],
|
||||
getSuggestions: () => [
|
||||
|
|
|
@ -18,5 +18,5 @@ img.lnsChartSwitch__chartIcon { // stylelint-disable-line selector-no-qualifying
|
|||
}
|
||||
|
||||
.lnsChartSwitch__search {
|
||||
width: 4 * $euiSizeXXL;
|
||||
width: 7 * $euiSizeXXL;
|
||||
}
|
|
@ -12,7 +12,19 @@ import {
|
|||
createMockFramePublicAPI,
|
||||
createMockDatasource,
|
||||
} from '../../mocks';
|
||||
import { EuiKeyPadMenuItem } from '@elastic/eui';
|
||||
|
||||
// Tests are executed in a jsdom environment who does not have sizing methods,
|
||||
// thus the AutoSizer will always compute a 0x0 size space
|
||||
// Mock the AutoSizer inside EuiSelectable (Chart Switch) and return some dimensions > 0
|
||||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
return function (props: {
|
||||
children: (dimensions: { width: number; height: number }) => React.ReactNode;
|
||||
}) {
|
||||
const { children } = props;
|
||||
return <div>{children({ width: 100, height: 100 })}</div>;
|
||||
};
|
||||
});
|
||||
|
||||
import { mountWithIntl as mount } from '@kbn/test/jest';
|
||||
import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types';
|
||||
import { Action } from '../state_management';
|
||||
|
@ -30,6 +42,7 @@ describe('chart_switch', () => {
|
|||
icon: 'empty',
|
||||
id,
|
||||
label: `Label ${id}`,
|
||||
groupLabel: `${id}Group`,
|
||||
},
|
||||
],
|
||||
initialize: jest.fn((_frame, state?: unknown) => {
|
||||
|
@ -70,16 +83,19 @@ describe('chart_switch', () => {
|
|||
icon: 'empty',
|
||||
id: 'subvisC1',
|
||||
label: 'C1',
|
||||
groupLabel: 'visCGroup',
|
||||
},
|
||||
{
|
||||
icon: 'empty',
|
||||
id: 'subvisC2',
|
||||
label: 'C2',
|
||||
groupLabel: 'visCGroup',
|
||||
},
|
||||
{
|
||||
icon: 'empty',
|
||||
id: 'subvisC3',
|
||||
label: 'C3',
|
||||
groupLabel: 'visCGroup',
|
||||
},
|
||||
],
|
||||
getVisualizationTypeId: jest.fn((state) => state.type),
|
||||
|
@ -166,10 +182,7 @@ describe('chart_switch', () => {
|
|||
|
||||
function getMenuItem(subType: string, component: ReactWrapper) {
|
||||
showFlyout(component);
|
||||
return component
|
||||
.find(EuiKeyPadMenuItem)
|
||||
.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`)
|
||||
.first();
|
||||
return component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first();
|
||||
}
|
||||
|
||||
it('should use suggested state if there is a suggestion from the target visualization', () => {
|
||||
|
@ -281,7 +294,12 @@ describe('chart_switch', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert');
|
||||
expect(
|
||||
getMenuItem('visB', component)
|
||||
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
.first()
|
||||
.props().type
|
||||
).toEqual('alert');
|
||||
});
|
||||
|
||||
it('should indicate data loss if not all layers will be used', () => {
|
||||
|
@ -301,7 +319,12 @@ describe('chart_switch', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert');
|
||||
expect(
|
||||
getMenuItem('visB', component)
|
||||
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
.first()
|
||||
.props().type
|
||||
).toEqual('alert');
|
||||
});
|
||||
|
||||
it('should support multi-layer suggestions without data loss', () => {
|
||||
|
@ -344,7 +367,9 @@ describe('chart_switch', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined();
|
||||
expect(
|
||||
getMenuItem('visB', component).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should indicate data loss if no data will be used', () => {
|
||||
|
@ -365,7 +390,12 @@ describe('chart_switch', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert');
|
||||
expect(
|
||||
getMenuItem('visB', component)
|
||||
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
.first()
|
||||
.props().type
|
||||
).toEqual('alert');
|
||||
});
|
||||
|
||||
it('should not indicate data loss if there is no data', () => {
|
||||
|
@ -387,7 +417,9 @@ describe('chart_switch', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined();
|
||||
expect(
|
||||
getMenuItem('visB', component).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not show a warning when the subvisualization is the same', () => {
|
||||
|
@ -411,7 +443,11 @@ describe('chart_switch', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined();
|
||||
expect(
|
||||
getMenuItem('subvisC2', component).find(
|
||||
'[data-test-subj="lnsChartSwitchPopoverAlert_subvisC2"]'
|
||||
)
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should get suggestions when switching subvisualization', () => {
|
||||
|
|
|
@ -11,17 +11,15 @@ import {
|
|||
EuiIcon,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiKeyPadMenu,
|
||||
EuiKeyPadMenuItem,
|
||||
EuiFieldSearch,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSelectableMessage,
|
||||
EuiSelectable,
|
||||
EuiIconTip,
|
||||
EuiSelectableOption,
|
||||
} from '@elastic/eui';
|
||||
import { flatten } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Visualization, FramePublicAPI, Datasource } from '../../../types';
|
||||
import { Visualization, FramePublicAPI, Datasource, VisualizationType } from '../../../types';
|
||||
import { Action } from '../state_management';
|
||||
import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
|
@ -54,6 +52,8 @@ interface Props {
|
|||
>;
|
||||
}
|
||||
|
||||
type SelectableEntry = EuiSelectableOption<{ value: string }>;
|
||||
|
||||
function VisualizationSummary(props: Props) {
|
||||
const visualization = props.visualizationMap[props.visualizationId || ''];
|
||||
|
||||
|
@ -79,6 +79,23 @@ function VisualizationSummary(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const MAX_LIST_HEIGHT = 380;
|
||||
const ENTRY_HEIGHT = 32;
|
||||
|
||||
function computeListHeight(list: SelectableEntry[], maxHeight: number): number {
|
||||
if (list.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(list.length * ENTRY_HEIGHT, maxHeight);
|
||||
}
|
||||
|
||||
function getCurrentVisualizationId(
|
||||
activeVisualization: Visualization,
|
||||
visualizationState: unknown
|
||||
) {
|
||||
return activeVisualization.getVisualizationTypeId(visualizationState);
|
||||
}
|
||||
|
||||
export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
||||
const [flyoutOpen, setFlyoutOpen] = useState<boolean>(false);
|
||||
|
||||
|
@ -189,28 +206,112 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const visualizationTypes = useMemo(
|
||||
() =>
|
||||
flyoutOpen &&
|
||||
flatten(
|
||||
Object.values(props.visualizationMap).map((v) =>
|
||||
v.visualizationTypes.map((t) => ({
|
||||
visualizationId: v.id,
|
||||
...t,
|
||||
icon: t.icon,
|
||||
}))
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
(visualizationType) =>
|
||||
visualizationType.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(visualizationType.fullLabel &&
|
||||
visualizationType.fullLabel.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
)
|
||||
.map((visualizationType) => ({
|
||||
...visualizationType,
|
||||
selection: getSelection(visualizationType.visualizationId, visualizationType.id),
|
||||
})),
|
||||
const { visualizationTypes, visualizationsLookup } = useMemo(
|
||||
() => {
|
||||
if (!flyoutOpen) {
|
||||
return { visualizationTypes: [], visualizationsLookup: {} };
|
||||
}
|
||||
const subVisualizationId = getCurrentVisualizationId(
|
||||
props.visualizationMap[props.visualizationId || ''],
|
||||
props.visualizationState
|
||||
);
|
||||
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||
// reorganize visualizations in groups
|
||||
const grouped: Record<
|
||||
string,
|
||||
Array<
|
||||
VisualizationType & {
|
||||
visualizationId: string;
|
||||
selection: VisualizationSelection;
|
||||
}
|
||||
>
|
||||
> = {};
|
||||
// Will need it later on to quickly pick up the metadata from it
|
||||
const lookup: Record<
|
||||
string,
|
||||
VisualizationType & {
|
||||
visualizationId: string;
|
||||
selection: VisualizationSelection;
|
||||
}
|
||||
> = {};
|
||||
Object.entries(props.visualizationMap).forEach(([visualizationId, v]) => {
|
||||
for (const visualizationType of v.visualizationTypes) {
|
||||
const isSearchMatch =
|
||||
visualizationType.label.toLowerCase().includes(lowercasedSearchTerm) ||
|
||||
visualizationType.fullLabel?.toLowerCase().includes(lowercasedSearchTerm);
|
||||
if (isSearchMatch) {
|
||||
grouped[visualizationType.groupLabel] = grouped[visualizationType.groupLabel] || [];
|
||||
const visualizationEntry = {
|
||||
...visualizationType,
|
||||
visualizationId,
|
||||
selection: getSelection(visualizationId, visualizationType.id),
|
||||
};
|
||||
grouped[visualizationType.groupLabel].push(visualizationEntry);
|
||||
lookup[`${visualizationId}:${visualizationType.id}`] = visualizationEntry;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
visualizationTypes: Object.keys(grouped)
|
||||
.sort()
|
||||
.flatMap((group): SelectableEntry[] => {
|
||||
const visualizations = grouped[group];
|
||||
if (visualizations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: group,
|
||||
label: group,
|
||||
isGroupLabel: true,
|
||||
'aria-label': group,
|
||||
'data-test-subj': `lnsChartSwitchPopover_${group}`,
|
||||
} as SelectableEntry,
|
||||
].concat(
|
||||
visualizations
|
||||
// alphabetical order within each group
|
||||
.sort((a, b) => {
|
||||
return (a.fullLabel || a.label).localeCompare(b.fullLabel || b.label);
|
||||
})
|
||||
.map(
|
||||
(v): SelectableEntry => ({
|
||||
'aria-label': v.fullLabel || v.label,
|
||||
isGroupLabel: false,
|
||||
key: `${v.visualizationId}:${v.id}`,
|
||||
value: `${v.visualizationId}:${v.id}`,
|
||||
'data-test-subj': `lnsChartSwitchPopover_${v.id}`,
|
||||
label: v.fullLabel || v.label,
|
||||
prepend: (
|
||||
<EuiIcon className="lnsChartSwitch__chartIcon" type={v.icon || 'empty'} />
|
||||
),
|
||||
append:
|
||||
v.selection.dataLoss !== 'nothing' ? (
|
||||
<EuiIconTip
|
||||
aria-label={i18n.translate('xpack.lens.chartSwitch.dataLossLabel', {
|
||||
defaultMessage: 'Warning',
|
||||
})}
|
||||
type="alert"
|
||||
color="warning"
|
||||
content={i18n.translate('xpack.lens.chartSwitch.dataLossDescription', {
|
||||
defaultMessage:
|
||||
'Selecting this chart type will result in a partial loss of currently applied configuration selections.',
|
||||
})}
|
||||
iconProps={{
|
||||
className: 'lnsChartSwitch__chartIcon',
|
||||
'data-test-subj': `lnsChartSwitchPopoverAlert_${v.id}`,
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
// Apparently checked: null is not valid for TS
|
||||
...(subVisualizationId === v.id && { checked: 'on' }),
|
||||
})
|
||||
)
|
||||
);
|
||||
}),
|
||||
visualizationsLookup: lookup,
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
flyoutOpen,
|
||||
|
@ -222,89 +323,77 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
]
|
||||
);
|
||||
|
||||
const popover = (
|
||||
<EuiPopover
|
||||
id="lnsChartSwitchPopover"
|
||||
ownFocus
|
||||
initialFocus=".lnsChartSwitch__popoverPanel"
|
||||
panelClassName="lnsChartSwitch__popoverPanel"
|
||||
panelPaddingSize="s"
|
||||
button={
|
||||
<ToolbarButton
|
||||
onClick={() => setFlyoutOpen(!flyoutOpen)}
|
||||
data-test-subj="lnsChartSwitchPopover"
|
||||
fontWeight="bold"
|
||||
>
|
||||
<VisualizationSummary {...props} />
|
||||
</ToolbarButton>
|
||||
}
|
||||
isOpen={flyoutOpen}
|
||||
closePopover={() => setFlyoutOpen(false)}
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate('xpack.lens.configPanel.chartType', {
|
||||
defaultMessage: 'Chart type',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldSearch
|
||||
compressed
|
||||
fullWidth={false}
|
||||
className="lnsChartSwitch__search"
|
||||
value={searchTerm}
|
||||
data-test-subj="lnsChartSwitchSearch"
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverTitle>
|
||||
<EuiKeyPadMenu>
|
||||
{(visualizationTypes || []).map((v) => (
|
||||
<EuiKeyPadMenuItem
|
||||
key={`${v.visualizationId}:${v.id}`}
|
||||
label={<span data-test-subj="visTypeTitle">{v.label}</span>}
|
||||
title={v.fullLabel}
|
||||
role="menuitem"
|
||||
data-test-subj={`lnsChartSwitchPopover_${v.id}`}
|
||||
onClick={() => commitSelection(v.selection)}
|
||||
betaBadgeLabel={
|
||||
v.selection.dataLoss !== 'nothing'
|
||||
? i18n.translate('xpack.lens.chartSwitch.dataLossLabel', {
|
||||
defaultMessage: 'Data loss',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
betaBadgeTooltipContent={
|
||||
v.selection.dataLoss !== 'nothing'
|
||||
? i18n.translate('xpack.lens.chartSwitch.dataLossDescription', {
|
||||
defaultMessage: 'Switching to this chart will lose some of the configuration',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
betaBadgeIconType={v.selection.dataLoss !== 'nothing' ? 'alert' : undefined}
|
||||
return (
|
||||
<div className="lnsChartSwitch__header">
|
||||
<EuiPopover
|
||||
id="lnsChartSwitchPopover"
|
||||
ownFocus
|
||||
initialFocus=".lnsChartSwitch__popoverPanel"
|
||||
panelClassName="lnsChartSwitch__popoverPanel"
|
||||
panelPaddingSize="s"
|
||||
button={
|
||||
<ToolbarButton
|
||||
onClick={() => setFlyoutOpen(!flyoutOpen)}
|
||||
data-test-subj="lnsChartSwitchPopover"
|
||||
fontWeight="bold"
|
||||
>
|
||||
<EuiIcon className="lnsChartSwitch__chartIcon" type={v.icon || 'empty'} size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
))}
|
||||
</EuiKeyPadMenu>
|
||||
{searchTerm && (visualizationTypes || []).length === 0 && (
|
||||
<EuiSelectableMessage>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.chartSwitch.noResults"
|
||||
defaultMessage="No results found for {term}."
|
||||
values={{
|
||||
term: <strong>{searchTerm}</strong>,
|
||||
}}
|
||||
/>
|
||||
</EuiSelectableMessage>
|
||||
)}
|
||||
</EuiPopover>
|
||||
<VisualizationSummary {...props} />
|
||||
</ToolbarButton>
|
||||
}
|
||||
isOpen={flyoutOpen}
|
||||
closePopover={() => setFlyoutOpen(false)}
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate('xpack.lens.configPanel.chartType', {
|
||||
defaultMessage: 'Chart type',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverTitle>
|
||||
<EuiSelectable
|
||||
height={computeListHeight(visualizationTypes, MAX_LIST_HEIGHT)}
|
||||
searchable
|
||||
singleSelection
|
||||
isPreFiltered
|
||||
data-test-subj="lnsChartSwitchList"
|
||||
searchProps={{
|
||||
incremental: true,
|
||||
className: 'lnsChartSwitch__search',
|
||||
'data-test-subj': 'lnsChartSwitchSearch',
|
||||
onSearch: (value) => setSearchTerm(value),
|
||||
}}
|
||||
options={visualizationTypes}
|
||||
onChange={(newOptions) => {
|
||||
const chosenType = newOptions.find(({ checked }) => checked === 'on')!;
|
||||
if (!chosenType) {
|
||||
return;
|
||||
}
|
||||
const id = chosenType.value!;
|
||||
commitSelection(visualizationsLookup[id].selection);
|
||||
}}
|
||||
noMatchesMessage={
|
||||
<FormattedMessage
|
||||
id="xpack.lens.chartSwitch.noResults"
|
||||
defaultMessage="No results found for {term}."
|
||||
values={{
|
||||
term: <strong>{searchTerm}</strong>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(list, search) => (
|
||||
<>
|
||||
{search}
|
||||
{list}
|
||||
</>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiPopover>
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div className="lnsChartSwitch__header">{popover}</div>;
|
||||
});
|
||||
|
||||
function getTopSuggestion(
|
||||
|
|
|
@ -29,6 +29,7 @@ export function createMockVisualization(): jest.Mocked<Visualization> {
|
|||
icon: 'empty',
|
||||
id: 'TEST_VIS',
|
||||
label: 'TEST',
|
||||
groupLabel: 'TEST_VISGroup',
|
||||
},
|
||||
],
|
||||
getVisualizationTypeId: jest.fn((_state) => 'empty'),
|
||||
|
|
|
@ -52,6 +52,9 @@ export const metricVisualization: Visualization<MetricState> = {
|
|||
label: i18n.translate('xpack.lens.metric.label', {
|
||||
defaultMessage: 'Metric',
|
||||
}),
|
||||
groupLabel: i18n.translate('xpack.lens.metric.groupLabel', {
|
||||
defaultMessage: 'Tabular and single value',
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
|
|
|
@ -10,24 +10,33 @@ import { LensIconChartDonut } from '../assets/chart_donut';
|
|||
import { LensIconChartPie } from '../assets/chart_pie';
|
||||
import { LensIconChartTreemap } from '../assets/chart_treemap';
|
||||
|
||||
const groupLabel = i18n.translate('xpack.lens.pie.groupLabel', {
|
||||
defaultMessage: 'Proportion',
|
||||
});
|
||||
|
||||
export const CHART_NAMES = {
|
||||
donut: {
|
||||
icon: LensIconChartDonut,
|
||||
label: i18n.translate('xpack.lens.pie.donutLabel', {
|
||||
defaultMessage: 'Donut',
|
||||
}),
|
||||
groupLabel,
|
||||
},
|
||||
pie: {
|
||||
icon: LensIconChartPie,
|
||||
label: i18n.translate('xpack.lens.pie.pielabel', {
|
||||
defaultMessage: 'Pie',
|
||||
}),
|
||||
|
||||
groupLabel,
|
||||
},
|
||||
treemap: {
|
||||
icon: LensIconChartTreemap,
|
||||
label: i18n.translate('xpack.lens.pie.treemaplabel', {
|
||||
defaultMessage: 'Treemap',
|
||||
}),
|
||||
|
||||
groupLabel,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -45,16 +45,19 @@ export const getPieVisualization = ({
|
|||
id: 'donut',
|
||||
icon: CHART_NAMES.donut.icon,
|
||||
label: CHART_NAMES.donut.label,
|
||||
groupLabel: CHART_NAMES.donut.groupLabel,
|
||||
},
|
||||
{
|
||||
id: 'pie',
|
||||
icon: CHART_NAMES.pie.icon,
|
||||
label: CHART_NAMES.pie.label,
|
||||
groupLabel: CHART_NAMES.pie.groupLabel,
|
||||
},
|
||||
{
|
||||
id: 'treemap',
|
||||
icon: CHART_NAMES.treemap.icon,
|
||||
label: CHART_NAMES.treemap.label,
|
||||
groupLabel: CHART_NAMES.treemap.groupLabel,
|
||||
},
|
||||
],
|
||||
|
||||
|
|
|
@ -516,6 +516,10 @@ export interface VisualizationType {
|
|||
* Optional label used in chart type search if chart switcher is expanded and for tooltips
|
||||
*/
|
||||
fullLabel?: string;
|
||||
/**
|
||||
* The group the visualization belongs to
|
||||
*/
|
||||
groupLabel: string;
|
||||
}
|
||||
|
||||
export interface Visualization<T = unknown> {
|
||||
|
|
|
@ -431,14 +431,22 @@ export interface XYState {
|
|||
}
|
||||
|
||||
export type State = XYState;
|
||||
const groupLabelForBar = i18n.translate('xpack.lens.xyVisualization.barGroupLabel', {
|
||||
defaultMessage: 'Bar',
|
||||
});
|
||||
|
||||
const groupLabelForLineAndArea = i18n.translate('xpack.lens.xyVisualization.lineGroupLabel', {
|
||||
defaultMessage: 'Line and area',
|
||||
});
|
||||
|
||||
export const visualizationTypes: VisualizationType[] = [
|
||||
{
|
||||
id: 'bar',
|
||||
icon: LensIconChartBar,
|
||||
label: i18n.translate('xpack.lens.xyVisualization.barLabel', {
|
||||
defaultMessage: 'Bar',
|
||||
defaultMessage: 'Bar vertical',
|
||||
}),
|
||||
groupLabel: groupLabelForBar,
|
||||
},
|
||||
{
|
||||
id: 'bar_horizontal',
|
||||
|
@ -447,22 +455,25 @@ export const visualizationTypes: VisualizationType[] = [
|
|||
defaultMessage: 'H. Bar',
|
||||
}),
|
||||
fullLabel: i18n.translate('xpack.lens.xyVisualization.barHorizontalFullLabel', {
|
||||
defaultMessage: 'Horizontal bar',
|
||||
defaultMessage: 'Bar horizontal',
|
||||
}),
|
||||
groupLabel: groupLabelForBar,
|
||||
},
|
||||
{
|
||||
id: 'bar_stacked',
|
||||
icon: LensIconChartBarStacked,
|
||||
label: i18n.translate('xpack.lens.xyVisualization.stackedBarLabel', {
|
||||
defaultMessage: 'Stacked bar',
|
||||
defaultMessage: 'Bar vertical stacked',
|
||||
}),
|
||||
groupLabel: groupLabelForBar,
|
||||
},
|
||||
{
|
||||
id: 'bar_percentage_stacked',
|
||||
icon: LensIconChartBarPercentage,
|
||||
label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageBarLabel', {
|
||||
defaultMessage: 'Percentage bar',
|
||||
defaultMessage: 'Bar vertical percentage',
|
||||
}),
|
||||
groupLabel: groupLabelForBar,
|
||||
},
|
||||
{
|
||||
id: 'bar_horizontal_stacked',
|
||||
|
@ -471,8 +482,9 @@ export const visualizationTypes: VisualizationType[] = [
|
|||
defaultMessage: 'H. Stacked bar',
|
||||
}),
|
||||
fullLabel: i18n.translate('xpack.lens.xyVisualization.stackedBarHorizontalFullLabel', {
|
||||
defaultMessage: 'Horizontal stacked bar',
|
||||
defaultMessage: 'Bar horizontal stacked',
|
||||
}),
|
||||
groupLabel: groupLabelForBar,
|
||||
},
|
||||
{
|
||||
id: 'bar_horizontal_percentage_stacked',
|
||||
|
@ -483,9 +495,10 @@ export const visualizationTypes: VisualizationType[] = [
|
|||
fullLabel: i18n.translate(
|
||||
'xpack.lens.xyVisualization.stackedPercentageBarHorizontalFullLabel',
|
||||
{
|
||||
defaultMessage: 'Horizontal percentage bar',
|
||||
defaultMessage: 'Bar horizontal percentage',
|
||||
}
|
||||
),
|
||||
groupLabel: groupLabelForBar,
|
||||
},
|
||||
{
|
||||
id: 'area',
|
||||
|
@ -493,20 +506,23 @@ export const visualizationTypes: VisualizationType[] = [
|
|||
label: i18n.translate('xpack.lens.xyVisualization.areaLabel', {
|
||||
defaultMessage: 'Area',
|
||||
}),
|
||||
groupLabel: groupLabelForLineAndArea,
|
||||
},
|
||||
{
|
||||
id: 'area_stacked',
|
||||
icon: LensIconChartAreaStacked,
|
||||
label: i18n.translate('xpack.lens.xyVisualization.stackedAreaLabel', {
|
||||
defaultMessage: 'Stacked area',
|
||||
defaultMessage: 'Area stacked',
|
||||
}),
|
||||
groupLabel: groupLabelForLineAndArea,
|
||||
},
|
||||
{
|
||||
id: 'area_percentage_stacked',
|
||||
icon: LensIconChartAreaPercentage,
|
||||
label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageAreaLabel', {
|
||||
defaultMessage: 'Percentage area',
|
||||
defaultMessage: 'Area percentage',
|
||||
}),
|
||||
groupLabel: groupLabelForLineAndArea,
|
||||
},
|
||||
{
|
||||
id: 'line',
|
||||
|
@ -514,5 +530,6 @@ export const visualizationTypes: VisualizationType[] = [
|
|||
label: i18n.translate('xpack.lens.xyVisualization.lineLabel', {
|
||||
defaultMessage: 'Line',
|
||||
}),
|
||||
groupLabel: groupLabelForLineAndArea,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -62,7 +62,7 @@ describe('xy_visualization', () => {
|
|||
const desc = xyVisualization.getDescription(mixedState());
|
||||
|
||||
expect(desc.icon).toEqual(LensIconChartBar);
|
||||
expect(desc.label).toEqual('Bar');
|
||||
expect(desc.label).toEqual('Bar vertical');
|
||||
});
|
||||
|
||||
it('should show mixed horizontal bar chart when multiple horizontal bar types', () => {
|
||||
|
@ -70,23 +70,23 @@ describe('xy_visualization', () => {
|
|||
mixedState('bar_horizontal', 'bar_horizontal_stacked')
|
||||
);
|
||||
|
||||
expect(desc.label).toEqual('Mixed H. bar');
|
||||
expect(desc.label).toEqual('Mixed bar horizontal');
|
||||
});
|
||||
|
||||
it('should show bar chart when bar only', () => {
|
||||
const desc = xyVisualization.getDescription(mixedState('bar_horizontal', 'bar_horizontal'));
|
||||
|
||||
expect(desc.label).toEqual('H. Bar');
|
||||
expect(desc.label).toEqual('Bar horizontal');
|
||||
});
|
||||
|
||||
it('should show the chart description if not mixed', () => {
|
||||
expect(xyVisualization.getDescription(mixedState('area')).label).toEqual('Area');
|
||||
expect(xyVisualization.getDescription(mixedState('line')).label).toEqual('Line');
|
||||
expect(xyVisualization.getDescription(mixedState('area_stacked')).label).toEqual(
|
||||
'Stacked area'
|
||||
'Area stacked'
|
||||
);
|
||||
expect(xyVisualization.getDescription(mixedState('bar_horizontal_stacked')).label).toEqual(
|
||||
'H. Stacked bar'
|
||||
'Bar horizontal stacked'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,7 +58,7 @@ function getDescription(state?: State) {
|
|||
return {
|
||||
icon: LensIconChartBarHorizontal,
|
||||
label: i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', {
|
||||
defaultMessage: 'Mixed H. bar',
|
||||
defaultMessage: 'Mixed bar horizontal',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ function getDescription(state?: State) {
|
|||
|
||||
return {
|
||||
icon: visualizationType.icon,
|
||||
label: visualizationType.label,
|
||||
label: visualizationType.fullLabel || visualizationType.label,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -484,7 +484,7 @@ describe('xy_suggestions', () => {
|
|||
});
|
||||
|
||||
expect(rest).toHaveLength(visualizationTypes.length - 1);
|
||||
expect(suggestion.title).toEqual('Stacked bar');
|
||||
expect(suggestion.title).toEqual('Bar vertical stacked');
|
||||
expect(suggestion.state).toEqual(
|
||||
expect.objectContaining({
|
||||
layers: [
|
||||
|
|
|
@ -11418,7 +11418,6 @@
|
|||
"xpack.lens.xySuggestions.unstackedChartTitle": "スタックが解除されました",
|
||||
"xpack.lens.xySuggestions.yAxixConjunctionSign": " & ",
|
||||
"xpack.lens.xyVisualization.areaLabel": "エリア",
|
||||
"xpack.lens.xyVisualization.barHorizontalFullLabel": "横棒",
|
||||
"xpack.lens.xyVisualization.barHorizontalLabel": "横棒",
|
||||
"xpack.lens.xyVisualization.barLabel": "棒",
|
||||
"xpack.lens.xyVisualization.dataFailureSplitLong": "{layers, plural, other {レイヤー}} {layersList} には {axis} のフィールドが{layers, plural, other {必要です}}。",
|
||||
|
@ -11430,11 +11429,9 @@
|
|||
"xpack.lens.xyVisualization.mixedLabel": "ミックスされた XY",
|
||||
"xpack.lens.xyVisualization.noDataLabel": "結果が見つかりませんでした",
|
||||
"xpack.lens.xyVisualization.stackedAreaLabel": "スタックされたエリア",
|
||||
"xpack.lens.xyVisualization.stackedBarHorizontalFullLabel": "積み上げ横棒",
|
||||
"xpack.lens.xyVisualization.stackedBarHorizontalLabel": "横積み上げ棒",
|
||||
"xpack.lens.xyVisualization.stackedBarLabel": "積み上げ棒",
|
||||
"xpack.lens.xyVisualization.stackedPercentageAreaLabel": "割合エリア",
|
||||
"xpack.lens.xyVisualization.stackedPercentageBarHorizontalFullLabel": "割合横棒",
|
||||
"xpack.lens.xyVisualization.stackedPercentageBarHorizontalLabel": "横割合棒",
|
||||
"xpack.lens.xyVisualization.stackedPercentageBarLabel": "割合棒",
|
||||
"xpack.lens.xyVisualization.xyLabel": "XY",
|
||||
|
|
|
@ -11446,7 +11446,6 @@
|
|||
"xpack.lens.xySuggestions.unstackedChartTitle": "非堆叠",
|
||||
"xpack.lens.xySuggestions.yAxixConjunctionSign": " & ",
|
||||
"xpack.lens.xyVisualization.areaLabel": "面积图",
|
||||
"xpack.lens.xyVisualization.barHorizontalFullLabel": "水平条形图",
|
||||
"xpack.lens.xyVisualization.barHorizontalLabel": "水平条形图",
|
||||
"xpack.lens.xyVisualization.barLabel": "条形图",
|
||||
"xpack.lens.xyVisualization.dataFailureSplitLong": "{layers, plural, other {图层}} {layersList} {layers, plural, other {需要}}一个针对{axis}的字段。",
|
||||
|
@ -11458,11 +11457,9 @@
|
|||
"xpack.lens.xyVisualization.mixedLabel": "混合 XY",
|
||||
"xpack.lens.xyVisualization.noDataLabel": "找不到结果",
|
||||
"xpack.lens.xyVisualization.stackedAreaLabel": "堆叠面积图",
|
||||
"xpack.lens.xyVisualization.stackedBarHorizontalFullLabel": "水平堆叠条形图",
|
||||
"xpack.lens.xyVisualization.stackedBarHorizontalLabel": "水平堆叠条形图",
|
||||
"xpack.lens.xyVisualization.stackedBarLabel": "堆叠条形图",
|
||||
"xpack.lens.xyVisualization.stackedPercentageAreaLabel": "百分比面积图",
|
||||
"xpack.lens.xyVisualization.stackedPercentageBarHorizontalFullLabel": "水平百分比条形图",
|
||||
"xpack.lens.xyVisualization.stackedPercentageBarHorizontalLabel": "水平百分比条形图",
|
||||
"xpack.lens.xyVisualization.stackedPercentageBarLabel": "百分比条形图",
|
||||
"xpack.lens.xyVisualization.xyLabel": "XY",
|
||||
|
|
|
@ -418,19 +418,20 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
* @param subVisualizationId - the ID of the sub-visualization to switch to, such as
|
||||
* lnsDatatable or bar_stacked
|
||||
*/
|
||||
async switchToVisualization(subVisualizationId: string) {
|
||||
async switchToVisualization(subVisualizationId: string, searchTerm?: string) {
|
||||
await this.openChartSwitchPopover();
|
||||
await this.searchOnChartSwitch(subVisualizationId, searchTerm);
|
||||
await testSubjects.click(`lnsChartSwitchPopover_${subVisualizationId}`);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
async openChartSwitchPopover() {
|
||||
if (await testSubjects.exists('visTypeTitle')) {
|
||||
if (await testSubjects.exists('lnsChartSwitchList')) {
|
||||
return;
|
||||
}
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('lnsChartSwitchPopover');
|
||||
await testSubjects.existOrFail('visTypeTitle');
|
||||
await testSubjects.existOrFail('lnsChartSwitchList');
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -451,17 +452,28 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
return errors?.length ?? 0;
|
||||
},
|
||||
|
||||
async searchOnChartSwitch(subVisualizationId: string, searchTerm?: string) {
|
||||
// Because the new chart switcher is now a virtualized list, the process needs some help
|
||||
// So either pass a search string or pick the last 3 letters from the id (3 because pie
|
||||
// is the smallest chart name) and use them to search
|
||||
const queryTerm = searchTerm ?? subVisualizationId.substring(subVisualizationId.length - 3);
|
||||
return await testSubjects.setValue('lnsChartSwitchSearch', queryTerm, {
|
||||
clearWithKeyboard: true,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks a specific subvisualization in the chart switcher for a "data loss" indicator
|
||||
*
|
||||
* @param subVisualizationId - the ID of the sub-visualization to switch to, such as
|
||||
* lnsDatatable or bar_stacked
|
||||
*/
|
||||
async hasChartSwitchWarning(subVisualizationId: string) {
|
||||
async hasChartSwitchWarning(subVisualizationId: string, searchTerm?: string) {
|
||||
await this.openChartSwitchPopover();
|
||||
await this.searchOnChartSwitch(subVisualizationId, searchTerm);
|
||||
const element = await testSubjects.find(`lnsChartSwitchPopover_${subVisualizationId}`);
|
||||
return await find.descendantExistsByCssSelector(
|
||||
'.euiKeyPadMenuItem__betaBadgeWrapper',
|
||||
return await testSubjects.descendantExists(
|
||||
`lnsChartSwitchPopoverAlert_${subVisualizationId}`,
|
||||
element
|
||||
);
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue