;
}
| undefined
@@ -499,7 +499,7 @@ export const VisualizationWrapper = ({
.map((validationError) => (
<>
diff --git a/x-pack/plugins/lens/public/editor_frame_service/types.ts b/x-pack/plugins/lens/public/editor_frame_service/types.ts
index ebfd098b5fb19..9435faf374420 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/types.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/types.ts
@@ -11,6 +11,6 @@ export type TableInspectorAdapter = Record;
export interface ErrorMessage {
shortMessage: string;
- longMessage: string;
+ longMessage: React.ReactNode;
type?: 'fixable' | 'critical';
}
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
index 74aac932a6861..be20118ba2941 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
@@ -11,6 +11,7 @@ import {
LensByReferenceInput,
LensSavedObjectAttributes,
LensEmbeddableInput,
+ ResolvedLensSavedObjectAttributes,
} from './embeddable';
import { ReactExpressionRendererProps } from 'src/plugins/expressions/public';
import { Query, TimeRange, Filter, IndexPatternsContract } from 'src/plugins/data/public';
@@ -68,12 +69,17 @@ const options = {
const attributeServiceMockFromSavedVis = (document: Document): LensAttributeService => {
const core = coreMock.createStart();
const service = new AttributeService<
- LensSavedObjectAttributes,
+ ResolvedLensSavedObjectAttributes,
LensByValueInput,
LensByReferenceInput
>('lens', jest.fn(), core.i18n.Context, core.notifications.toasts, options);
service.unwrapAttributes = jest.fn((input: LensByValueInput | LensByReferenceInput) => {
- return Promise.resolve({ ...document } as LensSavedObjectAttributes);
+ return Promise.resolve({
+ ...document,
+ sharingSavedObjectProps: {
+ outcome: 'exactMatch',
+ },
+ } as ResolvedLensSavedObjectAttributes);
});
service.wrapAttributes = jest.fn();
return service;
@@ -86,7 +92,7 @@ describe('embeddable', () => {
let trigger: { exec: jest.Mock };
let basePath: IBasePath;
let attributeService: AttributeService<
- LensSavedObjectAttributes,
+ ResolvedLensSavedObjectAttributes,
LensByValueInput,
LensByReferenceInput
>;
@@ -223,6 +229,50 @@ describe('embeddable', () => {
expect(expressionRenderer).toHaveBeenCalledTimes(0);
});
+ it('should not render the vis if loaded saved object conflicts', async () => {
+ attributeService.unwrapAttributes = jest.fn(
+ (input: LensByValueInput | LensByReferenceInput) => {
+ return Promise.resolve({
+ ...savedVis,
+ sharingSavedObjectProps: {
+ outcome: 'conflict',
+ errorJSON: '{targetType: "lens", sourceId: "1", targetSpace: "space"}',
+ aliasTargetId: '2',
+ },
+ } as ResolvedLensSavedObjectAttributes);
+ }
+ );
+ const embeddable = new Embeddable(
+ {
+ timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
+ attributeService,
+ inspector: inspectorPluginMock.createStartContract(),
+ expressionRenderer,
+ basePath,
+ indexPatternService: {} as IndexPatternsContract,
+ capabilities: {
+ canSaveDashboards: true,
+ canSaveVisualizations: true,
+ },
+ getTrigger,
+ documentToExpression: () =>
+ Promise.resolve({
+ ast: {
+ type: 'expression',
+ chain: [
+ { type: 'function', function: 'my', arguments: {} },
+ { type: 'function', function: 'expression', arguments: {} },
+ ],
+ },
+ errors: undefined,
+ }),
+ },
+ {} as LensEmbeddableInput
+ );
+ await embeddable.initializeSavedVis({} as LensEmbeddableInput);
+ expect(expressionRenderer).toHaveBeenCalledTimes(0);
+ });
+
it('should initialize output with deduped list of index patterns', async () => {
attributeService = attributeServiceMockFromSavedVis({
...savedVis,
@@ -516,7 +566,8 @@ describe('embeddable', () => {
timeRange,
query,
filters,
- renderMode: 'noInteractivity',
+ renderMode: 'view',
+ disableTriggers: true,
} as LensEmbeddableInput;
const embeddable = new Embeddable(
@@ -549,7 +600,12 @@ describe('embeddable', () => {
await embeddable.initializeSavedVis(input);
embeddable.render(mountpoint);
- expect(expressionRenderer.mock.calls[0][0].renderMode).toEqual('noInteractivity');
+ expect(expressionRenderer.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ interactive: false,
+ renderMode: 'view',
+ })
+ );
});
it('should merge external context with query and filters of the saved object', async () => {
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
index 172274b1f90bc..d10423c76686c 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
@@ -41,7 +41,11 @@ import {
ReferenceOrValueEmbeddable,
} from '../../../../../src/plugins/embeddable/public';
import { Document, injectFilterReferences } from '../persistence';
-import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper';
+import {
+ ExpressionWrapper,
+ ExpressionWrapperProps,
+ savedObjectConflictError,
+} from './expression_wrapper';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import {
isLensBrushEvent,
@@ -58,8 +62,12 @@ import { IBasePath } from '../../../../../src/core/public';
import { LensAttributeService } from '../lens_attribute_service';
import type { ErrorMessage } from '../editor_frame_service/types';
import { getLensInspectorService, LensInspector } from '../lens_inspector_service';
+import { SharingSavedObjectProps } from '../types';
export type LensSavedObjectAttributes = Omit;
+export interface ResolvedLensSavedObjectAttributes extends LensSavedObjectAttributes {
+ sharingSavedObjectProps?: SharingSavedObjectProps;
+}
interface LensBaseEmbeddableInput extends EmbeddableInput {
filters?: Filter[];
@@ -76,7 +84,7 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
}
export type LensByValueInput = {
- attributes: LensSavedObjectAttributes;
+ attributes: ResolvedLensSavedObjectAttributes;
} & LensBaseEmbeddableInput;
export type LensByReferenceInput = SavedObjectEmbeddableInput & LensBaseEmbeddableInput;
@@ -253,15 +261,18 @@ export class Embeddable
}
async initializeSavedVis(input: LensEmbeddableInput) {
- const attributes:
- | LensSavedObjectAttributes
+ const attrs:
+ | ResolvedLensSavedObjectAttributes
| false = await this.deps.attributeService.unwrapAttributes(input).catch((e: Error) => {
this.onFatalError(e);
return false;
});
- if (!attributes || this.isDestroyed) {
+ if (!attrs || this.isDestroyed) {
return;
}
+
+ const { sharingSavedObjectProps, ...attributes } = attrs;
+
this.savedVis = {
...attributes,
type: this.type,
@@ -269,8 +280,12 @@ export class Embeddable
};
const { ast, errors } = await this.deps.documentToExpression(this.savedVis);
this.errors = errors;
+ if (sharingSavedObjectProps?.outcome === 'conflict') {
+ const conflictError = savedObjectConflictError(sharingSavedObjectProps.errorJSON!);
+ this.errors = this.errors ? [...this.errors, conflictError] : [conflictError];
+ }
this.expression = ast ? toExpression(ast) : null;
- if (errors) {
+ if (this.errors) {
this.logError('validation');
}
await this.initializeOutput();
@@ -346,6 +361,7 @@ export class Embeddable
searchSessionId={this.externalSearchContext.searchSessionId}
handleEvent={this.handleEvent}
onData$={this.updateActiveData}
+ interactive={!input.disableTriggers}
renderMode={input.renderMode}
syncColors={input.syncColors}
hasCompatibleActions={this.hasCompatibleActions}
diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
index d57e1c450fea2..c827fe74cc52b 100644
--- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
+++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
@@ -5,10 +5,20 @@
* 2.0.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon, EuiEmptyPrompt } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiIcon,
+ EuiEmptyPrompt,
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiSpacer,
+ EuiLink,
+} from '@elastic/eui';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
@@ -18,6 +28,7 @@ import type { KibanaExecutionContext } from 'src/core/public';
import { ExecutionContextSearch } from 'src/plugins/data/public';
import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions';
import classNames from 'classnames';
+import { i18n } from '@kbn/i18n';
import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper';
import { ErrorMessage } from '../editor_frame_service/types';
import { LensInspector } from '../lens_inspector_service';
@@ -27,6 +38,7 @@ export interface ExpressionWrapperProps {
expression: string | null;
errors: ErrorMessage[] | undefined;
variables?: Record;
+ interactive?: boolean;
searchContext: ExecutionContextSearch;
searchSessionId?: string;
handleEvent: (event: ExpressionRendererEvent) => void;
@@ -102,6 +114,7 @@ export function ExpressionWrapper({
searchContext,
variables,
handleEvent,
+ interactive,
searchSessionId,
onData$,
renderMode,
@@ -126,6 +139,7 @@ export function ExpressionWrapper({
padding="s"
variables={variables}
expression={expression}
+ interactive={interactive}
searchContext={searchContext}
searchSessionId={searchSessionId}
onData$={onData$}
@@ -158,3 +172,52 @@ export function ExpressionWrapper({
);
}
+
+const SavedObjectConflictMessage = ({ json }: { json: string }) => {
+ const [expandError, setExpandError] = useState(false);
+ return (
+ <>
+
+ {i18n.translate('xpack.lens.embeddable.legacyURLConflict.documentationLinkText', {
+ defaultMessage: 'legacy URL alias',
+ })}
+
+ ),
+ }}
+ />
+
+ {expandError ? (
+
+ ) : (
+ setExpandError(true)}>
+ {i18n.translate('xpack.lens.embeddable.legacyURLConflict.expandError', {
+ defaultMessage: `Show more`,
+ })}
+
+ )}
+ >
+ );
+};
+
+export const savedObjectConflictError = (json: string): ErrorMessage => ({
+ shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', {
+ defaultMessage: `You've encountered a URL conflict`,
+ }),
+ longMessage: ,
+});
diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts
index 39a1903c6d0c4..09c98b3dcba72 100644
--- a/x-pack/plugins/lens/public/lens_attribute_service.ts
+++ b/x-pack/plugins/lens/public/lens_attribute_service.ts
@@ -9,47 +9,68 @@ import { CoreStart } from '../../../../src/core/public';
import { LensPluginStartDependencies } from './plugin';
import { AttributeService } from '../../../../src/plugins/embeddable/public';
import {
- LensSavedObjectAttributes,
+ ResolvedLensSavedObjectAttributes,
LensByValueInput,
LensByReferenceInput,
} from './embeddable/embeddable';
-import { SavedObjectIndexStore, Document } from './persistence';
+import { SavedObjectIndexStore } from './persistence';
import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public';
import { DOC_TYPE } from '../common';
export type LensAttributeService = AttributeService<
- LensSavedObjectAttributes,
+ ResolvedLensSavedObjectAttributes,
LensByValueInput,
LensByReferenceInput
>;
-function documentToAttributes(doc: Document): LensSavedObjectAttributes {
- delete doc.savedObjectId;
- delete doc.type;
- return { ...doc };
-}
-
export function getLensAttributeService(
core: CoreStart,
startDependencies: LensPluginStartDependencies
): LensAttributeService {
const savedObjectStore = new SavedObjectIndexStore(core.savedObjects.client);
return startDependencies.embeddable.getAttributeService<
- LensSavedObjectAttributes,
+ ResolvedLensSavedObjectAttributes,
LensByValueInput,
LensByReferenceInput
>(DOC_TYPE, {
- saveMethod: async (attributes: LensSavedObjectAttributes, savedObjectId?: string) => {
+ saveMethod: async (attributes: ResolvedLensSavedObjectAttributes, savedObjectId?: string) => {
+ const { sharingSavedObjectProps, ...attributesToSave } = attributes;
const savedDoc = await savedObjectStore.save({
- ...attributes,
+ ...attributesToSave,
savedObjectId,
type: DOC_TYPE,
});
return { id: savedDoc.savedObjectId };
},
- unwrapMethod: async (savedObjectId: string): Promise => {
- const attributes = documentToAttributes(await savedObjectStore.load(savedObjectId));
- return attributes;
+ unwrapMethod: async (savedObjectId: string): Promise => {
+ const {
+ saved_object: savedObject,
+ outcome,
+ alias_target_id: aliasTargetId,
+ } = await savedObjectStore.load(savedObjectId);
+ const { attributes, references, type, id } = savedObject;
+ const document = {
+ ...attributes,
+ references,
+ };
+
+ const sharingSavedObjectProps = {
+ aliasTargetId,
+ outcome,
+ errorJSON:
+ outcome === 'conflict'
+ ? JSON.stringify({
+ targetType: type,
+ sourceId: id,
+ targetSpace: (await startDependencies.spaces.getActiveSpace()).id,
+ })
+ : undefined,
+ };
+
+ return {
+ sharingSavedObjectProps,
+ ...document,
+ };
},
checkForDuplicateTitle: (props: OnSaveProps) => {
const savedObjectsClient = core.savedObjects.client;
diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx
index b2c8d3948b285..8fbd263fe909e 100644
--- a/x-pack/plugins/lens/public/mocks.tsx
+++ b/x-pack/plugins/lens/public/mocks.tsx
@@ -24,11 +24,12 @@ import { LensAppServices } from './app_plugin/types';
import { DOC_TYPE, layerTypes } from '../common';
import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public';
import { inspectorPluginMock } from '../../../../src/plugins/inspector/public/mocks';
+import { spacesPluginMock } from '../../spaces/public/mocks';
import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks';
import type {
LensByValueInput,
- LensSavedObjectAttributes,
LensByReferenceInput,
+ ResolvedLensSavedObjectAttributes,
} from './embeddable/embeddable';
import {
mockAttributeService,
@@ -352,7 +353,7 @@ export function makeDefaultServices(
function makeAttributeService(): LensAttributeService {
const attributeServiceMock = mockAttributeService<
- LensSavedObjectAttributes,
+ ResolvedLensSavedObjectAttributes,
LensByValueInput,
LensByReferenceInput
>(
@@ -365,7 +366,12 @@ export function makeDefaultServices(
core
);
- attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(doc);
+ attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue({
+ ...doc,
+ sharingSavedObjectProps: {
+ outcome: 'exactMatch',
+ },
+ });
attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({
savedObjectId: ((doc as unknown) as LensByReferenceInput).savedObjectId,
});
@@ -404,6 +410,7 @@ export function makeDefaultServices(
remove: jest.fn(),
clear: jest.fn(),
},
+ spaces: spacesPluginMock.createStartContract(),
};
}
diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts
index ab0708d99f082..5a42ea054b4d9 100644
--- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts
+++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts
@@ -15,7 +15,7 @@ describe('LensStore', () => {
bulkUpdate: jest.fn(([{ id }]: SavedObjectsBulkUpdateObject[]) =>
Promise.resolve({ savedObjects: [{ id }, { id }] })
),
- get: jest.fn(),
+ resolve: jest.fn(),
};
return {
@@ -142,15 +142,18 @@ describe('LensStore', () => {
describe('load', () => {
test('throws if an error is returned', async () => {
const { client, store } = testStore();
- client.get = jest.fn(async () => ({
- id: 'Paul',
- type: 'lens',
- attributes: {
- title: 'Hope clouds observation.',
- visualizationType: 'dune',
- state: '{ "datasource": { "giantWorms": true } }',
+ client.resolve = jest.fn(async () => ({
+ outcome: 'exactMatch',
+ saved_object: {
+ id: 'Paul',
+ type: 'lens',
+ attributes: {
+ title: 'Hope clouds observation.',
+ visualizationType: 'dune',
+ state: '{ "datasource": { "giantWorms": true } }',
+ },
+ error: new Error('shoot dang!'),
},
- error: new Error('shoot dang!'),
}));
await expect(store.load('Paul')).rejects.toThrow('shoot dang!');
diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts
index c87548daf53dc..79d7b78f768ae 100644
--- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts
+++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts
@@ -9,9 +9,11 @@ import {
SavedObjectAttributes,
SavedObjectsClientContract,
SavedObjectReference,
+ ResolvedSimpleSavedObject,
} from 'kibana/public';
import { Query } from '../../../../../src/plugins/data/public';
import { DOC_TYPE, PersistableFilter } from '../../common';
+import { LensSavedObjectAttributes } from '../async_services';
export interface Document {
savedObjectId?: string;
@@ -37,7 +39,7 @@ export interface DocumentSaver {
}
export interface DocumentLoader {
- load: (savedObjectId: string) => Promise;
+ load: (savedObjectId: string) => Promise;
}
export type SavedObjectStore = DocumentLoader & DocumentSaver;
@@ -87,18 +89,16 @@ export class SavedObjectIndexStore implements SavedObjectStore {
).savedObjects[1];
}
- async load(savedObjectId: string): Promise {
- const { type, attributes, references, error } = await this.client.get(DOC_TYPE, savedObjectId);
+ async load(savedObjectId: string): Promise> {
+ const resolveResult = await this.client.resolve(
+ DOC_TYPE,
+ savedObjectId
+ );
- if (error) {
- throw error;
+ if (resolveResult.saved_object.error) {
+ throw resolveResult.saved_object.error;
}
- return {
- ...(attributes as SavedObjectAttributes),
- references,
- savedObjectId,
- type,
- } as Document;
+ return resolveResult;
}
}
diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx
index c1b9f4c799e64..c947d50d5b910 100644
--- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx
@@ -44,6 +44,7 @@ export const getPieRenderer = (dependencies: {
{...config}
formatFactory={dependencies.formatFactory}
chartsThemeService={dependencies.chartsThemeService}
+ interactive={handlers.isInteractive()}
paletteService={dependencies.paletteService}
onClickValue={onClickValue}
renderMode={handlers.getRenderMode()}
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
index 93f16c49061e4..209d7ff652ea1 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
@@ -77,7 +77,7 @@ describe('PieVisualization component', () => {
onClickValue: jest.fn(),
chartsThemeService,
paletteService: chartPluginMock.createPaletteRegistry(),
- renderMode: 'display' as const,
+ renderMode: 'view' as const,
syncColors: false,
};
}
@@ -302,10 +302,10 @@ describe('PieVisualization component', () => {
`);
});
- test('does not set click listener on noInteractivity render mode', () => {
+ test('does not set click listener on non-interactive mode', () => {
const defaultArgs = getDefaultArgs();
const component = shallow(
-
+
);
expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined();
});
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index 41b96ff4324ae..a0a845dc96007 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -55,6 +55,7 @@ export function PieComponent(
props: PieExpressionProps & {
formatFactory: FormatFactory;
chartsThemeService: ChartsPluginSetup['theme'];
+ interactive?: boolean;
paletteService: PaletteRegistry;
onClickValue: (data: LensFilterEvent['data']) => void;
renderMode: RenderMode;
@@ -289,9 +290,7 @@ export function PieComponent(
}
legendPosition={legendPosition || Position.Right}
legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */}
- onElementClick={
- props.renderMode !== 'noInteractivity' ? onElementClickHandler : undefined
- }
+ onElementClick={props.interactive ?? true ? onElementClickHandler : undefined}
legendAction={getLegendAction(firstTable, onClickValue)}
theme={{
...chartTheme,
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts
index 95f2e13cbc464..26278f446c558 100644
--- a/x-pack/plugins/lens/public/plugin.ts
+++ b/x-pack/plugins/lens/public/plugin.ts
@@ -9,6 +9,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public';
import type { Start as InspectorStartContract } from 'src/plugins/inspector/public';
import type { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public';
import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public';
+import { SpacesPluginStart } from '../../spaces/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import { DashboardStart } from '../../../../src/plugins/dashboard/public';
@@ -100,6 +101,7 @@ export interface LensPluginStartDependencies {
presentationUtil: PresentationUtilPluginStart;
indexPatternFieldEditor: IndexPatternFieldEditorStart;
inspector: InspectorStartContract;
+ spaces: SpacesPluginStart;
usageCollection?: UsageCollectionStart;
}
diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx
index ad1755bdbe85c..cda891871168e 100644
--- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx
+++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { EuiColorPalettePickerPaletteProps } from '@elastic/eui';
+import { EuiButtonGroup, EuiColorPalettePickerPaletteProps } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test/jest';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
@@ -14,6 +14,7 @@ import { ReactWrapper } from 'enzyme';
import type { CustomPaletteParams } from '../../../common';
import { applyPaletteParams } from './utils';
import { CustomizablePalette } from './palette_configuration';
+import { act } from 'react-dom/test-utils';
// mocking random id generator function
jest.mock('@elastic/eui', () => {
@@ -128,71 +129,136 @@ describe('palette panel', () => {
});
});
- describe('reverse option', () => {
- beforeEach(() => {
- props = {
- activePalette: { type: 'palette', name: 'positive' },
- palettes: paletteRegistry,
- setPalette: jest.fn(),
- dataBounds: { min: 0, max: 100 },
- };
- });
+ it('should rewrite the min/max range values on palette change', () => {
+ const instance = mountWithIntl();
+
+ changePaletteIn(instance, 'custom');
- function toggleReverse(instance: ReactWrapper, checked: boolean) {
- return instance
- .find('[data-test-subj="lnsPalettePanel_dynamicColoring_reverse"]')
- .first()
- .prop('onClick')!({} as React.MouseEvent);
- }
-
- it('should reverse the colorStops on click', () => {
- const instance = mountWithIntl();
-
- toggleReverse(instance, true);
-
- expect(props.setPalette).toHaveBeenCalledWith(
- expect.objectContaining({
- params: expect.objectContaining({
- reverse: true,
- }),
- })
- );
+ expect(props.setPalette).toHaveBeenCalledWith({
+ type: 'palette',
+ name: 'custom',
+ params: expect.objectContaining({
+ rangeMin: 0,
+ rangeMax: 50,
+ }),
});
});
+ });
+
+ describe('reverse option', () => {
+ beforeEach(() => {
+ props = {
+ activePalette: { type: 'palette', name: 'positive' },
+ palettes: paletteRegistry,
+ setPalette: jest.fn(),
+ dataBounds: { min: 0, max: 100 },
+ };
+ });
+
+ function toggleReverse(instance: ReactWrapper, checked: boolean) {
+ return instance
+ .find('[data-test-subj="lnsPalettePanel_dynamicColoring_reverse"]')
+ .first()
+ .prop('onClick')!({} as React.MouseEvent);
+ }
+
+ it('should reverse the colorStops on click', () => {
+ const instance = mountWithIntl();
+
+ toggleReverse(instance, true);
+
+ expect(props.setPalette).toHaveBeenCalledWith(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ reverse: true,
+ }),
+ })
+ );
+ });
+ });
+
+ describe('percentage / number modes', () => {
+ beforeEach(() => {
+ props = {
+ activePalette: { type: 'palette', name: 'positive' },
+ palettes: paletteRegistry,
+ setPalette: jest.fn(),
+ dataBounds: { min: 5, max: 200 },
+ };
+ });
- describe('custom stops', () => {
- beforeEach(() => {
- props = {
- activePalette: { type: 'palette', name: 'positive' },
- palettes: paletteRegistry,
- setPalette: jest.fn(),
- dataBounds: { min: 0, max: 100 },
- };
+ it('should switch mode and range boundaries on click', () => {
+ const instance = mountWithIntl();
+ act(() => {
+ instance
+ .find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"]')
+ .find(EuiButtonGroup)
+ .prop('onChange')!('number');
});
- it('should be visible for predefined palettes', () => {
- const instance = mountWithIntl();
- expect(
- instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists()
- ).toEqual(true);
+
+ act(() => {
+ instance
+ .find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"]')
+ .find(EuiButtonGroup)
+ .prop('onChange')!('percent');
});
- it('should be visible for custom palettes', () => {
- const instance = mountWithIntl(
- {
+ beforeEach(() => {
+ props = {
+ activePalette: { type: 'palette', name: 'positive' },
+ palettes: paletteRegistry,
+ setPalette: jest.fn(),
+ dataBounds: { min: 0, max: 100 },
+ };
+ });
+ it('should be visible for predefined palettes', () => {
+ const instance = mountWithIntl();
+ expect(
+ instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists()
+ ).toEqual(true);
+ });
+
+ it('should be visible for custom palettes', () => {
+ const instance = mountWithIntl(
+
- );
- expect(
- instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists()
- ).toEqual(true);
- });
+ },
+ }}
+ />
+ );
+ expect(
+ instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists()
+ ).toEqual(true);
});
});
});
diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx
index bc6a590db0cb7..1d1e212b87c0c 100644
--- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx
+++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx
@@ -108,16 +108,21 @@ export function CustomizablePalette({
colorStops: undefined,
};
+ const newColorStops = getColorStops(palettes, [], activePalette, dataBounds);
if (isNewPaletteCustom) {
- newParams.colorStops = getColorStops(palettes, [], activePalette, dataBounds);
+ newParams.colorStops = newColorStops;
}
newParams.stops = getPaletteStops(palettes, newParams, {
prevPalette:
isNewPaletteCustom || isCurrentPaletteCustom ? undefined : newPalette.name,
dataBounds,
+ mapFromMinValue: true,
});
+ newParams.rangeMin = newColorStops[0].stop;
+ newParams.rangeMax = newColorStops[newColorStops.length - 1].stop;
+
setPalette({
...newPalette,
params: newParams,
@@ -266,18 +271,18 @@ export function CustomizablePalette({
) as RequiredPaletteParamTypes['rangeType'];
const params: CustomPaletteParams = { rangeType: newRangeType };
+ const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds);
+ const { min: oldMin, max: oldMax } = getDataMinMax(
+ activePalette.params?.rangeType,
+ dataBounds
+ );
+ const newColorStops = remapStopsByNewInterval(colorStopsToShow, {
+ oldInterval: oldMax - oldMin,
+ newInterval: newMax - newMin,
+ newMin,
+ oldMin,
+ });
if (isCurrentPaletteCustom) {
- const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds);
- const { min: oldMin, max: oldMax } = getDataMinMax(
- activePalette.params?.rangeType,
- dataBounds
- );
- const newColorStops = remapStopsByNewInterval(colorStopsToShow, {
- oldInterval: oldMax - oldMin,
- newInterval: newMax - newMin,
- newMin,
- oldMin,
- });
const stops = getPaletteStops(
palettes,
{ ...activePalette.params, colorStops: newColorStops, ...params },
@@ -285,8 +290,6 @@ export function CustomizablePalette({
);
params.colorStops = newColorStops;
params.stops = stops;
- params.rangeMin = newColorStops[0].stop;
- params.rangeMax = newColorStops[newColorStops.length - 1].stop;
} else {
params.stops = getPaletteStops(
palettes,
@@ -294,6 +297,11 @@ export function CustomizablePalette({
{ prevPalette: activePalette.name, dataBounds }
);
}
+ // why not use newMin/newMax here?
+ // That's because there's the concept of continuity to accomodate, where in some scenarios it has to
+ // take into account the stop value rather than the data value
+ params.rangeMin = newColorStops[0].stop;
+ params.rangeMax = newColorStops[newColorStops.length - 1].stop;
setPalette(mergePaletteParams(activePalette, params));
}}
/>
diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts
index 97dc2e45c96dc..07d93ca5c40c6 100644
--- a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts
+++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts
@@ -8,6 +8,7 @@
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import {
applyPaletteParams,
+ getColorStops,
getContrastColor,
getDataMinMax,
getPaletteStops,
@@ -59,6 +60,78 @@ describe('applyPaletteParams', () => {
});
});
+describe('getColorStops', () => {
+ const paletteRegistry = chartPluginMock.createPaletteRegistry();
+ it('should return the same colorStops if a custom palette is passed, avoiding recomputation', () => {
+ const colorStops = [
+ { stop: 0, color: 'red' },
+ { stop: 100, color: 'blue' },
+ ];
+ expect(
+ getColorStops(
+ paletteRegistry,
+ colorStops,
+ { name: 'custom', type: 'palette' },
+ { min: 0, max: 100 }
+ )
+ ).toBe(colorStops);
+ });
+
+ it('should get a fresh list of colors', () => {
+ expect(
+ getColorStops(
+ paletteRegistry,
+ [
+ { stop: 0, color: 'red' },
+ { stop: 100, color: 'blue' },
+ ],
+ { name: 'mocked', type: 'palette' },
+ { min: 0, max: 100 }
+ )
+ ).toEqual([
+ { color: 'blue', stop: 0 },
+ { color: 'yellow', stop: 50 },
+ ]);
+ });
+
+ it('should get a fresh list of colors even if custom palette but empty colorStops', () => {
+ expect(
+ getColorStops(paletteRegistry, [], { name: 'mocked', type: 'palette' }, { min: 0, max: 100 })
+ ).toEqual([
+ { color: 'blue', stop: 0 },
+ { color: 'yellow', stop: 50 },
+ ]);
+ });
+
+ it('should correctly map the new colorStop to the current data bound and minValue', () => {
+ expect(
+ getColorStops(
+ paletteRegistry,
+ [],
+ { name: 'mocked', type: 'palette', params: { rangeType: 'number' } },
+ { min: 100, max: 1000 }
+ )
+ ).toEqual([
+ { color: 'blue', stop: 100 },
+ { color: 'yellow', stop: 550 },
+ ]);
+ });
+
+ it('should reverse the colors', () => {
+ expect(
+ getColorStops(
+ paletteRegistry,
+ [],
+ { name: 'mocked', type: 'palette', params: { reverse: true } },
+ { min: 100, max: 1000 }
+ )
+ ).toEqual([
+ { color: 'yellow', stop: 0 },
+ { color: 'blue', stop: 50 },
+ ]);
+ });
+});
+
describe('remapStopsByNewInterval', () => {
it('should correctly remap the current palette from 0..1 to 0...100', () => {
expect(
diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts
index b2969565f5390..413e3708e9c9b 100644
--- a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts
+++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts
@@ -269,11 +269,10 @@ export function getColorStops(
palettes: PaletteRegistry,
colorStops: Required['stops'],
activePalette: PaletteOutput,
- dataBounds: { min: number; max: number },
- defaultPalette?: string
+ dataBounds: { min: number; max: number }
) {
// just forward the current stops if custom
- if (activePalette?.name === CUSTOM_PALETTE) {
+ if (activePalette?.name === CUSTOM_PALETTE && colorStops?.length) {
return colorStops;
}
// for predefined palettes create some stops, then drop the last one.
diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts
index bf13ca69e82c0..256684c5dbc25 100644
--- a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts
+++ b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts
@@ -19,13 +19,7 @@ export const initMiddleware = (storeDeps: LensStoreDeps) => (store: MiddlewareAP
);
return (next: Dispatch) => (action: PayloadAction) => {
if (lensSlice.actions.loadInitial.match(action)) {
- return loadInitial(
- store,
- storeDeps,
- action.payload.redirectCallback,
- action.payload.initialInput,
- action.payload.emptyState
- );
+ return loadInitial(store, storeDeps, action.payload);
} else if (lensSlice.actions.navigateAway.match(action)) {
return unsubscribeFromExternalContext();
}
diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx
index 79402b698af98..6d3b77c6476e5 100644
--- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx
+++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx
@@ -12,6 +12,7 @@ import {
createMockDatasource,
DatasourceMock,
} from '../../mocks';
+import { Location, History } from 'history';
import { act } from 'react-dom/test-utils';
import { loadInitial } from './load_initial';
import { LensEmbeddableInput } from '../../embeddable';
@@ -65,7 +66,12 @@ describe('Mounter', () => {
it('should initialize initial datasource', async () => {
const services = makeDefaultServices();
- services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
+ services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
+ ...defaultDoc,
+ sharingSavedObjectProps: {
+ outcome: 'exactMatch',
+ },
+ });
const lensStore = await makeLensStore({
data: services.data,
@@ -79,8 +85,10 @@ describe('Mounter', () => {
datasourceMap,
visualizationMap,
},
- jest.fn(),
- ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
+ {
+ redirectCallback: jest.fn(),
+ initialInput: ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput,
+ }
);
});
expect(mockDatasource.initialize).toHaveBeenCalled();
@@ -88,7 +96,12 @@ describe('Mounter', () => {
it('should have initialized only the initial datasource and visualization', async () => {
const services = makeDefaultServices();
- services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
+ services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
+ ...defaultDoc,
+ sharingSavedObjectProps: {
+ outcome: 'exactMatch',
+ },
+ });
const lensStore = await makeLensStore({ data: services.data, preloadedState });
await act(async () => {
@@ -99,7 +112,7 @@ describe('Mounter', () => {
datasourceMap,
visualizationMap,
},
- jest.fn()
+ { redirectCallback: jest.fn() }
);
});
expect(mockDatasource.initialize).toHaveBeenCalled();
@@ -129,7 +142,7 @@ describe('Mounter', () => {
datasourceMap,
visualizationMap,
},
- jest.fn()
+ { redirectCallback: jest.fn() }
);
expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled();
});
@@ -170,7 +183,11 @@ describe('Mounter', () => {
const emptyState = getPreloadedState(storeDeps) as LensAppState;
services.attributeService.unwrapAttributes = jest.fn();
await act(async () => {
- await loadInitial(lensStore, storeDeps, jest.fn(), undefined, emptyState);
+ await loadInitial(lensStore, storeDeps, {
+ redirectCallback: jest.fn(),
+ initialInput: undefined,
+ emptyState,
+ });
});
expect(lensStore.getState()).toEqual({
@@ -189,20 +206,28 @@ describe('Mounter', () => {
it('loads a document and uses query and filters if initial input is provided', async () => {
const services = makeDefaultServices();
- services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
+ services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
+ ...defaultDoc,
+ sharingSavedObjectProps: {
+ outcome: 'exactMatch',
+ },
+ });
+ const storeDeps = {
+ lensServices: services,
+ datasourceMap,
+ visualizationMap,
+ };
+ const emptyState = getPreloadedState(storeDeps) as LensAppState;
const lensStore = await makeLensStore({ data: services.data, preloadedState });
await act(async () => {
- await loadInitial(
- lensStore,
- {
- lensServices: services,
- datasourceMap,
- visualizationMap,
- },
- jest.fn(),
- ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
- );
+ await loadInitial(lensStore, storeDeps, {
+ redirectCallback: jest.fn(),
+ initialInput: ({
+ savedObjectId: defaultSavedObjectId,
+ } as unknown) as LensEmbeddableInput,
+ emptyState,
+ });
});
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({
@@ -235,8 +260,12 @@ describe('Mounter', () => {
datasourceMap,
visualizationMap,
},
- jest.fn(),
- ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
+ {
+ redirectCallback: jest.fn(),
+ initialInput: ({
+ savedObjectId: defaultSavedObjectId,
+ } as unknown) as LensEmbeddableInput,
+ }
);
});
@@ -248,8 +277,12 @@ describe('Mounter', () => {
datasourceMap,
visualizationMap,
},
- jest.fn(),
- ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
+ {
+ redirectCallback: jest.fn(),
+ initialInput: ({
+ savedObjectId: defaultSavedObjectId,
+ } as unknown) as LensEmbeddableInput,
+ }
);
});
@@ -263,8 +296,10 @@ describe('Mounter', () => {
datasourceMap,
visualizationMap,
},
- jest.fn(),
- ({ savedObjectId: '5678' } as unknown) as LensEmbeddableInput
+ {
+ redirectCallback: jest.fn(),
+ initialInput: ({ savedObjectId: '5678' } as unknown) as LensEmbeddableInput,
+ }
);
});
@@ -287,8 +322,12 @@ describe('Mounter', () => {
datasourceMap,
visualizationMap,
},
- redirectCallback,
- ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
+ {
+ redirectCallback,
+ initialInput: ({
+ savedObjectId: defaultSavedObjectId,
+ } as unknown) as LensEmbeddableInput,
+ }
);
});
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({
@@ -298,6 +337,50 @@ describe('Mounter', () => {
expect(redirectCallback).toHaveBeenCalled();
});
+ it('redirects if saved object is an aliasMatch', async () => {
+ const services = makeDefaultServices();
+
+ const lensStore = makeLensStore({ data: services.data, preloadedState });
+
+ services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
+ ...defaultDoc,
+ sharingSavedObjectProps: {
+ outcome: 'aliasMatch',
+ aliasTargetId: 'id2',
+ },
+ });
+
+ await act(async () => {
+ await loadInitial(
+ lensStore,
+ {
+ lensServices: services,
+ datasourceMap,
+ visualizationMap,
+ },
+ {
+ redirectCallback: jest.fn(),
+ initialInput: ({
+ savedObjectId: defaultSavedObjectId,
+ } as unknown) as LensEmbeddableInput,
+ history: {
+ location: {
+ search: '?search',
+ } as Location,
+ } as History,
+ }
+ );
+ });
+ expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({
+ savedObjectId: defaultSavedObjectId,
+ });
+
+ expect(services.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith(
+ '#/edit/id2?search',
+ 'Lens visualization'
+ );
+ });
+
it('adds to the recently accessed list on load', async () => {
const services = makeDefaultServices();
const lensStore = makeLensStore({ data: services.data, preloadedState });
@@ -309,8 +392,12 @@ describe('Mounter', () => {
datasourceMap,
visualizationMap,
},
- jest.fn(),
- ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput
+ {
+ redirectCallback: jest.fn(),
+ initialInput: ({
+ savedObjectId: defaultSavedObjectId,
+ } as unknown) as LensEmbeddableInput,
+ }
);
});
diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts
index 0be2bc9cfc00e..8ae6e58019c91 100644
--- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts
+++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts
@@ -8,8 +8,10 @@
import { MiddlewareAPI } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
+import { History } from 'history';
import { LensAppState, setState } from '..';
import { updateLayer, updateVisualizationState, LensStoreDeps } from '..';
+import { SharingSavedObjectProps } from '../../types';
import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable';
import { getInitialDatasourceId } from '../../utils';
import { initializeDatasources } from '../../editor_frame_service/editor_frame';
@@ -19,22 +21,50 @@ import {
switchToSuggestion,
} from '../../editor_frame_service/editor_frame/suggestion_helpers';
import { LensAppServices } from '../../app_plugin/types';
-import { getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants';
+import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants';
import { Document, injectFilterReferences } from '../../persistence';
export const getPersisted = async ({
initialInput,
lensServices,
+ history,
}: {
initialInput: LensEmbeddableInput;
lensServices: LensAppServices;
-}): Promise<{ doc: Document } | undefined> => {
- const { notifications, attributeService } = lensServices;
+ history?: History;
+}): Promise<
+ { doc: Document; sharingSavedObjectProps: Omit } | undefined
+> => {
+ const { notifications, spaces, attributeService } = lensServices;
let doc: Document;
try {
- const attributes = await attributeService.unwrapAttributes(initialInput);
-
+ const result = await attributeService.unwrapAttributes(initialInput);
+ if (!result) {
+ return {
+ doc: ({
+ ...initialInput,
+ type: LENS_EMBEDDABLE_TYPE,
+ } as unknown) as Document,
+ sharingSavedObjectProps: {
+ outcome: 'exactMatch',
+ },
+ };
+ }
+ const { sharingSavedObjectProps, ...attributes } = result;
+ if (spaces && sharingSavedObjectProps?.outcome === 'aliasMatch' && history) {
+ // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash
+ const newObjectId = sharingSavedObjectProps?.aliasTargetId; // This is always defined if outcome === 'aliasMatch'
+ const newPath = lensServices.http.basePath.prepend(
+ `${getEditPath(newObjectId)}${history.location.search}`
+ );
+ await spaces.ui.redirectLegacyUrl(
+ newPath,
+ i18n.translate('xpack.lens.legacyUrlConflict.objectNoun', {
+ defaultMessage: 'Lens visualization',
+ })
+ );
+ }
doc = {
...initialInput,
...attributes,
@@ -43,6 +73,10 @@ export const getPersisted = async ({
return {
doc,
+ sharingSavedObjectProps: {
+ aliasTargetId: sharingSavedObjectProps?.aliasTargetId,
+ outcome: sharingSavedObjectProps?.outcome,
+ },
};
} catch (e) {
notifications.toasts.addDanger(
@@ -62,9 +96,17 @@ export function loadInitial(
embeddableEditorIncomingState,
initialContext,
}: LensStoreDeps,
- redirectCallback: (savedObjectId?: string) => void,
- initialInput?: LensEmbeddableInput,
- emptyState?: LensAppState
+ {
+ redirectCallback,
+ initialInput,
+ emptyState,
+ history,
+ }: {
+ redirectCallback: (savedObjectId?: string) => void;
+ initialInput?: LensEmbeddableInput;
+ emptyState?: LensAppState;
+ history?: History;
+ }
) {
const { getState, dispatch } = store;
const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices;
@@ -146,11 +188,11 @@ export function loadInitial(
redirectCallback();
});
}
- getPersisted({ initialInput, lensServices })
+ getPersisted({ initialInput, lensServices, history })
.then(
(persisted) => {
if (persisted) {
- const { doc } = persisted;
+ const { doc, sharingSavedObjectProps } = persisted;
if (attributeService.inputIsRefType(initialInput)) {
lensServices.chrome.recentlyAccessed.add(
getFullPath(initialInput.savedObjectId),
@@ -190,6 +232,7 @@ export function loadInitial(
dispatch(
setState({
+ sharingSavedObjectProps,
query: doc.state.query,
searchSessionId:
dashboardFeatureFlag.allowByValueEmbeddables &&
diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts
index 85cb79f6ea5da..6cf0529b34575 100644
--- a/x-pack/plugins/lens/public/state_management/lens_slice.ts
+++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts
@@ -6,6 +6,7 @@
*/
import { createSlice, current, PayloadAction } from '@reduxjs/toolkit';
+import { History } from 'history';
import { LensEmbeddableInput } from '..';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import { getInitialDatasourceId, getResolvedDateRange } from '../utils';
@@ -301,6 +302,7 @@ export const lensSlice = createSlice({
initialInput?: LensEmbeddableInput;
redirectCallback: (savedObjectId?: string) => void;
emptyState: LensAppState;
+ history: History;
}>
) => state,
},
diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts
index 7321f72386b42..33f311a982f05 100644
--- a/x-pack/plugins/lens/public/state_management/types.ts
+++ b/x-pack/plugins/lens/public/state_management/types.ts
@@ -13,8 +13,7 @@ import { Document } from '../persistence';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import { DateRange } from '../../common';
import { LensAppServices } from '../app_plugin/types';
-import { DatasourceMap, VisualizationMap } from '../types';
-
+import { DatasourceMap, VisualizationMap, SharingSavedObjectProps } from '../types';
export interface VisualizationState {
activeId: string | null;
state: unknown;
@@ -44,6 +43,7 @@ export interface LensAppState extends EditorFrameState {
savedQuery?: SavedQuery;
searchSessionId: string;
resolvedDateRange: DateRange;
+ sharingSavedObjectProps?: Omit;
}
export type DispatchSetState = (
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 399e226a711db..844541cd2ad3e 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -256,7 +256,7 @@ export interface Datasource {
) =>
| Array<{
shortMessage: string;
- longMessage: string;
+ longMessage: React.ReactNode;
fixAction?: { label: string; newState: () => Promise };
}>
| undefined;
@@ -729,7 +729,7 @@ export interface Visualization {
) =>
| Array<{
shortMessage: string;
- longMessage: string;
+ longMessage: React.ReactNode;
}>
| undefined;
@@ -813,3 +813,9 @@ export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandle
| LensTableRowContextMenuEvent
) => void;
}
+
+export interface SharingSavedObjectProps {
+ outcome?: 'aliasMatch' | 'exactMatch' | 'conflict';
+ aliasTargetId?: string;
+ errorJSON?: string;
+}
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
index a41ad59ebee93..3994aadd9a989 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
@@ -480,7 +480,7 @@ describe('xy_expression', () => {
defaultProps = {
formatFactory: getFormatSpy,
timeZone: 'UTC',
- renderMode: 'display',
+ renderMode: 'view',
chartsThemeService,
chartsActiveCursorService,
paletteService,
@@ -1064,11 +1064,11 @@ describe('xy_expression', () => {
});
});
- test('onBrushEnd is not set on noInteractivity mode', () => {
+ test('onBrushEnd is not set on non-interactive mode', () => {
const { args, data } = sampleArgs();
const wrapper = mountWithIntl(
-
+
);
expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined();
@@ -1334,11 +1334,11 @@ describe('xy_expression', () => {
});
});
- test('onElementClick is not triggering event on noInteractivity mode', () => {
+ test('onElementClick is not triggering event on non-interactive mode', () => {
const { args, data } = sampleArgs();
const wrapper = mountWithIntl(
-
+
);
expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined();
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
index f8dff65969d57..75d14d9b48ee3 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
@@ -93,6 +93,7 @@ export type XYChartRenderProps = XYChartProps & {
formatFactory: FormatFactory;
timeZone: string;
minInterval: number | undefined;
+ interactive?: boolean;
onClickValue: (data: LensFilterEvent['data']) => void;
onSelectRange: (data: LensBrushEvent['data']) => void;
renderMode: RenderMode;
@@ -160,6 +161,7 @@ export const getXyChartRenderer = (dependencies: {
paletteService={dependencies.paletteService}
timeZone={dependencies.timeZone}
minInterval={calculateMinInterval(config)}
+ interactive={handlers.isInteractive()}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
renderMode={handlers.getRenderMode()}
@@ -233,7 +235,7 @@ export function XYChart({
minInterval,
onClickValue,
onSelectRange,
- renderMode,
+ interactive = true,
syncColors,
}: XYChartRenderProps) {
const {
@@ -528,8 +530,8 @@ export function XYChart({
}}
rotation={shouldRotate ? 90 : 0}
xDomain={xDomain}
- onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined}
- onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined}
+ onBrushEnd={interactive ? brushHandler : undefined}
+ onElementClick={interactive ? clickHandler : undefined}
legendAction={getLegendAction(
filteredLayers,
data.tables,
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
index 0a4b18f554f31..026c2827cedbd 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
@@ -383,7 +383,7 @@ export const getXyVisualization = ({
const errors: Array<{
shortMessage: string;
- longMessage: string;
+ longMessage: React.ReactNode;
}> = [];
// check if the layers in the state are compatible with this type of chart
@@ -488,7 +488,7 @@ function validateLayersForDimension(
| { valid: true }
| {
valid: false;
- payload: { shortMessage: string; longMessage: string };
+ payload: { shortMessage: string; longMessage: React.ReactNode };
} {
// Multiple layers must be consistent:
// * either a dimension is missing in ALL of them
diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json
index 04d3838df2063..16287ae596df3 100644
--- a/x-pack/plugins/lens/tsconfig.json
+++ b/x-pack/plugins/lens/tsconfig.json
@@ -15,6 +15,7 @@
"../../../typings/**/*"
],
"references": [
+ { "path": "../spaces/tsconfig.json" },
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../task_manager/tsconfig.json" },
{ "path": "../global_search/tsconfig.json"},
diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts
index e3611120348f4..e5e41b5fe4a85 100644
--- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts
+++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts
@@ -12,6 +12,6 @@ import { getExceptionListSchemaMock } from './exception_list_schema.mock';
export const getFoundExceptionListSchemaMock = (): FoundExceptionListSchema => ({
data: [getExceptionListSchemaMock()],
page: 1,
- per_page: 1,
+ per_page: 20,
total: 1,
});
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
index 4987de321c556..810fcaa15494f 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
@@ -41,13 +41,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
})
@@ -62,7 +62,8 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
- null,
+ expect.any(Function),
+ expect.any(Function),
]);
});
});
@@ -77,13 +78,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
})
@@ -100,10 +101,11 @@ describe('useExceptionLists', () => {
expectedListItemsResult,
{
page: 1,
- perPage: 1,
+ perPage: 20,
total: 1,
},
- result.current[3],
+ expect.any(Function),
+ expect.any(Function),
]);
});
});
@@ -117,13 +119,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: true,
})
@@ -153,13 +155,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
})
@@ -189,13 +191,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: true,
showTrustedApps: false,
})
@@ -225,13 +227,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
})
@@ -264,13 +266,13 @@ describe('useExceptionLists', () => {
name: 'Sample Endpoint',
},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
})
@@ -302,9 +304,9 @@ describe('useExceptionLists', () => {
errorMessage,
filterOptions,
http,
+ initialPagination,
namespaceTypes,
notifications,
- pagination,
showEventFilters,
showTrustedApps,
}) =>
@@ -312,9 +314,9 @@ describe('useExceptionLists', () => {
errorMessage,
filterOptions,
http,
+ initialPagination,
namespaceTypes,
notifications,
- pagination,
showEventFilters,
showTrustedApps,
}),
@@ -323,13 +325,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
},
@@ -344,13 +346,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
});
@@ -372,13 +374,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
})
@@ -390,8 +392,8 @@ describe('useExceptionLists', () => {
expect(typeof result.current[3]).toEqual('function');
- if (result.current[3] != null) {
- result.current[3]();
+ if (result.current[4] != null) {
+ result.current[4]();
}
// NOTE: Only need one call here because hook already initilaized
await waitForNextUpdate();
@@ -411,13 +413,13 @@ describe('useExceptionLists', () => {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
- namespaceTypes: ['single', 'agnostic'],
- notifications: mockKibanaNotificationsService,
- pagination: {
+ initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
showEventFilters: false,
showTrustedApps: false,
})
diff --git a/x-pack/plugins/ml/common/constants/messages.test.mock.ts b/x-pack/plugins/ml/common/constants/messages.test.mock.ts
index 6e539617604c1..fbfff20adc5c2 100644
--- a/x-pack/plugins/ml/common/constants/messages.test.mock.ts
+++ b/x-pack/plugins/ml/common/constants/messages.test.mock.ts
@@ -78,4 +78,7 @@ export const nonBasicIssuesMessages = [
{
id: 'missing_summary_count_field_name',
},
+ {
+ id: 'datafeed_preview_failed',
+ },
];
diff --git a/x-pack/plugins/ml/common/constants/messages.test.ts b/x-pack/plugins/ml/common/constants/messages.test.ts
index 59fc50757b674..c46eba458d1d2 100644
--- a/x-pack/plugins/ml/common/constants/messages.test.ts
+++ b/x-pack/plugins/ml/common/constants/messages.test.ts
@@ -173,6 +173,12 @@ describe('Constants: Messages parseMessages()', () => {
text:
'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.',
},
+ {
+ id: 'datafeed_preview_failed',
+ status: 'error',
+ text:
+ 'The datafeed preview failed. This may be due to an error in the job or datafeed configurations.',
+ },
]);
});
});
diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts
index 0327e8746c7d8..fd3b9aa9d19b9 100644
--- a/x-pack/plugins/ml/common/constants/messages.ts
+++ b/x-pack/plugins/ml/common/constants/messages.ts
@@ -626,6 +626,30 @@ export const getMessages = once((docLinks?: DocLinksStart) => {
'the UNIX epoch beginning. Timestamps before 01/01/1970 00:00:00 (UTC) are not supported for machine learning jobs.',
}),
},
+ datafeed_preview_no_documents: {
+ status: VALIDATION_STATUS.WARNING,
+ heading: i18n.translate(
+ 'xpack.ml.models.jobValidation.messages.datafeedPreviewNoDocumentsHeading',
+ {
+ defaultMessage: 'Datafeed preview',
+ }
+ ),
+ text: i18n.translate(
+ 'xpack.ml.models.jobValidation.messages.datafeedPreviewNoDocumentsMessage',
+ {
+ defaultMessage:
+ 'Running the datafeed preview over the current job configuration produces no results. ' +
+ 'If the index contains no documents this warning can be ignored, otherwise the job may be misconfigured.',
+ }
+ ),
+ },
+ datafeed_preview_failed: {
+ status: VALIDATION_STATUS.ERROR,
+ text: i18n.translate('xpack.ml.models.jobValidation.messages.datafeedPreviewFailedMessage', {
+ defaultMessage:
+ 'The datafeed preview failed. This may be due to an error in the job or datafeed configurations.',
+ }),
+ },
};
});
diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx
index bd4b805baa186..509c74c359657 100644
--- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx
@@ -63,6 +63,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => {
const [exporting, setExporting] = useState(false);
const [selectedJobType, setSelectedJobType] = useState(currentTab);
const [switchTabConfirmVisible, setSwitchTabConfirmVisible] = useState(false);
+ const [switchTabNextTab, setSwitchTabNextTab] = useState(currentTab);
const { displayErrorToast, displaySuccessToast } = useMemo(
() => toastNotificationServiceProvider(toasts),
[toasts]
@@ -170,16 +171,23 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => {
}
}
- const attemptTabSwitch = useCallback(() => {
- // if the user has already selected some jobs, open a confirm modal
- // rather than changing tabs
- if (selectedJobIds.length > 0) {
- setSwitchTabConfirmVisible(true);
- return;
- }
+ const attemptTabSwitch = useCallback(
+ (jobType: JobType) => {
+ if (jobType === selectedJobType) {
+ return;
+ }
+ // if the user has already selected some jobs, open a confirm modal
+ // rather than changing tabs
+ if (selectedJobIds.length > 0) {
+ setSwitchTabNextTab(jobType);
+ setSwitchTabConfirmVisible(true);
+ return;
+ }
- switchTab();
- }, [selectedJobIds]);
+ switchTab(jobType);
+ },
+ [selectedJobIds]
+ );
useEffect(() => {
setSelectedJobDependencies(
@@ -187,10 +195,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => {
);
}, [selectedJobIds]);
- function switchTab() {
- const jobType =
- selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector';
-
+ function switchTab(jobType: JobType) {
setSwitchTabConfirmVisible(false);
setSelectedJobIds([]);
setSelectedJobType(jobType);
@@ -211,7 +216,12 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => {
{showFlyout === true && isDisabled === false && (
<>
- setShowFlyout(false)} hideCloseButton size="s">
+ setShowFlyout(false)}
+ hideCloseButton
+ size="s"
+ data-test-subj="mlJobMgmtExportJobsFlyout"
+ >
@@ -227,8 +237,9 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => {
attemptTabSwitch('anomaly-detector')}
disabled={exporting}
+ data-test-subj="mlJobMgmtExportJobsADTab"
>
= ({ isDisabled, currentTab }) => {
attemptTabSwitch('data-frame-analytics')}
disabled={exporting}
+ data-test-subj="mlJobMgmtExportJobsDFATab"
>
= ({ isDisabled, currentTab }) => {
) : (
<>
-
-
+
+ {selectedJobIds.length === adJobIds.length ? (
+
+ ) : (
+
+ )}
- {adJobIds.map((id) => (
-
- toggleSelectedJob(e.target.checked, id)}
- />
-
-
- ))}
+
+ {adJobIds.map((id) => (
+
+ toggleSelectedJob(e.target.checked, id)}
+ />
+
+
+ ))}
+
>
)}
>
@@ -284,26 +310,39 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => {
) : (
<>
-
-
+
+ {selectedJobIds.length === dfaJobIds.length ? (
+
+ ) : (
+
+ )}
-
- {dfaJobIds.map((id) => (
-
- toggleSelectedJob(e.target.checked, id)}
- />
-
-
- ))}
+
+ {dfaJobIds.map((id) => (
+
+ toggleSelectedJob(e.target.checked, id)}
+ />
+
+
+ ))}
+
>
)}
>
@@ -329,6 +368,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => {
disabled={selectedJobIds.length === 0 || exporting === true}
onClick={onExport}
fill
+ data-test-subj="mlJobMgmtExportExportButton"
>
= ({ isDisabled, currentTab }) => {
{switchTabConfirmVisible === true ? (
switchTab(switchTabNextTab)}
/>
) : null}
>
diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx
index 732be345a1ee4..565ded9c6f6c3 100644
--- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx
+++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx
@@ -30,6 +30,7 @@ export const CannotImportJobsCallout: FC = ({ jobs, autoExpand = false })
values: { num: jobs.length },
})}
color="warning"
+ data-test-subj="mlJobMgmtImportJobsCannotBeImportedCallout"
>
{autoExpand ? (
diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx
index 4c7a2471db9d6..70f94d1e03155 100644
--- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx
+++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx
@@ -21,10 +21,12 @@ export const CannotReadFileCallout: FC = () => {
})}
color="warning"
>
-
+
+
+
>
);
diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx
index 68db42cdbf0eb..dfe07b1984e11 100644
--- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx
@@ -341,7 +341,12 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => {
{showFlyout === true && isDisabled === false && (
-
+
@@ -373,22 +378,26 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => {
{showFileReadError ? : null}
{totalJobsRead > 0 && jobType !== null && (
- <>
+
{jobType === 'anomaly-detector' && (
-
+
+
+
)}
{jobType === 'data-frame-analytics' && (
-
+
+
+
)}
@@ -426,6 +435,7 @@ export const ImportJobsFlyout: FC
= ({ isDisabled }) => {
value={jobId.jobId}
onChange={(e) => renameJob(e.target.value, i)}
isInvalid={jobId.jobIdValid === false}
+ data-test-subj="mlJobMgmtImportJobIdInput"
/>
@@ -465,7 +475,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => {
))}
- >
+
)}
>
@@ -484,7 +494,12 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => {
-
+
= ({
return { x: selection.times.map((v) => v * 1000), y: selection.lanes };
}, [selection, swimlaneData, swimlaneType]);
- const swimLaneConfig: HeatmapSpec['config'] = useMemo(() => {
+ const swimLaneConfig = useMemo(() => {
if (!showSwimlane) return {};
- return {
+ const config: HeatmapSpec['config'] = {
onBrushEnd: (e: HeatmapBrushEvent) => {
if (!e.cells.length) return;
@@ -318,7 +318,7 @@ export const SwimlaneContainer: FC = ({
yAxisLabel: {
visible: true,
width: Y_AXIS_LABEL_WIDTH,
- fill: euiTheme.euiTextSubduedColor,
+ textColor: euiTheme.euiTextSubduedColor,
padding: Y_AXIS_LABEL_PADDING,
formatter: (laneLabel: string) => {
return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel;
@@ -327,7 +327,7 @@ export const SwimlaneContainer: FC = ({
},
xAxisLabel: {
visible: true,
- fill: euiTheme.euiTextSubduedColor,
+ textColor: euiTheme.euiTextSubduedColor,
formatter: (v: number) => {
timeBuckets.setInterval(`${swimlaneData.interval}s`);
const scaledDateFormat = timeBuckets.getScaledDateFormat();
@@ -346,6 +346,8 @@ export const SwimlaneContainer: FC = ({
...(showLegend ? { maxLegendHeight: LEGEND_HEIGHT } : {}),
timeZone: 'UTC',
};
+
+ return config;
}, [
showSwimlane,
swimlaneType,
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx
index a1c7eab6b746f..d2a0e83200d92 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Fragment, FC, useContext, useEffect } from 'react';
+import React, { Fragment, FC, useContext, useEffect, useState } from 'react';
import { WizardNav } from '../wizard_nav';
import { WIZARD_STEPS, StepProps } from '../step_types';
import { JobCreatorContext } from '../job_creator_context';
@@ -22,6 +22,7 @@ const idFilterList = [
export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) => {
const { jobCreator, jobCreatorUpdate, jobValidator } = useContext(JobCreatorContext);
+ const [nextActive, setNextActive] = useState(false);
if (jobCreator.type === JOB_TYPE.ADVANCED) {
// for advanced jobs, ignore time range warning as the
@@ -52,6 +53,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep })
// keep a record of the advanced validation in the jobValidator
function setIsValid(valid: boolean) {
jobValidator.advancedValid = valid;
+ setNextActive(valid);
}
return (
@@ -69,7 +71,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep })
setCurrentStep(WIZARD_STEPS.JOB_DETAILS)}
next={() => setCurrentStep(WIZARD_STEPS.SUMMARY)}
- nextActive={true}
+ nextActive={nextActive}
/>