mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens] Don't block render on missing field (#149262)
This commit is contained in:
parent
4928487c32
commit
be37fa1190
64 changed files with 1582 additions and 739 deletions
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '@kbn/kibana-utils-plugin/common';
|
||||
import { SavedFieldTypeInvalidForAgg } from '@kbn/kibana-utils-plugin/common';
|
||||
import { isNestedField, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { IAggConfig } from '../agg_config';
|
||||
import { BaseParamType } from './base';
|
||||
|
@ -43,6 +43,7 @@ export class FieldParamType extends BaseParamType {
|
|||
this.scriptable = config.scriptable !== false;
|
||||
this.filterField = config.filterField;
|
||||
|
||||
// TODO - are there any custom write methods that do a missing check?
|
||||
if (!config.write) {
|
||||
this.write = (aggConfig: IAggConfig, output: Record<string, any>) => {
|
||||
const field = aggConfig.getField();
|
||||
|
@ -59,24 +60,10 @@ export class FieldParamType extends BaseParamType {
|
|||
);
|
||||
}
|
||||
|
||||
if (field.type === KBN_FIELD_TYPES.MISSING) {
|
||||
throw new SavedFieldNotFound(
|
||||
i18n.translate(
|
||||
'data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The field "{fieldParameter}" associated with this object no longer exists in the data view. Please use another field.',
|
||||
values: {
|
||||
fieldParameter: field.name,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const validField = this.getAvailableFields(aggConfig).find(
|
||||
(f: any) => f.name === field.name
|
||||
);
|
||||
const validField =
|
||||
field.type === KBN_FIELD_TYPES.MISSING // missing fields are always valid
|
||||
? field
|
||||
: this.getAvailableFields(aggConfig).find((f: any) => f.name === field.name);
|
||||
|
||||
if (!validField) {
|
||||
throw new SavedFieldTypeInvalidForAgg(
|
||||
|
|
|
@ -46,7 +46,7 @@ describe('extract search response warnings', () => {
|
|||
'Field [kubernetes.container.memory.available.bytes] of type' +
|
||||
' [aggregate_metric_double] is not supported for aggregation [percentiles]',
|
||||
},
|
||||
text: 'The data you are seeing might be incomplete or wrong.',
|
||||
text: 'The data might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -78,7 +78,7 @@ describe('extract search response warnings', () => {
|
|||
type: 'shard_failure',
|
||||
message: '77 of 79 shards failed',
|
||||
reason: { type: 'generic_shard_warning' },
|
||||
text: 'The data you are seeing might be incomplete or wrong.',
|
||||
text: 'The data might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -95,7 +95,7 @@ describe('extract search response warnings', () => {
|
|||
type: 'shard_failure',
|
||||
message: '77 of 79 shards failed',
|
||||
reason: { type: 'generic_shard_warning' },
|
||||
text: 'The data you are seeing might be incomplete or wrong.',
|
||||
text: 'The data might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ export function extractWarnings(rawResponse: estypes.SearchResponse): SearchResp
|
|||
);
|
||||
const text = i18n.translate(
|
||||
'data.search.searchSource.fetch.shardsFailedNotificationDescription',
|
||||
{ defaultMessage: 'The data you are seeing might be incomplete or wrong.' }
|
||||
{ defaultMessage: 'The data might be incomplete or wrong.' }
|
||||
);
|
||||
|
||||
if (rawResponse._shards.failures) {
|
||||
|
|
|
@ -198,7 +198,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
type: 'shard_failure',
|
||||
message: '2 of 4 shards failed',
|
||||
reason: { reason: shardFailureReason, type: shardFailureType },
|
||||
text: 'The data you are seeing might be incomplete or wrong.',
|
||||
text: 'The data might be incomplete or wrong.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -41,7 +41,10 @@ import {
|
|||
createIndexPatternService,
|
||||
} from '../data_views_service/service';
|
||||
import { replaceIndexpattern } from '../state_management/lens_slice';
|
||||
import { filterUserMessages, getApplicationUserMessages } from './get_application_user_messages';
|
||||
import {
|
||||
filterAndSortUserMessages,
|
||||
getApplicationUserMessages,
|
||||
} from './get_application_user_messages';
|
||||
|
||||
export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
|
@ -538,10 +541,10 @@ export function App({
|
|||
);
|
||||
|
||||
const getUserMessages: UserMessagesGetter = (locationId, filterArgs) =>
|
||||
filterUserMessages(
|
||||
filterAndSortUserMessages(
|
||||
[...userMessages, ...Object.values(additionalUserMessages)],
|
||||
locationId,
|
||||
filterArgs
|
||||
filterArgs ?? {}
|
||||
);
|
||||
|
||||
const addUserMessages: AddUserMessages = (messages) => {
|
||||
|
|
|
@ -14,7 +14,10 @@ import { shallow } from 'enzyme';
|
|||
import { Visualization } from '..';
|
||||
import { DataViewsState } from '../state_management';
|
||||
import { Datasource, UserMessage } from '../types';
|
||||
import { filterUserMessages, getApplicationUserMessages } from './get_application_user_messages';
|
||||
import {
|
||||
filterAndSortUserMessages,
|
||||
getApplicationUserMessages,
|
||||
} from './get_application_user_messages';
|
||||
|
||||
describe('application-level user messages', () => {
|
||||
it('should generate error if vis type is not provided', () => {
|
||||
|
@ -209,14 +212,14 @@ describe('filtering user messages', () => {
|
|||
{
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'dimensionTrigger', dimensionId: dimensionId1 }],
|
||||
displayLocations: [{ id: 'dimensionButton', dimensionId: dimensionId1 }],
|
||||
shortMessage: 'Warning on dimension 1!',
|
||||
longMessage: '',
|
||||
},
|
||||
{
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'dimensionTrigger', dimensionId: dimensionId2 }],
|
||||
displayLocations: [{ id: 'dimensionButton', dimensionId: dimensionId2 }],
|
||||
shortMessage: 'Warning on dimension 2!',
|
||||
longMessage: '',
|
||||
},
|
||||
|
@ -251,7 +254,7 @@ describe('filtering user messages', () => {
|
|||
];
|
||||
|
||||
it('filters by location', () => {
|
||||
expect(filterUserMessages(userMessages, 'banner', {})).toMatchInlineSnapshot(`
|
||||
expect(filterAndSortUserMessages(userMessages, 'banner', {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
|
@ -267,7 +270,7 @@ describe('filtering user messages', () => {
|
|||
]
|
||||
`);
|
||||
expect(
|
||||
filterUserMessages(userMessages, 'dimensionTrigger', {
|
||||
filterAndSortUserMessages(userMessages, 'dimensionButton', {
|
||||
dimensionId: dimensionId1,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
|
@ -276,7 +279,7 @@ describe('filtering user messages', () => {
|
|||
"displayLocations": Array [
|
||||
Object {
|
||||
"dimensionId": "foo",
|
||||
"id": "dimensionTrigger",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
|
@ -287,7 +290,7 @@ describe('filtering user messages', () => {
|
|||
]
|
||||
`);
|
||||
expect(
|
||||
filterUserMessages(userMessages, 'dimensionTrigger', {
|
||||
filterAndSortUserMessages(userMessages, 'dimensionButton', {
|
||||
dimensionId: dimensionId2,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
|
@ -296,7 +299,7 @@ describe('filtering user messages', () => {
|
|||
"displayLocations": Array [
|
||||
Object {
|
||||
"dimensionId": "baz",
|
||||
"id": "dimensionTrigger",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
|
@ -306,7 +309,7 @@ describe('filtering user messages', () => {
|
|||
},
|
||||
]
|
||||
`);
|
||||
expect(filterUserMessages(userMessages, ['visualization', 'visualizationInEditor'], {}))
|
||||
expect(filterAndSortUserMessages(userMessages, ['visualization', 'visualizationInEditor'], {}))
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
|
@ -336,8 +339,8 @@ describe('filtering user messages', () => {
|
|||
});
|
||||
|
||||
it('filters by severity', () => {
|
||||
const warnings = filterUserMessages(userMessages, undefined, { severity: 'warning' });
|
||||
const errors = filterUserMessages(userMessages, undefined, { severity: 'error' });
|
||||
const warnings = filterAndSortUserMessages(userMessages, undefined, { severity: 'warning' });
|
||||
const errors = filterAndSortUserMessages(userMessages, undefined, { severity: 'error' });
|
||||
|
||||
expect(warnings.length + errors.length).toBe(userMessages.length);
|
||||
expect(warnings.every((message) => message.severity === 'warning'));
|
||||
|
@ -346,7 +349,7 @@ describe('filtering user messages', () => {
|
|||
|
||||
it('filters by both', () => {
|
||||
expect(
|
||||
filterUserMessages(userMessages, ['visualization', 'visualizationOnEmbeddable'], {
|
||||
filterAndSortUserMessages(userMessages, ['visualization', 'visualizationOnEmbeddable'], {
|
||||
severity: 'warning',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
|
@ -365,4 +368,19 @@ describe('filtering user messages', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('sorts with warnings after errors', () => {
|
||||
expect(
|
||||
filterAndSortUserMessages(userMessages, undefined, {}).map((message) => message.severity)
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"error",
|
||||
"error",
|
||||
"error",
|
||||
"warning",
|
||||
"warning",
|
||||
"warning",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -180,7 +180,7 @@ function getMissingIndexPatternsErrors(
|
|||
];
|
||||
}
|
||||
|
||||
export const filterUserMessages = (
|
||||
export const filterAndSortUserMessages = (
|
||||
userMessages: UserMessage[],
|
||||
locationId: UserMessagesDisplayLocationId | UserMessagesDisplayLocationId[] | undefined,
|
||||
{ dimensionId, severity }: UserMessageFilters
|
||||
|
@ -191,14 +191,14 @@ export const filterUserMessages = (
|
|||
? [locationId]
|
||||
: [];
|
||||
|
||||
return userMessages.filter((message) => {
|
||||
const filteredMessages = userMessages.filter((message) => {
|
||||
if (locationIds.length) {
|
||||
const hasMatch = message.displayLocations.some((location) => {
|
||||
if (!locationIds.includes(location.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (location.id === 'dimensionTrigger' && location.dimensionId !== dimensionId) {
|
||||
if (location.id === 'dimensionButton' && location.dimensionId !== dimensionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -216,4 +216,16 @@ export const filterUserMessages = (
|
|||
|
||||
return true;
|
||||
});
|
||||
|
||||
return filteredMessages.sort(bySeverity);
|
||||
};
|
||||
|
||||
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 {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
3
x-pack/plugins/lens/public/assets/error.svg
Normal file
3
x-pack/plugins/lens/public/assets/error.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10 1a1 1 0 0 1 .707.293l4 4A1 1 0 0 1 15 6v5a1 1 0 0 1-.293.707l-4 4A1 1 0 0 1 10 16H5a1 1 0 0 1-.707-.293l-4-4A1 1 0 0 1 0 11V6a1 1 0 0 1 .293-.707l4-4A1 1 0 0 1 5 1h5ZM4.146 5.146a.5.5 0 0 1 .708 0L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708.708L8.207 8.5l2.647 2.646a.5.5 0 0 1-.708.708L7.5 9.207l-2.646 2.647a.5.5 0 0 1-.708-.708L6.793 8.5 4.146 5.854a.5.5 0 0 1 0-.708Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 495 B |
4
x-pack/plugins/lens/public/assets/warning.svg
Normal file
4
x-pack/plugins/lens/public/assets/warning.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="m8.55 9.502.35-3.507a.905.905 0 1 0-1.8 0l.35 3.507a.552.552 0 0 0 1.1 0ZM9 12a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/>
|
||||
<path d="M8.864 1.496a1 1 0 0 0-1.728 0l-7 12A1 1 0 0 0 1 15h14a1 1 0 0 0 .864-1.504l-7-12ZM1 14 8 2l7 12H1Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 327 B |
|
@ -52,3 +52,57 @@ Object {
|
|||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`indexpattern_datasource utils getPrecisionErrorWarningMessages if has precision error and sorting is by count ascending, show fix action and switch to rare terms 1`] = `
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": "",
|
||||
"severity": "warning",
|
||||
"shortMessage": "This may be approximate depending on how the data is indexed. For more precise results, sort by rarity.",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`indexpattern_datasource utils getPrecisionErrorWarningMessages precision error warning with accuracy mode should other suggestions if accuracy mode already enabled 1`] = `
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": "",
|
||||
"severity": "warning",
|
||||
"shortMessage": "This might be an approximation. For more precise results, use Filters or increase the number of Top Values.",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`indexpattern_datasource utils getPrecisionErrorWarningMessages precision error warning with accuracy mode should show accuracy mode prompt if currently disabled 1`] = `
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": "",
|
||||
"severity": "warning",
|
||||
"shortMessage": "This might be an approximation. For more precise results, you can enable accuracy mode, but it increases the load on the Elasticsearch cluster.",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -341,8 +341,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
});
|
||||
|
||||
const currentFieldIsInvalid = useMemo(
|
||||
() => fieldIsInvalid(selectedColumn, currentIndexPattern),
|
||||
[selectedColumn, currentIndexPattern]
|
||||
() => fieldIsInvalid(state.layers[layerId], columnId, currentIndexPattern),
|
||||
[state.layers, layerId, columnId, currentIndexPattern]
|
||||
);
|
||||
|
||||
const shouldDisplayDots =
|
||||
|
|
|
@ -50,7 +50,7 @@ import { createMockedFullReference } from './operations/mocks';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { createMockFramePublicAPI } from '../../mocks';
|
||||
import { filterUserMessages } from '../../app_plugin/get_application_user_messages';
|
||||
import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages';
|
||||
|
||||
jest.mock('./loader');
|
||||
jest.mock('../../id_generator');
|
||||
|
@ -3134,6 +3134,106 @@ describe('IndexPattern Data Source', () => {
|
|||
`);
|
||||
expect(getErrorMessages).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
describe('dimension button error behavior', () => {
|
||||
const state: FormBasedPrivateState = {
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: '1',
|
||||
columnOrder: [],
|
||||
columns: {
|
||||
col1: {
|
||||
operationType: 'terms',
|
||||
filter: {
|
||||
query: '::: bad query that will mark column invalid',
|
||||
language: 'kuery',
|
||||
},
|
||||
|
||||
sourceField: 'op',
|
||||
params: {
|
||||
size: 5,
|
||||
orderBy: { type: 'alphabetical' },
|
||||
orderDirection: 'asc',
|
||||
},
|
||||
label: 'My Op',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
} as TermsIndexPatternColumn,
|
||||
},
|
||||
},
|
||||
},
|
||||
currentIndexPatternId: '1',
|
||||
};
|
||||
|
||||
it('should generate generic error if column invalid', () => {
|
||||
(getErrorMessages as jest.Mock).mockClear();
|
||||
(getErrorMessages as jest.Mock).mockReturnValueOnce([]);
|
||||
|
||||
const messages = FormBasedDatasource.getUserMessages(state, {
|
||||
frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
|
||||
setState: () => {},
|
||||
});
|
||||
|
||||
expect(messages.length).toBe(1);
|
||||
|
||||
expect(messages).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": <p>
|
||||
Invalid configuration.
|
||||
<br />
|
||||
Click for more details.
|
||||
</p>,
|
||||
"severity": "error",
|
||||
"shortMessage": "",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should override generic error if operation generates something specific', () => {
|
||||
(getErrorMessages as jest.Mock).mockClear();
|
||||
(getErrorMessages as jest.Mock).mockReturnValueOnce([
|
||||
{
|
||||
displayLocations: [{ id: 'dimensionButton', dimensionId: 'col1' }],
|
||||
message: 'specific error',
|
||||
},
|
||||
] as ReturnType<typeof getErrorMessages>);
|
||||
|
||||
const messages = FormBasedDatasource.getUserMessages(state, {
|
||||
frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
|
||||
setState: () => {},
|
||||
});
|
||||
|
||||
expect(messages.length).toBe(1);
|
||||
|
||||
expect(messages).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": <React.Fragment>
|
||||
specific error
|
||||
</React.Fragment>,
|
||||
"severity": "error",
|
||||
"shortMessage": "",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('warning messages', () => {
|
||||
|
@ -3266,7 +3366,9 @@ describe('IndexPattern Data Source', () => {
|
|||
});
|
||||
|
||||
const extractTranslationIdsFromWarnings = (warnings: UserMessage[]) => {
|
||||
const onlyWarnings = filterUserMessages(warnings, undefined, { severity: 'warning' });
|
||||
const onlyWarnings = filterAndSortUserMessages(warnings, undefined, {
|
||||
severity: 'warning',
|
||||
});
|
||||
return onlyWarnings.map(({ longMessage }) =>
|
||||
isFragment(longMessage)
|
||||
? (longMessage as ReactElement).props.children[0].props.id
|
||||
|
|
|
@ -100,6 +100,7 @@ import { isColumnOfType } from './operations/definitions/helpers';
|
|||
import { LayerSettingsPanel } from './layer_settings';
|
||||
import { FormBasedLayer } from '../..';
|
||||
import { DimensionTrigger } from '../../shared_components/dimension_trigger';
|
||||
import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages';
|
||||
export type { OperationType, GenericIndexPatternColumn } from './operations';
|
||||
export { deleteColumn } from './operations';
|
||||
|
||||
|
@ -521,12 +522,6 @@ export function getFormBasedDatasource({
|
|||
return columnLabelMap;
|
||||
},
|
||||
|
||||
isValidColumn: (state, indexPatterns, layerId, columnId, dateRange) => {
|
||||
const layer = state.layers[layerId];
|
||||
|
||||
return !isColumnInvalid(layer, columnId, indexPatterns[layer.indexPatternId], dateRange);
|
||||
},
|
||||
|
||||
renderDimensionTrigger: (
|
||||
domElement: Element,
|
||||
props: DatasourceDimensionTriggerProps<FormBasedPrivateState>
|
||||
|
@ -550,13 +545,7 @@ export function getFormBasedDatasource({
|
|||
unifiedSearch,
|
||||
}}
|
||||
>
|
||||
<DimensionTrigger
|
||||
id={props.columnId}
|
||||
label={formattedLabel}
|
||||
isInvalid={props.invalid}
|
||||
hideTooltip={props.hideTooltip}
|
||||
invalidMessage={props.invalidMessage}
|
||||
/>
|
||||
<DimensionTrigger id={props.columnId} label={formattedLabel} />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
</KibanaThemeProvider>,
|
||||
|
@ -842,8 +831,18 @@ export function getFormBasedDatasource({
|
|||
data
|
||||
);
|
||||
|
||||
const dimensionErrorMessages = getDimensionErrorMessages(state, (layerId, columnId) =>
|
||||
this.isValidColumn(state, frameDatasourceAPI.dataViews.indexPatterns, layerId, columnId)
|
||||
const dimensionErrorMessages = getInvalidDimensionErrorMessages(
|
||||
state,
|
||||
layerErrorMessages,
|
||||
(layerId, columnId) => {
|
||||
const layer = state.layers[layerId];
|
||||
return !isColumnInvalid(
|
||||
layer,
|
||||
columnId,
|
||||
frameDatasourceAPI.dataViews.indexPatterns[layer.indexPatternId],
|
||||
frameDatasourceAPI.dateRange
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const warningMessages = [
|
||||
|
@ -853,13 +852,6 @@ export function getFormBasedDatasource({
|
|||
state,
|
||||
frameDatasourceAPI
|
||||
) || []),
|
||||
...getPrecisionErrorWarningMessages(
|
||||
data.datatableUtilities,
|
||||
state,
|
||||
frameDatasourceAPI,
|
||||
core.docLinks,
|
||||
setState
|
||||
),
|
||||
].map((longMessage) => {
|
||||
const message: UserMessage = {
|
||||
severity: 'warning',
|
||||
|
@ -871,6 +863,13 @@ export function getFormBasedDatasource({
|
|||
|
||||
return message;
|
||||
}),
|
||||
...getPrecisionErrorWarningMessages(
|
||||
data.datatableUtilities,
|
||||
state,
|
||||
frameDatasourceAPI,
|
||||
core.docLinks,
|
||||
setState
|
||||
),
|
||||
];
|
||||
|
||||
return [...layerErrorMessages, ...dimensionErrorMessages, ...warningMessages];
|
||||
|
@ -996,7 +995,10 @@ function getLayerErrorMessages(
|
|||
const message: UserMessage = {
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'visualization' }],
|
||||
displayLocations:
|
||||
typeof error !== 'string' && error.displayLocations
|
||||
? error.displayLocations
|
||||
: [{ id: 'visualization' }],
|
||||
shortMessage: '',
|
||||
longMessage:
|
||||
typeof error === 'string' ? (
|
||||
|
@ -1030,31 +1032,35 @@ function getLayerErrorMessages(
|
|||
// Single layer case, no need to explain more
|
||||
errorMessages = layerErrors[0]?.length ? layerErrors[0] : [];
|
||||
} else {
|
||||
// For multiple layers we will prepend each error with the layer number
|
||||
errorMessages = layerErrors.flatMap((errors, index) => {
|
||||
return errors.map((error) => {
|
||||
const message: UserMessage = {
|
||||
...error,
|
||||
shortMessage: i18n.translate('xpack.lens.indexPattern.layerErrorWrapper', {
|
||||
defaultMessage: 'Layer {position} error: {wrappedMessage}',
|
||||
values: {
|
||||
position: index + 1,
|
||||
wrappedMessage: error.shortMessage,
|
||||
},
|
||||
}),
|
||||
longMessage: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.layerErrorWrapper"
|
||||
defaultMessage="Layer {position} error: {wrappedMessage}"
|
||||
values={{
|
||||
// we will prepend each error with the layer number
|
||||
if (error.displayLocations.find((location) => location.id === 'visualization')) {
|
||||
const message: UserMessage = {
|
||||
...error,
|
||||
shortMessage: i18n.translate('xpack.lens.indexPattern.layerErrorWrapper', {
|
||||
defaultMessage: 'Layer {position} error: {wrappedMessage}',
|
||||
values: {
|
||||
position: index + 1,
|
||||
wrappedMessage: <>{error.longMessage}</>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
wrappedMessage: error.shortMessage,
|
||||
},
|
||||
}),
|
||||
longMessage: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.layerErrorWrapper"
|
||||
defaultMessage="Layer {position} error: {wrappedMessage}"
|
||||
values={{
|
||||
position: index + 1,
|
||||
wrappedMessage: <>{error.longMessage}</>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
return message;
|
||||
return message;
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1062,8 +1068,9 @@ function getLayerErrorMessages(
|
|||
return errorMessages;
|
||||
}
|
||||
|
||||
function getDimensionErrorMessages(
|
||||
function getInvalidDimensionErrorMessages(
|
||||
state: FormBasedPrivateState,
|
||||
currentErrorMessages: UserMessage[],
|
||||
isValidColumn: (layerId: string, columnId: string) => boolean
|
||||
) {
|
||||
// generate messages for invalid columns
|
||||
|
@ -1071,10 +1078,20 @@ function getDimensionErrorMessages(
|
|||
.map((layerId) => {
|
||||
const messages: UserMessage[] = [];
|
||||
for (const columnId of Object.keys(state.layers[layerId].columns)) {
|
||||
if (
|
||||
filterAndSortUserMessages(currentErrorMessages, 'dimensionButton', {
|
||||
dimensionId: columnId,
|
||||
}).length > 0
|
||||
) {
|
||||
// there is already a more specific user message assigned to this column, so no need
|
||||
// to add the default "is invalid" messaging
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isValidColumn(layerId, columnId)) {
|
||||
messages.push({
|
||||
severity: 'error',
|
||||
displayLocations: [{ id: 'dimensionTrigger', dimensionId: columnId }],
|
||||
displayLocations: [{ id: 'dimensionButton', dimensionId: columnId }],
|
||||
fixableInEditor: true,
|
||||
shortMessage: '',
|
||||
longMessage: (
|
||||
|
|
|
@ -91,7 +91,7 @@ export const cardinalityOperation: OperationDefinition<
|
|||
},
|
||||
getErrorMessage: (layer, columnId, indexPattern) =>
|
||||
combineErrorMessages([
|
||||
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
|
||||
getInvalidFieldMessage(layer, columnId, indexPattern),
|
||||
getColumnReducedTimeRangeError(layer, columnId, indexPattern),
|
||||
]),
|
||||
isTransferable: (column, newIndexPattern) => {
|
||||
|
|
|
@ -90,7 +90,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
|
|||
input: 'field',
|
||||
getErrorMessage: (layer, columnId, indexPattern) =>
|
||||
combineErrorMessages([
|
||||
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
|
||||
getInvalidFieldMessage(layer, columnId, indexPattern),
|
||||
getColumnReducedTimeRangeError(layer, columnId, indexPattern),
|
||||
]),
|
||||
allowAsReference: true,
|
||||
|
|
|
@ -86,10 +86,7 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
operationParams: [{ name: 'interval', type: 'string', required: false }],
|
||||
getErrorMessage: (layer, columnId, indexPattern) =>
|
||||
[
|
||||
...(getInvalidFieldMessage(
|
||||
layer.columns[columnId] as FieldBasedIndexPatternColumn,
|
||||
indexPattern
|
||||
) || []),
|
||||
...(getInvalidFieldMessage(layer, columnId, indexPattern) || []),
|
||||
getMultipleDateHistogramsErrorMessage(layer, columnId) || '',
|
||||
].filter(Boolean),
|
||||
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`formula getErrorMessage returns an error if the field is missing 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
noField
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`formula getErrorMessage returns an error if the field is missing 2`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
noField
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`formula getErrorMessage returns an error if the field is missing 3`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
noField
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`formula getErrorMessage returns an error if the field is missing 4`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
noField
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`formula getErrorMessage returns an error with plural form correctly handled 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 2,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
noField
|
||||
</strong>
|
||||
,
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
noField2
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`formula getErrorMessage returns an error with plural form correctly handled 2`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 2,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
noField
|
||||
</strong>
|
||||
,
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
noField2
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -1081,7 +1081,7 @@ invalid: "
|
|||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Field noField not found']);
|
||||
).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1097,7 +1097,7 @@ invalid: "
|
|||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Fields noField, noField2 not found']);
|
||||
).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { uniqBy } from 'lodash';
|
||||
import type { BaseIndexPatternColumn, OperationDefinition } from '..';
|
||||
import type { ReferenceBasedIndexPatternColumn } from '../column_types';
|
||||
import type { IndexPattern } from '../../../../../types';
|
||||
|
@ -15,7 +16,7 @@ import { insertOrReplaceFormulaColumn } from './parse';
|
|||
import { generateFormula } from './generate';
|
||||
import { filterByVisibleOperation, nonNullable } from './util';
|
||||
import { getManagedColumnsFrom } from '../../layer_helpers';
|
||||
import { getFilter, isColumnFormatted } from '../helpers';
|
||||
import { generateMissingFieldMessage, getFilter, isColumnFormatted } from '../helpers';
|
||||
|
||||
const defaultLabel = i18n.translate('xpack.lens.indexPattern.formulaLabel', {
|
||||
defaultMessage: 'Formula',
|
||||
|
@ -85,7 +86,11 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
|
||||
if (errors.length) {
|
||||
// remove duplicates
|
||||
return Array.from(new Set(errors.map(({ message }) => message)));
|
||||
return uniqBy(errors, ({ message }) => message).map(({ type, message, extraInfo }) =>
|
||||
type === 'missingField' && extraInfo?.missingFields
|
||||
? generateMissingFieldMessage(extraInfo.missingFields, columnId)
|
||||
: message
|
||||
);
|
||||
}
|
||||
|
||||
const managedColumns = getManagedColumnsFrom(columnId, layer.columns);
|
||||
|
|
|
@ -117,9 +117,11 @@ type ErrorTypes = keyof ValidationErrors;
|
|||
type ErrorValues<K extends ErrorTypes> = ValidationErrors[K]['type'];
|
||||
|
||||
export interface ErrorWrapper {
|
||||
type?: ErrorTypes; // TODO - make this required?
|
||||
message: string;
|
||||
locations: TinymathLocation[];
|
||||
severity?: 'error' | 'warning';
|
||||
extraInfo?: { missingFields: string[] };
|
||||
}
|
||||
|
||||
const DEFAULT_RETURN_TYPE = getTypeI18n('number');
|
||||
|
@ -408,7 +410,7 @@ function getMessageFromId<K extends ErrorTypes>({
|
|||
break;
|
||||
}
|
||||
|
||||
return { message, locations };
|
||||
return { type: messageId, message, locations };
|
||||
}
|
||||
|
||||
export function tryToParse(
|
||||
|
@ -501,16 +503,17 @@ function checkMissingVariableOrFunctions(
|
|||
|
||||
// need to check the arguments here: check only strings for now
|
||||
if (missingVariables.length) {
|
||||
missingErrors.push(
|
||||
getMessageFromId({
|
||||
missingErrors.push({
|
||||
...getMessageFromId({
|
||||
messageId: 'missingField',
|
||||
values: {
|
||||
variablesLength: missingVariables.length,
|
||||
variablesList: missingVariables.map(({ value }) => value).join(', '),
|
||||
},
|
||||
locations: missingVariables.map(({ location }) => location),
|
||||
})
|
||||
);
|
||||
}),
|
||||
extraInfo: { missingFields: missingVariables.map(({ value }) => value) },
|
||||
});
|
||||
}
|
||||
const invalidVariableErrors = checkVariableEdgeCases(
|
||||
ast,
|
||||
|
|
|
@ -6,35 +6,81 @@
|
|||
*/
|
||||
|
||||
import { createMockedIndexPattern } from '../../mocks';
|
||||
import type { FormBasedLayer } from '../../types';
|
||||
import type { GenericIndexPatternColumn } from './column_types';
|
||||
import { getInvalidFieldMessage } from './helpers';
|
||||
import type { TermsIndexPatternColumn } from './terms';
|
||||
|
||||
describe('helpers', () => {
|
||||
const columnId = 'column_id';
|
||||
const getLayerWithColumn = (column: GenericIndexPatternColumn) =>
|
||||
({
|
||||
columnOrder: [columnId],
|
||||
indexPatternId: '',
|
||||
columns: {
|
||||
[columnId]: column,
|
||||
},
|
||||
} as FormBasedLayer);
|
||||
|
||||
describe('getInvalidFieldMessage', () => {
|
||||
it('return an error if a field was removed', () => {
|
||||
const messages = getInvalidFieldMessage(
|
||||
{
|
||||
getLayerWithColumn({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Foo',
|
||||
operationType: 'count',
|
||||
sourceField: 'NoBytes', // <= invalid
|
||||
},
|
||||
}),
|
||||
columnId,
|
||||
createMockedIndexPattern()
|
||||
);
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages![0]).toEqual('Field NoBytes was not found');
|
||||
expect(messages![0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "column_id",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
NoBytes
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an error if a field is the wrong type', () => {
|
||||
const messages = getInvalidFieldMessage(
|
||||
{
|
||||
getLayerWithColumn({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Foo',
|
||||
operationType: 'average',
|
||||
sourceField: 'timestamp', // <= invalid type for average
|
||||
},
|
||||
}),
|
||||
columnId,
|
||||
createMockedIndexPattern()
|
||||
);
|
||||
expect(messages).toHaveLength(1);
|
||||
|
@ -43,7 +89,7 @@ describe('helpers', () => {
|
|||
|
||||
it('returns an error if one field amongst multiples does not exist', () => {
|
||||
const messages = getInvalidFieldMessage(
|
||||
{
|
||||
getLayerWithColumn({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Foo',
|
||||
|
@ -52,16 +98,49 @@ describe('helpers', () => {
|
|||
params: {
|
||||
secondaryFields: ['NoBytes'], // <= field does not exist
|
||||
},
|
||||
} as TermsIndexPatternColumn,
|
||||
} as TermsIndexPatternColumn),
|
||||
columnId,
|
||||
createMockedIndexPattern()
|
||||
);
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages![0]).toEqual('Field NoBytes was not found');
|
||||
expect(messages![0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "column_id",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
NoBytes
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an error if multiple fields do not exist', () => {
|
||||
const messages = getInvalidFieldMessage(
|
||||
{
|
||||
getLayerWithColumn({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Foo',
|
||||
|
@ -70,16 +149,55 @@ describe('helpers', () => {
|
|||
params: {
|
||||
secondaryFields: ['NoBytes'], // <= field does not exist
|
||||
},
|
||||
} as TermsIndexPatternColumn,
|
||||
} as TermsIndexPatternColumn),
|
||||
columnId,
|
||||
createMockedIndexPattern()
|
||||
);
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages![0]).toEqual('Fields NotExisting, NoBytes were not found');
|
||||
expect(messages![0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "column_id",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 2,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
NotExisting
|
||||
</strong>
|
||||
,
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
NoBytes
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an error if one field amongst multiples has the wrong type', () => {
|
||||
const messages = getInvalidFieldMessage(
|
||||
{
|
||||
getLayerWithColumn({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Foo',
|
||||
|
@ -88,7 +206,8 @@ describe('helpers', () => {
|
|||
params: {
|
||||
secondaryFields: ['timestamp'], // <= invalid type
|
||||
},
|
||||
} as TermsIndexPatternColumn,
|
||||
} as TermsIndexPatternColumn),
|
||||
columnId,
|
||||
createMockedIndexPattern()
|
||||
);
|
||||
expect(messages).toHaveLength(1);
|
||||
|
@ -97,7 +216,7 @@ describe('helpers', () => {
|
|||
|
||||
it('returns an error if multiple fields are of the wrong type', () => {
|
||||
const messages = getInvalidFieldMessage(
|
||||
{
|
||||
getLayerWithColumn({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Foo',
|
||||
|
@ -106,7 +225,8 @@ describe('helpers', () => {
|
|||
params: {
|
||||
secondaryFields: ['timestamp'], // <= invalid type
|
||||
},
|
||||
} as TermsIndexPatternColumn,
|
||||
} as TermsIndexPatternColumn),
|
||||
columnId,
|
||||
createMockedIndexPattern()
|
||||
);
|
||||
expect(messages).toHaveLength(1);
|
||||
|
@ -115,13 +235,14 @@ describe('helpers', () => {
|
|||
|
||||
it('returns no message if all fields are matching', () => {
|
||||
const messages = getInvalidFieldMessage(
|
||||
{
|
||||
getLayerWithColumn({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Foo',
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
}),
|
||||
columnId,
|
||||
createMockedIndexPattern()
|
||||
);
|
||||
expect(messages).toBeUndefined();
|
||||
|
|
|
@ -6,8 +6,14 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { IndexPattern, IndexPatternField } from '../../../../types';
|
||||
import { GenericIndexPatternColumn, operationDefinitionMap } from '.';
|
||||
import {
|
||||
type FieldBasedOperationErrorMessage,
|
||||
type GenericIndexPatternColumn,
|
||||
operationDefinitionMap,
|
||||
} from '.';
|
||||
import {
|
||||
FieldBasedIndexPatternColumn,
|
||||
FormattedIndexPatternColumn,
|
||||
|
@ -17,12 +23,15 @@ import type { FormBasedLayer } from '../../types';
|
|||
import { hasField } from '../../pure_utils';
|
||||
|
||||
export function getInvalidFieldMessage(
|
||||
column: FieldBasedIndexPatternColumn,
|
||||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern?: IndexPattern
|
||||
) {
|
||||
): FieldBasedOperationErrorMessage[] | undefined {
|
||||
if (!indexPattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
const column = layer.columns[columnId] as FieldBasedIndexPatternColumn;
|
||||
const { operationType } = column;
|
||||
const operationDefinition = operationType ? operationDefinitionMap[operationType] : undefined;
|
||||
const fieldNames =
|
||||
|
@ -55,18 +64,11 @@ export function getInvalidFieldMessage(
|
|||
// Missing fields have priority over wrong type
|
||||
// This has been moved as some transferable checks also perform exist checks internally and fail eventually
|
||||
// but that would make type mismatch error appear in place of missing fields scenarios
|
||||
const missingFields = fields.map((field, i) => (field ? null : fieldNames[i])).filter(Boolean);
|
||||
const missingFields = fields
|
||||
.map((field, i) => (field ? null : fieldNames[i]))
|
||||
.filter(Boolean) as string[];
|
||||
if (missingFields.length) {
|
||||
return [
|
||||
i18n.translate('xpack.lens.indexPattern.fieldsNotFound', {
|
||||
defaultMessage:
|
||||
'{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found',
|
||||
values: {
|
||||
count: missingFields.length,
|
||||
missingFields: missingFields.join(', '),
|
||||
},
|
||||
}),
|
||||
];
|
||||
return [generateMissingFieldMessage(missingFields, columnId)];
|
||||
}
|
||||
if (isWrongType) {
|
||||
// as fallback show all the fields as invalid?
|
||||
|
@ -88,10 +90,40 @@ export function getInvalidFieldMessage(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export const generateMissingFieldMessage = (
|
||||
missingFields: string[],
|
||||
columnId: string
|
||||
): FieldBasedOperationErrorMessage => ({
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
values={{
|
||||
count: missingFields.length,
|
||||
missingFields: (
|
||||
<>
|
||||
{missingFields.map((field, index) => (
|
||||
<>
|
||||
<strong>{field}</strong>
|
||||
{index + 1 === missingFields.length ? '' : ', '}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
displayLocations: [
|
||||
{ id: 'toolbar' },
|
||||
{ id: 'dimensionButton', dimensionId: columnId },
|
||||
{ id: 'embeddableBadge' },
|
||||
],
|
||||
});
|
||||
|
||||
export function combineErrorMessages(
|
||||
errorMessages: Array<string[] | undefined>
|
||||
): string[] | undefined {
|
||||
const messages = (errorMessages.filter(Boolean) as string[][]).flat();
|
||||
errorMessages: Array<FieldBasedOperationErrorMessage[] | undefined>
|
||||
): FieldBasedOperationErrorMessage[] | undefined {
|
||||
const messages = (errorMessages.filter(Boolean) as FieldBasedOperationErrorMessage[][]).flat();
|
||||
return messages.length ? messages : undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,12 +49,13 @@ import { countOperation } from './count';
|
|||
import { mathOperation, formulaOperation } from './formula';
|
||||
import { staticValueOperation } from './static_value';
|
||||
import { lastValueOperation } from './last_value';
|
||||
import {
|
||||
import type {
|
||||
FrameDatasourceAPI,
|
||||
IndexPattern,
|
||||
IndexPatternField,
|
||||
OperationMetadata,
|
||||
ParamEditorCustomProps,
|
||||
UserMessage,
|
||||
} from '../../../../types';
|
||||
import type {
|
||||
BaseIndexPatternColumn,
|
||||
|
@ -312,23 +313,7 @@ interface BaseOperationDefinitionProps<
|
|||
indexPattern: IndexPattern,
|
||||
dateRange?: DateRange,
|
||||
operationDefinitionMap?: Record<string, GenericOperationDefinition>
|
||||
) =>
|
||||
| Array<
|
||||
| string
|
||||
| {
|
||||
message: string;
|
||||
fixAction?: {
|
||||
label: string;
|
||||
newState: (
|
||||
data: DataPublicPluginStart,
|
||||
core: CoreStart,
|
||||
frame: FrameDatasourceAPI,
|
||||
layerId: string
|
||||
) => Promise<FormBasedLayer>;
|
||||
};
|
||||
}
|
||||
>
|
||||
| undefined;
|
||||
) => FieldBasedOperationErrorMessage[] | undefined;
|
||||
|
||||
/*
|
||||
* Flag whether this operation can be scaled by time unit if a date histogram is available.
|
||||
|
@ -468,6 +453,21 @@ interface FilterParams {
|
|||
lucene?: string;
|
||||
}
|
||||
|
||||
export type FieldBasedOperationErrorMessage =
|
||||
| {
|
||||
message: string | React.ReactNode;
|
||||
displayLocations?: UserMessage['displayLocations'];
|
||||
fixAction?: {
|
||||
label: string;
|
||||
newState: (
|
||||
data: DataPublicPluginStart,
|
||||
core: CoreStart,
|
||||
frame: FrameDatasourceAPI,
|
||||
layerId: string
|
||||
) => Promise<FormBasedLayer>;
|
||||
};
|
||||
}
|
||||
| string;
|
||||
interface FieldlessOperationDefinition<C extends BaseIndexPatternColumn, P = {}> {
|
||||
input: 'none';
|
||||
|
||||
|
@ -571,23 +571,7 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn, P = {}
|
|||
columnId: string,
|
||||
indexPattern: IndexPattern,
|
||||
operationDefinitionMap?: Record<string, GenericOperationDefinition>
|
||||
) =>
|
||||
| Array<
|
||||
| string
|
||||
| {
|
||||
message: string;
|
||||
fixAction?: {
|
||||
label: string;
|
||||
newState: (
|
||||
data: DataPublicPluginStart,
|
||||
core: CoreStart,
|
||||
frame: FrameDatasourceAPI,
|
||||
layerId: string
|
||||
) => Promise<FormBasedLayer>;
|
||||
};
|
||||
}
|
||||
>
|
||||
| undefined;
|
||||
) => FieldBasedOperationErrorMessage[] | undefined;
|
||||
}
|
||||
|
||||
export interface RequiredReference {
|
||||
|
|
|
@ -917,9 +917,42 @@ describe('last_value', () => {
|
|||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
};
|
||||
expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern)).toEqual([
|
||||
'Field notExisting was not found',
|
||||
]);
|
||||
expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern))
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
notExisting
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('shows error message if the sortField does not exist in index pattern', () => {
|
||||
|
@ -935,10 +968,58 @@ describe('last_value', () => {
|
|||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
};
|
||||
expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern)).toEqual([
|
||||
'Field notExisting was not found',
|
||||
]);
|
||||
expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern))
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="Sort field {sortField} was not found."
|
||||
id="xpack.lens.indexPattern.lastValue.sortFieldNotFound"
|
||||
values={
|
||||
Object {
|
||||
"sortField": <strong>
|
||||
notExisting
|
||||
</strong>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('shows both messages if neither field exists in index pattern', () => {
|
||||
errorLayer = {
|
||||
...errorLayer,
|
||||
columns: {
|
||||
col1: {
|
||||
...errorLayer.columns.col1,
|
||||
sourceField: 'notExisting1',
|
||||
params: {
|
||||
...(errorLayer.columns.col1 as LastValueIndexPatternColumn).params,
|
||||
sortField: 'notExisting2',
|
||||
},
|
||||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
};
|
||||
expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('shows error message if the sourceField is of unsupported type', () => {
|
||||
indexPattern.getFieldByName('start_date')!.type = 'unsupported_type';
|
||||
errorLayer = {
|
||||
|
|
|
@ -18,7 +18,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
||||
import { OperationDefinition } from '.';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { FieldBasedOperationErrorMessage, OperationDefinition } from '.';
|
||||
import { FieldBasedIndexPatternColumn, ValueFormatConfig } from './column_types';
|
||||
import type { IndexPatternField, IndexPattern } from '../../../../types';
|
||||
import { DataType } from '../../../../types';
|
||||
|
@ -62,16 +63,33 @@ const supportedTypes = new Set([
|
|||
'date_range',
|
||||
]);
|
||||
|
||||
export function getInvalidSortFieldMessage(sortField: string, indexPattern?: IndexPattern) {
|
||||
function getInvalidSortFieldMessage(
|
||||
sortField: string,
|
||||
columnId: string,
|
||||
indexPattern?: IndexPattern
|
||||
): FieldBasedOperationErrorMessage | undefined {
|
||||
if (!indexPattern) {
|
||||
return;
|
||||
}
|
||||
const field = indexPattern.getFieldByName(sortField);
|
||||
if (!field) {
|
||||
return i18n.translate('xpack.lens.indexPattern.lastValue.sortFieldNotFound', {
|
||||
defaultMessage: 'Field {invalidField} was not found',
|
||||
values: { invalidField: sortField },
|
||||
});
|
||||
return {
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.lastValue.sortFieldNotFound"
|
||||
defaultMessage="Sort field {sortField} was not found."
|
||||
values={{
|
||||
sortField: <strong>{sortField}</strong>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
displayLocations: [
|
||||
{ id: 'toolbar' },
|
||||
{ id: 'dimensionButton', dimensionId: columnId },
|
||||
{ id: 'visualization' },
|
||||
{ id: 'embeddableBadge' },
|
||||
],
|
||||
};
|
||||
}
|
||||
if (field.type !== 'date') {
|
||||
return i18n.translate('xpack.lens.indexPattern.lastValue.invalidTypeSortField', {
|
||||
|
@ -201,18 +219,22 @@ export const lastValueOperation: OperationDefinition<
|
|||
},
|
||||
getErrorMessage(layer, columnId, indexPattern) {
|
||||
const column = layer.columns[columnId] as LastValueIndexPatternColumn;
|
||||
let errorMessages: string[] = [];
|
||||
const invalidSourceFieldMessage = getInvalidFieldMessage(column, indexPattern);
|
||||
const errorMessages: FieldBasedOperationErrorMessage[] = [];
|
||||
|
||||
const invalidSourceFieldMessage = getInvalidFieldMessage(layer, columnId, indexPattern);
|
||||
if (invalidSourceFieldMessage) {
|
||||
errorMessages.push(...invalidSourceFieldMessage);
|
||||
}
|
||||
|
||||
const invalidSortFieldMessage = getInvalidSortFieldMessage(
|
||||
column.params.sortField,
|
||||
columnId,
|
||||
indexPattern
|
||||
);
|
||||
if (invalidSourceFieldMessage) {
|
||||
errorMessages = [...invalidSourceFieldMessage];
|
||||
}
|
||||
if (invalidSortFieldMessage) {
|
||||
errorMessages = [invalidSortFieldMessage];
|
||||
errorMessages.push(invalidSortFieldMessage);
|
||||
}
|
||||
|
||||
errorMessages.push(...(getColumnReducedTimeRangeError(layer, columnId, indexPattern) || []));
|
||||
return errorMessages.length ? errorMessages : undefined;
|
||||
},
|
||||
|
@ -316,6 +338,7 @@ export const lastValueOperation: OperationDefinition<
|
|||
const dateFields = getDateFields(indexPattern);
|
||||
const isSortFieldInvalid = !!getInvalidSortFieldMessage(
|
||||
currentColumn.params.sortField,
|
||||
'',
|
||||
indexPattern
|
||||
);
|
||||
|
||||
|
|
|
@ -210,10 +210,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
|
||||
getErrorMessage: (layer, columnId, indexPattern) =>
|
||||
combineErrorMessages([
|
||||
getInvalidFieldMessage(
|
||||
layer.columns[columnId] as FieldBasedIndexPatternColumn,
|
||||
indexPattern
|
||||
),
|
||||
getInvalidFieldMessage(layer, columnId, indexPattern),
|
||||
getColumnReducedTimeRangeError(layer, columnId, indexPattern),
|
||||
]),
|
||||
filterable: true,
|
||||
|
|
|
@ -288,7 +288,7 @@ export const percentileOperation: OperationDefinition<
|
|||
},
|
||||
getErrorMessage: (layer, columnId, indexPattern) =>
|
||||
combineErrorMessages([
|
||||
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
|
||||
getInvalidFieldMessage(layer, columnId, indexPattern),
|
||||
getColumnReducedTimeRangeError(layer, columnId, indexPattern),
|
||||
]),
|
||||
paramEditor: function PercentileParamEditor({
|
||||
|
|
|
@ -166,7 +166,7 @@ export const percentileRanksOperation: OperationDefinition<
|
|||
},
|
||||
getErrorMessage: (layer, columnId, indexPattern) =>
|
||||
combineErrorMessages([
|
||||
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
|
||||
getInvalidFieldMessage(layer, columnId, indexPattern),
|
||||
getColumnReducedTimeRangeError(layer, columnId, indexPattern),
|
||||
]),
|
||||
paramEditor: function PercentileParamEditor({
|
||||
|
|
|
@ -83,7 +83,7 @@ export const rangeOperation: OperationDefinition<
|
|||
priority: 4, // Higher than terms, so numbers get histogram
|
||||
input: 'field',
|
||||
getErrorMessage: (layer, columnId, indexPattern) =>
|
||||
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
|
||||
getInvalidFieldMessage(layer, columnId, indexPattern),
|
||||
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
|
||||
if (
|
||||
type === 'number' &&
|
||||
|
|
|
@ -29,11 +29,7 @@ import { DOCUMENT_FIELD_NAME } from '../../../../../../common';
|
|||
import { insertOrReplaceColumn, updateColumnParam, updateDefaultLabels } from '../../layer_helpers';
|
||||
import type { DataType, OperationMetadata } from '../../../../../types';
|
||||
import { OperationDefinition } from '..';
|
||||
import {
|
||||
FieldBasedIndexPatternColumn,
|
||||
GenericIndexPatternColumn,
|
||||
IncompleteColumn,
|
||||
} from '../column_types';
|
||||
import { GenericIndexPatternColumn, IncompleteColumn } from '../column_types';
|
||||
import { ValuesInput } from './values_input';
|
||||
import { getInvalidFieldMessage, isColumn } from '../helpers';
|
||||
import { FieldInputs, getInputFieldErrorMessage, MAX_MULTI_FIELDS_SIZE } from './field_inputs';
|
||||
|
@ -187,10 +183,7 @@ export const termsOperation: OperationDefinition<
|
|||
},
|
||||
getErrorMessage: (layer, columnId, indexPattern) => {
|
||||
const messages = [
|
||||
...(getInvalidFieldMessage(
|
||||
layer.columns[columnId] as FieldBasedIndexPatternColumn,
|
||||
indexPattern
|
||||
) || []),
|
||||
...(getInvalidFieldMessage(layer, columnId, indexPattern) || []),
|
||||
getDisallowedTermsMessage(layer, columnId, indexPattern) || '',
|
||||
getMultiTermsScriptedFieldErrorMessage(layer, columnId, indexPattern) || '',
|
||||
].filter(Boolean);
|
||||
|
|
|
@ -2636,9 +2636,41 @@ describe('terms', () => {
|
|||
} as TermsIndexPatternColumn,
|
||||
},
|
||||
};
|
||||
expect(termsOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([
|
||||
'Field notExisting was not found',
|
||||
]);
|
||||
expect(termsOperation.getErrorMessage!(layer, 'col1', indexPattern)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "toolbar",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "col1",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"id": "embeddableBadge",
|
||||
},
|
||||
],
|
||||
"message": <FormattedMessage
|
||||
defaultMessage="{count, plural, one {Field} other {Fields}} {missingFields} {count, plural, one {was} other {were}} not found."
|
||||
id="xpack.lens.indexPattern.fieldsNotFound"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"missingFields": <React.Fragment>
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
notExisting
|
||||
</strong>
|
||||
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('return no error for scripted field when in single mode', () => {
|
||||
|
|
|
@ -22,11 +22,12 @@ import type {
|
|||
import {
|
||||
operationDefinitionMap,
|
||||
operationDefinitions,
|
||||
OperationType,
|
||||
RequiredReference,
|
||||
OperationDefinition,
|
||||
GenericOperationDefinition,
|
||||
TermsIndexPatternColumn,
|
||||
type OperationType,
|
||||
type RequiredReference,
|
||||
type OperationDefinition,
|
||||
type GenericOperationDefinition,
|
||||
type TermsIndexPatternColumn,
|
||||
type FieldBasedOperationErrorMessage,
|
||||
} from './definitions';
|
||||
import type { DataViewDragDropOperation, FormBasedLayer, FormBasedPrivateState } from '../types';
|
||||
import { getSortScoreByPriorityForField } from './operations';
|
||||
|
@ -1536,6 +1537,10 @@ export function updateLayerIndexPattern(
|
|||
};
|
||||
}
|
||||
|
||||
type LayerErrorMessage = FieldBasedOperationErrorMessage & {
|
||||
fixAction: DatasourceFixAction<FormBasedPrivateState>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collects all errors from the columns in the layer, for display in the workspace. This includes:
|
||||
*
|
||||
|
@ -1552,15 +1557,7 @@ export function getErrorMessages(
|
|||
layerId: string,
|
||||
core: CoreStart,
|
||||
data: DataPublicPluginStart
|
||||
):
|
||||
| Array<
|
||||
| string
|
||||
| {
|
||||
message: string;
|
||||
fixAction?: DatasourceFixAction<FormBasedPrivateState>;
|
||||
}
|
||||
>
|
||||
| undefined {
|
||||
): LayerErrorMessage[] | undefined {
|
||||
const columns = Object.entries(layer.columns);
|
||||
const visibleManagedReferences = columns.filter(
|
||||
([columnId, column]) =>
|
||||
|
@ -1608,13 +1605,7 @@ export function getErrorMessages(
|
|||
};
|
||||
})
|
||||
// remove the undefined values
|
||||
.filter((v) => v != null) as Array<
|
||||
| string
|
||||
| {
|
||||
message: string;
|
||||
fixAction?: DatasourceFixAction<FormBasedPrivateState>;
|
||||
}
|
||||
>;
|
||||
.filter((v) => v != null) as LayerErrorMessage[];
|
||||
|
||||
return errors.length ? errors : undefined;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { FormBasedLayer } from './types';
|
||||
import type { IndexPattern } from '../../types';
|
||||
import type { FieldBasedOperationErrorMessage } from './operations/definitions';
|
||||
|
||||
export const reducedTimeRangeOptions = [
|
||||
{
|
||||
|
@ -56,7 +57,7 @@ export function getColumnReducedTimeRangeError(
|
|||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern: IndexPattern
|
||||
): string[] | undefined {
|
||||
): FieldBasedOperationErrorMessage[] | undefined {
|
||||
const currentColumn = layer.columns[columnId];
|
||||
if (!currentColumn.reducedTimeRange) {
|
||||
return;
|
||||
|
@ -65,7 +66,7 @@ export function getColumnReducedTimeRangeError(
|
|||
(column) => column.operationType === 'date_histogram'
|
||||
);
|
||||
const hasTimeField = Boolean(indexPattern.timeFieldName);
|
||||
return [
|
||||
const errors: FieldBasedOperationErrorMessage[] = [
|
||||
hasDateHistogram &&
|
||||
i18n.translate('xpack.lens.indexPattern.reducedTimeRangeWithDateHistogram', {
|
||||
defaultMessage:
|
||||
|
@ -82,5 +83,7 @@ export function getColumnReducedTimeRangeError(
|
|||
column: currentColumn.label,
|
||||
},
|
||||
}),
|
||||
].filter(Boolean) as string[];
|
||||
].filter(Boolean) as FieldBasedOperationErrorMessage[];
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
|
|
@ -121,7 +121,9 @@ describe('indexpattern_datasource utils', () => {
|
|||
|
||||
expect(warningMessages).toHaveLength(1);
|
||||
|
||||
const instance = mountWithIntl(<div>{warningMessages[0]!}</div>);
|
||||
expect({ ...warningMessages[0], longMessage: '' }).toMatchSnapshot();
|
||||
|
||||
const instance = mountWithIntl(<div>{warningMessages[0].longMessage}</div>);
|
||||
|
||||
const enableAccuracyButton = instance.find(enableAccuracyButtonSelector);
|
||||
|
||||
|
@ -146,7 +148,9 @@ describe('indexpattern_datasource utils', () => {
|
|||
|
||||
expect(warningMessages).toHaveLength(1);
|
||||
|
||||
const instance = shallow(<div>{warningMessages[0]!}</div>);
|
||||
expect({ ...warningMessages[0], longMessage: '' }).toMatchSnapshot();
|
||||
|
||||
const instance = shallow(<div>{warningMessages[0].longMessage}</div>);
|
||||
|
||||
expect(instance.exists(enableAccuracyButtonSelector)).toBeFalsy();
|
||||
|
||||
|
@ -185,7 +189,8 @@ describe('indexpattern_datasource utils', () => {
|
|||
);
|
||||
|
||||
expect(warnings).toHaveLength(1);
|
||||
const DummyComponent = () => <>{warnings[0]}</>;
|
||||
expect({ ...warnings[0], longMessage: '' }).toMatchSnapshot();
|
||||
const DummyComponent = () => <>{warnings[0].longMessage}</>;
|
||||
const warningUi = shallow(<DummyComponent />);
|
||||
warningUi.find(EuiLink).simulate('click');
|
||||
const stateSetter = setState.mock.calls[0][0];
|
||||
|
|
|
@ -78,6 +78,8 @@ export function isColumnInvalid(
|
|||
operationDefinitionMap
|
||||
);
|
||||
|
||||
// it looks like this is just a back-stop since we prevent
|
||||
// invalid filters from being set at the UI level
|
||||
const filterHasError = column.filter ? !isQueryValid(column.filter, indexPattern) : false;
|
||||
|
||||
return (
|
||||
|
@ -107,28 +109,91 @@ function getReferencesErrors(
|
|||
}
|
||||
|
||||
export function fieldIsInvalid(
|
||||
column: GenericIndexPatternColumn | undefined,
|
||||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern: IndexPattern
|
||||
) {
|
||||
const column = layer.columns[columnId];
|
||||
|
||||
if (!column || !hasField(column)) {
|
||||
return false;
|
||||
}
|
||||
return !!getInvalidFieldMessage(column, indexPattern)?.length;
|
||||
return !!getInvalidFieldMessage(layer, columnId, indexPattern)?.length;
|
||||
}
|
||||
|
||||
const accuracyModeDisabledWarning = (
|
||||
columnName: string,
|
||||
docLink: string,
|
||||
columnId: string,
|
||||
enableAccuracyMode: () => void
|
||||
) => (
|
||||
<>
|
||||
): UserMessage => ({
|
||||
severity: 'warning',
|
||||
displayLocations: [{ id: 'toolbar' }, { id: 'dimensionButton', dimensionId: columnId }],
|
||||
fixableInEditor: true,
|
||||
shortMessage: i18n.translate(
|
||||
'xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled.shortMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'This might be an approximation. For more precise results, you can enable accuracy mode, but it increases the load on the Elasticsearch cluster.',
|
||||
}
|
||||
),
|
||||
longMessage: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled"
|
||||
defaultMessage="{name} might be an approximation. You can enable accuracy mode for more precise results, but note that it increases the load on the Elasticsearch cluster."
|
||||
values={{
|
||||
name: <strong>{columnName}</strong>,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink data-test-subj="lnsPrecisionWarningEnableAccuracy" onClick={enableAccuracyMode}>
|
||||
{i18n.translate('xpack.lens.indexPattern.enableAccuracyMode', {
|
||||
defaultMessage: 'Enable accuracy mode',
|
||||
})}
|
||||
</EuiLink>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
const accuracyModeEnabledWarning = (
|
||||
columnName: string,
|
||||
columnId: string,
|
||||
docLink: string
|
||||
): UserMessage => ({
|
||||
severity: 'warning',
|
||||
displayLocations: [{ id: 'toolbar' }, { id: 'dimensionButton', dimensionId: columnId }],
|
||||
fixableInEditor: true,
|
||||
shortMessage: i18n.translate(
|
||||
'xpack.lens.indexPattern.precisionErrorWarning.accuracyEnabled.shortMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'This might be an approximation. For more precise results, use Filters or increase the number of Top Values.',
|
||||
}
|
||||
),
|
||||
longMessage: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled"
|
||||
defaultMessage="{name} might be an approximation. You can enable accuracy mode for more precise results, but note that it increases the load on the Elasticsearch cluster. {learnMoreLink}"
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.accuracyEnabled"
|
||||
defaultMessage="{name} might be an approximation. For more precise results, try increasing the number of {topValues} or using {filters} instead. {learnMoreLink}"
|
||||
values={{
|
||||
name: <strong>{columnName}</strong>,
|
||||
topValues: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.topValues"
|
||||
defaultMessage="Top Values"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
filters: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.filters"
|
||||
defaultMessage="Filters"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
learnMoreLink: (
|
||||
<EuiLink href={docLink} color="text" target="_blank" external={true}>
|
||||
<EuiLink href={docLink} target="_blank" external={true}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Learn more."
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.link"
|
||||
|
@ -137,48 +202,8 @@ const accuracyModeDisabledWarning = (
|
|||
),
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink data-test-subj="lnsPrecisionWarningEnableAccuracy" onClick={enableAccuracyMode}>
|
||||
{i18n.translate('xpack.lens.indexPattern.enableAccuracyMode', {
|
||||
defaultMessage: 'Enable accuracy mode',
|
||||
})}
|
||||
</EuiLink>
|
||||
</>
|
||||
);
|
||||
|
||||
const accuracyModeEnabledWarning = (columnName: string, docLink: string) => (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.accuracyEnabled"
|
||||
defaultMessage="{name} might be an approximation. For more precise results, try increasing the number of {topValues} or using {filters} instead. {learnMoreLink}"
|
||||
values={{
|
||||
name: <strong>{columnName}</strong>,
|
||||
topValues: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.topValues"
|
||||
defaultMessage="top values"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
filters: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.filters"
|
||||
defaultMessage="filters"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
learnMoreLink: (
|
||||
<EuiLink href={docLink} color="text" target="_blank" external={true}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Learn more."
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.link"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
),
|
||||
});
|
||||
|
||||
export function getShardFailuresWarningMessages(
|
||||
state: FormBasedPersistedState,
|
||||
|
@ -268,7 +293,7 @@ export function getPrecisionErrorWarningMessages(
|
|||
docLinks: DocLinksStart,
|
||||
setState: StateSetter<FormBasedPrivateState>
|
||||
) {
|
||||
const warningMessages: React.ReactNode[] = [];
|
||||
const warningMessages: UserMessage[] = [];
|
||||
|
||||
if (state && activeData) {
|
||||
Object.entries(activeData)
|
||||
|
@ -310,55 +335,12 @@ export function getPrecisionErrorWarningMessages(
|
|||
) {
|
||||
warningMessages.push(
|
||||
currentColumn.params.accuracyMode
|
||||
? accuracyModeEnabledWarning(column.name, docLinks.links.aggs.terms_doc_count_error)
|
||||
: accuracyModeDisabledWarning(
|
||||
? accuracyModeEnabledWarning(
|
||||
column.name,
|
||||
docLinks.links.aggs.terms_doc_count_error,
|
||||
() => {
|
||||
setState((prevState) =>
|
||||
mergeLayer({
|
||||
state: prevState,
|
||||
layerId,
|
||||
newLayer: updateDefaultLabels(
|
||||
updateColumnParam({
|
||||
layer: currentLayer,
|
||||
columnId: column.id,
|
||||
paramName: 'accuracyMode',
|
||||
value: true,
|
||||
}),
|
||||
indexPattern
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
column.id,
|
||||
docLinks.links.aggs.terms_doc_count_error
|
||||
)
|
||||
);
|
||||
} else {
|
||||
warningMessages.push(
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning"
|
||||
defaultMessage="{name} for this visualization may be approximate due to how the data is indexed. Try sorting by rarity instead of ascending count of records. To learn more about this limit, {link}."
|
||||
values={{
|
||||
name: <strong>{column.name}</strong>,
|
||||
link: (
|
||||
<EuiLink
|
||||
href={docLinks.links.aggs.rare_terms}
|
||||
color="text"
|
||||
target="_blank"
|
||||
external={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="visit the documentation"
|
||||
id="xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning.link"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
: accuracyModeDisabledWarning(column.name, column.id, () => {
|
||||
setState((prevState) =>
|
||||
mergeLayer({
|
||||
state: prevState,
|
||||
|
@ -367,24 +349,81 @@ export function getPrecisionErrorWarningMessages(
|
|||
updateColumnParam({
|
||||
layer: currentLayer,
|
||||
columnId: column.id,
|
||||
paramName: 'orderBy',
|
||||
value: {
|
||||
type: 'rare',
|
||||
maxDocCount: DEFAULT_MAX_DOC_COUNT,
|
||||
},
|
||||
paramName: 'accuracyMode',
|
||||
value: true,
|
||||
}),
|
||||
indexPattern
|
||||
),
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.lens.indexPattern.switchToRare', {
|
||||
defaultMessage: 'Rank by rarity',
|
||||
})}
|
||||
</EuiLink>
|
||||
</>
|
||||
})
|
||||
);
|
||||
} else {
|
||||
warningMessages.push({
|
||||
severity: 'warning',
|
||||
displayLocations: [
|
||||
{ id: 'toolbar' },
|
||||
{ id: 'dimensionButton', dimensionId: column.id },
|
||||
],
|
||||
shortMessage: i18n.translate(
|
||||
'xpack.lens.indexPattern.precisionErrorWarning.ascendingCountPrecisionErrorWarning.shortMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'This may be approximate depending on how the data is indexed. For more precise results, sort by rarity.',
|
||||
}
|
||||
),
|
||||
longMessage: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning"
|
||||
defaultMessage="{name} for this visualization may be approximate due to how the data is indexed. Try sorting by rarity instead of ascending count of records. To learn more about this limit, {link}."
|
||||
values={{
|
||||
name: <strong>{column.name}</strong>,
|
||||
link: (
|
||||
<EuiLink
|
||||
href={docLinks.links.aggs.rare_terms}
|
||||
target="_blank"
|
||||
external={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="visit the documentation"
|
||||
id="xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning.link"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
setState((prevState) =>
|
||||
mergeLayer({
|
||||
state: prevState,
|
||||
layerId,
|
||||
newLayer: updateDefaultLabels(
|
||||
updateColumnParam({
|
||||
layer: currentLayer,
|
||||
columnId: column.id,
|
||||
paramName: 'orderBy',
|
||||
value: {
|
||||
type: 'rare',
|
||||
maxDocCount: DEFAULT_MAX_DOC_COUNT,
|
||||
},
|
||||
}),
|
||||
indexPattern
|
||||
),
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.lens.indexPattern.switchToRare', {
|
||||
defaultMessage: 'Rank by rarity',
|
||||
})}
|
||||
</EuiLink>
|
||||
</>
|
||||
),
|
||||
fixableInEditor: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -233,13 +233,6 @@ export function getTextBasedDatasource({
|
|||
});
|
||||
return { state: { layers }, savedObjectReferences };
|
||||
},
|
||||
isValidColumn(state, indexPatterns, layerId, columnId) {
|
||||
const layer = state.layers[layerId];
|
||||
const column = layer.columns.find((c) => c.columnId === columnId);
|
||||
const indexPattern = indexPatterns[layer.index];
|
||||
if (!column || !indexPattern) return false;
|
||||
return true;
|
||||
},
|
||||
insertLayer(state: TextBasedPrivateState, newLayerId: string) {
|
||||
const layer = Object.values(state?.layers)?.[0];
|
||||
const query = layer?.query;
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonIcon, EuiLink } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiFlexItem, EuiLink, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorIndicator } from '../color_indicator';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { DimensionButtonIcon } from '../dimension_button_icon';
|
||||
import { PaletteIndicator } from '../palette_indicator';
|
||||
import { VisualizationDimensionGroupConfig, AccessorConfig } from '../../../../types';
|
||||
import { VisualizationDimensionGroupConfig, AccessorConfig, UserMessage } from '../../../../types';
|
||||
|
||||
const triggerLinkA11yText = (label: string) =>
|
||||
i18n.translate('xpack.lens.configure.editConfig', {
|
||||
|
@ -25,7 +27,7 @@ export function DimensionButton({
|
|||
onRemoveClick,
|
||||
accessorConfig,
|
||||
label,
|
||||
invalid,
|
||||
message,
|
||||
}: {
|
||||
group: VisualizationDimensionGroupConfig;
|
||||
children: React.ReactElement;
|
||||
|
@ -33,25 +35,39 @@ export function DimensionButton({
|
|||
onRemoveClick: (id: string) => void;
|
||||
accessorConfig: AccessorConfig;
|
||||
label: string;
|
||||
invalid?: boolean;
|
||||
message: UserMessage | undefined;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<EuiLink
|
||||
className="lnsLayerPanel__dimensionLink"
|
||||
data-test-subj="lnsLayerPanel-dimensionLink"
|
||||
onClick={() => onClick(accessorConfig.columnId)}
|
||||
aria-label={triggerLinkA11yText(label)}
|
||||
title={triggerLinkA11yText(label)}
|
||||
color={invalid ? 'danger' : undefined}
|
||||
>
|
||||
<ColorIndicator accessorConfig={accessorConfig}>{children}</ColorIndicator>
|
||||
</EuiLink>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={message?.shortMessage || message?.longMessage || undefined}
|
||||
position="left"
|
||||
>
|
||||
<EuiLink
|
||||
className="lnsLayerPanel__dimensionLink"
|
||||
data-test-subj="lnsLayerPanel-dimensionLink"
|
||||
onClick={() => onClick(accessorConfig.columnId)}
|
||||
aria-label={triggerLinkA11yText(label)}
|
||||
title={triggerLinkA11yText(label)}
|
||||
color={
|
||||
message?.severity === 'error'
|
||||
? 'danger'
|
||||
: message?.severity === 'warning'
|
||||
? 'warning'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<DimensionButtonIcon message={message} accessorConfig={accessorConfig}>
|
||||
{children}
|
||||
</DimensionButtonIcon>
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiButtonIcon
|
||||
className="lnsLayerPanel__dimensionRemove"
|
||||
data-test-subj="indexPattern-dimension-remove"
|
||||
iconType="cross"
|
||||
iconSize="s"
|
||||
iconType="trash"
|
||||
size="s"
|
||||
color="danger"
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
|
||||
|
@ -63,6 +79,12 @@ export function DimensionButton({
|
|||
values: { groupLabel: group.groupLabel },
|
||||
})}
|
||||
onClick={() => onRemoveClick(accessorConfig.columnId)}
|
||||
css={css`
|
||||
color: ${euiThemeVars.euiTextSubduedColor};
|
||||
&:hover {
|
||||
color: ${euiThemeVars.euiColorDangerText};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<PaletteIndicator accessorConfig={accessorConfig} />
|
||||
</>
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* 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';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AccessorConfig } from '../../../types';
|
||||
|
||||
export function ColorIndicator({
|
||||
accessorConfig,
|
||||
children,
|
||||
}: {
|
||||
accessorConfig: AccessorConfig;
|
||||
children: React.ReactChild;
|
||||
}) {
|
||||
let indicatorIcon = null;
|
||||
if (accessorConfig.triggerIconType && accessorConfig.triggerIconType !== 'none') {
|
||||
const baseIconProps = {
|
||||
size: 's',
|
||||
className: 'lnsLayerPanel__colorIndicator',
|
||||
} as const;
|
||||
|
||||
indicatorIcon = (
|
||||
<EuiFlexItem grow={false}>
|
||||
{accessorConfig.triggerIconType === 'color' && accessorConfig.color && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
color={accessorConfig.color}
|
||||
type="stopFilled"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.colorIndicatorLabel', {
|
||||
defaultMessage: 'Color of this dimension: {hex}',
|
||||
values: {
|
||||
hex: accessorConfig.color,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'disabled' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="stopSlash"
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.noColorIndicatorLabel', {
|
||||
defaultMessage: 'This dimension does not have an individual color',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'invisible' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="eyeClosed"
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.invisibleIndicatorLabel', {
|
||||
defaultMessage: 'This dimension is currently not visible in the chart',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'aggregate' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="fold"
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.aggregateIndicatorLabel', {
|
||||
defaultMessage:
|
||||
'This dimension is not visible in the chart because all individual values are aggregated into a single value',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'colorBy' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="brush"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.paletteColorIndicatorLabel', {
|
||||
defaultMessage: 'This dimension is using a palette',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'custom' && accessorConfig.customIcon && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
size="m"
|
||||
type={accessorConfig.customIcon}
|
||||
color={accessorConfig.color}
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.customIconIndicatorLabel', {
|
||||
defaultMessage: 'This dimension is using a custom icon',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
|
||||
{indicatorIcon}
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AccessorConfig, UserMessage } from '../../../types';
|
||||
import { IconError, IconWarning } from '../custom_icons';
|
||||
|
||||
const baseIconProps = {
|
||||
className: 'lnsLayerPanel__colorIndicator',
|
||||
} as const;
|
||||
|
||||
const getIconFromAccessorConfig = (accessorConfig: AccessorConfig) => (
|
||||
<>
|
||||
{accessorConfig.triggerIconType === 'color' && accessorConfig.color && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
color={accessorConfig.color}
|
||||
type="stopFilled"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.colorIndicatorLabel', {
|
||||
defaultMessage: 'Color of this dimension: {hex}',
|
||||
values: {
|
||||
hex: accessorConfig.color,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'disabled' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="stopSlash"
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.noColorIndicatorLabel', {
|
||||
defaultMessage: 'This dimension does not have an individual color',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'invisible' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="eyeClosed"
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.invisibleIndicatorLabel', {
|
||||
defaultMessage: 'This dimension is currently not visible in the chart',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'aggregate' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="fold"
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.aggregateIndicatorLabel', {
|
||||
defaultMessage:
|
||||
'This dimension is not visible in the chart because all individual values are aggregated into a single value',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'colorBy' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="color"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.paletteColorIndicatorLabel', {
|
||||
defaultMessage: 'This dimension is using a palette',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIconType === 'custom' && accessorConfig.customIcon && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type={accessorConfig.customIcon}
|
||||
color={accessorConfig.color}
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.customIconIndicatorLabel', {
|
||||
defaultMessage: 'This dimension is using a custom icon',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
export function DimensionButtonIcon({
|
||||
accessorConfig,
|
||||
message,
|
||||
children,
|
||||
}: {
|
||||
accessorConfig: AccessorConfig;
|
||||
message: UserMessage | undefined;
|
||||
children: React.ReactChild;
|
||||
}) {
|
||||
let indicatorIcon = null;
|
||||
if (message || accessorConfig.triggerIconType !== 'none') {
|
||||
indicatorIcon = (
|
||||
<>
|
||||
{accessorConfig.triggerIconType !== 'none' && (
|
||||
<EuiFlexItem grow={false}>{getIconFromAccessorConfig(accessorConfig)}</EuiFlexItem>
|
||||
)}
|
||||
{message && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type={message.severity === 'error' ? IconError : IconWarning}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
|
||||
{indicatorIcon}
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -512,17 +512,10 @@ export function LayerPanel(
|
|||
{group.accessors.map((accessorConfig, accessorIndex) => {
|
||||
const { columnId } = accessorConfig;
|
||||
|
||||
const messages = props.getUserMessages('dimensionTrigger', {
|
||||
// TODO - support warnings
|
||||
severity: 'error',
|
||||
const messages = props.getUserMessages('dimensionButton', {
|
||||
dimensionId: columnId,
|
||||
});
|
||||
|
||||
const hasMessages = Boolean(messages.length);
|
||||
const messageToDisplay = hasMessages
|
||||
? messages[0].shortMessage || messages[0].longMessage
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<DraggableDimensionButton
|
||||
activeVisualization={activeVisualization}
|
||||
|
@ -574,16 +567,7 @@ export function LayerPanel(
|
|||
props.onRemoveDimension({ columnId: id, layerId });
|
||||
removeButtonRef(id);
|
||||
}}
|
||||
invalid={
|
||||
layerDatasource &&
|
||||
!layerDatasource?.isValidColumn(
|
||||
layerDatasourceState,
|
||||
dataViews.indexPatterns,
|
||||
layerId,
|
||||
columnId,
|
||||
dateRange
|
||||
)
|
||||
}
|
||||
message={messages[0]}
|
||||
>
|
||||
{layerDatasource ? (
|
||||
<NativeRenderer
|
||||
|
@ -593,9 +577,6 @@ export function LayerPanel(
|
|||
columnId: accessorConfig.columnId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
hideTooltip,
|
||||
invalid: hasMessages,
|
||||
invalidMessage: messageToDisplay,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
}}
|
||||
/>
|
||||
|
@ -605,8 +586,6 @@ export function LayerPanel(
|
|||
columnId,
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
hideTooltip,
|
||||
invalid: hasMessages,
|
||||
invalidMessage: messageToDisplay,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// NOTICE — all this can be removed when https://github.com/elastic/eui/pull/6550 gets pulled into Kibana
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const IconError = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
aria-labelledby={titleId}
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId}>{title}</title> : null}
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 1a1 1 0 0 1 .707.293l4 4A1 1 0 0 1 15 6v5a1 1 0 0 1-.293.707l-4 4A1 1 0 0 1 10 16H5a1 1 0 0 1-.707-.293l-4-4A1 1 0 0 1 0 11V6a1 1 0 0 1 .293-.707l4-4A1 1 0 0 1 5 1h5ZM4.146 5.146a.5.5 0 0 1 .708 0L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708.708L8.207 8.5l2.647 2.646a.5.5 0 0 1-.708.708L7.5 9.207l-2.646 2.647a.5.5 0 0 1-.708-.708L6.793 8.5 4.146 5.854a.5.5 0 0 1 0-.708Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconWarning = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
aria-labelledby={titleId}
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId}>{title}</title> : null}
|
||||
<path d="m8.55 9.502.35-3.507a.905.905 0 1 0-1.8 0l.35 3.507a.552.552 0 0 0 1.1 0ZM9 12a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M8.864 1.496a1 1 0 0 0-1.728 0l-7 12A1 1 0 0 0 1 15h14a1 1 0 0 0 .864-1.504l-7-12ZM1 14 8 2l7 12H1Z" />
|
||||
</svg>
|
||||
);
|
|
@ -63,7 +63,7 @@ import {
|
|||
selectStagedActiveData,
|
||||
selectFrameDatasourceAPI,
|
||||
} from '../../state_management';
|
||||
import { filterUserMessages } from '../../app_plugin/get_application_user_messages';
|
||||
import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages';
|
||||
|
||||
const MAX_SUGGESTIONS_DISPLAYED = 5;
|
||||
const LOCAL_STORAGE_SUGGESTIONS_PANEL = 'LENS_SUGGESTIONS_PANEL_HIDDEN';
|
||||
|
@ -77,7 +77,7 @@ const configurationsValid = (
|
|||
): boolean => {
|
||||
try {
|
||||
return (
|
||||
filterUserMessages(
|
||||
filterAndSortUserMessages(
|
||||
[
|
||||
...(currentDataSource?.getUserMessages?.(currentDatasourceState, {
|
||||
frame,
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 './workspace_panel_wrapper.scss';
|
||||
import './message_list.scss';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
EuiButton,
|
||||
EuiIcon,
|
||||
EuiToolTip,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css, SerializedStyles } from '@emotion/react';
|
||||
import { IconError, IconWarning } from '../custom_icons';
|
||||
import { UserMessage } from '../../../types';
|
||||
|
||||
export const MessageList = ({
|
||||
messages,
|
||||
customButtonStyles,
|
||||
}: {
|
||||
messages: UserMessage[];
|
||||
customButtonStyles?: SerializedStyles;
|
||||
}) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
let warningCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
messages.forEach(({ severity }) => {
|
||||
if (severity === 'warning') {
|
||||
warningCount++;
|
||||
} else {
|
||||
errorCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const buttonLabel =
|
||||
errorCount > 0 && warningCount > 0
|
||||
? i18n.translate('xpack.lens.messagesButton.label.errorsAndWarnings', {
|
||||
defaultMessage:
|
||||
'{errorCount} {errorCount, plural, one {error} other {errors}}, {warningCount} {warningCount, plural, one {warning} other {warnings}}',
|
||||
values: {
|
||||
errorCount,
|
||||
warningCount,
|
||||
},
|
||||
})
|
||||
: errorCount > 0
|
||||
? i18n.translate('xpack.lens.messagesButton.label.errors', {
|
||||
defaultMessage: '{errorCount} {errorCount, plural, one {error} other {errors}}',
|
||||
values: {
|
||||
errorCount,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.messagesButton.label.warnings', {
|
||||
defaultMessage: '{warningCount} {warningCount, plural, one {warning} other {warnings}}',
|
||||
values: {
|
||||
warningCount,
|
||||
},
|
||||
});
|
||||
|
||||
const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
return (
|
||||
<EuiPopover
|
||||
panelPaddingSize="none"
|
||||
button={
|
||||
<EuiToolTip content={buttonLabel}>
|
||||
<EuiButton
|
||||
minWidth={0}
|
||||
color={errorCount ? 'danger' : 'warning'}
|
||||
onClick={onButtonClick}
|
||||
className="lnsWorkspaceWarning__button"
|
||||
data-test-subj="lens-message-list-trigger"
|
||||
title={buttonLabel}
|
||||
css={customButtonStyles}
|
||||
>
|
||||
{errorCount > 0 && (
|
||||
<>
|
||||
<EuiIcon type={IconError} />
|
||||
{errorCount}
|
||||
</>
|
||||
)}
|
||||
{warningCount > 0 && (
|
||||
<>
|
||||
<EuiIcon
|
||||
type={IconWarning}
|
||||
css={css`
|
||||
margin-left: 4px;
|
||||
`}
|
||||
/>
|
||||
{warningCount}
|
||||
</>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
>
|
||||
<ul className="lnsWorkspaceWarningList">
|
||||
{messages.map((message, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="lnsWorkspaceWarningList__item"
|
||||
data-test-subj={`lens-message-list-${message.severity}`}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
{message.severity === 'error' ? (
|
||||
<EuiIcon type={IconError} color="danger" />
|
||||
) : (
|
||||
<EuiIcon type={IconWarning} color="warning" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiText size="s">{message.longMessage}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* 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 './workspace_panel_wrapper.scss';
|
||||
import './warnings_popover.scss';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPopover, EuiText, EuiButtonEmpty } from '@elastic/eui';
|
||||
|
||||
export const WarningsPopover = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode | React.ReactNode[];
|
||||
}) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
const warningsCount = React.Children.count(children);
|
||||
return (
|
||||
<EuiPopover
|
||||
panelPaddingSize="none"
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
color="warning"
|
||||
onClick={onButtonClick}
|
||||
iconType="alert"
|
||||
className="lnsWorkspaceWarning__button"
|
||||
data-test-subj="lens-editor-warning-button"
|
||||
>
|
||||
{warningsCount}
|
||||
<span className="lnsWorkspaceWarning__buttonText">
|
||||
{' '}
|
||||
{i18n.translate('xpack.lens.chartWarnings.number', {
|
||||
defaultMessage: `{warningsCount, plural, one {warning} other {warnings}}`,
|
||||
values: {
|
||||
warningsCount,
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
>
|
||||
<ul className="lnsWorkspaceWarningList">
|
||||
{React.Children.map(children, (child, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="lnsWorkspaceWarningList__item"
|
||||
data-test-subj="lens-editor-warning"
|
||||
>
|
||||
<EuiText size="s">{child}</EuiText>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -20,7 +20,7 @@ import {
|
|||
import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../../../utils';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import { ChartSwitch } from './chart_switch';
|
||||
import { WarningsPopover } from './warnings_popover';
|
||||
import { MessageList } from './message_list';
|
||||
import {
|
||||
useLensDispatch,
|
||||
updateVisualizationState,
|
||||
|
@ -30,7 +30,6 @@ import {
|
|||
selectChangesApplied,
|
||||
applyChanges,
|
||||
selectAutoApplyEnabled,
|
||||
selectStagedRequestWarnings,
|
||||
} from '../../../state_management';
|
||||
import { WorkspaceTitle } from './title';
|
||||
import { LensInspector } from '../../../lens_inspector_service';
|
||||
|
@ -64,7 +63,6 @@ export function WorkspacePanelWrapper({
|
|||
|
||||
const changesApplied = useLensSelector(selectChangesApplied);
|
||||
const autoApplyEnabled = useLensSelector(selectAutoApplyEnabled);
|
||||
const requestWarnings = useLensSelector(selectStagedRequestWarnings);
|
||||
|
||||
const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null;
|
||||
const setVisualizationState = useCallback(
|
||||
|
@ -82,15 +80,8 @@ export function WorkspacePanelWrapper({
|
|||
[dispatchLens, activeVisualization]
|
||||
);
|
||||
|
||||
const warningMessages: React.ReactNode[] = [];
|
||||
const userMessages = getUserMessages('toolbar');
|
||||
|
||||
warningMessages.push(
|
||||
...getUserMessages('toolbar', { severity: 'warning' }).map(({ longMessage }) => longMessage)
|
||||
);
|
||||
|
||||
if (requestWarnings) {
|
||||
warningMessages.push(...requestWarnings);
|
||||
}
|
||||
return (
|
||||
<EuiPageTemplate
|
||||
direction="column"
|
||||
|
@ -99,7 +90,7 @@ export function WorkspacePanelWrapper({
|
|||
restrictWidth={false}
|
||||
mainProps={{ component: 'div' } as unknown as {}}
|
||||
>
|
||||
{!(isFullscreen && (autoApplyEnabled || warningMessages?.length)) && (
|
||||
{!(isFullscreen && (autoApplyEnabled || userMessages?.length)) && (
|
||||
<EuiPageTemplate.Section paddingSize="none" color="transparent">
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
|
@ -140,9 +131,9 @@ export function WorkspacePanelWrapper({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
{warningMessages?.length ? (
|
||||
{userMessages?.length ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<WarningsPopover>{warningMessages}</WarningsPopover>
|
||||
<MessageList messages={userMessages} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -70,10 +70,10 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
|||
import {
|
||||
BrushTriggerEvent,
|
||||
ClickTriggerEvent,
|
||||
Warnings,
|
||||
MultiClickTriggerEvent,
|
||||
} from '@kbn/charts-plugin/public';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { useEuiFontSize, useEuiTheme } from '@elastic/eui';
|
||||
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
|
||||
import { Document } from '../persistence';
|
||||
import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper';
|
||||
|
@ -112,9 +112,10 @@ import {
|
|||
} from '../utils';
|
||||
import { getLayerMetaInfo, combineQueryAndFilters } from '../app_plugin/show_underlying_data';
|
||||
import {
|
||||
filterUserMessages,
|
||||
filterAndSortUserMessages,
|
||||
getApplicationUserMessages,
|
||||
} from '../app_plugin/get_application_user_messages';
|
||||
import { MessageList } from '../editor_frame_service/editor_frame/workspace_panel/message_list';
|
||||
|
||||
export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>;
|
||||
|
||||
|
@ -274,6 +275,26 @@ function getViewUnderlyingDataArgs({
|
|||
};
|
||||
}
|
||||
|
||||
const EmbeddableMessagesPopover = ({ messages }: { messages: UserMessage[] }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const xsFontSize = useEuiFontSize('xs').fontSize;
|
||||
|
||||
return (
|
||||
<MessageList
|
||||
messages={messages}
|
||||
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};
|
||||
& > * {
|
||||
gap: ${euiTheme.size.xs};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export class Embeddable
|
||||
extends AbstractEmbeddable<LensEmbeddableInput, LensEmbeddableOutput>
|
||||
implements
|
||||
|
@ -289,7 +310,7 @@ export class Embeddable
|
|||
private savedVis: Document | undefined;
|
||||
private expression: string | undefined | null;
|
||||
private domNode: HTMLElement | Element | undefined;
|
||||
private warningDomNode: HTMLElement | Element | undefined;
|
||||
private badgeDomNode: HTMLElement | Element | undefined;
|
||||
private subscription: Subscription;
|
||||
private isInitialized = false;
|
||||
private inputReloadSubscriptions: Subscription[];
|
||||
|
@ -474,10 +495,10 @@ export class Embeddable
|
|||
}
|
||||
|
||||
public getUserMessages: UserMessagesGetter = (locationId, filters) => {
|
||||
return filterUserMessages(
|
||||
return filterAndSortUserMessages(
|
||||
[...this._userMessages, ...Object.values(this.additionalUserMessages)],
|
||||
locationId,
|
||||
filters
|
||||
filters ?? {}
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -878,28 +899,25 @@ export class Embeddable
|
|||
})}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
this.warningDomNode = el;
|
||||
this.badgeDomNode = el;
|
||||
this.renderBadgeMessages();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</KibanaThemeProvider>,
|
||||
domNode
|
||||
);
|
||||
|
||||
this.renderBadgeMessages();
|
||||
}
|
||||
|
||||
private renderBadgeMessages() {
|
||||
const warningsToDisplay = this.getUserMessages('embeddableBadge', {
|
||||
severity: 'warning',
|
||||
});
|
||||
const messages = this.getUserMessages('embeddableBadge');
|
||||
|
||||
if (warningsToDisplay.length && this.warningDomNode) {
|
||||
if (messages.length && this.badgeDomNode) {
|
||||
render(
|
||||
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
|
||||
<Warnings warnings={warningsToDisplay.map((message) => message.longMessage)} compressed />
|
||||
<EmbeddableMessagesPopover messages={messages} />
|
||||
</KibanaThemeProvider>,
|
||||
this.warningDomNode
|
||||
this.badgeDomNode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,6 @@ export function createMockDatasource(
|
|||
getUserMessages: jest.fn((_state, _deps) => []),
|
||||
checkIntegrity: jest.fn((_state, _indexPatterns) => []),
|
||||
isTimeBased: jest.fn(),
|
||||
isValidColumn: jest.fn(),
|
||||
isEqual: jest.fn(),
|
||||
getUsedDataView: jest.fn((state, layer) => 'mockip'),
|
||||
getUsedDataViews: jest.fn(),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiText, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiText, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -20,40 +20,7 @@ export const defaultDimensionTriggerTooltip = (
|
|||
</p>
|
||||
);
|
||||
|
||||
export const DimensionTrigger = ({
|
||||
id,
|
||||
label,
|
||||
isInvalid,
|
||||
hideTooltip,
|
||||
invalidMessage = defaultDimensionTriggerTooltip,
|
||||
}: {
|
||||
label: string;
|
||||
id: string;
|
||||
isInvalid?: boolean;
|
||||
hideTooltip?: boolean;
|
||||
invalidMessage?: string | React.ReactNode;
|
||||
}) => {
|
||||
if (isInvalid) {
|
||||
return (
|
||||
<EuiToolTip content={!hideTooltip ? invalidMessage : null} anchorClassName="eui-displayBlock">
|
||||
<EuiText
|
||||
size="s"
|
||||
color="danger"
|
||||
id={id}
|
||||
className="lnsLayerPanel__triggerText"
|
||||
data-test-subj="lns-dimensionTrigger"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="s" type="alert" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>{label}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
export const DimensionTrigger = ({ id, label }: { label: string; id: string }) => {
|
||||
return (
|
||||
<EuiText
|
||||
size="s"
|
||||
|
|
|
@ -28,8 +28,6 @@ export const selectVisualization = (state: LensState) => state.lens.visualizatio
|
|||
export const selectStagedPreview = (state: LensState) => state.lens.stagedPreview;
|
||||
export const selectStagedActiveData = (state: LensState) =>
|
||||
state.lens.stagedPreview?.activeData || state.lens.activeData;
|
||||
export const selectStagedRequestWarnings = (state: LensState) =>
|
||||
state.lens.stagedPreview?.requestWarnings || state.lens.requestWarnings;
|
||||
export const selectAutoApplyEnabled = (state: LensState) => !state.lens.autoApplyDisabled;
|
||||
export const selectChangesApplied = (state: LensState) =>
|
||||
!state.lens.autoApplyDisabled || Boolean(state.lens.changesApplied);
|
||||
|
|
|
@ -38,7 +38,6 @@ export interface PreviewState {
|
|||
visualization: VisualizationState;
|
||||
datasourceStates: DatasourceStates;
|
||||
activeData?: TableInspectorAdapter;
|
||||
requestWarnings?: string[];
|
||||
}
|
||||
export interface EditorFrameState extends PreviewState {
|
||||
activeDatasourceId: string | null;
|
||||
|
|
|
@ -289,7 +289,7 @@ type UserMessageDisplayLocation =
|
|||
| 'textBasedLanguagesQueryInput'
|
||||
| 'banner';
|
||||
}
|
||||
| { id: 'dimensionTrigger'; dimensionId: string };
|
||||
| { id: 'dimensionButton'; dimensionId: string };
|
||||
|
||||
export type UserMessagesDisplayLocationId = UserMessageDisplayLocation['id'];
|
||||
|
||||
|
@ -311,7 +311,7 @@ export interface UserMessageFilters {
|
|||
|
||||
export type UserMessagesGetter = (
|
||||
locationId: UserMessagesDisplayLocationId | UserMessagesDisplayLocationId[] | undefined,
|
||||
filters: UserMessageFilters
|
||||
filters?: UserMessageFilters
|
||||
) => UserMessage[];
|
||||
|
||||
export type AddUserMessages = (messages: RemovableUserMessage[]) => () => void;
|
||||
|
@ -505,16 +505,6 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
* Checks if the visualization created is time based, for example date histogram
|
||||
*/
|
||||
isTimeBased: (state: T, indexPatterns: IndexPatternMap) => boolean;
|
||||
/**
|
||||
* Given the current state layer and a columnId will verify if the column configuration has errors
|
||||
*/
|
||||
isValidColumn: (
|
||||
state: T,
|
||||
indexPatterns: IndexPatternMap,
|
||||
layerId: string,
|
||||
columnId: string,
|
||||
dateRange?: DateRange
|
||||
) => boolean;
|
||||
/**
|
||||
* Are these datasources equivalent?
|
||||
*/
|
||||
|
@ -656,9 +646,6 @@ export type DatasourceDimensionProps<T> = SharedDimensionProps & {
|
|||
activeData?: Record<string, Datatable>;
|
||||
dateRange: DateRange;
|
||||
indexPatterns: IndexPatternMap;
|
||||
hideTooltip?: boolean;
|
||||
invalid?: boolean;
|
||||
invalidMessage?: string | React.ReactNode;
|
||||
};
|
||||
export type ParamEditorCustomProps = Record<string, unknown> & {
|
||||
labels?: string[];
|
||||
|
@ -1245,8 +1232,6 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
columnId: string;
|
||||
label: string;
|
||||
hideTooltip?: boolean;
|
||||
invalid?: boolean;
|
||||
invalidMessage?: string | React.ReactNode;
|
||||
}) => JSX.Element | null;
|
||||
/**
|
||||
* Creates map of columns ids and unique lables. Used only for noDatasource layers
|
||||
|
|
|
@ -554,11 +554,11 @@ describe('gauge', () => {
|
|||
"displayLocations": Array [
|
||||
Object {
|
||||
"dimensionId": "min-accessor",
|
||||
"id": "dimensionTrigger",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
Object {
|
||||
"dimensionId": "max-accessor",
|
||||
"id": "dimensionTrigger",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
|
|
|
@ -98,8 +98,8 @@ const getErrorMessages = (row?: DatatableRow, state?: GaugeVisualizationState):
|
|||
errors.push({
|
||||
severity: 'error',
|
||||
displayLocations: [
|
||||
{ id: 'dimensionTrigger', dimensionId: minAccessor! },
|
||||
{ id: 'dimensionTrigger', dimensionId: maxAccessor! },
|
||||
{ id: 'dimensionButton', dimensionId: minAccessor! },
|
||||
{ id: 'dimensionButton', dimensionId: maxAccessor! },
|
||||
],
|
||||
fixableInEditor: true,
|
||||
shortMessage: i18n.translate(
|
||||
|
@ -115,8 +115,8 @@ const getErrorMessages = (row?: DatatableRow, state?: GaugeVisualizationState):
|
|||
errors.push({
|
||||
severity: 'error',
|
||||
displayLocations: [
|
||||
{ id: 'dimensionTrigger', dimensionId: minAccessor! },
|
||||
{ id: 'dimensionTrigger', dimensionId: maxAccessor! },
|
||||
{ id: 'dimensionButton', dimensionId: minAccessor! },
|
||||
{ id: 'dimensionButton', dimensionId: maxAccessor! },
|
||||
],
|
||||
fixableInEditor: true,
|
||||
shortMessage: i18n.translate('xpack.lens.guageVisualization.chartCannotRenderEqual', {
|
||||
|
|
|
@ -2622,7 +2622,7 @@ describe('xy_visualization', () => {
|
|||
"displayLocations": Array [
|
||||
Object {
|
||||
"dimensionId": "an1",
|
||||
"id": "dimensionTrigger",
|
||||
"id": "dimensionButton",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
|
|
|
@ -712,7 +712,7 @@ export const getXyVisualization = ({
|
|||
errors.push({
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'dimensionTrigger', dimensionId: annotation.id }],
|
||||
displayLocations: [{ id: 'dimensionButton', dimensionId: annotation.id }],
|
||||
shortMessage: i18n.translate(
|
||||
'xpack.lens.xyChart.addAnnotationsLayerLabelDisabledHelp',
|
||||
{
|
||||
|
@ -732,7 +732,7 @@ export const getXyVisualization = ({
|
|||
fixableInEditor: true,
|
||||
displayLocations: [
|
||||
{ id: 'visualization' },
|
||||
{ id: 'dimensionTrigger', dimensionId: annotation.id },
|
||||
{ id: 'dimensionButton', dimensionId: annotation.id },
|
||||
],
|
||||
shortMessage: errorMessage,
|
||||
longMessage: (
|
||||
|
@ -889,17 +889,9 @@ export const getXyVisualization = ({
|
|||
state?.layers.filter(isAnnotationsLayer).map(({ indexPatternId }) => indexPatternId) ?? []
|
||||
);
|
||||
},
|
||||
renderDimensionTrigger({ columnId, label, hideTooltip, invalid, invalidMessage }) {
|
||||
renderDimensionTrigger({ columnId, label }) {
|
||||
if (label) {
|
||||
return (
|
||||
<DimensionTrigger
|
||||
id={columnId}
|
||||
hideTooltip={hideTooltip}
|
||||
isInvalid={invalid}
|
||||
invalidMessage={invalidMessage}
|
||||
label={label || defaultAnnotationLabel}
|
||||
/>
|
||||
);
|
||||
return <DimensionTrigger id={columnId} label={label || defaultAnnotationLabel} />;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
|
|
@ -1206,7 +1206,6 @@
|
|||
"data.search.aggs.metrics.uniqueCountLabel": "Décompte unique de {field}",
|
||||
"data.search.aggs.metrics.valueCountLabel": "Décompte de la valeur de {field}",
|
||||
"data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "Le champ enregistré \"{fieldParameter}\" de la vue de données \"{indexPatternTitle}\" n'est pas valide pour une utilisation avec l'agrégation \"{aggType}\". Veuillez sélectionner un nouveau champ.",
|
||||
"data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage": "Le champ \"{fieldParameter}\" associé à cet objet n'existe plus dans la vue de données. Veuillez utiliser un autre champ.",
|
||||
"data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} est un paramètre requis.",
|
||||
"data.search.aggs.percentageOfLabel": "Pourcentage de {label}",
|
||||
"data.search.aggs.rareTerms.aggTypesLabel": "Termes rares de {fieldName}",
|
||||
|
@ -17939,7 +17938,6 @@
|
|||
"xpack.lens.app.lensContext": "Contexte Lens ({language})",
|
||||
"xpack.lens.app.updatePanel": "Mettre à jour le panneau sur {originatingAppName}",
|
||||
"xpack.lens.chartSwitch.noResults": "Résultats introuvables pour {term}.",
|
||||
"xpack.lens.chartWarnings.number": "{warningsCount, plural, one {avertissement} other {avertissements}}",
|
||||
"xpack.lens.configure.configurePanelTitle": "{groupLabel}",
|
||||
"xpack.lens.configure.editConfig": "Modifier la configuration {label}",
|
||||
"xpack.lens.configure.suggestedValuee": "Valeur suggérée : {value}",
|
||||
|
@ -18040,7 +18038,7 @@
|
|||
"xpack.lens.indexPattern.formulaWithTooManyArguments": "L'opération {operation} a trop d'arguments",
|
||||
"xpack.lens.indexPattern.invalidReferenceConfiguration": "La dimension \"{dimensionLabel}\" n'est pas configurée correctement",
|
||||
"xpack.lens.indexPattern.lastValue.invalidTypeSortField": "Le champ {invalidField} n'est pas un champ de date et ne peut pas être utilisé pour le tri",
|
||||
"xpack.lens.indexPattern.lastValue.sortFieldNotFound": "Champ {invalidField} introuvable",
|
||||
"xpack.lens.indexPattern.lastValue.sortFieldNotFound": "Champ {sortField} introuvable",
|
||||
"xpack.lens.indexPattern.lastValueOf": "Dernière valeur de {name}",
|
||||
"xpack.lens.indexPattern.layerErrorWrapper": "Erreur de {position} pour le calque : {wrappedMessage}",
|
||||
"xpack.lens.indexPattern.maxOf": "Maximum de {name}",
|
||||
|
@ -18060,7 +18058,7 @@
|
|||
"xpack.lens.indexPattern.percentileOf": "{percentile, selectordinal, one {#er} two {#e} few {#e} other {#e}} centile de {name}",
|
||||
"xpack.lens.indexPattern.percentileRanksOf": "Rang centile ({value}) de {name}",
|
||||
"xpack.lens.indexPattern.pinnedTopValuesLabel": "Filtres de {field}",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled": "{name} peut être une approximation. Vous pouvez activer le mode de précision pour obtenir des résultats plus fins, mais notez que ce mode augmente la charge sur le cluster Elasticsearch. {learnMoreLink}",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled": "{name} peut être une approximation. Vous pouvez activer le mode de précision pour obtenir des résultats plus fins, mais notez que ce mode augmente la charge sur le cluster Elasticsearch.",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyEnabled": "{name} peut être une approximation. Pour obtenir des résultats plus précis, essayez d'augmenter le nombre de {topValues} ou d'utiliser des {filters} à la place. {learnMoreLink}",
|
||||
"xpack.lens.indexPattern.ranges.granularityPopoverExplanation": "La taille de l'intervalle est une valeur de \"gentillesse\". Lorsque la granularité du curseur change, l'intervalle reste le même lorsque l'intervalle de \"gentillesse\" est le même. La granularité minimale est 1, et la valeur maximale est {setting}. Pour modifier la granularité maximale, accédez aux Paramètres avancés.",
|
||||
"xpack.lens.indexPattern.rareTermsOf": "Valeurs rares de {name}",
|
||||
|
|
|
@ -1206,7 +1206,6 @@
|
|||
"data.search.aggs.metrics.uniqueCountLabel": "{field} のユニークカウント",
|
||||
"data.search.aggs.metrics.valueCountLabel": "{field}の値カウント",
|
||||
"data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "「{aggType}」アグリゲーションで使用するには、データビュー「{indexPatternTitle}」の保存されたフィールド「{fieldParameter}」が無効です。新しいフィールドを選択してください。",
|
||||
"data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage": "このオブジェクトに関連付けられたフィールド\"{fieldParameter}\"は、データビューに存在しません。別のフィールドを使用してください。",
|
||||
"data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} は必須パラメーターです",
|
||||
"data.search.aggs.percentageOfLabel": "{label} の割合",
|
||||
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName}の希少な用語",
|
||||
|
@ -17924,7 +17923,6 @@
|
|||
"xpack.lens.app.lensContext": "Lensコンテキスト({language})",
|
||||
"xpack.lens.app.updatePanel": "{originatingAppName}でパネルを更新",
|
||||
"xpack.lens.chartSwitch.noResults": "{term}の結果が見つかりませんでした。",
|
||||
"xpack.lens.chartWarnings.number": "{warningsCount, plural, other {警告}}",
|
||||
"xpack.lens.configure.configurePanelTitle": "{groupLabel}",
|
||||
"xpack.lens.configure.editConfig": "{label}構成の編集",
|
||||
"xpack.lens.configure.suggestedValuee": "候補の値:{value}",
|
||||
|
@ -18020,7 +18018,7 @@
|
|||
"xpack.lens.indexPattern.formulaWithTooManyArguments": "演算{operation}の引数が多すぎます",
|
||||
"xpack.lens.indexPattern.invalidReferenceConfiguration": "ディメンション\"{dimensionLabel}\"の構成が正しくありません",
|
||||
"xpack.lens.indexPattern.lastValue.invalidTypeSortField": "フィールド {invalidField} は日付フィールドではないため、並べ替えで使用できません",
|
||||
"xpack.lens.indexPattern.lastValue.sortFieldNotFound": "フィールド {invalidField} が見つかりませんでした",
|
||||
"xpack.lens.indexPattern.lastValue.sortFieldNotFound": "フィールド {sortField} が見つかりませんでした",
|
||||
"xpack.lens.indexPattern.lastValueOf": "{name} の最後の値",
|
||||
"xpack.lens.indexPattern.layerErrorWrapper": "レイヤー{position}エラー:{wrappedMessage}",
|
||||
"xpack.lens.indexPattern.maxOf": "{name} の最高値",
|
||||
|
@ -18040,7 +18038,7 @@
|
|||
"xpack.lens.indexPattern.percentileOf": "{name}の{percentile, selectordinal, other {#}}パーセンタイル",
|
||||
"xpack.lens.indexPattern.percentileRanksOf": "{name}のパーセンタイルランク({value})",
|
||||
"xpack.lens.indexPattern.pinnedTopValuesLabel": "{field}のフィルター",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled": "{name}は近似値の可能性があります。より正確な結果を得るために精度モードを有効にできますが、Elasticsearchクラスターの負荷が大きくなります。{learnMoreLink}",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled": "{name}は近似値の可能性があります。より正確な結果を得るために精度モードを有効にできますが、Elasticsearchクラスターの負荷が大きくなります。",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyEnabled": "{name}は近似値の可能性があります。より正確な結果を得るには、{topValues}の数を増やすか、{filters}を使用してください。{learnMoreLink}",
|
||||
"xpack.lens.indexPattern.ranges.granularityPopoverExplanation": "間隔のサイズは「nice」値です。スライダーの粒度を変更すると、「nice」間隔が同じときには、間隔が同じままです。最小粒度は1です。最大値は{setting}です。最大粒度を変更するには、[高度な設定]に移動します。",
|
||||
"xpack.lens.indexPattern.rareTermsOf": "{name}の希少な値",
|
||||
|
|
|
@ -1208,7 +1208,6 @@
|
|||
"data.search.aggs.metrics.uniqueCountLabel": "“{field}”的唯一计数",
|
||||
"data.search.aggs.metrics.valueCountLabel": "“{field}”的值计数",
|
||||
"data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "数据视图“{indexPatternTitle}”的已保存字段“{fieldParameter}”无效,无法用于“{aggType}”聚合。请选择新字段。",
|
||||
"data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage": "与此对象关联的字段“{fieldParameter}”在该数据视图中已不再存在。请使用其他字段。",
|
||||
"data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} 是必需字段",
|
||||
"data.search.aggs.percentageOfLabel": "{label} 的百分比",
|
||||
"data.search.aggs.rareTerms.aggTypesLabel": "{fieldName} 的稀有词",
|
||||
|
@ -17944,7 +17943,6 @@
|
|||
"xpack.lens.app.lensContext": "Lens 上下文 ({language})",
|
||||
"xpack.lens.app.updatePanel": "更新 {originatingAppName} 中的面板",
|
||||
"xpack.lens.chartSwitch.noResults": "找不到 {term} 的结果。",
|
||||
"xpack.lens.chartWarnings.number": "{warningsCount, plural, other {警告}}",
|
||||
"xpack.lens.configure.configurePanelTitle": "{groupLabel}",
|
||||
"xpack.lens.configure.editConfig": "编辑 {label} 配置",
|
||||
"xpack.lens.configure.suggestedValuee": "建议值:{value}",
|
||||
|
@ -18045,7 +18043,7 @@
|
|||
"xpack.lens.indexPattern.formulaWithTooManyArguments": "运算 {operation} 的参数过多",
|
||||
"xpack.lens.indexPattern.invalidReferenceConfiguration": "维度“{dimensionLabel}”配置不正确",
|
||||
"xpack.lens.indexPattern.lastValue.invalidTypeSortField": "字段 {invalidField} 不是日期字段,不能用于排序",
|
||||
"xpack.lens.indexPattern.lastValue.sortFieldNotFound": "未找到字段 {invalidField}",
|
||||
"xpack.lens.indexPattern.lastValue.sortFieldNotFound": "未找到字段 {sortField}",
|
||||
"xpack.lens.indexPattern.lastValueOf": "{name} 的最后一个值",
|
||||
"xpack.lens.indexPattern.layerErrorWrapper": "图层 {position} 错误:{wrappedMessage}",
|
||||
"xpack.lens.indexPattern.maxOf": "{name} 的最大值",
|
||||
|
@ -18065,7 +18063,7 @@
|
|||
"xpack.lens.indexPattern.percentileOf": "{name} 的{percentile, selectordinal, one {第一个} two {第二个} few {第三个} other {第 #th 个}}百分位数",
|
||||
"xpack.lens.indexPattern.percentileRanksOf": "{name} 的百分位等级 ({value})",
|
||||
"xpack.lens.indexPattern.pinnedTopValuesLabel": "{field} 的筛选",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled": "{name} 可能为近似值。可以启用准确性模式以获得更精确的结果,但请注意,这会增加 Elasticsearch 集群的负载。{learnMoreLink}",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled": "{name} 可能为近似值。可以启用准确性模式以获得更精确的结果,但请注意,这会增加 Elasticsearch 集群的负载.",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyEnabled": "{name} 可能为近似值。要获得更精确的结果,请尝试增加 {topValues} 的数目或改用 {filters}。{learnMoreLink}",
|
||||
"xpack.lens.indexPattern.ranges.granularityPopoverExplanation": "时间间隔的大小是“好”值。滑块的粒度更改时,如果“好的”时间间隔不变,时间间隔也不变。最小粒度为 1,最大值为 {setting}。要更改最大粒度,请前往“高级设置”。",
|
||||
"xpack.lens.indexPattern.rareTermsOf": "{name} 的稀有值",
|
||||
|
|
|
@ -116,16 +116,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await testSubjects.existOrFail('median-partial-warning');
|
||||
await testSubjects.click('lns-indexPatternDimension-median');
|
||||
await PageObjects.lens.waitForVisualization('xyVisChart');
|
||||
await PageObjects.lens.assertEditorWarning(
|
||||
'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.'
|
||||
await PageObjects.lens.assertMessageListContains(
|
||||
'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
|
||||
'warning'
|
||||
);
|
||||
});
|
||||
it('shows warnings in dashboards as well', async () => {
|
||||
await PageObjects.lens.save('New', false, false, false, 'new');
|
||||
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await PageObjects.lens.assertInlineWarning(
|
||||
'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.'
|
||||
await PageObjects.lens.assertMessageListContains(
|
||||
'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
|
||||
'warning'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const PageObjects = getPageObjects([
|
||||
'visualize',
|
||||
'lens',
|
||||
'dashboard',
|
||||
'header',
|
||||
'timePicker',
|
||||
'common',
|
||||
|
@ -19,30 +20,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const security = getService('security');
|
||||
const listingTable = getService('listingTable');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
|
||||
describe('Lens error handling', () => {
|
||||
before(async () => {
|
||||
await security.testUser.setRoles(
|
||||
['global_discover_read', 'global_visualize_read', 'test_logstash_reader'],
|
||||
{ skipBrowserRefresh: true }
|
||||
);
|
||||
// loading an object without reference fails, so we load data view + lens object and then unload data view
|
||||
await kibanaServer.importExport.load(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/lens/errors'
|
||||
);
|
||||
await kibanaServer.importExport.unload(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/lens/errors2'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.testUser.restoreDefaults();
|
||||
await kibanaServer.importExport.unload(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/lens/errors'
|
||||
);
|
||||
});
|
||||
|
||||
describe('Index Pattern missing', () => {
|
||||
before(async () => {
|
||||
await security.testUser.setRoles(
|
||||
['global_discover_read', 'global_visualize_read', 'test_logstash_reader'],
|
||||
{ skipBrowserRefresh: true }
|
||||
);
|
||||
// loading an object without reference fails, so we load data view + lens object and then unload data view
|
||||
await kibanaServer.importExport.load(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/lens/errors'
|
||||
);
|
||||
await kibanaServer.importExport.unload(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/lens/errors2'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.testUser.restoreDefaults();
|
||||
await kibanaServer.importExport.unload(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/lens/errors'
|
||||
);
|
||||
});
|
||||
|
||||
it('the warning is shown and user can fix the state', async () => {
|
||||
await PageObjects.visualize.gotoVisualizationLandingPage();
|
||||
await listingTable.searchForItemWithName('lnsMetricWithNonExistingDataView');
|
||||
|
@ -76,5 +78,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.lens.waitForMissingDataViewWarning();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not block render when missing fields', async () => {
|
||||
await kibanaServer.importExport.load(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/lens/missing_fields'
|
||||
);
|
||||
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.loadSavedDashboard('Dashboard with missing field Lens');
|
||||
await PageObjects.lens.assertMessageListContains(
|
||||
'Field missing field was not found.',
|
||||
'error'
|
||||
);
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
await dashboardPanelActions.editPanelByTitle();
|
||||
await PageObjects.lens.assertMessageListContains(
|
||||
'Field missing field was not found.',
|
||||
'error'
|
||||
);
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
await kibanaServer.importExport.unload(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/lens/missing_fields'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
formula: `asdf`,
|
||||
});
|
||||
|
||||
expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(1);
|
||||
await PageObjects.lens.assertMessageListContains('Field asdf was not found.', 'error');
|
||||
});
|
||||
|
||||
it('should keep the formula when entering expanded mode', async () => {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"attributes": {
|
||||
"description": "",
|
||||
"kibanaSavedObjectMeta": {
|
||||
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
|
||||
},
|
||||
"optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}",
|
||||
"panelsJSON": "[{\"version\":\"8.7.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"7d75b51d-19fc-4dba-8174-b82e2905745d\"},\"panelIndex\":\"7d75b51d-19fc-4dba-8174-b82e2905745d\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsMetric\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"logstash-*\",\"name\":\"indexpattern-datasource-layer-78c0d2e4-4bb8-49e7-ae98-cbe5851bb959\"}],\"state\":{\"visualization\":{\"layerId\":\"78c0d2e4-4bb8-49e7-ae98-cbe5851bb959\",\"layerType\":\"data\",\"metricAccessor\":\"8630dc6f-828f-4a7d-8283-821cf6b54e4f\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"78c0d2e4-4bb8-49e7-ae98-cbe5851bb959\":{\"columns\":{\"8630dc6f-828f-4a7d-8283-821cf6b54e4f\":{\"label\":\"Median of missing field\",\"dataType\":\"number\",\"operationType\":\"median\",\"sourceField\":\"missing field\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true}}},\"columnOrder\":[\"8630dc6f-828f-4a7d-8283-821cf6b54e4f\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}}}]",
|
||||
"timeRestore": false,
|
||||
"title": "Dashboard with missing field Lens",
|
||||
"version": 1
|
||||
},
|
||||
"coreMigrationVersion": "8.7.0",
|
||||
"created_at": "2023-01-28T17:47:28.069Z",
|
||||
"id": "d4cc9840-9f33-11ed-896f-1111cbb8731e",
|
||||
"migrationVersion": { "dashboard": "8.7.0" },
|
||||
"references": [
|
||||
{
|
||||
"id": "logstash-*",
|
||||
"name": "7d75b51d-19fc-4dba-8174-b82e2905745d:indexpattern-datasource-layer-78c0d2e4-4bb8-49e7-ae98-cbe5851bb959",
|
||||
"type": "index-pattern"
|
||||
}
|
||||
],
|
||||
"type": "dashboard",
|
||||
"updated_at": "2023-01-28T17:47:28.069Z",
|
||||
"version": "WzM0OCwxXQ=="
|
||||
}
|
|
@ -1600,39 +1600,25 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await testSubjects.missingOrFail('lens-editor-warning');
|
||||
},
|
||||
|
||||
async assertInlineWarning(warningText: string) {
|
||||
await testSubjects.click('chart-inline-warning-button');
|
||||
await testSubjects.existOrFail('chart-inline-warning');
|
||||
const warnings = await testSubjects.findAll('chart-inline-warning');
|
||||
/**
|
||||
* Applicable both on the embeddable and in the editor. In both scenarios, a popover containing user messages (errors, warnings) is shown.
|
||||
*/
|
||||
async assertMessageListContains(assertText: string, severity: 'warning' | 'error') {
|
||||
await testSubjects.click('lens-message-list-trigger');
|
||||
const messageSelector = `lens-message-list-${severity}`;
|
||||
await testSubjects.existOrFail(messageSelector);
|
||||
const messages = await testSubjects.findAll(messageSelector);
|
||||
let found = false;
|
||||
for (const warning of warnings) {
|
||||
const text = await warning.getVisibleText();
|
||||
for (const message of messages) {
|
||||
const text = await message.getVisibleText();
|
||||
log.info(text);
|
||||
if (text === warningText) {
|
||||
if (text === assertText) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
await testSubjects.click('chart-inline-warning-button');
|
||||
await testSubjects.click('lens-message-list-trigger');
|
||||
if (!found) {
|
||||
throw new Error(`Warning with text "${warningText}" not found`);
|
||||
}
|
||||
},
|
||||
|
||||
async assertEditorWarning(warningText: string) {
|
||||
await testSubjects.click('lens-editor-warning-button');
|
||||
await testSubjects.existOrFail('lens-editor-warning');
|
||||
const warnings = await testSubjects.findAll('lens-editor-warning');
|
||||
let found = false;
|
||||
for (const warning of warnings) {
|
||||
const text = await warning.getVisibleText();
|
||||
log.info(text);
|
||||
if (text === warningText) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
await testSubjects.click('lens-editor-warning-button');
|
||||
if (!found) {
|
||||
throw new Error(`Warning with text "${warningText}" not found`);
|
||||
throw new Error(`Message with text "${assertText}" not found`);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue