mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Lens] Enabling Random Sampling (#151749)
## Summary This PR is a design implementation to improve the Random sampling feedback for the user. Some work has been done to extract locally the `SamplingSlider` component, that will be eventually moved into a separate package outside of Lens. In terms of design, to start, the Layer setting now inherits the same `Data/Appearance` design from the dimension editor: <img width="377" alt="Screenshot 2023-03-28 at 12 10 57" src="https://user-images.githubusercontent.com/924948/228204053-06bbbbf8-5eda-4765-b401-fb8976a4b107.png"> Next, on the dataView picker of the layer panel the random sampling information is shown when enabled: <img width="356" alt="Screenshot 2023-03-28 at 12 06 15" src="https://user-images.githubusercontent.com/924948/228204168-3514952c-09e7-40f0-acbd-c3f1fe66dc27.png"> when transitioning to `Maximum`/`Minimum` operation during editing the sampling is disabled and a info toast is shown: <img width="344" alt="Screenshot 2023-02-21 at 17 15 46" src="https://user-images.githubusercontent.com/924948/220409514-667a0dc0-4247-44ea-98bf-9e4660ab1799.png"> The toast will show only when transitioning to such operations. If the user goes from `Maximum`/`Minimum` to other supported operations then the toast flag is reset, therefore going again into `Maximum` will trigger a new toast. Transitioning from a quick function `Maximum` into a formula `max(...)` will not trigger a toast as the flag is persisted within the same "editing session". If the user configured a random sampling setting but then picked a `Maximum` operation the Layer setting becomes disabled: <img width="379" alt="Screenshot 2023-03-28 at 12 15 40" src="https://user-images.githubusercontent.com/924948/228205186-a7b587bf-6c97-4546-9f85-b8dce0debbec.png"> and last the embeddable view with the new visualization modifiers view on the bottom-left: <img width="759" alt="Screenshot 2023-03-28 at 12 13 36" src="https://user-images.githubusercontent.com/924948/228204606-3878e953-1027-4184-b2d7-d017cf56b319.png"> <img width="770" alt="Screenshot 2023-03-28 at 12 12 57" src="https://user-images.githubusercontent.com/924948/228204428-73f54add-90af-42e8-9d85-709914a58e62.png"> <img width="761" alt="Screenshot 2023-03-28 at 11 59 27" src="https://user-images.githubusercontent.com/924948/228204463-c6ab16f2-10bb-4bb6-9cf9-2ecf331eabf8.png"> <img width="759" alt="Screenshot 2023-03-28 at 11 56 19" src="https://user-images.githubusercontent.com/924948/228204479-e997ea40-e102-4026-a0f2-5794baccfa0b.png"> <details> <summary>Previous PoC design</summary> This PR works as a PoC for random sampling with the current state of the design. The UI is still a bit rough and not final. <img width="329" alt="Screenshot 2023-02-21 at 17 52 23" src="https://user-images.githubusercontent.com/924948/220409386-763a17f7-e120-4caf-bc63-2b87871af9dc.png"> when transitioning to `Maximum`/`Minimum` operation: <img width="344" alt="Screenshot 2023-02-21 at 17 15 46" src="https://user-images.githubusercontent.com/924948/220409514-667a0dc0-4247-44ea-98bf-9e4660ab1799.png"> The toast will show only when transitioning to such operations. If the user goes from `Maximum`/`Minimum` to other supported operations then the toast flag is reset, therefore going again into `Maximum` will trigger a new toast. Transitioning from a quick function `Maximum` into a formula `max(...)` will not trigger a toast as the flag is persisted within the same "editing session". If the user configured a random sampling setting but then picked a `Maximum` operation the Layer setting becomes disabled: <img width="377" alt="Screenshot 2023-02-21 at 17 15 35" src="https://user-images.githubusercontent.com/924948/220410419-ec853a23-5718-48bf-bbc2-824285fbce9d.png"> At dashboard level the random sampling is notified via an icon on the bottom left: <img width="1434" alt="Screenshot 2023-02-21 at 17 14 50" src="https://user-images.githubusercontent.com/924948/220410051-37588420-aac3-41d2-ac74-9ab6bda2f046.png"> Hovering the icon will show a detailed tooltip: <img width="626" alt="Screenshot 2023-02-21 at 17 15 00" src="https://user-images.githubusercontent.com/924948/220410216-f8602681-886d-4468-8e98-8267cabb9d6a.png"> At the dashboard level a new `i` icon is displayed when a random sampling feature is enabled in the panel and a popup with more details is shown on hover: <img width="755" alt="Screenshot 2023-03-20 at 10 08 23" src="https://user-images.githubusercontent.com/924948/226294970-356fc2b2-e254-44c3-adba-89896dfecce2.png"> </details> ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Michael Marcialis <michael.l.marcialis@gmail.com>
This commit is contained in:
parent
73068f1e87
commit
606eb9cd61
33 changed files with 1365 additions and 281 deletions
|
@ -198,11 +198,7 @@ export const filterAndSortUserMessages = (
|
|||
return false;
|
||||
}
|
||||
|
||||
if (location.id === 'dimensionButton' && location.dimensionId !== dimensionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !(location.id === 'dimensionButton' && location.dimensionId !== dimensionId);
|
||||
});
|
||||
|
||||
if (!hasMatch) {
|
||||
|
@ -221,11 +217,17 @@ export const filterAndSortUserMessages = (
|
|||
};
|
||||
|
||||
function bySeverity(a: UserMessage, b: UserMessage) {
|
||||
if (a.severity === 'warning' && b.severity === 'error') {
|
||||
return 1;
|
||||
} else if (a.severity === 'error' && b.severity === 'warning') {
|
||||
return -1;
|
||||
} else {
|
||||
if (a.severity === b.severity) {
|
||||
return 0;
|
||||
}
|
||||
if (a.severity === 'error') {
|
||||
return -1;
|
||||
}
|
||||
if (b.severity === 'error') {
|
||||
return 1;
|
||||
}
|
||||
if (a.severity === 'warning') {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ import {
|
|||
} from '../operations';
|
||||
import { mergeLayer } from '../state_helpers';
|
||||
import { getReferencedField, hasField } from '../pure_utils';
|
||||
import { fieldIsInvalid } from '../utils';
|
||||
import { fieldIsInvalid, getSamplingValue, isSamplingValueEnabled } from '../utils';
|
||||
import { BucketNestingEditor } from './bucket_nesting_editor';
|
||||
import type { FormBasedLayer } from '../types';
|
||||
import { FormatSelector } from './format_selector';
|
||||
|
@ -126,6 +126,10 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
|
||||
const [temporaryState, setTemporaryState] = useState<TemporaryState>('none');
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
// If a layer has sampling disabled, assume the toast has already fired in the past
|
||||
const [hasRandomSamplingToastFired, setSamplingToastAsFired] = useState(
|
||||
!isSamplingValueEnabled(state.layers[layerId])
|
||||
);
|
||||
|
||||
const onHelpClick = () => setIsHelpOpen((prevIsHelpOpen) => !prevIsHelpOpen);
|
||||
const closeHelp = () => setIsHelpOpen(false);
|
||||
|
@ -139,6 +143,28 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
[layerId, setState]
|
||||
);
|
||||
|
||||
const fireOrResetRandomSamplingToast = useCallback(
|
||||
(newLayer: FormBasedLayer) => {
|
||||
// if prev and current sampling state is different, show a toast to the user
|
||||
if (isSamplingValueEnabled(state.layers[layerId]) && !isSamplingValueEnabled(newLayer)) {
|
||||
if (newLayer.sampling != null && newLayer.sampling < 1) {
|
||||
props.notifications.toasts.add({
|
||||
title: i18n.translate('xpack.lens.uiInfo.samplingDisabledTitle', {
|
||||
defaultMessage: 'Layer sampling changed to 100%',
|
||||
}),
|
||||
text: i18n.translate('xpack.lens.uiInfo.samplingDisabledMessage', {
|
||||
defaultMessage:
|
||||
'The use of a maximum or minimum function on a layer requires all documents to be sampled in order to function properly.',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
// reset the flag if the user switches to another supported operation
|
||||
setSamplingToastAsFired(!hasRandomSamplingToastFired);
|
||||
},
|
||||
[hasRandomSamplingToastFired, layerId, props.notifications.toasts, state.layers]
|
||||
);
|
||||
|
||||
const setStateWrapper = useCallback(
|
||||
(
|
||||
setter:
|
||||
|
@ -177,10 +203,14 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
} else {
|
||||
outputLayer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
|
||||
}
|
||||
const newLayer = adjustColumnReferencesForChangedColumn(outputLayer, columnId);
|
||||
// Fire an info toast (eventually) on layer update
|
||||
fireOrResetRandomSamplingToast(newLayer);
|
||||
|
||||
return mergeLayer({
|
||||
state: prevState,
|
||||
layerId,
|
||||
newLayer: adjustColumnReferencesForChangedColumn(outputLayer, columnId),
|
||||
newLayer,
|
||||
});
|
||||
},
|
||||
{
|
||||
|
@ -189,7 +219,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
}
|
||||
);
|
||||
},
|
||||
[columnId, layerId, setState, state.layers]
|
||||
[columnId, fireOrResetRandomSamplingToast, layerId, setState, state.layers]
|
||||
);
|
||||
|
||||
const setIsCloseable = (isCloseable: boolean) => {
|
||||
|
@ -337,6 +367,9 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
state.layers[layerId],
|
||||
layerType
|
||||
),
|
||||
compatibleWithSampling:
|
||||
getSamplingValue(state.layers[layerId]) === 1 ||
|
||||
(definition.getUnsupportedSettings?.()?.sampling ?? true),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -350,7 +383,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
(selectedColumn?.operationType != null && isQuickFunction(selectedColumn?.operationType));
|
||||
|
||||
const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map(
|
||||
({ operationType, compatibleWithCurrentField, disabledStatus }) => {
|
||||
({ operationType, compatibleWithCurrentField, disabledStatus, compatibleWithSampling }) => {
|
||||
const isActive = Boolean(
|
||||
incompleteOperation === operationType ||
|
||||
(!incompleteOperation && selectedColumn && selectedColumn.operationType === operationType)
|
||||
|
@ -417,6 +450,26 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
} else if (!compatibleWithSampling) {
|
||||
label = (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false} style={{ marginRight: euiTheme.size.xs }}>
|
||||
{label}
|
||||
</EuiFlexItem>
|
||||
{shouldDisplayDots && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.lens.indexPattern.settingsSamplingUnsupported', {
|
||||
defaultMessage: `Selecting this function will change this layer's sampling to 100% in order to function properly.`,
|
||||
})}
|
||||
size="s"
|
||||
type="dot"
|
||||
color="warning"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -741,16 +794,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
);
|
||||
}}
|
||||
onChooseFunction={(operationType: string, field?: IndexPatternField) => {
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field,
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
const newLayer = insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field,
|
||||
visualizationGroups: dimensionGroups,
|
||||
});
|
||||
fireOrResetRandomSamplingToast(newLayer);
|
||||
updateLayer(newLayer);
|
||||
}}
|
||||
onChooseField={(choice: FieldChoiceWithOperationType) => {
|
||||
updateLayer(
|
||||
|
@ -784,6 +837,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
} else {
|
||||
newLayer = setter;
|
||||
}
|
||||
fireOrResetRandomSamplingToast(newLayer);
|
||||
return updateLayer(adjustColumnReferencesForChangedColumn(newLayer, referenceId));
|
||||
}}
|
||||
validation={validation}
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
SavedObjectsClientContract,
|
||||
HttpSetup,
|
||||
CoreStart,
|
||||
NotificationsStart,
|
||||
} from '@kbn/core/public';
|
||||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields';
|
||||
|
@ -232,6 +233,7 @@ describe('FormBasedDimensionEditor', () => {
|
|||
fieldFormats: fieldFormatsServiceMock.createStartContract(),
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
notifications: {} as NotificationsStart,
|
||||
data: {
|
||||
fieldFormats: {
|
||||
getType: jest.fn().mockReturnValue({
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public';
|
||||
import type {
|
||||
IUiSettingsClient,
|
||||
SavedObjectsClientContract,
|
||||
HttpSetup,
|
||||
NotificationsStart,
|
||||
} from '@kbn/core/public';
|
||||
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
|
@ -38,6 +43,7 @@ export type FormBasedDimensionEditorProps =
|
|||
dataViews: DataViewsPublicPluginStart;
|
||||
uniqueLabel: string;
|
||||
dateRange: DateRange;
|
||||
notifications: NotificationsStart;
|
||||
};
|
||||
|
||||
export const FormBasedDimensionEditorComponent = function FormBasedDimensionPanel(
|
||||
|
|
|
@ -48,7 +48,7 @@ import {
|
|||
} from './operations';
|
||||
import { createMockedFullReference } from './operations/mocks';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { createMockFramePublicAPI } from '../../mocks';
|
||||
import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages';
|
||||
|
||||
|
@ -194,11 +194,17 @@ describe('IndexPattern Data Source', () => {
|
|||
let FormBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState>;
|
||||
|
||||
beforeEach(() => {
|
||||
const data = dataPluginMock.createStartContract();
|
||||
data.query.timefilter.timefilter.getAbsoluteTime = jest.fn(() => ({
|
||||
from: '',
|
||||
to: '',
|
||||
}));
|
||||
|
||||
FormBasedDatasource = getFormBasedDatasource({
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
storage: {} as IStorageWrapper,
|
||||
core: coreMock.createStart(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
data,
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
fieldFormats: fieldFormatsServiceMock.createStartContract(),
|
||||
charts: chartPluginMock.createSetupContract(),
|
||||
|
@ -3013,6 +3019,22 @@ describe('IndexPattern Data Source', () => {
|
|||
});
|
||||
|
||||
describe('#getUserMessages', () => {
|
||||
function createMockFrameDatasourceAPI({
|
||||
activeData,
|
||||
dataViews,
|
||||
}: Partial<Omit<FramePublicAPI, 'dataViews'>> & {
|
||||
dataViews?: Partial<FramePublicAPI['dataViews']>;
|
||||
}): FrameDatasourceAPI {
|
||||
return {
|
||||
...createMockFramePublicAPI({
|
||||
activeData,
|
||||
dataViews,
|
||||
}),
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe('error messages', () => {
|
||||
it('should generate error messages for a single layer', () => {
|
||||
(getErrorMessages as jest.Mock).mockClear();
|
||||
|
@ -3029,7 +3051,7 @@ describe('IndexPattern Data Source', () => {
|
|||
};
|
||||
expect(
|
||||
FormBasedDatasource.getUserMessages(state, {
|
||||
frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
|
||||
frame: createMockFrameDatasourceAPI({ dataViews: { indexPatterns } }),
|
||||
setState: () => {},
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
|
@ -3081,7 +3103,7 @@ describe('IndexPattern Data Source', () => {
|
|||
};
|
||||
expect(
|
||||
FormBasedDatasource.getUserMessages(state, {
|
||||
frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
|
||||
frame: createMockFrameDatasourceAPI({ dataViews: { indexPatterns } }),
|
||||
setState: () => {},
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
|
@ -3170,7 +3192,7 @@ describe('IndexPattern Data Source', () => {
|
|||
(getErrorMessages as jest.Mock).mockReturnValueOnce([]);
|
||||
|
||||
const messages = FormBasedDatasource.getUserMessages(state, {
|
||||
frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
|
||||
frame: createMockFrameDatasourceAPI({ dataViews: { indexPatterns } }),
|
||||
setState: () => {},
|
||||
});
|
||||
|
||||
|
@ -3208,7 +3230,7 @@ describe('IndexPattern Data Source', () => {
|
|||
] as ReturnType<typeof getErrorMessages>);
|
||||
|
||||
const messages = FormBasedDatasource.getUserMessages(state, {
|
||||
frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
|
||||
frame: createMockFrameDatasourceAPI({ dataViews: { indexPatterns } }),
|
||||
setState: () => {},
|
||||
});
|
||||
|
||||
|
@ -3238,7 +3260,7 @@ describe('IndexPattern Data Source', () => {
|
|||
|
||||
describe('warning messages', () => {
|
||||
let state: FormBasedPrivateState;
|
||||
let framePublicAPI: FramePublicAPI;
|
||||
let framePublicAPI: FrameDatasourceAPI;
|
||||
|
||||
beforeEach(() => {
|
||||
(getErrorMessages as jest.Mock).mockReturnValueOnce([]);
|
||||
|
@ -3320,7 +3342,7 @@ describe('IndexPattern Data Source', () => {
|
|||
currentIndexPatternId: '1',
|
||||
};
|
||||
|
||||
framePublicAPI = {
|
||||
framePublicAPI = createMockFrameDatasourceAPI({
|
||||
activeData: {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
|
@ -3355,14 +3377,9 @@ describe('IndexPattern Data Source', () => {
|
|||
},
|
||||
},
|
||||
dataViews: {
|
||||
...createMockFramePublicAPI().dataViews,
|
||||
indexPatterns: expectedIndexPatterns,
|
||||
indexPatternRefs: Object.values(expectedIndexPatterns).map(({ id, title }) => ({
|
||||
id,
|
||||
title,
|
||||
})),
|
||||
},
|
||||
} as unknown as FramePublicAPI;
|
||||
});
|
||||
});
|
||||
|
||||
const extractTranslationIdsFromWarnings = (warnings: UserMessage[]) => {
|
||||
|
@ -3378,7 +3395,7 @@ describe('IndexPattern Data Source', () => {
|
|||
|
||||
it('should return mismatched time shifts', () => {
|
||||
const warnings = FormBasedDatasource.getUserMessages!(state, {
|
||||
frame: framePublicAPI as FrameDatasourceAPI,
|
||||
frame: framePublicAPI,
|
||||
setState: () => {},
|
||||
});
|
||||
|
||||
|
@ -3394,7 +3411,7 @@ describe('IndexPattern Data Source', () => {
|
|||
framePublicAPI.activeData!.first.columns[1].meta.sourceParams!.hasPrecisionError = true;
|
||||
|
||||
const warnings = FormBasedDatasource.getUserMessages!(state, {
|
||||
frame: framePublicAPI as FrameDatasourceAPI,
|
||||
frame: framePublicAPI,
|
||||
setState: () => {},
|
||||
});
|
||||
|
||||
|
@ -3407,6 +3424,133 @@ describe('IndexPattern Data Source', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('info messages', () => {
|
||||
function createLayer(
|
||||
index: number = 0,
|
||||
sampling?: number
|
||||
): FormBasedPrivateState['layers'][number] {
|
||||
return {
|
||||
sampling,
|
||||
indexPatternId: '1',
|
||||
columnOrder: [`col-${index}-1`, `col-${index}-2`],
|
||||
columns: {
|
||||
[`col-${index}-1`]: {
|
||||
operationType: 'date_histogram',
|
||||
params: {
|
||||
interval: '12h',
|
||||
},
|
||||
label: '',
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
sourceField: 'timestamp',
|
||||
} as DateHistogramIndexPatternColumn,
|
||||
[`col-${index}-2`]: {
|
||||
operationType: 'count',
|
||||
label: '',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: 'records',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createDatatableForLayer(index: number): Datatable {
|
||||
return {
|
||||
type: 'datatable' as const,
|
||||
rows: [],
|
||||
columns: [
|
||||
{
|
||||
id: `col-${index}-1`,
|
||||
name: `col-${index}-1`,
|
||||
meta: {
|
||||
type: 'date',
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
type: 'date_histogram',
|
||||
params: {
|
||||
used_interval: '12h',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `col-${index}-2`,
|
||||
name: `col-${index}-2`,
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(getErrorMessages as jest.Mock).mockReturnValueOnce([]);
|
||||
});
|
||||
|
||||
it.each`
|
||||
sampling | infoMessages
|
||||
${undefined} | ${0}
|
||||
${1} | ${0}
|
||||
${0.1} | ${1}
|
||||
`(
|
||||
'should return $infoMessages info messages when sampling is set to $sampling',
|
||||
({ sampling, infoMessages }) => {
|
||||
const messages = FormBasedDatasource.getUserMessages!(
|
||||
{
|
||||
layers: {
|
||||
first: createLayer(0, sampling),
|
||||
},
|
||||
currentIndexPatternId: '1',
|
||||
},
|
||||
{
|
||||
frame: createMockFrameDatasourceAPI({
|
||||
activeData: {
|
||||
first: createDatatableForLayer(0),
|
||||
},
|
||||
dataViews: {
|
||||
indexPatterns: expectedIndexPatterns,
|
||||
},
|
||||
}),
|
||||
setState: () => {},
|
||||
visualizationInfo: { layers: [] },
|
||||
}
|
||||
);
|
||||
expect(messages.filter(({ severity }) => severity === 'info')).toHaveLength(infoMessages);
|
||||
}
|
||||
);
|
||||
|
||||
it('should return a single info message for multiple layers with sampling < 100%', () => {
|
||||
const state: FormBasedPrivateState = {
|
||||
layers: {
|
||||
first: createLayer(0, 0.1),
|
||||
second: createLayer(1, 0.001),
|
||||
},
|
||||
currentIndexPatternId: '1',
|
||||
};
|
||||
const messages = FormBasedDatasource.getUserMessages!(state, {
|
||||
frame: createMockFrameDatasourceAPI({
|
||||
activeData: {
|
||||
first: createDatatableForLayer(0),
|
||||
second: createDatatableForLayer(1),
|
||||
},
|
||||
dataViews: {
|
||||
indexPatterns: expectedIndexPatterns,
|
||||
},
|
||||
}),
|
||||
setState: () => {},
|
||||
visualizationInfo: { layers: [] },
|
||||
});
|
||||
const infoMessages = messages.filter(({ severity }) => severity === 'info');
|
||||
expect(infoMessages).toHaveLength(1);
|
||||
const [info] = infoMessages;
|
||||
if (isFragment(info.longMessage)) {
|
||||
expect(info.longMessage.props.layers).toHaveLength(2);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateStateOnCloseDimension', () => {
|
||||
|
|
|
@ -69,6 +69,7 @@ import {
|
|||
getVisualDefaultsForLayer,
|
||||
isColumnInvalid,
|
||||
cloneLayer,
|
||||
getNotifiableFeatures,
|
||||
} from './utils';
|
||||
import { isDraggedDataViewField } from '../../utils';
|
||||
import { hasField, normalizeOperationDataType } from './pure_utils';
|
||||
|
@ -585,6 +586,7 @@ export function getFormBasedDatasource({
|
|||
unifiedSearch={unifiedSearch}
|
||||
dataViews={dataViews}
|
||||
uniqueLabel={columnLabelMap[props.columnId]}
|
||||
notifications={core.notifications}
|
||||
{...props}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
|
@ -818,7 +820,7 @@ export function getFormBasedDatasource({
|
|||
getDatasourceSuggestionsForVisualizeField,
|
||||
getDatasourceSuggestionsForVisualizeCharts,
|
||||
|
||||
getUserMessages(state, { frame: frameDatasourceAPI, setState }) {
|
||||
getUserMessages(state, { frame: frameDatasourceAPI, setState, visualizationInfo }) {
|
||||
if (!state) {
|
||||
return [];
|
||||
}
|
||||
|
@ -872,7 +874,9 @@ export function getFormBasedDatasource({
|
|||
),
|
||||
];
|
||||
|
||||
return [...layerErrorMessages, ...dimensionErrorMessages, ...warningMessages];
|
||||
const infoMessages = getNotifiableFeatures(state, frameDatasourceAPI, visualizationInfo);
|
||||
|
||||
return layerErrorMessages.concat(dimensionErrorMessages, warningMessages, infoMessages);
|
||||
},
|
||||
|
||||
getSearchWarningMessages: (state, warning, request, response) => {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { FormBasedLayer } from '../..';
|
||||
import { FramePublicAPI, VisualizationInfo } from '../../types';
|
||||
import { getSamplingValue } from './utils';
|
||||
|
||||
export function ReducedSamplingSectionEntries({
|
||||
layers,
|
||||
visualizationInfo,
|
||||
dataViews,
|
||||
}: {
|
||||
layers: Array<[string, FormBasedLayer]>;
|
||||
visualizationInfo: VisualizationInfo;
|
||||
dataViews: FramePublicAPI['dataViews'];
|
||||
}) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<>
|
||||
{layers.map(([id, layer], layerIndex) => {
|
||||
const dataView = dataViews.indexPatterns[layer.indexPatternId];
|
||||
const layerTitle =
|
||||
visualizationInfo.layers.find(({ layerId }) => layerId === id)?.label ||
|
||||
i18n.translate('xpack.lens.indexPattern.samplingPerLayer.fallbackLayerName', {
|
||||
defaultMessage: 'Data layer',
|
||||
});
|
||||
return (
|
||||
<li
|
||||
key={`${layerTitle}-${dataView}-${layerIndex}`}
|
||||
data-test-subj={`lns-feature-badges-reducedSampling-${layerIndex}`}
|
||||
css={css`
|
||||
margin: ${euiTheme.size.base} 0 0;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">{layerTitle}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
padding-right: 0;
|
||||
`}
|
||||
>
|
||||
<EuiText size="s">{`${Number(getSamplingValue(layer)) * 100}%`}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -14,6 +14,8 @@ import {
|
|||
EuiText,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
useEuiTheme,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -21,107 +23,187 @@ import React from 'react';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { DatasourceLayerSettingsProps } from '../../types';
|
||||
import type { FormBasedPrivateState } from './types';
|
||||
import { isSamplingValueEnabled } from './utils';
|
||||
import { TooltipWrapper } from '../../shared_components';
|
||||
|
||||
const samplingValue = [0.0001, 0.001, 0.01, 0.1, 1];
|
||||
|
||||
export function LayerSettingsPanel({
|
||||
state,
|
||||
setState,
|
||||
layerId,
|
||||
}: DatasourceLayerSettingsProps<FormBasedPrivateState>) {
|
||||
const samplingIndex = samplingValue.findIndex((v) => v === state.layers[layerId].sampling);
|
||||
const currentSamplingIndex = samplingIndex > -1 ? samplingIndex : samplingValue.length - 1;
|
||||
const samplingValues = [0.00001, 0.0001, 0.001, 0.01, 0.1, 1];
|
||||
interface SamplingSliderProps {
|
||||
values: number[];
|
||||
currentValue: number | undefined;
|
||||
disabled: boolean;
|
||||
disabledReason: string;
|
||||
onChange: (value: number) => void;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
/**
|
||||
* Stub for a shared component
|
||||
*/
|
||||
function SamplingSlider({
|
||||
values,
|
||||
currentValue,
|
||||
disabled,
|
||||
disabledReason,
|
||||
onChange,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}: SamplingSliderProps) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const samplingIndex = values.findIndex((v) => v === currentValue);
|
||||
const currentSamplingIndex = samplingIndex > -1 ? samplingIndex : values.length - 1;
|
||||
return (
|
||||
<EuiFormRow
|
||||
display="rowCompressed"
|
||||
data-test-subj="lns-indexPattern-random-sampling-row"
|
||||
fullWidth
|
||||
helpText={
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyChart.randomSampling.help"
|
||||
defaultMessage="Lower sampling percentages increase speed, but decrease accuracy. As a best practice, use lower sampling only for large datasets. {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/master/search-aggregations-random-sampler-aggregation.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyChart.randomSampling.learnMore"
|
||||
defaultMessage="View documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
{i18n.translate('xpack.lens.xyChart.randomSampling.label', {
|
||||
defaultMessage: 'Random sampling',
|
||||
})}{' '}
|
||||
<EuiBetaBadge
|
||||
css={css`
|
||||
vertical-align: middle;
|
||||
`}
|
||||
iconType="beaker"
|
||||
label={i18n.translate('xpack.lens.randomSampling.experimentalLabel', {
|
||||
defaultMessage: 'Technical preview',
|
||||
})}
|
||||
size="s"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
<TooltipWrapper
|
||||
tooltipContent={disabledReason}
|
||||
condition={disabled}
|
||||
delay="regular"
|
||||
display="block"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="xs">
|
||||
<EuiText
|
||||
color={disabled ? euiTheme.colors.disabledText : euiTheme.colors.subduedText}
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyChart.randomSampling.speedLabel"
|
||||
defaultMessage="Speed"
|
||||
id="xpack.lens.indexPattern.randomSampling.performanceLabel"
|
||||
defaultMessage="Performance"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiRange
|
||||
data-test-subj="lns-indexPattern-random-sampling"
|
||||
data-test-subj={dataTestSubj}
|
||||
value={currentSamplingIndex}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: {
|
||||
...state.layers[layerId],
|
||||
sampling: samplingValue[Number(e.currentTarget.value)],
|
||||
},
|
||||
},
|
||||
});
|
||||
onChange(values[Number(e.currentTarget.value)]);
|
||||
}}
|
||||
showInput={false}
|
||||
showRange={false}
|
||||
showTicks
|
||||
step={1}
|
||||
min={0}
|
||||
max={samplingValue.length - 1}
|
||||
ticks={samplingValue.map((v, i) => ({ label: `${v * 100}%`, value: i }))}
|
||||
max={values.length - 1}
|
||||
ticks={values.map((v, i) => ({
|
||||
label: `${v * 100}%`.slice(Number.isInteger(v * 100) ? 0 : 1),
|
||||
value: i,
|
||||
}))}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="xs">
|
||||
<EuiText
|
||||
color={disabled ? euiTheme.colors.disabledText : euiTheme.colors.subduedText}
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyChart.randomSampling.accuracyLabel"
|
||||
id="xpack.lens.indexPattern.randomSampling.accuracyLabel"
|
||||
defaultMessage="Accuracy"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayerSettingsPanel({
|
||||
state,
|
||||
setState,
|
||||
layerId,
|
||||
}: DatasourceLayerSettingsProps<FormBasedPrivateState>) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const isSamplingValueDisabled = !isSamplingValueEnabled(state.layers[layerId]);
|
||||
const currentValue = isSamplingValueDisabled
|
||||
? samplingValues[samplingValues.length - 1]
|
||||
: state.layers[layerId].sampling;
|
||||
return (
|
||||
<div id={layerId}>
|
||||
<EuiText
|
||||
size="s"
|
||||
css={css`
|
||||
margin-bottom: ${euiTheme.size.base};
|
||||
`}
|
||||
>
|
||||
<h4>
|
||||
{i18n.translate('xpack.lens.indexPattern.layerSettings.headingData', {
|
||||
defaultMessage: 'Data',
|
||||
})}
|
||||
</h4>
|
||||
</EuiText>
|
||||
<EuiFormRow
|
||||
display="rowCompressed"
|
||||
data-test-subj="lns-indexPattern-random-sampling-row"
|
||||
fullWidth
|
||||
helpText={
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.randomSampling.help"
|
||||
defaultMessage="Lower sampling percentages increases the performance, but lowers the accuracy. Lower sampling percentages are best for large datasets. {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/master/search-aggregations-random-sampler-aggregation.html"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.randomSampling.learnMore"
|
||||
defaultMessage="View documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
{i18n.translate('xpack.lens.indexPattern.randomSampling.label', {
|
||||
defaultMessage: 'Sampling',
|
||||
})}{' '}
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.lens.indexPattern.randomSampling.experimentalLabel', {
|
||||
defaultMessage: 'Technical preview',
|
||||
})}
|
||||
>
|
||||
<EuiBetaBadge
|
||||
css={css`
|
||||
vertical-align: middle;
|
||||
`}
|
||||
iconType="beaker"
|
||||
label={i18n.translate('xpack.lens.indexPattern.randomSampling.experimentalLabel', {
|
||||
defaultMessage: 'Technical preview',
|
||||
})}
|
||||
size="s"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SamplingSlider
|
||||
disabled={isSamplingValueDisabled}
|
||||
disabledReason={i18n.translate('xpack.lens.indexPattern.randomSampling.disabledMessage', {
|
||||
defaultMessage:
|
||||
'In order to select a reduced sampling percentage, you must remove any maximum or minimum functions applied on this layer.',
|
||||
})}
|
||||
values={samplingValues}
|
||||
currentValue={currentValue}
|
||||
data-test-subj="lns-indexPattern-random-sampling-slider"
|
||||
onChange={(newSamplingValue) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: {
|
||||
...state.layers[layerId],
|
||||
sampling: newSamplingValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { DatasourceLayerPanelProps } from '../../types';
|
||||
import { FormBasedPrivateState } from './types';
|
||||
import { ChangeIndexPattern } from '../../shared_components/dataview_picker/dataview_picker';
|
||||
import { getSamplingValue } from './utils';
|
||||
|
||||
export interface FormBasedLayerPanelProps extends DatasourceLayerPanelProps<FormBasedPrivateState> {
|
||||
state: FormBasedPrivateState;
|
||||
|
@ -36,6 +37,7 @@ export function LayerPanel({
|
|||
isAdhoc: !isPersisted,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<ChangeIndexPattern
|
||||
|
@ -46,6 +48,7 @@ export function LayerPanel({
|
|||
'data-test-subj': 'lns_layerIndexPatternLabel',
|
||||
size: 's',
|
||||
fontWeight: 'normal',
|
||||
samplingValue: getSamplingValue(layer),
|
||||
}}
|
||||
indexPatternId={layer.indexPatternId}
|
||||
indexPatternRefs={indexPatternRefs}
|
||||
|
|
|
@ -56,6 +56,11 @@ export const counterRateOperation: OperationDefinition<
|
|||
validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed,
|
||||
},
|
||||
],
|
||||
// return false for quick function as the built-in reference will use max
|
||||
// in formula this check won't be used and the check is performed on the formula AST tree traversal independently
|
||||
getUnsupportedSettings: () => ({
|
||||
sampling: false,
|
||||
}),
|
||||
getPossibleOperation: (indexPattern) => {
|
||||
if (hasDateField(indexPattern)) {
|
||||
return {
|
||||
|
|
|
@ -229,6 +229,8 @@ export interface HelpProps<C> {
|
|||
|
||||
export type TimeScalingMode = 'disabled' | 'mandatory' | 'optional';
|
||||
|
||||
export type LayerSettingsFeatures = Record<'sampling', boolean>;
|
||||
|
||||
export interface AdvancedOption {
|
||||
dataTestSubj: string;
|
||||
inlineElement: React.ReactElement | null;
|
||||
|
@ -434,6 +436,10 @@ interface BaseOperationDefinitionProps<
|
|||
* Boolean flag whether the data section extra element passed in from the visualization is handled by the param editor of the operation or whether the datasource general logic should be used.
|
||||
*/
|
||||
handleDataSectionExtra?: boolean;
|
||||
/**
|
||||
* When present returns a dictionary of unsupported layer settings
|
||||
*/
|
||||
getUnsupportedSettings?: () => LayerSettingsFeatures;
|
||||
}
|
||||
|
||||
interface BaseBuildColumnArgs {
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { EuiSwitch, EuiText } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
||||
import { OperationDefinition, ParamEditorProps } from '.';
|
||||
import { LayerSettingsFeatures, OperationDefinition, ParamEditorProps } from '.';
|
||||
import {
|
||||
getFormatFromPreviousColumn,
|
||||
getInvalidFieldMessage,
|
||||
|
@ -64,6 +64,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
aggConfigParams,
|
||||
documentationDescription,
|
||||
quickFunctionDocumentation,
|
||||
unsupportedSettings,
|
||||
}: {
|
||||
type: T['operationType'];
|
||||
displayName: string;
|
||||
|
@ -76,6 +77,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
aggConfigParams?: Record<string, string | number | boolean>;
|
||||
documentationDescription?: string;
|
||||
quickFunctionDocumentation?: string;
|
||||
unsupportedSettings?: LayerSettingsFeatures;
|
||||
}) {
|
||||
const labelLookup = (name: string, column?: BaseIndexPatternColumn) => {
|
||||
const label = ofName(name);
|
||||
|
@ -98,6 +100,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
description,
|
||||
input: 'field',
|
||||
timeScalingMode: optionalTimeScaling ? 'optional' : undefined,
|
||||
getUnsupportedSettings: () => unsupportedSettings,
|
||||
getPossibleOperationForField: ({
|
||||
aggregationRestrictions,
|
||||
aggregatable,
|
||||
|
@ -281,6 +284,7 @@ export const minOperation = buildMetricOperation<MinIndexPatternColumn>({
|
|||
}
|
||||
),
|
||||
supportsDate: true,
|
||||
unsupportedSettings: { sampling: false },
|
||||
});
|
||||
|
||||
export const maxOperation = buildMetricOperation<MaxIndexPatternColumn>({
|
||||
|
@ -304,6 +308,7 @@ export const maxOperation = buildMetricOperation<MaxIndexPatternColumn>({
|
|||
}
|
||||
),
|
||||
supportsDate: true,
|
||||
unsupportedSettings: { sampling: false },
|
||||
});
|
||||
|
||||
export const averageOperation = buildMetricOperation<AvgIndexPatternColumn>({
|
||||
|
|
|
@ -31,6 +31,7 @@ import { isColumnFormatted, isColumnOfType } from './operations/definitions/help
|
|||
import type { IndexPattern, IndexPatternMap } from '../../types';
|
||||
import { dedupeAggs } from './dedupe_aggs';
|
||||
import { resolveTimeShift } from './time_shift_utils';
|
||||
import { getSamplingValue } from './utils';
|
||||
|
||||
export type OriginalColumn = { id: string } & GenericIndexPatternColumn;
|
||||
|
||||
|
@ -415,7 +416,7 @@ function getExpressionForLayer(
|
|||
metricsAtAllLevels: false,
|
||||
partialRows: false,
|
||||
timeFields: allDateHistogramFields,
|
||||
probability: layer.sampling || 1,
|
||||
probability: getSamplingValue(layer),
|
||||
samplerSeed: seedrandom(searchSessionId).int32(),
|
||||
}).toAst(),
|
||||
{
|
||||
|
|
|
@ -26,7 +26,13 @@ import {
|
|||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import type { DateRange } from '../../../common/types';
|
||||
import type { FramePublicAPI, IndexPattern, StateSetter, UserMessage } from '../../types';
|
||||
import type {
|
||||
FramePublicAPI,
|
||||
IndexPattern,
|
||||
StateSetter,
|
||||
UserMessage,
|
||||
VisualizationInfo,
|
||||
} from '../../types';
|
||||
import { renewIDs } from '../../utils';
|
||||
import type { FormBasedLayer, FormBasedPersistedState, FormBasedPrivateState } from './types';
|
||||
import type { ReferenceBasedIndexPatternColumn } from './operations/definitions/column_types';
|
||||
|
@ -41,6 +47,8 @@ import {
|
|||
RangeIndexPatternColumn,
|
||||
FormulaIndexPatternColumn,
|
||||
DateHistogramIndexPatternColumn,
|
||||
MaxIndexPatternColumn,
|
||||
MinIndexPatternColumn,
|
||||
} from './operations';
|
||||
|
||||
import { getInvalidFieldMessage, isColumnOfType } from './operations/definitions/helpers';
|
||||
|
@ -51,6 +59,43 @@ import { supportsRarityRanking } from './operations/definitions/terms';
|
|||
import { DEFAULT_MAX_DOC_COUNT } from './operations/definitions/terms/constants';
|
||||
import { getOriginalId } from '../../../common/expressions/datatable/transpose_helpers';
|
||||
import { isQueryValid } from '../../shared_components';
|
||||
import { ReducedSamplingSectionEntries } from './info_badges';
|
||||
|
||||
function isMinOrMaxColumn(
|
||||
column?: GenericIndexPatternColumn
|
||||
): column is MaxIndexPatternColumn | MinIndexPatternColumn {
|
||||
if (!column) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isColumnOfType<MaxIndexPatternColumn>('max', column) ||
|
||||
isColumnOfType<MinIndexPatternColumn>('min', column)
|
||||
);
|
||||
}
|
||||
|
||||
function isReferenceColumn(
|
||||
column: GenericIndexPatternColumn
|
||||
): column is ReferenceBasedIndexPatternColumn {
|
||||
return 'references' in column;
|
||||
}
|
||||
|
||||
export function isSamplingValueEnabled(layer: FormBasedLayer) {
|
||||
// Do not use columnOrder here as it needs to check also inside formulas columns
|
||||
return !Object.values(layer.columns).some(
|
||||
(column) =>
|
||||
isMinOrMaxColumn(column) ||
|
||||
(isReferenceColumn(column) && isMinOrMaxColumn(layer.columns[column.references[0]]))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized logic to get the actual random sampling value for a layer
|
||||
* @param layer
|
||||
* @returns
|
||||
*/
|
||||
export function getSamplingValue(layer: FormBasedLayer) {
|
||||
return isSamplingValueEnabled(layer) ? layer.sampling ?? 1 : 1;
|
||||
}
|
||||
|
||||
export function isColumnInvalid(
|
||||
layer: FormBasedLayer,
|
||||
|
@ -449,6 +494,40 @@ export function getVisualDefaultsForLayer(layer: FormBasedLayer) {
|
|||
);
|
||||
}
|
||||
|
||||
export function getNotifiableFeatures(
|
||||
state: FormBasedPrivateState,
|
||||
frame: FramePublicAPI,
|
||||
visualizationInfo?: VisualizationInfo
|
||||
): UserMessage[] {
|
||||
if (!visualizationInfo) {
|
||||
return [];
|
||||
}
|
||||
const layersWithCustomSamplingValues = Object.entries(state.layers).filter(
|
||||
([, layer]) => getSamplingValue(layer) !== 1
|
||||
);
|
||||
if (!layersWithCustomSamplingValues.length) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
uniqueId: 'random_sampling_info',
|
||||
severity: 'info',
|
||||
fixableInEditor: false,
|
||||
shortMessage: i18n.translate('xpack.lens.indexPattern.samplingPerLayer', {
|
||||
defaultMessage: 'Layers with reduced sampling',
|
||||
}),
|
||||
longMessage: (
|
||||
<ReducedSamplingSectionEntries
|
||||
layers={layersWithCustomSamplingValues}
|
||||
dataViews={frame.dataViews}
|
||||
visualizationInfo={visualizationInfo}
|
||||
/>
|
||||
),
|
||||
displayLocations: [{ id: 'embeddableBadge' }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Some utilities to extract queries/filters from specific column types
|
||||
*/
|
||||
|
|
|
@ -51,9 +51,6 @@ import { onDropForVisualization, shouldRemoveSource } from './buttons/drop_targe
|
|||
import { getSharedActions } from './layer_actions/layer_actions';
|
||||
import { FlyoutContainer } from './flyout_container';
|
||||
|
||||
// hide the random sampling settings from the UI
|
||||
const DISPLAY_RANDOM_SAMPLING_SETTINGS = false;
|
||||
|
||||
const initialActiveDimensionState = {
|
||||
isNew: false,
|
||||
};
|
||||
|
@ -350,7 +347,7 @@ export function LayerPanel(
|
|||
frame: props.framePublicAPI,
|
||||
}) &&
|
||||
activeVisualization.renderLayerSettings) ||
|
||||
(layerDatasource?.renderLayerSettings && DISPLAY_RANDOM_SAMPLING_SETTINGS)
|
||||
layerDatasource?.renderLayerSettings
|
||||
),
|
||||
openLayerSettings: () => setPanelSettingsOpen(true),
|
||||
onCloneLayer,
|
||||
|
@ -684,8 +681,8 @@ export function LayerPanel(
|
|||
}}
|
||||
>
|
||||
<div id={layerId}>
|
||||
<div className="lnsIndexPatternDimensionEditor--padded lnsIndexPatternDimensionEditor--collapseNext">
|
||||
{layerDatasource?.renderLayerSettings && DISPLAY_RANDOM_SAMPLING_SETTINGS && (
|
||||
<div className="lnsIndexPatternDimensionEditor--padded">
|
||||
{layerDatasource?.renderLayerSettings && (
|
||||
<>
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderLayerSettings}
|
||||
|
|
|
@ -25,11 +25,9 @@ import { UserMessage } from '../../../types';
|
|||
|
||||
export const MessageList = ({
|
||||
messages,
|
||||
useSmallIconsOnButton,
|
||||
customButtonStyles,
|
||||
}: {
|
||||
messages: UserMessage[];
|
||||
useSmallIconsOnButton?: boolean;
|
||||
customButtonStyles?: SerializedStyles;
|
||||
}) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
@ -87,7 +85,7 @@ export const MessageList = ({
|
|||
>
|
||||
{errorCount > 0 && (
|
||||
<>
|
||||
<EuiIcon type={IconError} size={useSmallIconsOnButton ? 's' : undefined} />
|
||||
<EuiIcon type={IconError} />
|
||||
{errorCount}
|
||||
</>
|
||||
)}
|
||||
|
@ -95,7 +93,6 @@ export const MessageList = ({
|
|||
<>
|
||||
<EuiIcon
|
||||
type={IconWarning}
|
||||
size={useSmallIconsOnButton ? 's' : undefined}
|
||||
css={css`
|
||||
margin-left: 4px;
|
||||
`}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { uniqBy } from 'lodash';
|
||||
import { partition, uniqBy } from 'lodash';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -75,8 +75,7 @@ import {
|
|||
} from '@kbn/charts-plugin/public';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { useEuiFontSize, useEuiTheme } from '@elastic/eui';
|
||||
import { useEuiFontSize, useEuiTheme, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
|
||||
import { Document } from '../persistence';
|
||||
import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper';
|
||||
|
@ -126,6 +125,7 @@ import {
|
|||
getApplicationUserMessages,
|
||||
} from '../app_plugin/get_application_user_messages';
|
||||
import { MessageList } from '../editor_frame_service/editor_frame/workspace_panel/message_list';
|
||||
import { EmbeddableFeatureBadge } from './embeddable_info_badges';
|
||||
|
||||
export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>;
|
||||
|
||||
|
@ -347,13 +347,15 @@ const EmbeddableMessagesPopover = ({ messages }: { messages: UserMessage[] }) =>
|
|||
const { euiTheme } = useEuiTheme();
|
||||
const xsFontSize = useEuiFontSize('xs').fontSize;
|
||||
|
||||
if (!messages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageList
|
||||
messages={messages}
|
||||
useSmallIconsOnButton={true}
|
||||
customButtonStyles={css`
|
||||
block-size: ${euiTheme.size.l};
|
||||
border-radius: 0 ${euiTheme.border.radius.medium} 0 ${euiTheme.border.radius.small};
|
||||
font-size: ${xsFontSize};
|
||||
padding: 0 ${euiTheme.size.xs};
|
||||
& > * {
|
||||
|
@ -630,6 +632,9 @@ export class Embeddable
|
|||
...(this.activeDatasource?.getUserMessages(this.activeDatasourceState, {
|
||||
setState: () => {},
|
||||
frame: frameDatasourceAPI,
|
||||
visualizationInfo: this.activeVisualization?.getVisualizationInfo?.(
|
||||
this.activeVisualizationState
|
||||
),
|
||||
}) ?? []),
|
||||
...(this.activeVisualization?.getUserMessages?.(this.activeVisualizationState, {
|
||||
frame: frameDatasourceAPI,
|
||||
|
@ -981,11 +986,16 @@ export class Embeddable
|
|||
*/
|
||||
private renderBadgeMessages = () => {
|
||||
const messages = this.getUserMessages('embeddableBadge');
|
||||
const [warningOrErrorMessages, infoMessages] = partition(
|
||||
messages,
|
||||
({ severity }) => severity !== 'info'
|
||||
);
|
||||
|
||||
if (messages.length && this.badgeDomNode) {
|
||||
if (this.badgeDomNode) {
|
||||
render(
|
||||
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
|
||||
<EmbeddableMessagesPopover messages={messages} />
|
||||
<EmbeddableMessagesPopover messages={warningOrErrorMessages} />
|
||||
<EmbeddableFeatureBadge messages={infoMessages} />
|
||||
</KibanaThemeProvider>,
|
||||
this.badgeDomNode
|
||||
);
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
.lnsEmbeddablePanelFeatureList {
|
||||
@include euiYScroll;
|
||||
max-height: $euiSize * 20;
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiToolTip,
|
||||
EuiHorizontalRule,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
EuiButtonEmpty,
|
||||
useEuiFontSize,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { UserMessage } from '../types';
|
||||
import './embeddable_info_badges.scss';
|
||||
|
||||
export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const xsFontSize = useEuiFontSize('xs').fontSize;
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
if (!messages.length) {
|
||||
return null;
|
||||
}
|
||||
const iconTitle = i18n.translate('xpack.lens.embeddable.featureBadge.iconDescription', {
|
||||
defaultMessage: `{count} visualization {count, plural, one {modifier} other {modifiers}}`,
|
||||
values: {
|
||||
count: messages.length,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<EuiPopover
|
||||
panelPaddingSize="none"
|
||||
button={
|
||||
<EuiToolTip content={iconTitle}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="lns-feature-badges-trigger"
|
||||
className="lnsEmbeddablePanelFeatureList_button"
|
||||
color={'text'}
|
||||
onClick={onButtonClick}
|
||||
title={iconTitle}
|
||||
size="s"
|
||||
css={css`
|
||||
color: ${euiTheme.colors.emptyShade};
|
||||
font-size: ${xsFontSize};
|
||||
height: ${euiTheme.size.l} !important;
|
||||
.euiButtonEmpty__content {
|
||||
padding: 0 ${euiTheme.size.xs};
|
||||
}
|
||||
.euiButtonEmpty__text {
|
||||
margin-inline-start: ${euiTheme.size.xs};
|
||||
}
|
||||
`}
|
||||
iconType="wrench"
|
||||
>
|
||||
{messages.length}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
>
|
||||
<div>
|
||||
{messages.map(({ shortMessage, longMessage }, index) => (
|
||||
<aside
|
||||
key={`${shortMessage}-${index}`}
|
||||
css={css`
|
||||
padding: ${index > 0 ? 0 : euiTheme.size.base} ${euiTheme.size.base}
|
||||
${index > 0 ? euiTheme.size.s : 0};
|
||||
`}
|
||||
>
|
||||
{index ? <EuiHorizontalRule margin="s" /> : null}
|
||||
<EuiTitle size="xxs" css={css`color=${euiTheme.colors.title}`}>
|
||||
<h3>{shortMessage}</h3>
|
||||
</EuiTitle>
|
||||
<ul className="lnsEmbeddablePanelFeatureList">{longMessage}</ul>
|
||||
</aside>
|
||||
))}
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -35,7 +35,9 @@ export const createMockFramePublicAPI = ({
|
|||
dateRange,
|
||||
dataViews,
|
||||
activeData,
|
||||
}: Partial<FramePublicAPI> = {}): FrameMock => ({
|
||||
}: Partial<Omit<FramePublicAPI, 'dataViews'>> & {
|
||||
dataViews?: Partial<FramePublicAPI['dataViews']>;
|
||||
} = {}): FrameMock => ({
|
||||
datasourceLayers: datasourceLayers ?? {},
|
||||
dateRange: dateRange ?? {
|
||||
fromDate: '2022-03-17T08:25:00.000Z',
|
||||
|
|
|
@ -7,17 +7,110 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui';
|
||||
import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiSelectableProps,
|
||||
EuiTextColor,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { DataViewsList } from '@kbn/unified-search-plugin/public';
|
||||
import { IndexPatternRef } from '../../types';
|
||||
import { css } from '@emotion/react';
|
||||
import { type IndexPatternRef } from '../../types';
|
||||
import { type ToolbarButtonProps, ToolbarButton } from './toolbar_button';
|
||||
import { RandomSamplingIcon } from './sampling_icon';
|
||||
|
||||
export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & {
|
||||
label: string;
|
||||
title?: string;
|
||||
isDisabled?: boolean;
|
||||
samplingValue?: number;
|
||||
};
|
||||
|
||||
function TriggerButton({
|
||||
label,
|
||||
title,
|
||||
togglePopover,
|
||||
isMissingCurrent,
|
||||
samplingValue,
|
||||
...rest
|
||||
}: ChangeIndexPatternTriggerProps &
|
||||
ToolbarButtonProps & {
|
||||
togglePopover: () => void;
|
||||
isMissingCurrent?: boolean;
|
||||
}) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
// be careful to only add color with a value, otherwise it will fallbacks to "primary"
|
||||
const colorProp = isMissingCurrent
|
||||
? {
|
||||
color: 'danger' as const,
|
||||
}
|
||||
: {};
|
||||
const content =
|
||||
samplingValue != null && samplingValue !== 1 ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem
|
||||
className="eui-textTruncate"
|
||||
css={css`
|
||||
display: block;
|
||||
min-width: 0;
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
data-test-subj="lnsChangeIndexPatternSamplingInfo"
|
||||
css={css`
|
||||
display: block;
|
||||
*:hover &,
|
||||
*:focus & {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.lens.indexPattern.randomSamplingInfo', {
|
||||
defaultMessage: '{value}% sampling',
|
||||
values: {
|
||||
value: samplingValue * 100,
|
||||
},
|
||||
})}
|
||||
position="top"
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RandomSamplingIcon color={euiTheme.colors.disabledText} fill="currentColor" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTextColor color={euiTheme.colors.disabledText}>
|
||||
{samplingValue * 100}%
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
label
|
||||
);
|
||||
return (
|
||||
<ToolbarButton
|
||||
title={title}
|
||||
onClick={() => togglePopover()}
|
||||
fullWidth
|
||||
{...colorProp}
|
||||
{...rest}
|
||||
textProps={{ style: { width: '100%' } }}
|
||||
>
|
||||
{content}
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChangeIndexPattern({
|
||||
indexPatternRefs,
|
||||
isMissingCurrent,
|
||||
|
@ -35,33 +128,17 @@ export function ChangeIndexPattern({
|
|||
}) {
|
||||
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
|
||||
|
||||
// be careful to only add color with a value, otherwise it will fallbacks to "primary"
|
||||
const colorProp = isMissingCurrent
|
||||
? {
|
||||
color: 'danger' as const,
|
||||
}
|
||||
: {};
|
||||
|
||||
const createTrigger = function () {
|
||||
const { label, title, ...rest } = trigger;
|
||||
return (
|
||||
<ToolbarButton
|
||||
title={title}
|
||||
onClick={() => setPopoverIsOpen(!isPopoverOpen)}
|
||||
fullWidth
|
||||
{...colorProp}
|
||||
{...rest}
|
||||
>
|
||||
{label}
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
panelClassName="lnsChangeIndexPatternPopover"
|
||||
button={createTrigger()}
|
||||
button={
|
||||
<TriggerButton
|
||||
{...trigger}
|
||||
isMissingCurrent={isMissingCurrent}
|
||||
togglePopover={() => setPopoverIsOpen(!isPopoverOpen)}
|
||||
/>
|
||||
}
|
||||
panelProps={{
|
||||
['data-test-subj']: 'lnsChangeIndexPatternPopover',
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
interface CustomProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
|
||||
export function RandomSamplingIcon({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & CustomProps) {
|
||||
return (
|
||||
<svg
|
||||
width="15"
|
||||
height="16"
|
||||
viewBox="0 0 15 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-labelledby={titleId}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId}>{title}</title> : null}
|
||||
<path d="M1 0C0.447715 0 0 0.447715 0 1V14C0 14.5523 0.447715 15 1 15H7.67099C7.54918 14.9013 7.43133 14.7953 7.31802 14.682C7.10539 14.4693 6.91848 14.2407 6.75731 14H1V1H8V4.5C8 4.77614 8.22386 5 8.5 5H12V7.25625C12.3483 7.3791 12.6846 7.54612 13 7.75731V4C13 3.73478 12.8946 3.48043 12.7071 3.29289L9.70711 0.292893C9.51957 0.105357 9.26522 0 9 0H1Z" />
|
||||
<path d="M10.5 13C11.3284 13 12 12.3284 12 11.5C12 10.6716 11.3284 10 10.5 10C9.67157 10 9 10.6716 9 11.5C9 12.3284 9.67157 13 10.5 13Z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.1465 15.8536L12.5962 14.3033C11.9771 14.7674 11.2393 14.9997 10.5013 15H10.4987C9.61445 14.9997 8.7303 14.6663 8.05051 14L8.02517 13.9749C6.65833 12.608 6.65833 10.392 8.02517 9.02513C9.392 7.65829 11.6081 7.65829 12.9749 9.02513C14.2217 10.2719 14.3312 12.2253 13.3034 13.5963L14.8536 15.1464C15.0489 15.3417 15.0489 15.6583 14.8536 15.8535C14.6584 16.0488 14.3418 16.0488 14.1465 15.8536ZM12.2678 13.9749L12.5962 14.3033C11.9805 14.7648 11.2474 14.997 10.5135 14.9998C10.7834 14.9927 11.0001 14.7716 11.0001 14.5C11.0001 14.2239 10.7762 14 10.5001 14C9.85948 14 9.22053 13.756 8.73227 13.2678C7.75596 12.2915 7.75596 10.7085 8.73227 9.73223C9.70858 8.75592 11.2915 8.75592 12.2678 9.73223C13.2441 10.7085 13.2441 12.2915 12.2678 13.2678C12.174 13.3615 12.1214 13.4887 12.1214 13.6213C12.1214 13.7539 12.174 13.8811 12.2678 13.9749Z"
|
||||
/>
|
||||
<path d="M3 2.5C3 2.77614 2.77614 3 2.5 3C2.22386 3 2 2.77614 2 2.5C2 2.22386 2.22386 2 2.5 2C2.77614 2 3 2.22386 3 2.5Z" />
|
||||
<path d="M2.5 5C2.77614 5 3 4.77614 3 4.5C3 4.22386 2.77614 4 2.5 4C2.22386 4 2 4.22386 2 4.5C2 4.77614 2.22386 5 2.5 5Z" />
|
||||
<path d="M3 6.5C3 6.77614 2.77614 7 2.5 7C2.22386 7 2 6.77614 2 6.5C2 6.22386 2.22386 6 2.5 6C2.77614 6 3 6.22386 3 6.5Z" />
|
||||
<path d="M2.5 9C2.77614 9 3 8.77614 3 8.5C3 8.22386 2.77614 8 2.5 8C2.22386 8 2 8.22386 2 8.5C2 8.77614 2.22386 9 2.5 9Z" />
|
||||
<path d="M3 10.5C3 10.7761 2.77614 11 2.5 11C2.22386 11 2 10.7761 2 10.5C2 10.2239 2.22386 10 2.5 10C2.77614 10 3 10.2239 3 10.5Z" />
|
||||
<path d="M3 12.5C3 12.7761 2.77614 13 2.5 13C2.22386 13 2 12.7761 2 12.5C2 12.2239 2.22386 12 2.5 12C2.77614 12 3 12.2239 3 12.5Z" />
|
||||
<path d="M4.5 9C4.77614 9 5 8.77614 5 8.5C5 8.22386 4.77614 8 4.5 8C4.22386 8 4 8.22386 4 8.5C4 8.77614 4.22386 9 4.5 9Z" />
|
||||
<path d="M5 6.5C5 6.77614 4.77614 7 4.5 7C4.22386 7 4 6.77614 4 6.5C4 6.22386 4.22386 6 4.5 6C4.77614 6 5 6.22386 5 6.5Z" />
|
||||
<path d="M4.5 5C4.77614 5 5 4.77614 5 4.5C5 4.22386 4.77614 4 4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5Z" />
|
||||
<path d="M5 2.5C5 2.77614 4.77614 3 4.5 3C4.22386 3 4 2.77614 4 2.5C4 2.22386 4.22386 2 4.5 2C4.77614 2 5 2.22386 5 2.5Z" />
|
||||
<path d="M6.5 5C6.77614 5 7 4.77614 7 4.5C7 4.22386 6.77614 4 6.5 4C6.22386 4 6 4.22386 6 4.5C6 4.77614 6.22386 5 6.5 5Z" />
|
||||
<path d="M7 2.5C7 2.77614 6.77614 3 6.5 3C6.22386 3 6 2.77614 6 2.5C6 2.22386 6.22386 2 6.5 2C6.77614 2 7 2.22386 7 2.5Z" />
|
||||
<path d="M6.5 7C6.77614 7 7 6.77614 7 6.5C7 6.22386 6.77614 6 6.5 6C6.22386 6 6 6.22386 6 6.5C6 6.77614 6.22386 7 6.5 7Z" />
|
||||
<path d="M9 6.5C9 6.77614 8.77614 7 8.5 7C8.22386 7 8 6.77614 8 6.5C8 6.22386 8.22386 6 8.5 6C8.77614 6 9 6.22386 9 6.5Z" />
|
||||
<path d="M10.5 7C10.7761 7 11 6.77614 11 6.5C11 6.22386 10.7761 6 10.5 6C10.2239 6 10 6.22386 10 6.5C10 6.77614 10.2239 7 10.5 7Z" />
|
||||
<path d="M7 8.5C7 8.77614 6.77614 9 6.5 9C6.22386 9 6 8.77614 6 8.5C6 8.22386 6.22386 8 6.5 8C6.77614 8 7 8.22386 7 8.5Z" />
|
||||
<path d="M4.5 11C4.77614 11 5 10.7761 5 10.5C5 10.2239 4.77614 10 4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11Z" />
|
||||
<path d="M5 12.5C5 12.7761 4.77614 13 4.5 13C4.22386 13 4 12.7761 4 12.5C4 12.2239 4.22386 12 4.5 12C4.77614 12 5 12.2239 5 12.5Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
.kbnToolbarButton {
|
||||
line-height: $euiButtonHeight; // Keeps alignment of text and chart icon
|
||||
|
||||
// Override background color for non-disabled buttons
|
||||
&:not(:disabled) {
|
||||
background-color: $euiColorEmptyShade;
|
||||
}
|
||||
|
||||
// todo: once issue https://github.com/elastic/eui/issues/4730 is merged, this code might be safe to remove
|
||||
// Some toolbar buttons are just icons, but EuiButton comes with margin and min-width that need to be removed
|
||||
min-width: 0;
|
||||
border-width: $euiBorderWidthThin;
|
||||
border-style: solid;
|
||||
border-color: $euiBorderColor; // Lighten the border color for all states
|
||||
|
||||
.kbnToolbarButton__text > svg {
|
||||
margin-top: -1px; // Just some weird alignment issue when icon is the child not the `iconType`
|
||||
}
|
||||
|
||||
.kbnToolbarButton__text:empty {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Toolbar buttons don't look good with centered text when fullWidth
|
||||
&[class*='fullWidth'] {
|
||||
text-align: left;
|
||||
|
||||
.kbnToolbarButton__content {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kbnToolbarButton--groupLeft {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.kbnToolbarButton--groupCenter {
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.kbnToolbarButton--groupRight {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.kbnToolbarButton--bold {
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
.kbnToolbarButton--normal {
|
||||
font-weight: $euiFontWeightRegular;
|
||||
}
|
||||
|
||||
.kbnToolbarButton--s {
|
||||
box-shadow: none !important; // sass-lint:disable-line no-important
|
||||
font-size: $euiFontSizeS;
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import './toolbar_button.scss';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { EuiButton, PropsOf, EuiButtonProps } from '@elastic/eui';
|
||||
|
||||
const groupPositionToClassMap = {
|
||||
none: null,
|
||||
left: 'kbnToolbarButton--groupLeft',
|
||||
center: 'kbnToolbarButton--groupCenter',
|
||||
right: 'kbnToolbarButton--groupRight',
|
||||
};
|
||||
|
||||
type ButtonPositions = keyof typeof groupPositionToClassMap;
|
||||
export const POSITIONS = Object.keys(groupPositionToClassMap) as ButtonPositions[];
|
||||
|
||||
type Weights = 'normal' | 'bold';
|
||||
export const WEIGHTS = ['normal', 'bold'] as Weights[];
|
||||
|
||||
export const TOOLBAR_BUTTON_SIZES: Array<EuiButtonProps['size']> = ['s', 'm'];
|
||||
|
||||
export type ToolbarButtonProps = PropsOf<typeof EuiButton> & {
|
||||
/**
|
||||
* Determines prominence
|
||||
*/
|
||||
fontWeight?: Weights;
|
||||
/**
|
||||
* Smaller buttons also remove extra shadow for less prominence
|
||||
*/
|
||||
size?: EuiButtonProps['size'];
|
||||
/**
|
||||
* Determines if the button will have a down arrow or not
|
||||
*/
|
||||
hasArrow?: boolean;
|
||||
/**
|
||||
* Adjusts the borders for groupings
|
||||
*/
|
||||
groupPosition?: ButtonPositions;
|
||||
dataTestSubj?: string;
|
||||
textProps?: EuiButtonProps['textProps'];
|
||||
};
|
||||
|
||||
export const ToolbarButton: React.FunctionComponent<ToolbarButtonProps> = ({
|
||||
children,
|
||||
className,
|
||||
fontWeight = 'normal',
|
||||
size = 'm',
|
||||
hasArrow = true,
|
||||
groupPosition = 'none',
|
||||
dataTestSubj = '',
|
||||
textProps,
|
||||
...rest
|
||||
}) => {
|
||||
const classes = classNames(
|
||||
'kbnToolbarButton',
|
||||
groupPositionToClassMap[groupPosition],
|
||||
[`kbnToolbarButton--${fontWeight}`, `kbnToolbarButton--${size}`],
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
data-test-subj={dataTestSubj}
|
||||
className={classes}
|
||||
iconSide="right"
|
||||
iconType={hasArrow ? 'arrowDown' : ''}
|
||||
color="text"
|
||||
contentProps={{
|
||||
className: 'kbnToolbarButton__content',
|
||||
}}
|
||||
textProps={{
|
||||
...textProps,
|
||||
className: classNames('kbnToolbarButton__text', textProps?.className),
|
||||
}}
|
||||
{...rest}
|
||||
size={size}
|
||||
>
|
||||
{children}
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
|
@ -282,7 +282,7 @@ export type UserMessagesDisplayLocationId = UserMessageDisplayLocation['id'];
|
|||
|
||||
export interface UserMessage {
|
||||
uniqueId?: string;
|
||||
severity: 'error' | 'warning';
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
shortMessage: string;
|
||||
longMessage: React.ReactNode | string;
|
||||
fixableInEditor: boolean;
|
||||
|
@ -475,6 +475,7 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
deps: {
|
||||
frame: FrameDatasourceAPI;
|
||||
setState: StateSetter<T>;
|
||||
visualizationInfo?: VisualizationInfo;
|
||||
}
|
||||
) => UserMessage[];
|
||||
|
||||
|
|
|
@ -25,4 +25,10 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// Make the visualization modifiers icon appear only on panel hover
|
||||
.embPanel__content:hover .lnsEmbeddablePanelFeatureList_button {
|
||||
color: $euiTextColor;
|
||||
transition: color $euiAnimSpeedSlow;
|
||||
}
|
52
x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx
Normal file
52
x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { FramePublicAPI, VisualizationInfo } from '../../types';
|
||||
import { XYAnnotationLayerConfig } from './types';
|
||||
|
||||
export function IgnoredGlobalFiltersEntries({
|
||||
layers,
|
||||
visualizationInfo,
|
||||
dataViews,
|
||||
}: {
|
||||
layers: XYAnnotationLayerConfig[];
|
||||
visualizationInfo: VisualizationInfo;
|
||||
dataViews: FramePublicAPI['dataViews'];
|
||||
}) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<>
|
||||
{layers.map((layer, layerIndex) => {
|
||||
const dataView = dataViews.indexPatterns[layer.indexPatternId];
|
||||
const layerTitle =
|
||||
visualizationInfo.layers.find(({ layerId, label }) => layerId === layer.layerId)?.label ||
|
||||
i18n.translate('xpack.lens.xyChart.layerAnnotationsLabel', {
|
||||
defaultMessage: 'Annotations',
|
||||
});
|
||||
return (
|
||||
<li
|
||||
key={`${layerTitle}-${dataView}-${layerIndex}`}
|
||||
data-test-subj={`lns-feature-badges-ignoreGlobalFilters-${layerIndex}`}
|
||||
css={css`
|
||||
margin: ${euiTheme.size.base} 0 0;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">{layerTitle}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -40,6 +40,7 @@ import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks
|
|||
import { KEEP_GLOBAL_FILTERS_ACTION_ID } from './annotations/actions';
|
||||
import { layerTypes, Visualization } from '../..';
|
||||
|
||||
const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column';
|
||||
const exampleAnnotation: EventAnnotationConfig = {
|
||||
id: 'an1',
|
||||
type: 'manual',
|
||||
|
@ -2623,8 +2624,6 @@ describe('xy_visualization', () => {
|
|||
});
|
||||
|
||||
describe('Annotation layers', () => {
|
||||
const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column';
|
||||
|
||||
function createStateWithAnnotationProps(annotation: Partial<EventAnnotationConfig>) {
|
||||
return {
|
||||
layers: [
|
||||
|
@ -2693,7 +2692,7 @@ describe('xy_visualization', () => {
|
|||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
ignoreGlobalFilters: true,
|
||||
ignoreGlobalFilters: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -2879,6 +2878,57 @@ describe('xy_visualization', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('info', () => {
|
||||
function getFrameMock() {
|
||||
const datasourceMock = createMockDatasource('testDatasource');
|
||||
datasourceMock.publicAPIMock.getOperationForColumnId.mockImplementation((id) =>
|
||||
id === DATE_HISTORGRAM_COLUMN_ID
|
||||
? ({
|
||||
label: DATE_HISTORGRAM_COLUMN_ID,
|
||||
dataType: 'date',
|
||||
scale: 'interval',
|
||||
} as OperationDescriptor)
|
||||
: ({
|
||||
dataType: 'number',
|
||||
label: 'MyOperation',
|
||||
} as OperationDescriptor)
|
||||
);
|
||||
|
||||
return createMockFramePublicAPI({
|
||||
datasourceLayers: { first: datasourceMock.publicAPIMock },
|
||||
dataViews: createMockDataViewsState({
|
||||
indexPatterns: { first: createMockedIndexPattern() },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
it('should return an info message if annotation layer is ignoring the global filters', () => {
|
||||
const initialState = exampleState();
|
||||
const state: State = {
|
||||
...initialState,
|
||||
layers: [
|
||||
...initialState.layers,
|
||||
{
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
annotations: [exampleAnnotation2],
|
||||
ignoreGlobalFilters: true,
|
||||
indexPatternId: 'myIndexPattern',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(xyVisualization.getUserMessages!(state, { frame: getFrameMock() })).toContainEqual(
|
||||
expect.objectContaining({
|
||||
displayLocations: [{ id: 'embeddableBadge' }],
|
||||
fixableInEditor: false,
|
||||
severity: 'info',
|
||||
shortMessage: 'Layers ignoring global filters',
|
||||
uniqueId: 'ignoring-global-filters-layers',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUniqueLabels', () => {
|
||||
|
|
|
@ -108,6 +108,7 @@ import {
|
|||
IGNORE_GLOBAL_FILTERS_ACTION_ID,
|
||||
KEEP_GLOBAL_FILTERS_ACTION_ID,
|
||||
} from './annotations/actions';
|
||||
import { IgnoredGlobalFiltersEntries } from './info_badges';
|
||||
|
||||
const XY_ID = 'lnsXY';
|
||||
export const getXyVisualization = ({
|
||||
|
@ -876,7 +877,9 @@ export const getXyVisualization = ({
|
|||
);
|
||||
}
|
||||
|
||||
return [...errors, ...warnings];
|
||||
const info = getNotifiableFeatures(state, frame.dataViews);
|
||||
|
||||
return errors.concat(warnings, info);
|
||||
},
|
||||
|
||||
getUniqueLabels(state) {
|
||||
|
@ -919,88 +922,7 @@ export const getXyVisualization = ({
|
|||
return suggestion;
|
||||
},
|
||||
|
||||
getVisualizationInfo(state: XYState) {
|
||||
const isHorizontal = isHorizontalChart(state.layers);
|
||||
const visualizationLayersInfo = state.layers.map((layer) => {
|
||||
const dimensions = [];
|
||||
let chartType: SeriesType | undefined;
|
||||
let icon;
|
||||
let label;
|
||||
if (isDataLayer(layer)) {
|
||||
chartType = layer.seriesType;
|
||||
const layerVisType = visualizationTypes.find((visType) => visType.id === chartType);
|
||||
icon = layerVisType?.icon;
|
||||
label = layerVisType?.fullLabel || layerVisType?.label;
|
||||
if (layer.xAccessor) {
|
||||
dimensions.push({
|
||||
name: getAxisName('x', { isHorizontal }),
|
||||
id: layer.xAccessor,
|
||||
dimensionType: 'x',
|
||||
});
|
||||
}
|
||||
if (layer.accessors && layer.accessors.length) {
|
||||
layer.accessors.forEach((accessor) => {
|
||||
dimensions.push({
|
||||
name: getAxisName('y', { isHorizontal }),
|
||||
id: accessor,
|
||||
dimensionType: 'y',
|
||||
});
|
||||
});
|
||||
}
|
||||
if (layer.splitAccessor) {
|
||||
dimensions.push({
|
||||
name: i18n.translate('xpack.lens.xyChart.splitSeries', {
|
||||
defaultMessage: 'Breakdown',
|
||||
}),
|
||||
dimensionType: 'breakdown',
|
||||
id: layer.splitAccessor,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isReferenceLayer(layer) && layer.accessors && layer.accessors.length) {
|
||||
layer.accessors.forEach((accessor) => {
|
||||
dimensions.push({
|
||||
name: i18n.translate('xpack.lens.xyChart.layerReferenceLine', {
|
||||
defaultMessage: 'Reference line',
|
||||
}),
|
||||
dimensionType: 'reference_line',
|
||||
id: accessor,
|
||||
});
|
||||
});
|
||||
label = i18n.translate('xpack.lens.xyChart.layerReferenceLineLabel', {
|
||||
defaultMessage: 'Reference lines',
|
||||
});
|
||||
icon = IconChartBarReferenceLine;
|
||||
}
|
||||
if (isAnnotationsLayer(layer) && layer.annotations && layer.annotations.length) {
|
||||
layer.annotations.forEach((annotation) => {
|
||||
dimensions.push({
|
||||
name: i18n.translate('xpack.lens.xyChart.layerAnnotation', {
|
||||
defaultMessage: 'Annotation',
|
||||
}),
|
||||
dimensionType: 'annotation',
|
||||
id: annotation.id,
|
||||
});
|
||||
});
|
||||
label = i18n.translate('xpack.lens.xyChart.layerAnnotationsLabel', {
|
||||
defaultMessage: 'Annotations',
|
||||
});
|
||||
icon = IconChartBarAnnotations;
|
||||
}
|
||||
|
||||
return {
|
||||
layerId: layer.layerId,
|
||||
layerType: layer.layerType,
|
||||
chartType,
|
||||
icon,
|
||||
label,
|
||||
dimensions,
|
||||
};
|
||||
});
|
||||
return {
|
||||
layers: visualizationLayersInfo,
|
||||
};
|
||||
},
|
||||
getVisualizationInfo,
|
||||
});
|
||||
|
||||
const getMappedAccessors = ({
|
||||
|
@ -1040,3 +962,118 @@ const getMappedAccessors = ({
|
|||
}
|
||||
return mappedAccessors;
|
||||
};
|
||||
|
||||
function getVisualizationInfo(state: XYState) {
|
||||
const isHorizontal = isHorizontalChart(state.layers);
|
||||
const visualizationLayersInfo = state.layers.map((layer) => {
|
||||
const dimensions = [];
|
||||
let chartType: SeriesType | undefined;
|
||||
let icon;
|
||||
let label;
|
||||
if (isDataLayer(layer)) {
|
||||
chartType = layer.seriesType;
|
||||
const layerVisType = visualizationTypes.find((visType) => visType.id === chartType);
|
||||
icon = layerVisType?.icon;
|
||||
label = layerVisType?.fullLabel || layerVisType?.label;
|
||||
if (layer.xAccessor) {
|
||||
dimensions.push({
|
||||
name: getAxisName('x', { isHorizontal }),
|
||||
id: layer.xAccessor,
|
||||
dimensionType: 'x',
|
||||
});
|
||||
}
|
||||
if (layer.accessors && layer.accessors.length) {
|
||||
layer.accessors.forEach((accessor) => {
|
||||
dimensions.push({
|
||||
name: getAxisName('y', { isHorizontal }),
|
||||
id: accessor,
|
||||
dimensionType: 'y',
|
||||
});
|
||||
});
|
||||
}
|
||||
if (layer.splitAccessor) {
|
||||
dimensions.push({
|
||||
name: i18n.translate('xpack.lens.xyChart.splitSeries', {
|
||||
defaultMessage: 'Breakdown',
|
||||
}),
|
||||
dimensionType: 'breakdown',
|
||||
id: layer.splitAccessor,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isReferenceLayer(layer) && layer.accessors && layer.accessors.length) {
|
||||
layer.accessors.forEach((accessor) => {
|
||||
dimensions.push({
|
||||
name: i18n.translate('xpack.lens.xyChart.layerReferenceLine', {
|
||||
defaultMessage: 'Reference line',
|
||||
}),
|
||||
dimensionType: 'reference_line',
|
||||
id: accessor,
|
||||
});
|
||||
});
|
||||
label = i18n.translate('xpack.lens.xyChart.layerReferenceLineLabel', {
|
||||
defaultMessage: 'Reference lines',
|
||||
});
|
||||
icon = IconChartBarReferenceLine;
|
||||
}
|
||||
if (isAnnotationsLayer(layer) && layer.annotations && layer.annotations.length) {
|
||||
layer.annotations.forEach((annotation) => {
|
||||
dimensions.push({
|
||||
name: i18n.translate('xpack.lens.xyChart.layerAnnotation', {
|
||||
defaultMessage: 'Annotation',
|
||||
}),
|
||||
dimensionType: 'annotation',
|
||||
id: annotation.id,
|
||||
});
|
||||
});
|
||||
label = i18n.translate('xpack.lens.xyChart.layerAnnotationsLabel', {
|
||||
defaultMessage: 'Annotations',
|
||||
});
|
||||
icon = IconChartBarAnnotations;
|
||||
}
|
||||
|
||||
return {
|
||||
layerId: layer.layerId,
|
||||
layerType: layer.layerType,
|
||||
chartType,
|
||||
icon,
|
||||
label,
|
||||
dimensions,
|
||||
};
|
||||
});
|
||||
return {
|
||||
layers: visualizationLayersInfo,
|
||||
};
|
||||
}
|
||||
|
||||
function getNotifiableFeatures(
|
||||
state: XYState,
|
||||
dataViews: FramePublicAPI['dataViews']
|
||||
): UserMessage[] {
|
||||
const annotationsWithIgnoreFlag = getAnnotationsLayers(state.layers).filter(
|
||||
(layer) => layer.ignoreGlobalFilters
|
||||
);
|
||||
if (!annotationsWithIgnoreFlag.length) {
|
||||
return [];
|
||||
}
|
||||
const visualizationInfo = getVisualizationInfo(state);
|
||||
|
||||
return [
|
||||
{
|
||||
uniqueId: 'ignoring-global-filters-layers',
|
||||
severity: 'info',
|
||||
fixableInEditor: false,
|
||||
shortMessage: i18n.translate('xpack.lens.xyChart.layerAnnotationsIgnoreTitle', {
|
||||
defaultMessage: 'Layers ignoring global filters',
|
||||
}),
|
||||
longMessage: (
|
||||
<IgnoredGlobalFiltersEntries
|
||||
layers={annotationsWithIgnoreFlag}
|
||||
visualizationInfo={visualizationInfo}
|
||||
dataViews={dataViews}
|
||||
/>
|
||||
),
|
||||
displayLocations: [{ id: 'embeddableBadge' }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -19199,7 +19199,6 @@
|
|||
"xpack.lens.xyChart.annotationError.textFieldNotFound": "Champ de texte {textField} introuvable dans la vue de données {dataView}",
|
||||
"xpack.lens.xyChart.annotationError.timeFieldNotFound": "Champ temporel {timeField} introuvable dans la vue de données {dataView}",
|
||||
"xpack.lens.xyChart.annotationError.tooltipFieldNotFound": "{missingFields, plural, one {Champ d'infobulle introuvable} other {Champs d'infobulle introuvables}} {missingTooltipFields} dans la vue de données {dataView}",
|
||||
"xpack.lens.xyChart.randomSampling.help": "Des pourcentages d'échantillonnage plus faibles augmentent la vitesse, mais diminuent la précision. Une bonne pratique consiste à utiliser un échantillonnage plus faible uniquement pour les ensembles de données volumineux. {link}",
|
||||
"xpack.lens.xySuggestions.dateSuggestion": "{yTitle} sur {xTitle}",
|
||||
"xpack.lens.xySuggestions.nonDateSuggestion": "{yTitle} de {xTitle}",
|
||||
"xpack.lens.xyVisualization.arrayValues": "{label} contient des valeurs de tableau. Le rendu de votre visualisation peut ne pas se présenter comme attendu.",
|
||||
|
@ -19939,7 +19938,6 @@
|
|||
"xpack.lens.primaryMetric.headingLabel": "Valeur",
|
||||
"xpack.lens.primaryMetric.label": "Indicateur principal",
|
||||
"xpack.lens.queryInput.appName": "Lens",
|
||||
"xpack.lens.randomSampling.experimentalLabel": "Version d'évaluation technique",
|
||||
"xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "Téléchargement CSV",
|
||||
"xpack.lens.resetLayerAriaLabel": "Effacer le calque",
|
||||
"xpack.lens.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.",
|
||||
|
@ -20151,10 +20149,6 @@
|
|||
"xpack.lens.xyChart.missingValuesStyle": "Afficher sous la forme d’une ligne pointillée",
|
||||
"xpack.lens.xyChart.nestUnderRoot": "Ensemble de données entier",
|
||||
"xpack.lens.xyChart.placement": "Placement",
|
||||
"xpack.lens.xyChart.randomSampling.accuracyLabel": "Précision",
|
||||
"xpack.lens.xyChart.randomSampling.label": "Échantillonnage aléatoire",
|
||||
"xpack.lens.xyChart.randomSampling.learnMore": "Afficher la documentation",
|
||||
"xpack.lens.xyChart.randomSampling.speedLabel": "Rapidité",
|
||||
"xpack.lens.xyChart.rightAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe de droite est activé.",
|
||||
"xpack.lens.xyChart.rightAxisLabel": "Axe de droite",
|
||||
"xpack.lens.xyChart.scaleLinear": "Linéaire",
|
||||
|
|
|
@ -19198,7 +19198,6 @@
|
|||
"xpack.lens.xyChart.annotationError.textFieldNotFound": "テキストフィールド{textField}がデータビュー{dataView}で見つかりません",
|
||||
"xpack.lens.xyChart.annotationError.timeFieldNotFound": "時刻フィールド{timeField}がデータビュー{dataView}で見つかりません",
|
||||
"xpack.lens.xyChart.annotationError.tooltipFieldNotFound": "フィールド{missingFields, plural, other {フィールド}}{missingTooltipFields}がデータビュー{dataView}で見つかりません",
|
||||
"xpack.lens.xyChart.randomSampling.help": "サンプリング割合が低いと、速度が上がりますが、精度が低下します。ベストプラクティスとして、大きいデータセットの場合にのみ低サンプリングを使用してください。{link}",
|
||||
"xpack.lens.xySuggestions.dateSuggestion": "{xTitle} の {yTitle}",
|
||||
"xpack.lens.xySuggestions.nonDateSuggestion": "{yTitle} / {xTitle}",
|
||||
"xpack.lens.xyVisualization.arrayValues": "{label}には配列値が含まれます。可視化が想定通りに表示されない場合があります。",
|
||||
|
@ -19939,7 +19938,6 @@
|
|||
"xpack.lens.primaryMetric.headingLabel": "値",
|
||||
"xpack.lens.primaryMetric.label": "主メトリック",
|
||||
"xpack.lens.queryInput.appName": "レンズ",
|
||||
"xpack.lens.randomSampling.experimentalLabel": "テクニカルプレビュー",
|
||||
"xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "CSVダウンロード",
|
||||
"xpack.lens.resetLayerAriaLabel": "レイヤーをクリア",
|
||||
"xpack.lens.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました",
|
||||
|
@ -20151,10 +20149,6 @@
|
|||
"xpack.lens.xyChart.missingValuesStyle": "点線として表示",
|
||||
"xpack.lens.xyChart.nestUnderRoot": "データセット全体",
|
||||
"xpack.lens.xyChart.placement": "配置",
|
||||
"xpack.lens.xyChart.randomSampling.accuracyLabel": "精度",
|
||||
"xpack.lens.xyChart.randomSampling.label": "無作為抽出",
|
||||
"xpack.lens.xyChart.randomSampling.learnMore": "ドキュメンテーションを表示",
|
||||
"xpack.lens.xyChart.randomSampling.speedLabel": "スピード",
|
||||
"xpack.lens.xyChart.rightAxisDisabledHelpText": "この設定は、右の軸が有効であるときにのみ適用されます。",
|
||||
"xpack.lens.xyChart.rightAxisLabel": "右の軸",
|
||||
"xpack.lens.xyChart.scaleLinear": "線形",
|
||||
|
|
|
@ -19199,7 +19199,6 @@
|
|||
"xpack.lens.xyChart.annotationError.textFieldNotFound": "在数据视图 {dataView} 中未找到文本字段 {textField}",
|
||||
"xpack.lens.xyChart.annotationError.timeFieldNotFound": "在数据视图 {dataView} 中未找到时间字段 {timeField}",
|
||||
"xpack.lens.xyChart.annotationError.tooltipFieldNotFound": "在数据视图 {dataView} 中未找到工具提示{missingFields, plural, other {字段}} {missingTooltipFields}",
|
||||
"xpack.lens.xyChart.randomSampling.help": "较低采样百分比会提高速度,但会降低准确性。作为最佳做法,请仅将较低采样用于大型数据库。{link}",
|
||||
"xpack.lens.xySuggestions.dateSuggestion": "{yTitle} / {xTitle}",
|
||||
"xpack.lens.xySuggestions.nonDateSuggestion": "{xTitle} 的 {yTitle}",
|
||||
"xpack.lens.xyVisualization.arrayValues": "{label} 包含数组值。您的可视化可能无法正常渲染。",
|
||||
|
@ -19940,7 +19939,6 @@
|
|||
"xpack.lens.primaryMetric.headingLabel": "值",
|
||||
"xpack.lens.primaryMetric.label": "主要指标",
|
||||
"xpack.lens.queryInput.appName": "Lens",
|
||||
"xpack.lens.randomSampling.experimentalLabel": "技术预览",
|
||||
"xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 下载",
|
||||
"xpack.lens.resetLayerAriaLabel": "清除图层",
|
||||
"xpack.lens.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认",
|
||||
|
@ -20152,10 +20150,6 @@
|
|||
"xpack.lens.xyChart.missingValuesStyle": "显示为虚线",
|
||||
"xpack.lens.xyChart.nestUnderRoot": "整个数据集",
|
||||
"xpack.lens.xyChart.placement": "位置",
|
||||
"xpack.lens.xyChart.randomSampling.accuracyLabel": "准确性",
|
||||
"xpack.lens.xyChart.randomSampling.label": "随机采样",
|
||||
"xpack.lens.xyChart.randomSampling.learnMore": "查看文档",
|
||||
"xpack.lens.xyChart.randomSampling.speedLabel": "速度",
|
||||
"xpack.lens.xyChart.rightAxisDisabledHelpText": "此设置仅在启用右轴时应用。",
|
||||
"xpack.lens.xyChart.rightAxisLabel": "右轴",
|
||||
"xpack.lens.xyChart.scaleLinear": "线性",
|
||||
|
|
|
@ -13,13 +13,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const find = getService('find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
// skip random sampling FTs until we figure out next steps
|
||||
describe.skip('lens layer actions tests', () => {
|
||||
describe('lens layer actions tests', () => {
|
||||
it('should allow creation of lens xy chart', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
|
||||
// check that no sampling info is shown in the dataView picker
|
||||
expect(await testSubjects.exists('lnsChangeIndexPatternSamplingInfo')).to.be(false);
|
||||
|
||||
await PageObjects.lens.openLayerContextMenu();
|
||||
|
||||
// should be 3 actions available
|
||||
|
@ -28,18 +30,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
).to.eql(3);
|
||||
});
|
||||
|
||||
it('should open layer settings for a data layer', async () => {
|
||||
it('should open layer settings for a data layer and set a sampling rate', async () => {
|
||||
// click on open layer settings
|
||||
await testSubjects.click('lnsLayerSettings');
|
||||
// random sampling available
|
||||
await testSubjects.existOrFail('lns-indexPattern-random-sampling-row');
|
||||
// tweak the value
|
||||
await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling', 2, 'left');
|
||||
await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling-slider', 2, 'left');
|
||||
|
||||
expect(await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling')).to.eql(
|
||||
2 // 0.01
|
||||
expect(
|
||||
await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling-slider')
|
||||
).to.eql(
|
||||
3 // 1%
|
||||
);
|
||||
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
|
||||
|
||||
// now check that the dataView picker has the sampling info
|
||||
await testSubjects.existOrFail('lnsChangeIndexPatternSamplingInfo');
|
||||
expect(await testSubjects.getVisibleText('lnsChangeIndexPatternSamplingInfo')).to.be('1%');
|
||||
});
|
||||
|
||||
it('should add an annotation layer and settings shoud not be available', async () => {
|
||||
|
@ -56,13 +64,54 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
field: 'bytes',
|
||||
});
|
||||
// add annotation layer
|
||||
await testSubjects.click('lnsLayerAddButton');
|
||||
await testSubjects.click(`lnsLayerAddButton-annotations`);
|
||||
await PageObjects.lens.createLayer('annotations');
|
||||
await PageObjects.lens.openLayerContextMenu(1);
|
||||
await testSubjects.existOrFail('lnsXY_annotationLayer_keepFilters');
|
||||
// layer settings not available
|
||||
await testSubjects.missingOrFail('lnsLayerSettings');
|
||||
});
|
||||
|
||||
it('should add a new visualization layer and disable the sampling if max operation is chosen', async () => {
|
||||
await PageObjects.lens.createLayer('data');
|
||||
|
||||
await PageObjects.lens.openLayerContextMenu(2);
|
||||
// click on open layer settings
|
||||
await testSubjects.click('lnsLayerSettings');
|
||||
// tweak the value
|
||||
await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling-slider', 2, 'left');
|
||||
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
|
||||
// check the sampling is shown
|
||||
await testSubjects.existOrFail('lns-layerPanel-2 > lnsChangeIndexPatternSamplingInfo');
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lns-layerPanel-2 > lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
});
|
||||
|
||||
// now configure a max operation
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lns-layerPanel-2 > lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'max',
|
||||
field: 'bytes',
|
||||
keepOpen: true, // keep it open as the toast will cover the close button anyway
|
||||
});
|
||||
|
||||
// close the toast about disabling sampling
|
||||
// note: this has also the side effect to close the dimension editor
|
||||
await testSubjects.click('toastCloseButton');
|
||||
|
||||
// check that sampling info is hidden as disabled now the dataView picker
|
||||
await testSubjects.missingOrFail('lns-layerPanel-2 > lnsChangeIndexPatternSamplingInfo');
|
||||
// open the layer settings and check that the slider is disabled
|
||||
await PageObjects.lens.openLayerContextMenu(2);
|
||||
// click on open layer settings
|
||||
await testSubjects.click('lnsLayerSettings');
|
||||
expect(
|
||||
await testSubjects.getAttribute('lns-indexPattern-random-sampling-slider', 'disabled')
|
||||
).to.be('true');
|
||||
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
|
||||
});
|
||||
|
||||
it('should switch to pie chart and have layer settings available', async () => {
|
||||
await PageObjects.lens.switchToVisualization('pie');
|
||||
await PageObjects.lens.openLayerContextMenu();
|
||||
|
@ -70,8 +119,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
// open the panel
|
||||
await testSubjects.click('lnsLayerSettings');
|
||||
// check the sampling value
|
||||
expect(await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling')).to.eql(
|
||||
2 // 0.01
|
||||
expect(
|
||||
await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling-slider')
|
||||
).to.eql(
|
||||
3 // 1%
|
||||
);
|
||||
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
|
||||
});
|
||||
|
@ -83,10 +134,79 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
// open the panel
|
||||
await testSubjects.click('lnsLayerSettings');
|
||||
// check the sampling value
|
||||
expect(await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling')).to.eql(
|
||||
2 // 0.01
|
||||
expect(
|
||||
await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling-slider')
|
||||
).to.eql(
|
||||
3 // 1%
|
||||
);
|
||||
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
|
||||
});
|
||||
|
||||
it('should show visualization modifiers for layer settings when embedded in a dashboard', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
// click on open layer settings
|
||||
await PageObjects.lens.openLayerContextMenu();
|
||||
await testSubjects.click('lnsLayerSettings');
|
||||
// tweak the value
|
||||
await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling-slider', 2, 'left');
|
||||
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
|
||||
// add another layer with a different sampling rate
|
||||
await PageObjects.lens.createLayer('data');
|
||||
|
||||
await PageObjects.lens.openLayerContextMenu(1);
|
||||
// click on open layer settings
|
||||
await testSubjects.click('lnsLayerSettings');
|
||||
// tweak the value
|
||||
await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling-slider', 3, 'left');
|
||||
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
|
||||
// add annotation layer
|
||||
// by default annotations ignore global filters
|
||||
await PageObjects.lens.createLayer('annotations');
|
||||
|
||||
await PageObjects.lens.save('sampledVisualization', false, true, false, 'new');
|
||||
|
||||
// now check for the bottom-left badge
|
||||
await testSubjects.existOrFail('lns-feature-badges-trigger');
|
||||
|
||||
// click on the badge and check the popover
|
||||
await testSubjects.click('lns-feature-badges-trigger');
|
||||
expect(
|
||||
(await testSubjects.getVisibleText('lns-feature-badges-reducedSampling-0')).split('\n')
|
||||
).to.contain('1%');
|
||||
expect(
|
||||
(await testSubjects.getVisibleText('lns-feature-badges-reducedSampling-1')).split('\n')
|
||||
).to.contain('0.1%');
|
||||
expect(
|
||||
(await testSubjects.getVisibleText('lns-feature-badges-ignoreGlobalFilters-0')).split('\n')
|
||||
).to.contain('Annotations');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue