diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc
index c66970283580c4..c8d54a4c0f9da7 100644
--- a/docs/CHANGELOG.asciidoc
+++ b/docs/CHANGELOG.asciidoc
@@ -205,6 +205,21 @@ The existing agents in {kib} are not migrated as part of the migration to Fleet.
The existing agent API keys are invalidated and display as `Inactive` on the *Agents* page.
====
+[discrete]
+[[breaking-98039]]
+.Disable Explore underlying data context menu
+[%collapsible]
+====
+*Details* +
+The *Explore underlying data* context menu on dashboards is now disabled by default. For more information, refer to {kibana-pull}98039[#98039].
+
+*Impact* +
+To enable the *Explore underlying data* context menu, set `xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled` to `true` in kibana.yml.
+====
+
+// end::notable-breaking-changes[]
+
+
[float]
[[deprecations-7.13.0]]
=== Deprecations
diff --git a/docs/discover/search-sessions.asciidoc b/docs/discover/search-sessions.asciidoc
index fec1b8b26dd741..b503e8cfba3b40 100644
--- a/docs/discover/search-sessions.asciidoc
+++ b/docs/discover/search-sessions.asciidoc
@@ -68,3 +68,19 @@ behaves differently:
* Relative dates are converted to absolute dates.
* Panning and zooming is disabled for maps.
* Changing a filter, query, or drilldown starts a new search session, which can be slow.
+
+[float]
+==== Limitations
+
+Certain visualization features do not fully support background search sessions yet. If a dashboard using these features gets restored,
+all panels using unsupported features won't load immediately, but instead send out additional data requests which can take a while to complete.
+In this case a warning *Your search session is still running* will be shown.
+
+You can either wait for these additional requests to complete or come back to the dashboard later when all data requests have been finished.
+
+A panel on a dashboard can behave like this if one of the following features is used:
+* *Lens* - A *top values* dimension with an enabled setting *Group other values as "Other"* (configurable in the *Advanced* section of the dimension)
+* *Lens* - An *intervals* dimension is used
+* *Aggregation based* visualizations - A *terms* aggregation is used with an enabled setting *Group other values in separate bucket*
+* *Aggregation based* visualizations - A *histogram* aggregation is used
+* *Maps* - Layers using joins, blended layers or tracks layers are used
diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc
index 39e596df4af347..001114578a1cd0 100644
--- a/docs/user/dashboard/aggregation-reference.asciidoc
+++ b/docs/user/dashboard/aggregation-reference.asciidoc
@@ -23,7 +23,7 @@ This reference can help simplify the comparison if you need a specific feature.
| Table with summary row
^| X
-|
+^| X
|
|
|
@@ -65,7 +65,7 @@ This reference can help simplify the comparison if you need a specific feature.
| Heat map
^| X
-|
+^| X
|
|
^| X
@@ -333,7 +333,7 @@ build their advanced visualization.
| Math on aggregated data
|
-|
+^| X
^| X
^| X
^| X
@@ -352,6 +352,13 @@ build their advanced visualization.
^| X
^| X
+| Time shifts
+|
+^| X
+^| X
+^| X
+^| X
+
| Fully custom {es} queries
|
|
diff --git a/docs/user/dashboard/create-panels-with-editors.asciidoc b/docs/user/dashboard/create-panels-with-editors.asciidoc
index 17d3b5fb8a8a5a..77a4706e249fd8 100644
--- a/docs/user/dashboard/create-panels-with-editors.asciidoc
+++ b/docs/user/dashboard/create-panels-with-editors.asciidoc
@@ -30,13 +30,16 @@
[[lens-editor]]
=== Lens
-*Lens* is the drag and drop editor that creates visualizations of your data.
+*Lens* is the drag and drop editor that creates visualizations of your data, recommended for most
+users.
With *Lens*, you can:
* Use the automatically generated suggestions to change the visualization type.
* Create visualizations with multiple layers and indices.
* Change the aggregation and labels to customize the data.
+* Perform math on aggregations using *Formula*.
+* Use time shifts to compare data at two times, such as month over month.
[role="screenshot"]
image:dashboard/images/lens_advanced_1_1.png[Lens]
diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc
index 9f17a380bc209a..7927489c596d77 100644
--- a/docs/user/dashboard/lens.asciidoc
+++ b/docs/user/dashboard/lens.asciidoc
@@ -300,7 +300,9 @@ image::images/lens_missing_values_strategy.png[Lens Missing values strategies me
[[is-it-possible-to-change-the-scale-of-Y-axis]]
===== Is it possible to statically define the scale of the y-axis in a visualization?
-The ability to start the y-axis from another value than 0, or use a logarithmic scale, is unsupported in *Lens*.
+Yes, you can set the bounds on bar, line and area chart types in Lens, unless using percentage mode. Bar
+and area charts must have 0 in the bounds. Logarithmic scales are unsupported in *Lens*.
+To set the y-axis bounds, click the icon representing the axis you want to customize.
[float]
[[is-it-possible-to-have-pagination-for-datatable]]
diff --git a/package.json b/package.json
index dbe5fe0b78620c..8b327f760753ff 100644
--- a/package.json
+++ b/package.json
@@ -667,7 +667,7 @@
"callsites": "^3.1.0",
"chai": "3.5.0",
"chance": "1.0.18",
- "chromedriver": "^90.0.0",
+ "chromedriver": "^91.0.1",
"clean-webpack-plugin": "^3.0.0",
"cmd-shim": "^2.1.0",
"compression-webpack-plugin": "^4.0.0",
@@ -835,4 +835,4 @@
"yargs": "^15.4.1",
"zlib": "^1.0.5"
}
-}
+}
\ No newline at end of file
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx
index 14086e6ee81370..1352081eaa30bc 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx
@@ -11,9 +11,7 @@ import React, { createContext, useContext } from 'react';
import { useRequest } from '../../../public';
-import { Error as CustomError } from './section_error';
-
-import { Privileges } from '../types';
+import { Privileges, Error as CustomError } from '../types';
interface Authorization {
isLoading: boolean;
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts
index b1306028737243..f8eb7e3c7c0c83 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts
@@ -16,4 +16,6 @@ export { WithPrivileges } from './with_privileges';
export { NotAuthorizedSection } from './not_authorized_section';
-export { Error, SectionError } from './section_error';
+export { SectionError } from './section_error';
+
+export { PageError } from './page_error';
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
new file mode 100644
index 00000000000000..0a27b4098681b6
--- /dev/null
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
@@ -0,0 +1,72 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiSpacer, EuiEmptyPrompt, EuiPageContent } from '@elastic/eui';
+import React from 'react';
+import { APP_WRAPPER_CLASS } from '../../../../../../src/core/public';
+import { Error } from '../types';
+
+interface Props {
+ title: React.ReactNode;
+ error: Error;
+ actions?: JSX.Element;
+ isCentered?: boolean;
+}
+
+/*
+ * A reusable component to handle full page errors.
+ * This is based on Kibana design guidelines related
+ * to the new management navigation structure.
+ * In some scenarios, it replaces the usage of .
+ */
+
+export const PageError: React.FunctionComponent = ({
+ title,
+ error,
+ actions,
+ isCentered,
+ ...rest
+}) => {
+ const {
+ error: errorString,
+ cause, // wrapEsError() on the server adds a "cause" array
+ message,
+ } = error;
+
+ const errorContent = (
+
+ {title}}
+ body={
+ <>
+ {cause ? message || errorString : {message || errorString}
}
+ {cause && (
+ <>
+
+
+ {cause.map((causeMsg, i) => (
+ {causeMsg}
+ ))}
+
+ >
+ )}
+ >
+ }
+ iconType="alert"
+ actions={actions}
+ {...rest}
+ />
+
+ );
+
+ if (isCentered) {
+ return {errorContent}
;
+ }
+
+ return errorContent;
+};
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx
index c0b3533c8594b4..a1652b4e153f58 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx
@@ -8,12 +8,7 @@
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { Fragment } from 'react';
-
-export interface Error {
- error: string;
- cause?: string[];
- message?: string;
-}
+import { Error } from '../types';
interface Props {
title: React.ReactNode;
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts
index 089dc890c3e6cf..e63d98512a2cd8 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts
@@ -12,8 +12,8 @@ export {
AuthorizationProvider,
AuthorizationContext,
SectionError,
- Error,
+ PageError,
useAuthorizationContext,
} from './components';
-export { Privileges, MissingPrivileges } from './types';
+export { Privileges, MissingPrivileges, Error } from './types';
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts
index b10318aa415b34..70b54b0b6e425e 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts
@@ -14,3 +14,9 @@ export interface Privileges {
hasAllPrivileges: boolean;
missingPrivileges: MissingPrivileges;
}
+
+export interface Error {
+ error: string;
+ cause?: string[];
+ message?: string;
+}
diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts
index 483fffd9c48595..f68ad3da2a4b54 100644
--- a/src/plugins/es_ui_shared/public/authorization/index.ts
+++ b/src/plugins/es_ui_shared/public/authorization/index.ts
@@ -14,6 +14,7 @@ export {
NotAuthorizedSection,
Privileges,
SectionError,
+ PageError,
useAuthorizationContext,
WithPrivileges,
} from '../../__packages_do_not_import__/authorization';
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index b46a23994fe935..7b9013c043a0e1 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -40,6 +40,7 @@ export {
Privileges,
MissingPrivileges,
SectionError,
+ PageError,
Error,
useAuthorizationContext,
} from './authorization';
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
index 181bd9959c1bbd..fb334afb22b137 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts
@@ -18,7 +18,7 @@ const DEFAULT_OPTIONS = {
stripEmptyFields: true,
};
-interface UseFormReturn {
+export interface UseFormReturn {
form: FormHook;
}
diff --git a/src/plugins/newsfeed/kibana.json b/src/plugins/newsfeed/kibana.json
index b9f37b67f6921b..0e7ae7cd11c35f 100644
--- a/src/plugins/newsfeed/kibana.json
+++ b/src/plugins/newsfeed/kibana.json
@@ -2,5 +2,6 @@
"id": "newsfeed",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredPlugins": ["screenshotMode"]
}
diff --git a/src/plugins/newsfeed/public/lib/api.test.mocks.ts b/src/plugins/newsfeed/public/lib/api.test.mocks.ts
index 677bc203cbef3f..8ac66eae6c2f6c 100644
--- a/src/plugins/newsfeed/public/lib/api.test.mocks.ts
+++ b/src/plugins/newsfeed/public/lib/api.test.mocks.ts
@@ -8,6 +8,7 @@
import { storageMock } from './storage.mock';
import { driverMock } from './driver.mock';
+import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver';
export const storageInstanceMock = storageMock.create();
jest.doMock('./storage', () => ({
@@ -18,3 +19,7 @@ export const driverInstanceMock = driverMock.create();
jest.doMock('./driver', () => ({
NewsfeedApiDriver: jest.fn().mockImplementation(() => driverInstanceMock),
}));
+
+jest.doMock('./never_fetch_driver', () => ({
+ NeverFetchNewsfeedApiDriver: jest.fn(() => new NeverFetchNewsfeedApiDriver()),
+}));
diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts
index a4894573932e6c..58d06e72cd77c4 100644
--- a/src/plugins/newsfeed/public/lib/api.test.ts
+++ b/src/plugins/newsfeed/public/lib/api.test.ts
@@ -7,12 +7,16 @@
*/
import { driverInstanceMock, storageInstanceMock } from './api.test.mocks';
+
import moment from 'moment';
import { getApi } from './api';
import { TestScheduler } from 'rxjs/testing';
import { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
import { take } from 'rxjs/operators';
+import { NewsfeedApiDriver as MockNewsfeedApiDriver } from './driver';
+import { NeverFetchNewsfeedApiDriver as MockNeverFetchNewsfeedApiDriver } from './never_fetch_driver';
+
const kibanaVersion = '8.0.0';
const newsfeedId = 'test';
@@ -46,6 +50,8 @@ describe('getApi', () => {
afterEach(() => {
storageInstanceMock.isAnyUnread$.mockReset();
driverInstanceMock.fetchNewsfeedItems.mockReset();
+ (MockNewsfeedApiDriver as jest.Mock).mockClear();
+ (MockNeverFetchNewsfeedApiDriver as jest.Mock).mockClear();
});
it('merges the newsfeed and unread observables', () => {
@@ -60,7 +66,7 @@ describe('getApi', () => {
a: createFetchResult({ feedItems: ['item' as any] }),
})
);
- const api = getApi(createConfig(1000), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(1000), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(1))).toBe('(a|)', {
a: createFetchResult({
@@ -83,7 +89,7 @@ describe('getApi', () => {
a: createFetchResult({ feedItems: ['item' as any] }),
})
);
- const api = getApi(createConfig(2), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(2), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(2))).toBe('a-(b|)', {
a: createFetchResult({
@@ -111,7 +117,7 @@ describe('getApi', () => {
a: createFetchResult({}),
})
);
- const api = getApi(createConfig(10), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(10), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(2))).toBe('a--(b|)', {
a: createFetchResult({
@@ -123,4 +129,16 @@ describe('getApi', () => {
});
});
});
+
+ it('uses the news feed API driver if in not screenshot mode', () => {
+ getApi(createConfig(10), kibanaVersion, newsfeedId, false);
+ expect(MockNewsfeedApiDriver).toHaveBeenCalled();
+ expect(MockNeverFetchNewsfeedApiDriver).not.toHaveBeenCalled();
+ });
+
+ it('uses the never fetch news feed API driver if in not screenshot mode', () => {
+ getApi(createConfig(10), kibanaVersion, newsfeedId, true);
+ expect(MockNewsfeedApiDriver).not.toHaveBeenCalled();
+ expect(MockNeverFetchNewsfeedApiDriver).toHaveBeenCalled();
+ });
});
diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts
index 4fbbd8687b73fc..7aafc9fd276250 100644
--- a/src/plugins/newsfeed/public/lib/api.ts
+++ b/src/plugins/newsfeed/public/lib/api.ts
@@ -11,6 +11,7 @@ import { map, catchError, filter, mergeMap, tap } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
import { NewsfeedApiDriver } from './driver';
+import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver';
import { NewsfeedStorage } from './storage';
export enum NewsfeedApiEndpoint {
@@ -40,13 +41,23 @@ export interface NewsfeedApi {
export function getApi(
config: NewsfeedPluginBrowserConfig,
kibanaVersion: string,
- newsfeedId: string
+ newsfeedId: string,
+ isScreenshotMode: boolean
): NewsfeedApi {
- const userLanguage = i18n.getLocale();
- const fetchInterval = config.fetchInterval.asMilliseconds();
- const mainInterval = config.mainInterval.asMilliseconds();
const storage = new NewsfeedStorage(newsfeedId);
- const driver = new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage);
+ const mainInterval = config.mainInterval.asMilliseconds();
+
+ const createNewsfeedApiDriver = () => {
+ if (isScreenshotMode) {
+ return new NeverFetchNewsfeedApiDriver();
+ }
+
+ const userLanguage = i18n.getLocale();
+ const fetchInterval = config.fetchInterval.asMilliseconds();
+ return new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage);
+ };
+
+ const driver = createNewsfeedApiDriver();
const results$ = timer(0, mainInterval).pipe(
filter(() => driver.shouldFetch()),
diff --git a/src/plugins/newsfeed/public/lib/driver.ts b/src/plugins/newsfeed/public/lib/driver.ts
index 0efa981e8c89d6..1762c4a4287844 100644
--- a/src/plugins/newsfeed/public/lib/driver.ts
+++ b/src/plugins/newsfeed/public/lib/driver.ts
@@ -10,6 +10,7 @@ import moment from 'moment';
import * as Rx from 'rxjs';
import { NEWSFEED_DEFAULT_SERVICE_BASE_URL } from '../../common/constants';
import { ApiItem, FetchResult, NewsfeedPluginBrowserConfig } from '../types';
+import { INewsfeedApiDriver } from './types';
import { convertItems } from './convert_items';
import type { NewsfeedStorage } from './storage';
@@ -19,7 +20,7 @@ interface NewsfeedResponse {
items: ApiItem[];
}
-export class NewsfeedApiDriver {
+export class NewsfeedApiDriver implements INewsfeedApiDriver {
private readonly kibanaVersion: string;
private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service
diff --git a/src/plugins/newsfeed/public/lib/never_fetch_driver.ts b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts
new file mode 100644
index 00000000000000..e95ca9c2d499a7
--- /dev/null
+++ b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Observable } from 'rxjs';
+import { FetchResult } from '../types';
+import { INewsfeedApiDriver } from './types';
+
+/**
+ * NewsfeedApiDriver variant that never fetches results. This is useful for instances where Kibana is started
+ * without any user interaction like when generating a PDF or PNG report.
+ */
+export class NeverFetchNewsfeedApiDriver implements INewsfeedApiDriver {
+ shouldFetch(): boolean {
+ return false;
+ }
+
+ fetchNewsfeedItems(): Observable {
+ throw new Error('Not implemented!');
+ }
+}
diff --git a/src/plugins/newsfeed/public/lib/types.ts b/src/plugins/newsfeed/public/lib/types.ts
new file mode 100644
index 00000000000000..5a62a929eeb7ff
--- /dev/null
+++ b/src/plugins/newsfeed/public/lib/types.ts
@@ -0,0 +1,19 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Observable } from 'rxjs';
+import type { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
+
+export interface INewsfeedApiDriver {
+ /**
+ * Check whether newsfeed items should be (re-)fetched
+ */
+ shouldFetch(): boolean;
+
+ fetchNewsfeedItems(config: NewsfeedPluginBrowserConfig['service']): Observable;
+}
diff --git a/src/plugins/newsfeed/public/plugin.test.ts b/src/plugins/newsfeed/public/plugin.test.ts
new file mode 100644
index 00000000000000..4be69feb79f555
--- /dev/null
+++ b/src/plugins/newsfeed/public/plugin.test.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { take } from 'rxjs/operators';
+import { coreMock } from '../../../core/public/mocks';
+import { NewsfeedPublicPlugin } from './plugin';
+import { NewsfeedApiEndpoint } from './lib/api';
+
+describe('Newsfeed plugin', () => {
+ let plugin: NewsfeedPublicPlugin;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(() => {
+ plugin = new NewsfeedPublicPlugin(coreMock.createPluginInitializerContext());
+ });
+
+ describe('#start', () => {
+ beforeEach(() => {
+ plugin.setup(coreMock.createSetup());
+ });
+
+ beforeEach(() => {
+ /**
+ * We assume for these tests that the newsfeed stream exposed by start will fetch newsfeed items
+ * on the first tick for new subscribers
+ */
+ jest.spyOn(window, 'fetch');
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('base case', () => {
+ it('makes fetch requests', () => {
+ const startContract = plugin.start(coreMock.createStart(), {
+ screenshotMode: { isScreenshotMode: () => false },
+ });
+ const sub = startContract
+ .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do
+ .pipe(take(1))
+ .subscribe(() => {});
+ jest.runOnlyPendingTimers();
+ expect(window.fetch).toHaveBeenCalled();
+ sub.unsubscribe();
+ });
+ });
+
+ describe('when in screenshot mode', () => {
+ it('makes no fetch requests in screenshot mode', () => {
+ const startContract = plugin.start(coreMock.createStart(), {
+ screenshotMode: { isScreenshotMode: () => true },
+ });
+ const sub = startContract
+ .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do
+ .pipe(take(1))
+ .subscribe(() => {});
+ jest.runOnlyPendingTimers();
+ expect(window.fetch).not.toHaveBeenCalled();
+ sub.unsubscribe();
+ });
+ });
+ });
+});
diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx
index fdda0a24b8bd56..656fc2ef00bb9f 100644
--- a/src/plugins/newsfeed/public/plugin.tsx
+++ b/src/plugins/newsfeed/public/plugin.tsx
@@ -13,7 +13,7 @@ import React from 'react';
import moment from 'moment';
import { I18nProvider } from '@kbn/i18n/react';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
-import { NewsfeedPluginBrowserConfig } from './types';
+import { NewsfeedPluginBrowserConfig, NewsfeedPluginStartDependencies } from './types';
import { NewsfeedNavButton } from './components/newsfeed_header_nav_button';
import { getApi, NewsfeedApi, NewsfeedApiEndpoint } from './lib/api';
@@ -41,8 +41,10 @@ export class NewsfeedPublicPlugin
return {};
}
- public start(core: CoreStart) {
- const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA);
+ public start(core: CoreStart, { screenshotMode }: NewsfeedPluginStartDependencies) {
+ const isScreenshotMode = screenshotMode.isScreenshotMode();
+
+ const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA, isScreenshotMode);
core.chrome.navControls.registerRight({
order: 1000,
mount: (target) => this.mount(api, target),
@@ -56,7 +58,7 @@ export class NewsfeedPublicPlugin
pathTemplate: `/${endpoint}/v{VERSION}.json`,
},
});
- const { fetchResults$ } = this.createNewsfeedApi(config, endpoint);
+ const { fetchResults$ } = this.createNewsfeedApi(config, endpoint, isScreenshotMode);
return fetchResults$;
},
};
@@ -68,9 +70,10 @@ export class NewsfeedPublicPlugin
private createNewsfeedApi(
config: NewsfeedPluginBrowserConfig,
- newsfeedId: NewsfeedApiEndpoint
+ newsfeedId: NewsfeedApiEndpoint,
+ isScreenshotMode: boolean
): NewsfeedApi {
- const api = getApi(config, this.kibanaVersion, newsfeedId);
+ const api = getApi(config, this.kibanaVersion, newsfeedId, isScreenshotMode);
return {
markAsRead: api.markAsRead,
fetchResults$: api.fetchResults$.pipe(
diff --git a/src/plugins/newsfeed/public/types.ts b/src/plugins/newsfeed/public/types.ts
index cca656565f4ca5..a7ff917f6f9750 100644
--- a/src/plugins/newsfeed/public/types.ts
+++ b/src/plugins/newsfeed/public/types.ts
@@ -7,6 +7,10 @@
*/
import { Duration, Moment } from 'moment';
+import type { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public';
+export interface NewsfeedPluginStartDependencies {
+ screenshotMode: ScreenshotModePluginStart;
+}
// Ideally, we may want to obtain the type from the configSchema and exposeToBrowser keys...
export interface NewsfeedPluginBrowserConfig {
diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json
index 66244a22336c77..18e6f2de1bc6fb 100644
--- a/src/plugins/newsfeed/tsconfig.json
+++ b/src/plugins/newsfeed/tsconfig.json
@@ -7,13 +7,9 @@
"declaration": true,
"declarationMap": true
},
- "include": [
- "public/**/*",
- "server/**/*",
- "common/*",
- "../../../typings/**/*"
- ],
+ "include": ["public/**/*", "server/**/*", "common/*", "../../../typings/**/*"],
"references": [
- { "path": "../../core/tsconfig.json" }
+ { "path": "../../core/tsconfig.json" },
+ { "path": "../screenshot_mode/tsconfig.json" }
]
}
diff --git a/src/plugins/presentation_util/public/mocks.ts b/src/plugins/presentation_util/public/mocks.ts
new file mode 100644
index 00000000000000..91c461646c280c
--- /dev/null
+++ b/src/plugins/presentation_util/public/mocks.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { CoreStart } from 'kibana/public';
+import { PresentationUtilPluginStart } from './types';
+import { pluginServices } from './services';
+import { registry } from './services/kibana';
+
+const createStartContract = (coreStart: CoreStart): PresentationUtilPluginStart => {
+ pluginServices.setRegistry(registry.start({ coreStart, startPlugins: {} as any }));
+
+ const startContract: PresentationUtilPluginStart = {
+ ContextProvider: pluginServices.getContextProvider(),
+ labsService: pluginServices.getServices().labs,
+ };
+ return startContract;
+};
+
+export const presentationUtilPluginMock = {
+ createStartContract,
+};
diff --git a/src/plugins/screenshot_mode/public/index.ts b/src/plugins/screenshot_mode/public/index.ts
index a5ad37dd5b760d..012f57e837f416 100644
--- a/src/plugins/screenshot_mode/public/index.ts
+++ b/src/plugins/screenshot_mode/public/index.ts
@@ -18,4 +18,4 @@ export {
KBN_SCREENSHOT_MODE_ENABLED_KEY,
} from '../common';
-export { ScreenshotModePluginSetup } from './types';
+export { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types';
diff --git a/src/plugins/screenshot_mode/public/plugin.test.ts b/src/plugins/screenshot_mode/public/plugin.test.ts
index 33ae5014668760..f2c0970d0ff60c 100644
--- a/src/plugins/screenshot_mode/public/plugin.test.ts
+++ b/src/plugins/screenshot_mode/public/plugin.test.ts
@@ -21,7 +21,7 @@ describe('Screenshot mode public', () => {
setScreenshotModeDisabled();
});
- describe('setup contract', () => {
+ describe('public contract', () => {
it('detects screenshot mode "true"', () => {
setScreenshotModeEnabled();
const screenshotMode = plugin.setup(coreMock.createSetup());
@@ -34,10 +34,4 @@ describe('Screenshot mode public', () => {
expect(screenshotMode.isScreenshotMode()).toBe(false);
});
});
-
- describe('start contract', () => {
- it('returns nothing', () => {
- expect(plugin.start(coreMock.createStart())).toBe(undefined);
- });
- });
});
diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts
index 7a166566a0173b..a005bb7c3d055d 100644
--- a/src/plugins/screenshot_mode/public/plugin.ts
+++ b/src/plugins/screenshot_mode/public/plugin.ts
@@ -8,18 +8,22 @@
import { CoreSetup, CoreStart, Plugin } from '../../../core/public';
-import { ScreenshotModePluginSetup } from './types';
+import { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types';
import { getScreenshotMode } from '../common';
export class ScreenshotModePlugin implements Plugin {
+ private publicContract = Object.freeze({
+ isScreenshotMode: () => getScreenshotMode() === true,
+ });
+
public setup(core: CoreSetup): ScreenshotModePluginSetup {
- return {
- isScreenshotMode: () => getScreenshotMode() === true,
- };
+ return this.publicContract;
}
- public start(core: CoreStart) {}
+ public start(core: CoreStart): ScreenshotModePluginStart {
+ return this.publicContract;
+ }
public stop() {}
}
diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts
index 744ea8615f2a79..f6963de0cbd63f 100644
--- a/src/plugins/screenshot_mode/public/types.ts
+++ b/src/plugins/screenshot_mode/public/types.ts
@@ -15,3 +15,4 @@ export interface IScreenshotModeService {
}
export type ScreenshotModePluginSetup = IScreenshotModeService;
+export type ScreenshotModePluginStart = IScreenshotModeService;
diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx
index 33bb1f06d045c7..6a39951ad49583 100644
--- a/x-pack/examples/embedded_lens_example/public/app.tsx
+++ b/x-pack/examples/embedded_lens_example/public/app.tsx
@@ -24,6 +24,7 @@ import {
TypedLensByValueInput,
PersistedIndexPatternLayer,
XYState,
+ LensEmbeddableInput,
} from '../../../plugins/lens/public';
import { StartDependencies } from './plugin';
@@ -112,12 +113,15 @@ export const App = (props: {
}) => {
const [color, setColor] = useState('green');
const [isLoading, setIsLoading] = useState(false);
+ const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);
const LensComponent = props.plugins.lens.EmbeddableComponent;
+ const LensSaveModalComponent = props.plugins.lens.SaveModalComponent;
const [time, setTime] = useState({
from: 'now-5d',
to: 'now',
});
+
return (
@@ -172,7 +176,18 @@ export const App = (props: {
setColor(newColor);
}}
>
- Edit
+ Edit in Lens
+
+
+
+ {
+ setIsSaveModalVisible(true);
+ }}
+ >
+ Save Visualization
@@ -197,6 +212,19 @@ export const App = (props: {
// call back event for on table row click event
}}
/>
+ {isSaveModalVisible && (
+ {}}
+ onClose={() => setIsSaveModalVisible(false)}
+ />
+ )}
>
) : (
This demo only works if your default index pattern is set and time based
diff --git a/x-pack/examples/embedded_lens_example/public/mount.tsx b/x-pack/examples/embedded_lens_example/public/mount.tsx
index 5cf7c25fbf160e..ff1e6ef8818f05 100644
--- a/x-pack/examples/embedded_lens_example/public/mount.tsx
+++ b/x-pack/examples/embedded_lens_example/public/mount.tsx
@@ -23,7 +23,13 @@ export const mount = (coreSetup: CoreSetup) => async ({
const defaultIndexPattern = await plugins.data.indexPatterns.getDefault();
- const reactElement = ;
+ const i18nCore = core.i18n;
+
+ const reactElement = (
+
+
+
+ );
render(reactElement, element);
return () => unmountComponentAtNode(element);
};
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index bfc0a3daf6f0ed..77e7f2834b080d 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -107,7 +107,7 @@ export class ApmPlugin implements Plugin {
// APM navigation
{
label: 'APM',
- sortKey: 200,
+ sortKey: 400,
entries: [
{ label: servicesTitle, app: 'apm', path: '/services' },
{ label: tracesTitle, app: 'apm', path: '/traces' },
@@ -118,7 +118,7 @@ export class ApmPlugin implements Plugin {
// UX navigation
{
label: 'User Experience',
- sortKey: 201,
+ sortKey: 600,
entries: [
{
label: i18n.translate('xpack.apm.ux.overview.heading', {
diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
index 79eb0fbce54987..b54e9726667e6c 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
@@ -58,6 +58,11 @@ Object {
"transaction.type": "page-load",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
@@ -492,6 +497,11 @@ Object {
"field": "transaction.marks.navigationTiming.fetchStart",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
@@ -534,6 +544,11 @@ Object {
"transaction.type": "page-load",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
index aaf6401b9f407a..452f451b11f863 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
@@ -16,6 +16,7 @@ import { getRumServices } from './get_rum_services';
import { getLongTaskMetrics } from './get_long_task_metrics';
import { getWebCoreVitals } from './get_web_core_vitals';
import { getJSErrors } from './get_js_errors';
+import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
describe('rum client dashboard queries', () => {
let mock: SearchParamsMock;
@@ -25,32 +26,38 @@ describe('rum client dashboard queries', () => {
});
it('fetches client metrics', async () => {
- mock = await inspectSearchParams((setup) =>
- getClientMetrics({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getClientMetrics({
+ setup,
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
it('fetches page view trends', async () => {
- mock = await inspectSearchParams((setup) =>
- getPageViewTrends({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getPageViewTrends({
+ setup,
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
it('fetches page load distribution', async () => {
- mock = await inspectSearchParams((setup) =>
- getPageLoadDistribution({
- setup,
- minPercentile: '0',
- maxPercentile: '99',
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getPageLoadDistribution({
+ setup,
+ minPercentile: '0',
+ maxPercentile: '99',
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
@@ -65,10 +72,12 @@ describe('rum client dashboard queries', () => {
});
it('fetches rum core vitals', async () => {
- mock = await inspectSearchParams((setup) =>
- getWebCoreVitals({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getWebCoreVitals({
+ setup,
+ }),
+ { uiFilters: { environment: ENVIRONMENT_ALL.value } }
);
expect(mock.params).toMatchSnapshot();
});
diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
index 43cbb485c4510f..6b8cee244a1926 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
@@ -8,6 +8,7 @@
import { ESFilter } from '../../../../../../../typings/elasticsearch';
import { UIFilters } from '../../../../typings/ui_filters';
import { localUIFilters, localUIFilterNames } from './local_ui_filters/config';
+import { environmentQuery } from '../../../utils/queries';
export function getEsFilter(uiFilters: UIFilters) {
const localFilterValues = uiFilters;
@@ -23,5 +24,5 @@ export function getEsFilter(uiFilters: UIFilters) {
};
}) as ESFilter[];
- return mappedFilters;
+ return [...mappedFilters, ...environmentQuery(uiFilters.environment)];
}
diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx
index 6252c33c5994dd..29879cf5c0ebcc 100644
--- a/x-pack/plugins/apm/server/utils/test_helpers.tsx
+++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx
@@ -17,6 +17,7 @@ interface Options {
mockResponse?: (
request: ESSearchRequest
) => ESSearchResponse;
+ uiFilters?: Record;
}
interface MockSetup {
@@ -86,7 +87,7 @@ export async function inspectSearchParams(
},
}
) as APMConfig,
- uiFilters: {},
+ uiFilters: options?.uiFilters ?? {},
indices: {
/* eslint-disable @typescript-eslint/naming-convention */
'apm_oss.sourcemapIndices': 'myIndex',
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
index 87e6ee62460fae..870e303a2930d8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
@@ -16,18 +16,13 @@ import { engines } from '../../__mocks__/engines.mock';
import { nextTick } from '@kbn/test/jest';
import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { RoleMappingsLogic } from './role_mappings_logic';
describe('RoleMappingsLogic', () => {
const { http } = mockHttpValues;
- const {
- clearFlashMessages,
- flashAPIErrors,
- setSuccessMessage,
- setErrorMessage,
- } = mockFlashMessageHelpers;
+ const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
const { mount } = new LogicMounter(RoleMappingsLogic);
const DEFAULT_VALUES = {
attributes: [],
@@ -50,15 +45,14 @@ describe('RoleMappingsLogic', () => {
roleMappingErrors: [],
};
- const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [asRoleMapping] };
- const mappingServerProps = {
+ const mappingsServerProps = {
+ multipleAuthProvidersConfig: true,
+ roleMappings: [asRoleMapping],
attributes: ['email', 'metadata', 'username', 'role'],
authProviders: [ANY_AUTH_PROVIDER],
availableEngines: engines,
elasticsearchRoles: [],
hasAdvancedRoles: false,
- multipleAuthProvidersConfig: false,
- roleMapping: asRoleMapping,
};
beforeEach(() => {
@@ -75,48 +69,20 @@ describe('RoleMappingsLogic', () => {
it('sets data based on server response from the `mappings` (plural) endpoint', () => {
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]);
- expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
- expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true);
- });
- });
-
- describe('setRoleMappingData', () => {
- it('sets state based on server response from the `mapping` (singular) endpoint', () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
-
expect(RoleMappingsLogic.values).toEqual({
...DEFAULT_VALUES,
- roleMapping: asRoleMapping,
+ roleMappings: [asRoleMapping],
dataLoading: false,
- attributes: mappingServerProps.attributes,
- availableAuthProviders: mappingServerProps.authProviders,
- availableEngines: mappingServerProps.availableEngines,
+ attributes: mappingsServerProps.attributes,
+ availableAuthProviders: mappingsServerProps.authProviders,
+ availableEngines: mappingsServerProps.availableEngines,
accessAllEngines: true,
- attributeName: 'role',
- attributeValue: 'superuser',
- elasticsearchRoles: mappingServerProps.elasticsearchRoles,
- selectedEngines: new Set(engines.map((e) => e.name)),
- selectedOptions: [
- { label: engines[0].name, value: engines[0].name },
- { label: engines[1].name, value: engines[1].name },
- ],
- });
- });
-
- it('will remove all selected engines if no roleMapping was returned from the server', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: undefined,
- });
-
- expect(RoleMappingsLogic.values).toEqual({
- ...DEFAULT_VALUES,
- dataLoading: false,
+ multipleAuthProvidersConfig: true,
+ attributeName: 'username',
+ attributeValue: '',
+ elasticsearchRoles: mappingsServerProps.elasticsearchRoles,
selectedEngines: new Set(),
- attributes: mappingServerProps.attributes,
- availableAuthProviders: mappingServerProps.authProviders,
- availableEngines: mappingServerProps.availableEngines,
+ selectedOptions: [],
});
});
});
@@ -135,11 +101,13 @@ describe('RoleMappingsLogic', () => {
const engine = engines[0];
const otherEngine = engines[1];
const mountedValues = {
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- engines: [engine, otherEngine],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ engines: [engine, otherEngine],
+ },
+ ],
selectedEngines: new Set([engine.name]),
};
@@ -153,11 +121,18 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.selectedEngines).toEqual(
new Set([engine.name, otherEngine.name])
);
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: engine.name, value: engine.name },
+ { label: otherEngine.name, value: otherEngine.name },
+ ]);
});
it('handles removing an engine from selected engines', () => {
RoleMappingsLogic.actions.handleEngineSelectionChange([engine.name]);
expect(RoleMappingsLogic.values.selectedEngines).toEqual(new Set([engine.name]));
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: engine.name, value: engine.name },
+ ]);
});
});
@@ -175,17 +150,19 @@ describe('RoleMappingsLogic', () => {
it('sets values correctly', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
elasticsearchRoles,
});
RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]);
expect(RoleMappingsLogic.values).toEqual({
...DEFAULT_VALUES,
+ multipleAuthProvidersConfig: true,
attributeValue: elasticsearchRoles[0],
- roleMapping: asRoleMapping,
- attributes: mappingServerProps.attributes,
- availableEngines: mappingServerProps.availableEngines,
+ roleMappings: [asRoleMapping],
+ roleMapping: null,
+ attributes: mappingsServerProps.attributes,
+ availableEngines: mappingsServerProps.availableEngines,
accessAllEngines: true,
attributeName: 'role',
elasticsearchRoles,
@@ -215,11 +192,13 @@ describe('RoleMappingsLogic', () => {
describe('handleAuthProviderChange', () => {
beforeEach(() => {
mount({
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- authProvider: ['foo'],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ authProvider: ['foo'],
+ },
+ ],
});
});
const providers = ['bar', 'baz'];
@@ -244,11 +223,13 @@ describe('RoleMappingsLogic', () => {
it('handles "any" auth in previous state', () => {
mount({
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- authProvider: [ANY_AUTH_PROVIDER],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ authProvider: [ANY_AUTH_PROVIDER],
+ },
+ ],
});
RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
@@ -258,7 +239,6 @@ describe('RoleMappingsLogic', () => {
it('resetState', () => {
mount(mappingsServerProps);
- mount(mappingServerProps);
RoleMappingsLogic.actions.resetState();
expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES);
@@ -266,7 +246,7 @@ describe('RoleMappingsLogic', () => {
});
it('openRoleMappingFlyout', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
RoleMappingsLogic.actions.openRoleMappingFlyout();
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true);
@@ -275,7 +255,7 @@ describe('RoleMappingsLogic', () => {
it('closeRoleMappingFlyout', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
roleMappingFlyoutOpen: true,
});
RoleMappingsLogic.actions.closeRoleMappingFlyout();
@@ -307,40 +287,20 @@ describe('RoleMappingsLogic', () => {
});
describe('initializeRoleMapping', () => {
- it('calls API and sets values for new mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping();
-
- expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/new');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('calls API and sets values for existing mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping('123');
-
- expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/123');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('handles error', async () => {
- http.get.mockReturnValue(Promise.reject('this is an error'));
- RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
+ it('sets values for existing mapping', () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ RoleMappingsLogic.actions.initializeRoleMapping(asRoleMapping.id);
- expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ expect(setRoleMappingDataSpy).toHaveBeenCalledWith(asRoleMapping);
});
- it('shows error when there is a 404 status', async () => {
- http.get.mockReturnValue(Promise.reject({ status: 404 }));
+ it('does not set data for new mapping', async () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
- expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND);
+ expect(setRoleMappingDataSpy).not.toHaveBeenCalledWith(asRoleMapping);
});
});
@@ -362,7 +322,7 @@ describe('RoleMappingsLogic', () => {
'initializeRoleMappings'
);
- http.post.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.post).toHaveBeenCalledWith('/api/app_search/role_mappings', {
@@ -374,13 +334,16 @@ describe('RoleMappingsLogic', () => {
});
it('calls API and refreshes list when existing mapping', async () => {
- mount(mappingServerProps);
+ mount({
+ ...mappingsServerProps,
+ roleMapping: asRoleMapping,
+ });
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, {
@@ -396,12 +359,13 @@ describe('RoleMappingsLogic', () => {
const engine = engines[0];
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
accessAllEngines: false,
selectedEngines: new Set([engine.name]),
+ roleMapping: asRoleMapping,
});
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, {
@@ -449,7 +413,7 @@ describe('RoleMappingsLogic', () => {
});
it('calls API and refreshes list', async () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
'initializeRoleMappings'
@@ -465,7 +429,7 @@ describe('RoleMappingsLogic', () => {
});
it('handles error', async () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
http.delete.mockReturnValue(Promise.reject('this is an error'));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
await nextTick();
@@ -474,7 +438,7 @@ describe('RoleMappingsLogic', () => {
});
it('will do nothing if not confirmed', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
index 863e6746dbe958..fc0a235b23c77c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
@@ -13,10 +13,9 @@ import {
clearFlashMessages,
flashAPIErrors,
setSuccessMessage,
- setErrorMessage,
} from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { AttributeName } from '../../../shared/types';
import { ASRoleMapping, RoleTypes } from '../../types';
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
@@ -31,17 +30,12 @@ import {
interface RoleMappingsServerDetails {
roleMappings: ASRoleMapping[];
- multipleAuthProvidersConfig: boolean;
-}
-
-interface RoleMappingServerDetails {
attributes: string[];
authProviders: string[];
availableEngines: Engine[];
elasticsearchRoles: string[];
hasAdvancedRoles: boolean;
multipleAuthProvidersConfig: boolean;
- roleMapping?: ASRoleMapping;
}
const getFirstAttributeName = (roleMapping: ASRoleMapping) =>
@@ -64,7 +58,7 @@ interface RoleMappingsActions {
initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
initializeRoleMappings(): void;
resetState(): void;
- setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails;
+ setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
@@ -96,7 +90,7 @@ export const RoleMappingsLogic = kea data,
- setRoleMappingData: (data: RoleMappingServerDetails) => data,
+ setRoleMapping: (roleMapping: ASRoleMapping) => ({ roleMapping }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string) => ({ value }),
handleRoleChange: (roleType: RoleTypes) => ({ roleType }),
@@ -120,7 +114,6 @@ export const RoleMappingsLogic = kea false,
- setRoleMappingData: () => false,
resetState: () => true,
},
],
@@ -135,40 +128,39 @@ export const RoleMappingsLogic = kea multipleAuthProvidersConfig,
- setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig,
resetState: () => false,
},
],
hasAdvancedRoles: [
false,
{
- setRoleMappingData: (_, { hasAdvancedRoles }) => hasAdvancedRoles,
+ setRoleMappingsData: (_, { hasAdvancedRoles }) => hasAdvancedRoles,
},
],
availableEngines: [
[],
{
- setRoleMappingData: (_, { availableEngines }) => availableEngines,
+ setRoleMappingsData: (_, { availableEngines }) => availableEngines,
resetState: () => [],
},
],
attributes: [
[],
{
- setRoleMappingData: (_, { attributes }) => attributes,
+ setRoleMappingsData: (_, { attributes }) => attributes,
resetState: () => [],
},
],
elasticsearchRoles: [
[],
{
- setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles,
+ setRoleMappingsData: (_, { elasticsearchRoles }) => elasticsearchRoles,
},
],
roleMapping: [
null,
{
- setRoleMappingData: (_, { roleMapping }) => roleMapping || null,
+ setRoleMapping: (_, { roleMapping }) => roleMapping,
resetState: () => null,
closeRoleMappingFlyout: () => null,
},
@@ -176,16 +168,14 @@ export const RoleMappingsLogic = kea
- roleMapping ? (roleMapping.roleType as RoleTypes) : 'owner',
+ setRoleMapping: (_, { roleMapping }) => roleMapping.roleType as RoleTypes,
handleRoleChange: (_, { roleType }) => roleType,
},
],
accessAllEngines: [
true,
{
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.accessAllEngines : true,
+ setRoleMapping: (_, { roleMapping }) => roleMapping.accessAllEngines,
handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType),
handleAccessAllEnginesChange: (_, { selected }) => selected,
},
@@ -193,8 +183,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeValue(roleMapping) : '',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeValue(roleMapping),
handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) =>
value === 'role' ? firstElasticsearchRole : '',
handleAttributeValueChange: (_, { value }) => value,
@@ -205,8 +194,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeName(roleMapping) : 'username',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeName(roleMapping),
handleAttributeSelectorChange: (_, { value }) => value,
resetState: () => 'username',
closeRoleMappingFlyout: () => 'username',
@@ -215,8 +203,8 @@ export const RoleMappingsLogic = kea
- roleMapping ? new Set(roleMapping.engines.map((engine) => engine.name)) : new Set(),
+ setRoleMapping: (_, { roleMapping }) =>
+ new Set(roleMapping.engines.map((engine: Engine) => engine.name)),
handleAccessAllEnginesChange: () => new Set(),
handleEngineSelectionChange: (_, { engineNames }) => {
const newSelectedEngineNames = new Set() as Set;
@@ -229,7 +217,7 @@ export const RoleMappingsLogic = kea authProviders,
+ setRoleMappingsData: (_, { authProviders }) => authProviders,
},
],
selectedAuthProviders: [
@@ -246,8 +234,7 @@ export const RoleMappingsLogic = kea v !== ANY_AUTH_PROVIDER);
return [ANY_AUTH_PROVIDER];
},
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER],
+ setRoleMapping: (_, { roleMapping }) => roleMapping.authProvider,
},
],
roleMappingFlyoutOpen: [
@@ -292,21 +279,8 @@ export const RoleMappingsLogic = kea {
- const { http } = HttpLogic.values;
- const route = roleMappingId
- ? `/api/app_search/role_mappings/${roleMappingId}`
- : '/api/app_search/role_mappings/new';
-
- try {
- const response = await http.get(route);
- actions.setRoleMappingData(response);
- } catch (e) {
- if (e.status === 404) {
- setErrorMessage(ROLE_MAPPING_NOT_FOUND);
- } else {
- flashAPIErrors(e);
- }
- }
+ const roleMapping = values.roleMappings.find(({ id }) => id === roleMappingId);
+ if (roleMapping) actions.setRoleMapping(roleMapping);
},
handleDeleteMapping: async ({ roleMappingId }) => {
const { http } = HttpLogic.values;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
index 82284be0907fba..7696cf03ed4b15 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
@@ -7,7 +7,7 @@
import React, { Fragment } from 'react';
-import { EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
+import { EuiIconTip, EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
import { ASRoleMapping } from '../../app_search/types';
import { WSRoleMapping } from '../../workplace_search/types';
@@ -84,7 +84,7 @@ export const RoleMappingsTable: React.FC = ({
const roleCol: EuiBasicTableColumn = {
field: 'roleType',
name: ROLE_LABEL,
- render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules),
+ render: (_, { roleType }: SharedRoleMapping) => roleType,
};
const accessItemsCol: EuiBasicTableColumn = {
@@ -124,11 +124,16 @@ export const RoleMappingsTable: React.FC = ({
field: 'id',
name: '',
align: 'right',
- render: (_, { id }: SharedRoleMapping) => (
- initializeRoleMapping(id)}
- onDeleteClick={() => handleDeleteMapping(id)}
- />
+ render: (_, { id, toolTip }: SharedRoleMapping) => (
+ <>
+ {id && (
+ initializeRoleMapping(id)}
+ onDeleteClick={() => handleDeleteMapping(id)}
+ />
+ )}
+ {toolTip && }
+ >
),
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts
index b9a49c416f2831..e1c2a3b76e3ff1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts
@@ -8,3 +8,5 @@
export { WorkplaceSearchNav } from './nav';
export { WorkplaceSearchHeaderActions } from './kibana_header_actions';
export { AccountHeader } from './account_header';
+export { PersonalDashboardLayout } from './personal_dashboard_layout';
+export { PrivateSourcesSidebar, AccountSettingsSidebar } from './personal_dashboard_sidebar';
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/index.ts
new file mode 100644
index 00000000000000..40347aaee747d2
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export { PersonalDashboardLayout } from './personal_dashboard_layout';
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
new file mode 100644
index 00000000000000..175f6b9ebca208
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+.personalDashboardLayout {
+ $sideBarWidth: $euiSize * 30;
+ $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes
+ $pageHeight: calc(100vh - #{$consoleHeaderHeight});
+
+ left: $sideBarWidth;
+ width: calc(100% - #{$sideBarWidth});
+ min-height: $pageHeight;
+
+ &__sideBar {
+ padding: 32px 40px 40px;
+ width: $sideBarWidth;
+ margin-left: -$sideBarWidth;
+ height: $pageHeight;
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
new file mode 100644
index 00000000000000..faeaa7323e93f0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { shallow } from 'enzyme';
+
+import { EuiCallOut } from '@elastic/eui';
+
+import { AccountHeader } from '..';
+
+import { PersonalDashboardLayout } from './personal_dashboard_layout';
+
+describe('PersonalDashboardLayout', () => {
+ const children = test
;
+ const sidebar = test
;
+
+ it('renders', () => {
+ const wrapper = shallow(
+ {children}
+ );
+
+ expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="TestSidebar"]')).toHaveLength(1);
+ expect(wrapper.find(AccountHeader)).toHaveLength(1);
+ });
+
+ it('renders callout when in read-only mode', () => {
+ const wrapper = shallow(
+
+ {children}
+
+ );
+
+ expect(wrapper.find(EuiCallOut)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
new file mode 100644
index 00000000000000..1ab9e07dfa14d5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui';
+
+import { AccountHeader } from '..';
+
+import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING } from '../../../views/content_sources/constants';
+
+import './personal_dashboard_layout.scss';
+
+interface LayoutProps {
+ restrictWidth?: boolean;
+ readOnlyMode?: boolean;
+ sidebar: React.ReactNode;
+}
+
+export const PersonalDashboardLayout: React.FC = ({
+ children,
+ restrictWidth,
+ readOnlyMode,
+ sidebar,
+}) => {
+ return (
+ <>
+
+
+
+ {sidebar}
+
+
+ {readOnlyMode && (
+
+ )}
+ {children}
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.test.tsx
new file mode 100644
index 00000000000000..8edcf83a57e6f4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.test.tsx
@@ -0,0 +1,22 @@
+/*
+ * 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 { shallow } from 'enzyme';
+
+import { ViewContentHeader } from '../../shared/view_content_header';
+
+import { AccountSettingsSidebar } from './account_settings_sidebar';
+
+describe('AccountSettingsSidebar', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx
new file mode 100644
index 00000000000000..490f3ff0ae4a51
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx
@@ -0,0 +1,17 @@
+/*
+ * 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 { ACCOUNT_SETTINGS_TITLE, ACCOUNT_SETTINGS_DESCRIPTION } from '../../../constants';
+import { ViewContentHeader } from '../../shared/view_content_header';
+
+export const AccountSettingsSidebar = () => {
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/index.ts
new file mode 100644
index 00000000000000..ffd241000c8a03
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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.
+ */
+
+export { PrivateSourcesSidebar } from './private_sources_sidebar';
+export { AccountSettingsSidebar } from './account_settings_sidebar';
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
similarity index 54%
rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx
rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
index 6af439814b8918..387724af970f89 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
@@ -5,50 +5,42 @@
* 2.0.
*/
-import '../../../__mocks__/shallow_useeffect.mock';
-
-import { setMockValues } from '../../../__mocks__/kea_logic';
+import { setMockValues } from '../../../../__mocks__/kea_logic';
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiCallOut } from '@elastic/eui';
-
-import { AccountHeader } from '../../components/layout';
-import { ViewContentHeader } from '../../components/shared/view_content_header';
-
-import { SourceSubNav } from './components/source_sub_nav';
-
import {
PRIVATE_CAN_CREATE_PAGE_TITLE,
PRIVATE_VIEW_ONLY_PAGE_TITLE,
PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION,
PRIVATE_CAN_CREATE_PAGE_DESCRIPTION,
-} from './constants';
-import { PrivateSourcesLayout } from './private_sources_layout';
+} from '../../../constants';
+import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
+
+import { ViewContentHeader } from '../../shared/view_content_header';
-describe('PrivateSourcesLayout', () => {
+import { PrivateSourcesSidebar } from './private_sources_sidebar';
+
+describe('PrivateSourcesSidebar', () => {
const mockValues = {
account: { canCreatePersonalSources: true },
};
- const children = test
;
-
beforeEach(() => {
setMockValues({ ...mockValues });
});
it('renders', () => {
- const wrapper = shallow({children} );
+ const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
+ expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
expect(wrapper.find(SourceSubNav)).toHaveLength(1);
- expect(wrapper.find(AccountHeader)).toHaveLength(1);
});
it('uses correct title and description when private sources are enabled', () => {
- const wrapper = shallow({children} );
+ const wrapper = shallow( );
expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE);
expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
@@ -58,17 +50,11 @@ describe('PrivateSourcesLayout', () => {
it('uses correct title and description when private sources are disabled', () => {
setMockValues({ account: { canCreatePersonalSources: false } });
- const wrapper = shallow({children} );
+ const wrapper = shallow( );
expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE);
expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION
);
});
-
- it('renders callout when in read-only mode', () => {
- const wrapper = shallow({children} );
-
- expect(wrapper.find(EuiCallOut)).toHaveLength(1);
- });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
new file mode 100644
index 00000000000000..5505ae57b2ad5f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { useValues } from 'kea';
+
+import { AppLogic } from '../../../app_logic';
+import {
+ PRIVATE_CAN_CREATE_PAGE_TITLE,
+ PRIVATE_VIEW_ONLY_PAGE_TITLE,
+ PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION,
+ PRIVATE_CAN_CREATE_PAGE_DESCRIPTION,
+} from '../../../constants';
+import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
+import { ViewContentHeader } from '../../shared/view_content_header';
+
+export const PrivateSourcesSidebar = () => {
+ const {
+ account: { canCreatePersonalSources },
+ } = useValues(AppLogic);
+
+ const PAGE_TITLE = canCreatePersonalSources
+ ? PRIVATE_CAN_CREATE_PAGE_TITLE
+ : PRIVATE_VIEW_ONLY_PAGE_TITLE;
+ const PAGE_DESCRIPTION = canCreatePersonalSources
+ ? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
+ : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION;
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
index dcebc35d45f711..aa5419f12c7f30 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
@@ -662,6 +662,49 @@ export const PRIVATE_SOURCES = i18n.translate(
}
);
+export const PRIVATE_CAN_CREATE_PAGE_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.private.canCreate.title',
+ {
+ defaultMessage: 'Manage private content sources',
+ }
+);
+
+export const PRIVATE_VIEW_ONLY_PAGE_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.private.vewOnly.title',
+ {
+ defaultMessage: 'Review Group Sources',
+ }
+);
+
+export const PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.private.vewOnly.description',
+ {
+ defaultMessage: 'Review the status of all sources shared with your Group.',
+ }
+);
+
+export const PRIVATE_CAN_CREATE_PAGE_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.private.canCreate.description',
+ {
+ defaultMessage:
+ 'Review the status of all connected private sources, and manage private sources for your account.',
+ }
+);
+
+export const ACCOUNT_SETTINGS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.accountSettings.title',
+ {
+ defaultMessage: 'Account Settings',
+ }
+);
+
+export const ACCOUNT_SETTINGS_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.accountSettings.description',
+ {
+ defaultMessage: 'Manage access, passwords, and other account settings.',
+ }
+);
+
export const CONFIRM_CHANGES_TEXT = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.confirmChanges.text',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
index 0fc8a6e7c7c0d0..7e911b31c516b7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
@@ -19,6 +19,11 @@ import { NotFound } from '../shared/not_found';
import { AppLogic } from './app_logic';
import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout';
+import {
+ PersonalDashboardLayout,
+ PrivateSourcesSidebar,
+ AccountSettingsSidebar,
+} from './components/layout';
import {
GROUPS_PATH,
SETUP_GUIDE_PATH,
@@ -34,7 +39,6 @@ import { AccountSettings } from './views/account_settings';
import { SourcesRouter } from './views/content_sources';
import { SourceAdded } from './views/content_sources/components/source_added';
import { SourceSubNav } from './views/content_sources/components/source_sub_nav';
-import { PrivateSourcesLayout } from './views/content_sources/private_sources_layout';
import { ErrorState } from './views/error_state';
import { GroupsRouter } from './views/groups';
import { GroupSubNav } from './views/groups/components/group_sub_nav';
@@ -101,14 +105,22 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
)}
-
+ }
+ >
-
+
-
+ }
+ >
-
+
= ({
- children,
- restrictWidth,
- readOnlyMode,
-}) => {
- const {
- account: { canCreatePersonalSources },
- } = useValues(AppLogic);
-
- const PAGE_TITLE = canCreatePersonalSources
- ? PRIVATE_CAN_CREATE_PAGE_TITLE
- : PRIVATE_VIEW_ONLY_PAGE_TITLE;
- const PAGE_DESCRIPTION = canCreatePersonalSources
- ? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
- : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION;
-
- return (
- <>
-
-
-
-
-
-
-
- {readOnlyMode && (
-
- )}
- {children}
-
-
- >
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss
index 549ca3ae9154e0..feccc0e1924d21 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss
@@ -18,23 +18,6 @@
}
}
-.privateSourcesLayout {
- $sideBarWidth: $euiSize * 30;
- $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes
- $pageHeight: calc(100vh - #{$consoleHeaderHeight});
-
- left: $sideBarWidth;
- width: calc(100% - #{$sideBarWidth});
- min-height: $pageHeight;
-
- &__sideBar {
- padding: 32px 40px 40px;
- width: $sideBarWidth;
- margin-left: -$sideBarWidth;
- height: $pageHeight;
- }
-}
-
.sourcesSubNav {
li {
display: block;
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
index 716cb8ebb6d47d..4ee530870284e0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
@@ -16,13 +16,13 @@ import { groups } from '../../__mocks__/groups.mock';
import { nextTick } from '@kbn/test/jest';
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { RoleMappingsLogic } from './role_mappings_logic';
describe('RoleMappingsLogic', () => {
const { http } = mockHttpValues;
- const { clearFlashMessages, flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
+ const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers;
const { mount } = new LogicMounter(RoleMappingsLogic);
const defaultValues = {
attributes: [],
@@ -52,14 +52,13 @@ describe('RoleMappingsLogic', () => {
name: 'Default',
};
- const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [wsRoleMapping] };
- const mappingServerProps = {
+ const mappingsServerProps = {
+ multipleAuthProvidersConfig: true,
+ roleMappings: [wsRoleMapping],
attributes: [],
authProviders: [],
availableGroups: [roleGroup, defaultGroup],
elasticsearchRoles: [],
- multipleAuthProvidersConfig: false,
- roleMapping: wsRoleMapping,
};
beforeEach(() => {
@@ -78,36 +77,17 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]);
expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true);
- });
-
- describe('setRoleMappingData', () => {
- it('sets data correctly', () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
-
- expect(RoleMappingsLogic.values.roleMapping).toEqual(wsRoleMapping);
- expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
- expect(RoleMappingsLogic.values.attributes).toEqual(mappingServerProps.attributes);
- expect(RoleMappingsLogic.values.availableGroups).toEqual(
- mappingServerProps.availableGroups
- );
- expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(true);
- expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual(
- mappingServerProps.elasticsearchRoles
- );
- expect(RoleMappingsLogic.values.selectedGroups).toEqual(
- new Set([wsRoleMapping.groups[0].id])
- );
- expect(RoleMappingsLogic.values.selectedOptions).toEqual([]);
- });
-
- it('sets default group with new role mapping', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: undefined,
- });
-
- expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
- });
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
+ expect(RoleMappingsLogic.values.attributes).toEqual(mappingsServerProps.attributes);
+ expect(RoleMappingsLogic.values.availableGroups).toEqual(mappingsServerProps.availableGroups);
+ expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(false);
+ expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual(
+ mappingsServerProps.elasticsearchRoles
+ );
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: defaultGroup.name, value: defaultGroup.id },
+ ]);
+ expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
});
it('handleRoleChange', () => {
@@ -119,14 +99,17 @@ describe('RoleMappingsLogic', () => {
it('handleGroupSelectionChange', () => {
const group = wsRoleMapping.groups[0];
const otherGroup = groups[0];
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- groups: [group, otherGroup],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ groups: [group, otherGroup],
+ },
+ ],
});
+ RoleMappingsLogic.actions.initializeRoleMapping(wsRoleMapping.id);
RoleMappingsLogic.actions.handleGroupSelectionChange([group.id, otherGroup.id]);
expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([group.id, otherGroup.id]));
expect(RoleMappingsLogic.values.selectedOptions).toEqual([
@@ -147,8 +130,8 @@ describe('RoleMappingsLogic', () => {
const elasticsearchRoles = ['foo', 'bar'];
it('sets values correctly', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
elasticsearchRoles,
});
RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]);
@@ -172,12 +155,14 @@ describe('RoleMappingsLogic', () => {
describe('handleAuthProviderChange', () => {
beforeEach(() => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- authProvider: ['foo'],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ authProvider: ['foo'],
+ },
+ ],
});
});
const providers = ['bar', 'baz'];
@@ -201,28 +186,23 @@ describe('RoleMappingsLogic', () => {
});
it('handles "any" auth in previous state', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- authProvider: [ANY_AUTH_PROVIDER],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ authProvider: [ANY_AUTH_PROVIDER],
+ },
+ ],
});
RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[1]]);
});
-
- it('handles catch-all state', () => {
- RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
-
- expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([ANY_AUTH_PROVIDER]);
- });
});
it('resetState', () => {
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
RoleMappingsLogic.actions.resetState();
expect(RoleMappingsLogic.values.dataLoading).toEqual(true);
@@ -234,7 +214,7 @@ describe('RoleMappingsLogic', () => {
});
it('openRoleMappingFlyout', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
RoleMappingsLogic.actions.openRoleMappingFlyout();
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true);
@@ -243,7 +223,7 @@ describe('RoleMappingsLogic', () => {
it('closeRoleMappingFlyout', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
roleMappingFlyoutOpen: true,
});
RoleMappingsLogic.actions.closeRoleMappingFlyout();
@@ -275,40 +255,20 @@ describe('RoleMappingsLogic', () => {
});
describe('initializeRoleMapping', () => {
- it('calls API and sets values for new mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping();
-
- expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings/new');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('calls API and sets values for existing mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping('123');
-
- expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings/123');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('handles error', async () => {
- http.get.mockReturnValue(Promise.reject('this is an error'));
- RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
+ it('sets values for existing mapping', () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ RoleMappingsLogic.actions.initializeRoleMapping(wsRoleMapping.id);
- expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ expect(setRoleMappingDataSpy).toHaveBeenCalledWith(wsRoleMapping);
});
- it('shows error when there is a 404 status', async () => {
- http.get.mockReturnValue(Promise.reject({ status: 404 }));
+ it('does not set data for new mapping', async () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
- expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND);
+ expect(setRoleMappingDataSpy).not.toHaveBeenCalledWith(wsRoleMapping);
});
});
@@ -320,7 +280,7 @@ describe('RoleMappingsLogic', () => {
);
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- http.post.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.post).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings', {
@@ -331,7 +291,7 @@ describe('RoleMappingsLogic', () => {
rules: {
username: '',
},
- groups: [],
+ groups: [defaultGroup.id],
}),
});
await nextTick();
@@ -344,9 +304,9 @@ describe('RoleMappingsLogic', () => {
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(
@@ -408,7 +368,7 @@ describe('RoleMappingsLogic', () => {
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
http.delete.mockReturnValue(Promise.resolve({}));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
@@ -421,7 +381,7 @@ describe('RoleMappingsLogic', () => {
});
it('handles error', async () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
http.delete.mockReturnValue(Promise.reject('this is an error'));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
await nextTick();
@@ -430,7 +390,7 @@ describe('RoleMappingsLogic', () => {
});
it('will do nothing if not confirmed', async () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
window.confirm = () => false;
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
index aee780ac18971f..361425b7a78a12 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
@@ -13,10 +13,9 @@ import {
clearFlashMessages,
flashAPIErrors,
setSuccessMessage,
- setErrorMessage,
} from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { AttributeName } from '../../../shared/types';
import { RoleGroup, WSRoleMapping, Role } from '../../types';
@@ -30,16 +29,11 @@ import {
interface RoleMappingsServerDetails {
roleMappings: WSRoleMapping[];
- multipleAuthProvidersConfig: boolean;
-}
-
-interface RoleMappingServerDetails {
attributes: string[];
authProviders: string[];
availableGroups: RoleGroup[];
elasticsearchRoles: string[];
multipleAuthProvidersConfig: boolean;
- roleMapping?: WSRoleMapping;
}
const getFirstAttributeName = (roleMapping: WSRoleMapping): AttributeName =>
@@ -62,7 +56,7 @@ interface RoleMappingsActions {
initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
initializeRoleMappings(): void;
resetState(): void;
- setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails;
+ setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
@@ -93,7 +87,7 @@ export const RoleMappingsLogic = kea data,
- setRoleMappingData: (data: RoleMappingServerDetails) => data,
+ setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string[]) => ({ value }),
handleRoleChange: (roleType: Role) => ({ roleType }),
@@ -117,7 +111,6 @@ export const RoleMappingsLogic = kea false,
- setRoleMappingData: () => false,
resetState: () => true,
},
],
@@ -132,32 +125,31 @@ export const RoleMappingsLogic = kea multipleAuthProvidersConfig,
- setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig,
resetState: () => false,
},
],
availableGroups: [
[],
{
- setRoleMappingData: (_, { availableGroups }) => availableGroups,
+ setRoleMappingsData: (_, { availableGroups }) => availableGroups,
},
],
attributes: [
[],
{
- setRoleMappingData: (_, { attributes }) => attributes,
+ setRoleMappingsData: (_, { attributes }) => attributes,
},
],
elasticsearchRoles: [
[],
{
- setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles,
+ setRoleMappingsData: (_, { elasticsearchRoles }) => elasticsearchRoles,
},
],
roleMapping: [
null,
{
- setRoleMappingData: (_, { roleMapping }) => roleMapping || null,
+ setRoleMapping: (_, { roleMapping }) => roleMapping,
resetState: () => null,
closeRoleMappingFlyout: () => null,
},
@@ -165,23 +157,21 @@ export const RoleMappingsLogic = kea
- roleMapping ? (roleMapping.roleType as Role) : 'admin',
+ setRoleMapping: (_, { roleMapping }) => roleMapping.roleType as Role,
handleRoleChange: (_, { roleType }) => roleType,
},
],
includeInAllGroups: [
false,
{
- setRoleMappingData: (_, { roleMapping }) => (roleMapping ? roleMapping.allGroups : false),
+ setRoleMapping: (_, { roleMapping }) => roleMapping.allGroups,
handleAllGroupsSelectionChange: (_, { selected }) => selected,
},
],
attributeValue: [
'',
{
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? getFirstAttributeValue(roleMapping) : '',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeValue(roleMapping),
handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) =>
value === 'role' ? firstElasticsearchRole : '',
handleAttributeValueChange: (_, { value }) => value,
@@ -192,8 +182,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeName(roleMapping) : 'username',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeName(roleMapping),
handleAttributeSelectorChange: (_, { value }) => value,
resetState: () => 'username',
closeRoleMappingFlyout: () => 'username',
@@ -202,14 +191,14 @@ export const RoleMappingsLogic = kea
- roleMapping
- ? new Set(roleMapping.groups.map((group) => group.id))
- : new Set(
- availableGroups
- .filter((group) => group.name === DEFAULT_GROUP_NAME)
- .map((group) => group.id)
- ),
+ setRoleMappingsData: (_, { availableGroups }) =>
+ new Set(
+ availableGroups
+ .filter((group) => group.name === DEFAULT_GROUP_NAME)
+ .map((group) => group.id)
+ ),
+ setRoleMapping: (_, { roleMapping }) =>
+ new Set(roleMapping.groups.map((group: RoleGroup) => group.id)),
handleGroupSelectionChange: (_, { groupIds }) => {
const newSelectedGroupNames = new Set() as Set;
groupIds.forEach((groupId) => newSelectedGroupNames.add(groupId));
@@ -221,7 +210,7 @@ export const RoleMappingsLogic = kea authProviders,
+ setRoleMappingsData: (_, { authProviders }) => authProviders,
},
],
selectedAuthProviders: [
@@ -230,15 +219,15 @@ export const RoleMappingsLogic = kea {
const previouslyContainedAny = previous.includes(ANY_AUTH_PROVIDER);
const newSelectionsContainAny = value.includes(ANY_AUTH_PROVIDER);
+ const hasItems = value.length > 0;
- if (value.length < 1) return [ANY_AUTH_PROVIDER];
if (value.length === 1) return value;
- if (!newSelectionsContainAny) return value;
- if (previouslyContainedAny) return value.filter((v) => v !== ANY_AUTH_PROVIDER);
+ if (!newSelectionsContainAny && hasItems) return value;
+ if (previouslyContainedAny && hasItems)
+ return value.filter((v) => v !== ANY_AUTH_PROVIDER);
return [ANY_AUTH_PROVIDER];
},
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER],
+ setRoleMapping: (_, { roleMapping }) => roleMapping.authProvider,
},
],
roleMappingFlyoutOpen: [
@@ -283,21 +272,8 @@ export const RoleMappingsLogic = kea {
- const { http } = HttpLogic.values;
- const route = roleMappingId
- ? `/api/workplace_search/org/role_mappings/${roleMappingId}`
- : '/api/workplace_search/org/role_mappings/new';
-
- try {
- const response = await http.get(route);
- actions.setRoleMappingData(response);
- } catch (e) {
- if (e.status === 404) {
- setErrorMessage(ROLE_MAPPING_NOT_FOUND);
- } else {
- flashAPIErrors(e);
- }
- }
+ const roleMapping = values.roleMappings.find(({ id }) => id === roleMappingId);
+ if (roleMapping) actions.setRoleMapping(roleMapping);
},
handleDeleteMapping: async ({ roleMappingId }) => {
const { http } = HttpLogic.values;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
index a126d06f303b4c..718597c12e9c5b 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
@@ -7,11 +7,7 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import {
- registerRoleMappingsRoute,
- registerRoleMappingRoute,
- registerNewRoleMappingRoute,
-} from './role_mappings';
+import { registerRoleMappingsRoute, registerRoleMappingRoute } from './role_mappings';
const roleMappingBaseSchema = {
rules: { username: 'user' },
@@ -80,29 +76,6 @@ describe('role mappings routes', () => {
});
});
- describe('GET /api/app_search/role_mappings/{id}', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/app_search/role_mappings/{id}',
- });
-
- registerRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
- });
- });
- });
-
describe('PUT /api/app_search/role_mappings/{id}', () => {
let mockRouter: MockRouter;
@@ -160,27 +133,4 @@ describe('role mappings routes', () => {
});
});
});
-
- describe('GET /api/app_search/role_mappings/new', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/app_search/role_mappings/new',
- });
-
- registerNewRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/new',
- });
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
index 86e17b575e019a..75724a3344d6de 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
@@ -48,20 +48,6 @@ export function registerRoleMappingRoute({
router,
enterpriseSearchRequestHandler,
}: RouteDependencies) {
- router.get(
- {
- path: '/api/app_search/role_mappings/{id}',
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
- })
- );
-
router.put(
{
path: '/api/app_search/role_mappings/{id}',
@@ -92,23 +78,7 @@ export function registerRoleMappingRoute({
);
}
-export function registerNewRoleMappingRoute({
- router,
- enterpriseSearchRequestHandler,
-}: RouteDependencies) {
- router.get(
- {
- path: '/api/app_search/role_mappings/new',
- validate: false,
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/new',
- })
- );
-}
-
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
registerRoleMappingsRoute(dependencies);
registerRoleMappingRoute(dependencies);
- registerNewRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
index 0dade134767e44..a945866da5ef21 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
@@ -7,11 +7,7 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import {
- registerOrgRoleMappingsRoute,
- registerOrgRoleMappingRoute,
- registerOrgNewRoleMappingRoute,
-} from './role_mappings';
+import { registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute } from './role_mappings';
describe('role mappings routes', () => {
describe('GET /api/workplace_search/org/role_mappings', () => {
@@ -60,29 +56,6 @@ describe('role mappings routes', () => {
});
});
- describe('GET /api/workplace_search/org/role_mappings/{id}', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/workplace_search/org/role_mappings/{id}',
- });
-
- registerOrgRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/ws/org/role_mappings/:id',
- });
- });
- });
-
describe('PUT /api/workplace_search/org/role_mappings/{id}', () => {
let mockRouter: MockRouter;
@@ -128,27 +101,4 @@ describe('role mappings routes', () => {
});
});
});
-
- describe('GET /api/workplace_search/org/role_mappings/new', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/workplace_search/org/role_mappings/new',
- });
-
- registerOrgNewRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/ws/org/role_mappings/new',
- });
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
index 5a6359c1cd8369..a0fcec63cbb272 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
@@ -48,20 +48,6 @@ export function registerOrgRoleMappingRoute({
router,
enterpriseSearchRequestHandler,
}: RouteDependencies) {
- router.get(
- {
- path: '/api/workplace_search/org/role_mappings/{id}',
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/ws/org/role_mappings/:id',
- })
- );
-
router.put(
{
path: '/api/workplace_search/org/role_mappings/{id}',
@@ -92,23 +78,7 @@ export function registerOrgRoleMappingRoute({
);
}
-export function registerOrgNewRoleMappingRoute({
- router,
- enterpriseSearchRequestHandler,
-}: RouteDependencies) {
- router.get(
- {
- path: '/api/workplace_search/org/role_mappings/new',
- validate: false,
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/ws/org/role_mappings/new',
- })
- );
-}
-
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
registerOrgRoleMappingsRoute(dependencies);
registerOrgRoleMappingRoute(dependencies);
- registerOrgNewRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
index 49836e9ed4ca68..d707fd162ae020 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
@@ -18,7 +18,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import type { Section } from '../sections';
-import { AlphaMessaging, SettingFlyout } from '../components';
+import { SettingFlyout } from '../components';
import { useLink, useConfig, useUrlModal } from '../hooks';
interface Props {
@@ -144,7 +144,6 @@ export const DefaultLayout: React.FunctionComponent = ({
) : null}
{children}
-
>
);
diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts
index a6958ba88449a0..b3626a83c41d1f 100644
--- a/x-pack/plugins/fleet/server/services/package_policy.test.ts
+++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts
@@ -688,6 +688,129 @@ describe('Package policy service', () => {
expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south']));
expect(modifiedStream.vars!.period.value).toEqual('12mo');
});
+
+ it('should add new input vars when updating', async () => {
+ const savedObjectsClient = savedObjectsClientMock.create();
+ const mockPackagePolicy = createPackagePolicyMock();
+ const mockInputs = [
+ {
+ config: {},
+ enabled: true,
+ keep_enabled: true,
+ type: 'endpoint',
+ vars: {
+ dog: {
+ type: 'text',
+ value: 'dalmatian',
+ },
+ cat: {
+ type: 'text',
+ value: 'siamese',
+ frozen: true,
+ },
+ },
+ streams: [
+ {
+ data_stream: {
+ type: 'birds',
+ dataset: 'migratory.patterns',
+ },
+ enabled: false,
+ id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`,
+ vars: {
+ paths: {
+ value: ['north', 'south'],
+ type: 'text',
+ frozen: true,
+ },
+ },
+ },
+ ],
+ },
+ ];
+ const inputsUpdate = [
+ {
+ config: {},
+ enabled: false,
+ type: 'endpoint',
+ vars: {
+ dog: {
+ type: 'text',
+ value: 'labrador',
+ },
+ cat: {
+ type: 'text',
+ value: 'tabby',
+ },
+ },
+ streams: [
+ {
+ data_stream: {
+ type: 'birds',
+ dataset: 'migratory.patterns',
+ },
+ enabled: false,
+ id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`,
+ vars: {
+ paths: {
+ value: ['east', 'west'],
+ type: 'text',
+ },
+ period: {
+ value: '12mo',
+ type: 'text',
+ },
+ },
+ },
+ ],
+ },
+ ];
+ const attributes = {
+ ...mockPackagePolicy,
+ inputs: mockInputs,
+ };
+
+ savedObjectsClient.get.mockResolvedValue({
+ id: 'test',
+ type: 'abcd',
+ references: [],
+ version: 'test',
+ attributes,
+ });
+
+ savedObjectsClient.update.mockImplementation(
+ async (
+ type: string,
+ id: string,
+ attrs: any
+ ): Promise> => {
+ savedObjectsClient.get.mockResolvedValue({
+ id: 'test',
+ type: 'abcd',
+ references: [],
+ version: 'test',
+ attributes: attrs,
+ });
+ return attrs;
+ }
+ );
+ const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ const result = await packagePolicyService.update(
+ savedObjectsClient,
+ elasticsearchClient,
+ 'the-package-policy-id',
+ { ...mockPackagePolicy, inputs: inputsUpdate }
+ );
+
+ const [modifiedInput] = result.inputs;
+ expect(modifiedInput.enabled).toEqual(true);
+ expect(modifiedInput.vars!.dog.value).toEqual('labrador');
+ expect(modifiedInput.vars!.cat.value).toEqual('siamese');
+ const [modifiedStream] = modifiedInput.streams;
+ expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south']));
+ expect(modifiedStream.vars!.period.value).toEqual('12mo');
+ });
});
describe('runExternalCallbacks', () => {
diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts
index 93bcef458279c0..1cda159429984c 100644
--- a/x-pack/plugins/fleet/server/services/package_policy.ts
+++ b/x-pack/plugins/fleet/server/services/package_policy.ts
@@ -649,11 +649,16 @@ function _enforceFrozenVars(
newVars: Record
) {
const resultVars: Record = {};
+ for (const [key, val] of Object.entries(newVars)) {
+ if (oldVars[key]?.frozen) {
+ resultVars[key] = oldVars[key];
+ } else {
+ resultVars[key] = val;
+ }
+ }
for (const [key, val] of Object.entries(oldVars)) {
- if (val.frozen) {
+ if (!newVars[key] && val.frozen) {
resultVars[key] = val;
- } else {
- resultVars[key] = newVars[key];
}
}
return resultVars;
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
index 76ebe21a513671..78e3f2dab0d1db 100644
--- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
@@ -162,7 +162,7 @@ describe(' ', () => {
const { exists, find } = testBed;
expect(exists('pipelineLoadError')).toBe(true);
- expect(find('pipelineLoadError').text()).toContain('Unable to load pipelines.');
+ expect(find('pipelineLoadError').text()).toContain('Unable to load pipelines');
});
});
});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx
index 1cca7a0721fbc0..da8f74e1efae59 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx
@@ -6,7 +6,7 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageContent } from '@elastic/eui';
+import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui';
import React, { FunctionComponent } from 'react';
import { Router, Switch, Route } from 'react-router-dom';
@@ -19,7 +19,6 @@ import {
useAuthorizationContext,
WithPrivileges,
SectionLoading,
- NotAuthorizedSection,
} from '../shared_imports';
import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections';
@@ -61,35 +60,42 @@ export const App: FunctionComponent = () => {
{({ isLoading, hasPrivileges, privilegesMissing }) => {
if (isLoading) {
return (
-
-
-
+
+
+
+
+
);
}
if (!hasPrivileges) {
return (
-
-
+
+
+
+
}
- message={
-
+ body={
+
+
+
}
/>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
index c51ed94cbc1166..f68b64cc5f6136 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
@@ -8,6 +8,7 @@
import React, { FunctionComponent, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
+import { EuiPageContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { SectionLoading, useKibana, attemptToURIDecode } from '../../../shared_imports';
@@ -45,12 +46,14 @@ export const PipelinesClone: FunctionComponent>
if (isLoading && isInitialRequest) {
return (
-
-
-
+
+
+
+
+
);
} else {
// We still show the create form even if we were not able to load the
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
index eefc74e2dc6fad..5aa9205e1e1e55 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
@@ -8,15 +8,7 @@
import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiPageBody,
- EuiPageContent,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { getListPath } from '../../services/navigation';
import { Pipeline } from '../../../../common/types';
@@ -64,49 +56,43 @@ export const PipelinesCreate: React.FunctionComponent
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
index 6011a36d292fac..ea47f4c9a25e92 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
@@ -9,16 +9,14 @@ import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiPageBody,
+ EuiPageHeader,
+ EuiEmptyPrompt,
EuiPageContent,
EuiSpacer,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
+ EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
-import { EuiCallOut } from '@elastic/eui';
import { Pipeline } from '../../../../common/types';
import { useKibana, SectionLoading, attemptToURIDecode } from '../../../shared_imports';
@@ -42,7 +40,9 @@ export const PipelinesEdit: React.FunctionComponent {
setIsSaving(true);
@@ -68,88 +68,92 @@ export const PipelinesEdit: React.FunctionComponent
-
-
+ return (
+
+
+
+
+
);
- } else if (error) {
- content = (
- <>
-
+
+
+
+
}
- color="danger"
- iconType="alert"
- data-test-subj="fetchPipelineError"
- >
- {error.message}
-
-
- >
+ body={{error.message}
}
+ actions={
+
+
+
+ }
+ />
+
);
- } else if (pipeline) {
- content = (
+ }
+
+ return (
+ <>
+
+
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {content}
-
-
+ >
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
index 7d69bd3fb8cf37..9f401bca5431f2 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
@@ -8,7 +8,7 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiEmptyPrompt, EuiLink, EuiPageBody, EuiPageContent, EuiButton } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiLink, EuiPageContent, EuiButton } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { ScopedHistory } from 'kibana/public';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
@@ -20,46 +20,43 @@ export const EmptyList: FunctionComponent = () => {
const history = useHistory() as ScopedHistory;
return (
-
-
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', {
- defaultMessage: 'Start by creating a pipeline',
+
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', {
+ defaultMessage: 'Start by creating a pipeline',
+ })}
+
+ }
+ body={
+
+ {' '}
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptDocumentionLink', {
+ defaultMessage: 'Learn more',
})}
-
- }
- body={
-
-
-
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptDocumentionLink', {
- defaultMessage: 'Learn more',
- })}
-
-
- }
- actions={
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', {
- defaultMessage: 'Create a pipeline',
- })}
-
- }
- />
-
-
+
+
+ }
+ actions={
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', {
+ defaultMessage: 'Create a pipeline',
+ })}
+
+ }
+ />
+
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
index 454747fe0870e4..ae68cfcb399f09 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
@@ -12,16 +12,12 @@ import { Location } from 'history';
import { parse } from 'query-string';
import {
- EuiPageBody,
- EuiPageContent,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
+ EuiPageHeader,
EuiButtonEmpty,
- EuiCallOut,
- EuiLink,
+ EuiPageContent,
+ EuiEmptyPrompt,
+ EuiButton,
EuiSpacer,
- EuiText,
} from '@elastic/eui';
import { Pipeline } from '../../../../common/types';
@@ -81,33 +77,50 @@ export const PipelinesList: React.FunctionComponent = ({
history.push(getListPath());
};
- if (data && data.length === 0) {
- return ;
+ if (error) {
+ return (
+
+
+
+
+ }
+ body={{error.message}
}
+ actions={
+
+
+
+ }
+ />
+
+ );
}
- let content: React.ReactNode;
-
if (isLoading) {
- content = (
-
-
-
- );
- } else if (data?.length) {
- content = (
-
+ return (
+
+
+
+
+
);
}
+ if (data && data.length === 0) {
+ return ;
+ }
+
const renderFlyout = (): React.ReactNode => {
if (!showFlyout) {
return;
@@ -134,71 +147,47 @@ export const PipelinesList: React.FunctionComponent = ({
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Error call out for pipeline table */}
- {error ? (
-
-
-
- ),
- }}
- />
- }
+
+
- ) : (
- content
- )}
-
-
+
+ }
+ description={
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
+
+
{renderFlyout()}
{pipelinesToDelete?.length > 0 ? (
& {
+ returnToOrigin: boolean;
+ dashboardId?: string | null;
+ onTitleDuplicate?: OnSaveProps['onTitleDuplicate'];
+ newDescription?: string;
+ newTags?: string[];
+};
export function App({
history,
@@ -48,26 +53,23 @@ export function App({
initialInput,
incomingState,
redirectToOrigin,
- redirectToDashboard,
setHeaderActionMenu,
initialContext,
}: LensAppProps) {
+ const lensAppServices = useKibana().services;
+
const {
data,
chrome,
- overlays,
uiSettings,
application,
- stateTransfer,
notifications,
- attributeService,
- savedObjectsClient,
savedObjectsTagging,
getOriginatingAppName,
// Temporarily required until the 'by value' paradigm is default.
dashboardFeatureFlag,
- } = useKibana().services;
+ } = lensAppServices;
const dispatch = useLensDispatch();
const dispatchSetState: DispatchSetState = useCallback(
@@ -205,150 +207,39 @@ export function App({
getIsByValueMode,
application,
chrome,
- initialInput,
appState.isLinkedToOriginatingApp,
appState.persistedDoc,
]);
- const tagsIds =
- appState.persistedDoc && savedObjectsTagging
- ? savedObjectsTagging.ui.getTagIdsFromReferences(appState.persistedDoc.references)
- : [];
-
- const runSave: RunSave = async (saveProps, options) => {
- if (!lastKnownDoc) {
- return;
- }
-
- let references = lastKnownDoc.references;
- if (savedObjectsTagging) {
- references = savedObjectsTagging.ui.updateTagsReferences(
- references,
- saveProps.newTags || tagsIds
- );
- }
-
- const docToSave = {
- ...getLastKnownDocWithoutPinnedFilters(lastKnownDoc)!,
- description: saveProps.newDescription,
- title: saveProps.newTitle,
- references,
- };
-
- // Required to serialize filters in by value mode until
- // https://github.com/elastic/kibana/issues/77588 is fixed
- if (getIsByValueMode()) {
- docToSave.state.filters.forEach((filter) => {
- if (typeof filter.meta.value === 'function') {
- delete filter.meta.value;
+ const runSave = (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
+ return runSaveLensVisualization(
+ {
+ lastKnownDoc,
+ getIsByValueMode,
+ savedObjectsTagging,
+ initialInput,
+ redirectToOrigin,
+ persistedDoc: appState.persistedDoc,
+ onAppLeave,
+ redirectTo,
+ ...lensAppServices,
+ },
+ saveProps,
+ options
+ ).then(
+ (newState) => {
+ if (newState) {
+ dispatchSetState(newState);
+ setIsSaveModalVisible(false);
}
- });
- }
-
- const originalInput = saveProps.newCopyOnSave ? undefined : initialInput;
- const originalSavedObjectId = (originalInput as LensByReferenceInput)?.savedObjectId;
- if (options.saveToLibrary) {
- try {
- await checkForDuplicateTitle(
- {
- id: originalSavedObjectId,
- title: docToSave.title,
- copyOnSave: saveProps.newCopyOnSave,
- lastSavedTitle: lastKnownDoc.title,
- getEsType: () => 'lens',
- getDisplayName: () =>
- i18n.translate('xpack.lens.app.saveModalType', {
- defaultMessage: 'Lens visualization',
- }),
- },
- saveProps.isTitleDuplicateConfirmed,
- saveProps.onTitleDuplicate,
- {
- savedObjectsClient,
- overlays,
- }
- );
- } catch (e) {
- // ignore duplicate title failure, user notified in save modal
- return;
- }
- }
- try {
- const newInput = (await attributeService.wrapAttributes(
- docToSave,
- options.saveToLibrary,
- originalInput
- )) as LensEmbeddableInput;
-
- if (saveProps.returnToOrigin && redirectToOrigin) {
- // disabling the validation on app leave because the document has been saved.
- onAppLeave((actions) => {
- return actions.default();
- });
- redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave });
- return;
- } else if (saveProps.dashboardId && redirectToDashboard) {
- // disabling the validation on app leave because the document has been saved.
- onAppLeave((actions) => {
- return actions.default();
- });
- redirectToDashboard(newInput, saveProps.dashboardId);
- return;
- }
-
- notifications.toasts.addSuccess(
- i18n.translate('xpack.lens.app.saveVisualization.successNotificationText', {
- defaultMessage: `Saved '{visTitle}'`,
- values: {
- visTitle: docToSave.title,
- },
- })
- );
-
- if (
- attributeService.inputIsRefType(newInput) &&
- newInput.savedObjectId !== originalSavedObjectId
- ) {
- chrome.recentlyAccessed.add(
- getFullPath(newInput.savedObjectId),
- docToSave.title,
- newInput.savedObjectId
- );
-
- dispatchSetState({ isLinkedToOriginatingApp: false });
-
- setIsSaveModalVisible(false);
- // remove editor state so the connection is still broken after reload
- stateTransfer.clearEditorState(APP_ID);
-
- redirectTo(newInput.savedObjectId);
- return;
+ },
+ () => {
+ // error is handled inside the modal
+ // so ignoring it here
}
-
- const newDoc = {
- ...docToSave,
- ...newInput,
- };
-
- dispatchSetState({
- isLinkedToOriginatingApp: false,
- persistedDoc: newDoc,
- lastKnownDoc: newDoc,
- });
-
- setIsSaveModalVisible(false);
- } catch (e) {
- // eslint-disable-next-line no-console
- console.dir(e);
- trackUiEvent('save_failed');
- setIsSaveModalVisible(false);
- }
+ );
};
- const savingToLibraryPermitted = Boolean(
- appState.isSaveable && application.capabilities.visualize.save
- );
-
return (
<>
@@ -371,21 +262,24 @@ export function App({
/>
)}
- {
setIsSaveModalVisible(false);
}}
getAppNameFromId={() => getOriginatingAppName()}
lastKnownDoc={lastKnownDoc}
+ onAppLeave={onAppLeave}
+ persistedDoc={appState.persistedDoc}
+ initialInput={initialInput}
+ redirectTo={redirectTo}
+ redirectToOrigin={redirectToOrigin}
returnToOriginSwitchLabel={
getIsByValueMode() && initialInput
? i18n.translate('xpack.lens.app.updatePanel', {
@@ -419,20 +313,3 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
/>
);
});
-
-function getLastKnownDocWithoutPinnedFilters(doc?: Document) {
- if (!doc) return undefined;
- const [pinnedFilters, appFilters] = partition(
- injectFilterReferences(doc.state?.filters || [], doc.references),
- esFilters.isFilterPinned
- );
- return pinnedFilters?.length
- ? {
- ...doc,
- state: {
- ...doc.state,
- filters: appFilters,
- },
- }
- : doc;
-}
diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
index 3e56fbb2003cb4..46d2009756d2ce 100644
--- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
@@ -7,7 +7,7 @@
import React, { FC, useCallback } from 'react';
-import { AppMountParameters, CoreSetup } from 'kibana/public';
+import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { History } from 'history';
@@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n';
import { DashboardFeatureFlagConfig } from 'src/plugins/dashboard/public';
import { Provider } from 'react-redux';
-import { uniq, isEqual } from 'lodash';
+import { isEqual } from 'lodash';
import { EmbeddableEditorState } from 'src/plugins/embeddable/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
@@ -26,7 +26,7 @@ import { App } from './app';
import { EditorFrameStart } from '../types';
import { addHelpMenuToAppChrome } from '../help_menu_util';
import { LensPluginStartDependencies } from '../plugin';
-import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID, getFullPath } from '../../common';
+import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common';
import {
LensEmbeddableInput,
LensByReferenceInput,
@@ -44,37 +44,21 @@ import {
LensRootStore,
setState,
} from '../state_management';
-import { getAllIndexPatterns, getResolvedDateRange } from '../utils';
-import { injectFilterReferences } from '../persistence';
+import { getResolvedDateRange } from '../utils';
+import { getLastKnownDoc } from './save_modal_container';
-export async function mountApp(
- core: CoreSetup,
- params: AppMountParameters,
- mountProps: {
- createEditorFrame: EditorFrameStart['createInstance'];
- getByValueFeatureFlag: () => Promise;
- attributeService: () => Promise;
- getPresentationUtilContext: () => Promise;
- }
-) {
- const {
- createEditorFrame,
- getByValueFeatureFlag,
- attributeService,
- getPresentationUtilContext,
- } = mountProps;
- const [coreStart, startDependencies] = await core.getStartServices();
+export async function getLensServices(
+ coreStart: CoreStart,
+ startDependencies: LensPluginStartDependencies,
+ attributeService: () => Promise
+): Promise {
const { data, navigation, embeddable, savedObjectsTagging } = startDependencies;
- const instance = await createEditorFrame();
const storage = new Storage(localStorage);
const stateTransfer = embeddable?.getStateTransfer();
- const historyLocationState = params.history.location.state as HistoryLocationState;
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
- const dashboardFeatureFlag = await getByValueFeatureFlag();
-
- const lensServices: LensAppServices = {
+ return {
data,
storage,
navigation,
@@ -88,6 +72,8 @@ export async function mountApp(
application: coreStart.application,
notifications: coreStart.notifications,
savedObjectsClient: coreStart.savedObjects.client,
+ presentationUtil: startDependencies.presentationUtil,
+ dashboard: startDependencies.dashboard,
getOriginatingAppName: () => {
return embeddableEditorIncomingState?.originatingApp
? stateTransfer?.getAppNameFromId(embeddableEditorIncomingState.originatingApp)
@@ -95,8 +81,29 @@ export async function mountApp(
},
// Temporarily required until the 'by value' paradigm is default.
- dashboardFeatureFlag,
+ dashboardFeatureFlag: startDependencies.dashboard.dashboardFeatureFlagConfig,
};
+}
+
+export async function mountApp(
+ core: CoreSetup,
+ params: AppMountParameters,
+ mountProps: {
+ createEditorFrame: EditorFrameStart['createInstance'];
+ attributeService: () => Promise;
+ getPresentationUtilContext: () => Promise;
+ }
+) {
+ const { createEditorFrame, attributeService, getPresentationUtilContext } = mountProps;
+ const [coreStart, startDependencies] = await core.getStartServices();
+ const instance = await createEditorFrame();
+ const historyLocationState = params.history.location.state as HistoryLocationState;
+
+ const lensServices = await getLensServices(coreStart, startDependencies, attributeService);
+
+ const { stateTransfer, data, storage, dashboardFeatureFlag } = lensServices;
+
+ const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks);
coreStart.chrome.docTitle.change(
@@ -130,23 +137,6 @@ export async function mountApp(
}
};
- const redirectToDashboard = (embeddableInput: LensEmbeddableInput, dashboardId: string) => {
- if (!lensServices.dashboardFeatureFlag.allowByValueEmbeddables) {
- throw new Error('redirectToDashboard called with by-value embeddables disabled');
- }
-
- const state = {
- input: embeddableInput,
- type: LENS_EMBEDDABLE_TYPE,
- };
-
- const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
- stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
- state,
- path,
- });
- };
-
const redirectToOrigin = (props?: RedirectToOriginProps) => {
if (!embeddableEditorIncomingState?.originatingApp) {
throw new Error('redirectToOrigin called without an originating app');
@@ -215,7 +205,6 @@ export async function mountApp(
initialInput={initialInput}
redirectTo={redirectCallback}
redirectToOrigin={redirectToOrigin}
- redirectToDashboard={redirectToDashboard}
onAppLeave={params.onAppLeave}
setHeaderActionMenu={params.setHeaderActionMenu}
history={props.history}
@@ -299,73 +288,45 @@ export function loadDocument(
}
lensStore.dispatch(setState({ isAppLoading: true }));
- attributeService
- .unwrapAttributes(initialInput)
- .then((attributes) => {
- if (!initialInput) {
- return;
- }
- const doc = {
- ...initialInput,
- ...attributes,
- type: LENS_EMBEDDABLE_TYPE,
- };
-
- if (attributeService.inputIsRefType(initialInput)) {
- chrome.recentlyAccessed.add(
- getFullPath(initialInput.savedObjectId),
- attributes.title,
- initialInput.savedObjectId
+ getLastKnownDoc({
+ initialInput,
+ attributeService,
+ data,
+ chrome,
+ notifications,
+ }).then(
+ (newState) => {
+ if (newState) {
+ const { doc, indexPatterns } = newState;
+ const currentSessionId = data.search.session.getSessionId();
+ lensStore.dispatch(
+ setState({
+ query: doc.state.query,
+ isAppLoading: false,
+ indexPatternsForTopNav: indexPatterns,
+ lastKnownDoc: doc,
+ searchSessionId:
+ dashboardFeatureFlag.allowByValueEmbeddables &&
+ Boolean(embeddableEditorIncomingState?.originatingApp) &&
+ !(initialInput as LensByReferenceInput)?.savedObjectId &&
+ currentSessionId
+ ? currentSessionId
+ : data.search.session.start(),
+ ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
+ })
);
+ } else {
+ redirectCallback();
}
- const indexPatternIds = uniq(
- doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
- );
- getAllIndexPatterns(indexPatternIds, data.indexPatterns)
- .then(({ indexPatterns }) => {
- // Don't overwrite any pinned filters
- data.query.filterManager.setAppFilters(
- injectFilterReferences(doc.state.filters, doc.references)
- );
- const currentSessionId = data.search.session.getSessionId();
- lensStore.dispatch(
- setState({
- query: doc.state.query,
- isAppLoading: false,
- indexPatternsForTopNav: indexPatterns,
- lastKnownDoc: doc,
- searchSessionId:
- dashboardFeatureFlag.allowByValueEmbeddables &&
- Boolean(embeddableEditorIncomingState?.originatingApp) &&
- !(initialInput as LensByReferenceInput)?.savedObjectId &&
- currentSessionId
- ? currentSessionId
- : data.search.session.start(),
- ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
- })
- );
- })
- .catch((e) => {
- lensStore.dispatch(
- setState({
- isAppLoading: false,
- })
- );
- redirectCallback();
- });
- })
- .catch((e) => {
+ },
+ () => {
lensStore.dispatch(
setState({
isAppLoading: false,
})
);
- notifications.toasts.addDanger(
- i18n.translate('xpack.lens.app.docLoadingError', {
- defaultMessage: 'Error loading saved document',
- })
- );
redirectCallback();
- });
+ }
+ );
}
diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx
new file mode 100644
index 00000000000000..27e8031f5fb6b8
--- /dev/null
+++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx
@@ -0,0 +1,405 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import { ChromeStart, NotificationsStart } from 'kibana/public';
+import { i18n } from '@kbn/i18n';
+import { partition, uniq } from 'lodash';
+import { SaveModal } from './save_modal';
+import { LensAppProps, LensAppServices } from './types';
+import type { SaveProps } from './app';
+import { Document, injectFilterReferences } from '../persistence';
+import { LensByReferenceInput, LensEmbeddableInput } from '../editor_frame_service/embeddable';
+import { LensAttributeService } from '../lens_attribute_service';
+import {
+ DataPublicPluginStart,
+ esFilters,
+ IndexPattern,
+} from '../../../../../src/plugins/data/public';
+import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common';
+import { getAllIndexPatterns } from '../utils';
+import { trackUiEvent } from '../lens_ui_telemetry';
+import { checkForDuplicateTitle } from '../../../../../src/plugins/saved_objects/public';
+import { LensAppState } from '../state_management';
+
+type ExtraProps = Pick &
+ Partial>;
+
+export type SaveModalContainerProps = {
+ isVisible: boolean;
+ originatingApp?: string;
+ persistedDoc?: Document;
+ lastKnownDoc?: Document;
+ returnToOriginSwitchLabel?: string;
+ onClose: () => void;
+ onSave?: () => void;
+ runSave?: (saveProps: SaveProps, options: { saveToLibrary: boolean }) => void;
+ isSaveable?: boolean;
+ getAppNameFromId?: () => string | undefined;
+ lensServices: LensAppServices;
+} & ExtraProps;
+
+export function SaveModalContainer({
+ returnToOriginSwitchLabel,
+ onClose,
+ onSave,
+ runSave,
+ isVisible,
+ persistedDoc,
+ originatingApp,
+ initialInput,
+ redirectTo,
+ redirectToOrigin,
+ getAppNameFromId = () => undefined,
+ isSaveable = true,
+ lastKnownDoc: initLastKnowDoc,
+ lensServices,
+}: SaveModalContainerProps) {
+ const [lastKnownDoc, setLastKnownDoc] = useState(initLastKnowDoc);
+
+ const {
+ attributeService,
+ notifications,
+ data,
+ chrome,
+ savedObjectsTagging,
+ application,
+ dashboardFeatureFlag,
+ } = lensServices;
+
+ useEffect(() => {
+ setLastKnownDoc(initLastKnowDoc);
+ }, [initLastKnowDoc]);
+
+ useEffect(() => {
+ async function loadLastKnownDoc() {
+ if (initialInput && isVisible) {
+ getLastKnownDoc({
+ data,
+ initialInput,
+ chrome,
+ notifications,
+ attributeService,
+ }).then((result) => {
+ if (result) setLastKnownDoc(result.doc);
+ });
+ }
+ }
+
+ loadLastKnownDoc();
+ }, [chrome, data, initialInput, notifications, attributeService, isVisible]);
+
+ const tagsIds =
+ persistedDoc && savedObjectsTagging
+ ? savedObjectsTagging.ui.getTagIdsFromReferences(persistedDoc.references)
+ : [];
+
+ const runLensSave = (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
+ if (runSave) {
+ // inside lens, we use the function that's passed to it
+ runSave(saveProps, options);
+ } else {
+ if (attributeService && lastKnownDoc) {
+ runSaveLensVisualization(
+ {
+ ...lensServices,
+ lastKnownDoc,
+ initialInput,
+ attributeService,
+ redirectTo,
+ redirectToOrigin,
+ getIsByValueMode: () => false,
+ onAppLeave: () => {},
+ },
+ saveProps,
+ options
+ ).then(() => {
+ onSave?.();
+ onClose();
+ });
+ }
+ }
+ };
+
+ const savingToLibraryPermitted = Boolean(isSaveable && application.capabilities.visualize.save);
+
+ return (
+ {
+ runLensSave(saveProps, options);
+ }}
+ onClose={onClose}
+ getAppNameFromId={getAppNameFromId}
+ lastKnownDoc={lastKnownDoc}
+ returnToOriginSwitchLabel={returnToOriginSwitchLabel}
+ />
+ );
+}
+
+const redirectToDashboard = ({
+ embeddableInput,
+ dashboardFeatureFlag,
+ dashboardId,
+ stateTransfer,
+}: {
+ embeddableInput: LensEmbeddableInput;
+ dashboardId: string;
+ dashboardFeatureFlag: LensAppServices['dashboardFeatureFlag'];
+ stateTransfer: LensAppServices['stateTransfer'];
+}) => {
+ if (!dashboardFeatureFlag.allowByValueEmbeddables) {
+ throw new Error('redirectToDashboard called with by-value embeddables disabled');
+ }
+
+ const state = {
+ input: embeddableInput,
+ type: LENS_EMBEDDABLE_TYPE,
+ };
+
+ const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
+ stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
+ state,
+ path,
+ });
+};
+
+export const runSaveLensVisualization = async (
+ props: {
+ lastKnownDoc?: Document;
+ getIsByValueMode: () => boolean;
+ persistedDoc?: Document;
+ } & ExtraProps &
+ LensAppServices,
+ saveProps: SaveProps,
+ options: { saveToLibrary: boolean }
+): Promise | undefined> => {
+ if (!props.lastKnownDoc) {
+ return;
+ }
+
+ const {
+ chrome,
+ initialInput,
+ lastKnownDoc,
+ persistedDoc,
+ savedObjectsClient,
+ overlays,
+ notifications,
+ stateTransfer,
+ attributeService,
+ savedObjectsTagging,
+ getIsByValueMode,
+ redirectToOrigin,
+ onAppLeave,
+ redirectTo,
+ dashboardFeatureFlag,
+ } = props;
+
+ const tagsIds =
+ persistedDoc && savedObjectsTagging
+ ? savedObjectsTagging.ui.getTagIdsFromReferences(persistedDoc.references)
+ : [];
+
+ let references = lastKnownDoc.references;
+ if (savedObjectsTagging) {
+ references = savedObjectsTagging.ui.updateTagsReferences(
+ references,
+ saveProps.newTags || tagsIds
+ );
+ }
+
+ const docToSave = {
+ ...getLastKnownDocWithoutPinnedFilters(lastKnownDoc)!,
+ description: saveProps.newDescription,
+ title: saveProps.newTitle,
+ references,
+ };
+
+ // Required to serialize filters in by value mode until
+ // https://github.com/elastic/kibana/issues/77588 is fixed
+ if (getIsByValueMode()) {
+ docToSave.state.filters.forEach((filter) => {
+ if (typeof filter.meta.value === 'function') {
+ delete filter.meta.value;
+ }
+ });
+ }
+
+ const originalInput = saveProps.newCopyOnSave ? undefined : initialInput;
+ const originalSavedObjectId = (originalInput as LensByReferenceInput)?.savedObjectId;
+ if (options.saveToLibrary) {
+ try {
+ await checkForDuplicateTitle(
+ {
+ id: originalSavedObjectId,
+ title: docToSave.title,
+ copyOnSave: saveProps.newCopyOnSave,
+ lastSavedTitle: lastKnownDoc.title,
+ getEsType: () => 'lens',
+ getDisplayName: () =>
+ i18n.translate('xpack.lens.app.saveModalType', {
+ defaultMessage: 'Lens visualization',
+ }),
+ },
+ saveProps.isTitleDuplicateConfirmed,
+ saveProps.onTitleDuplicate,
+ {
+ savedObjectsClient,
+ overlays,
+ }
+ );
+ } catch (e) {
+ // ignore duplicate title failure, user notified in save modal
+ throw e;
+ }
+ }
+ try {
+ const newInput = (await attributeService.wrapAttributes(
+ docToSave,
+ options.saveToLibrary,
+ originalInput
+ )) as LensEmbeddableInput;
+
+ if (saveProps.returnToOrigin && redirectToOrigin) {
+ // disabling the validation on app leave because the document has been saved.
+ onAppLeave?.((actions) => {
+ return actions.default();
+ });
+ redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave });
+ return;
+ } else if (saveProps.dashboardId) {
+ // disabling the validation on app leave because the document has been saved.
+ onAppLeave?.((actions) => {
+ return actions.default();
+ });
+ redirectToDashboard({
+ embeddableInput: newInput,
+ dashboardId: saveProps.dashboardId,
+ stateTransfer,
+ dashboardFeatureFlag,
+ });
+ return;
+ }
+
+ notifications.toasts.addSuccess(
+ i18n.translate('xpack.lens.app.saveVisualization.successNotificationText', {
+ defaultMessage: `Saved '{visTitle}'`,
+ values: {
+ visTitle: docToSave.title,
+ },
+ })
+ );
+
+ if (
+ attributeService.inputIsRefType(newInput) &&
+ newInput.savedObjectId !== originalSavedObjectId
+ ) {
+ chrome.recentlyAccessed.add(
+ getFullPath(newInput.savedObjectId),
+ docToSave.title,
+ newInput.savedObjectId
+ );
+
+ // remove editor state so the connection is still broken after reload
+ stateTransfer.clearEditorState?.(APP_ID);
+
+ redirectTo?.(newInput.savedObjectId);
+ return { isLinkedToOriginatingApp: false };
+ }
+
+ const newDoc = {
+ ...docToSave,
+ ...newInput,
+ };
+
+ return { persistedDoc: newDoc, lastKnownDoc: newDoc, isLinkedToOriginatingApp: false };
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.dir(e);
+ trackUiEvent('save_failed');
+ throw e;
+ }
+};
+
+export function getLastKnownDocWithoutPinnedFilters(doc?: Document) {
+ if (!doc) return undefined;
+ const [pinnedFilters, appFilters] = partition(
+ injectFilterReferences(doc.state?.filters || [], doc.references),
+ esFilters.isFilterPinned
+ );
+ return pinnedFilters?.length
+ ? {
+ ...doc,
+ state: {
+ ...doc.state,
+ filters: appFilters,
+ },
+ }
+ : doc;
+}
+
+export const getLastKnownDoc = async ({
+ initialInput,
+ attributeService,
+ data,
+ notifications,
+ chrome,
+}: {
+ initialInput: LensEmbeddableInput;
+ attributeService: LensAttributeService;
+ data: DataPublicPluginStart;
+ notifications: NotificationsStart;
+ chrome: ChromeStart;
+}): Promise<{ doc: Document; indexPatterns: IndexPattern[] } | undefined> => {
+ let doc: Document;
+
+ try {
+ const attributes = await attributeService.unwrapAttributes(initialInput);
+
+ doc = {
+ ...initialInput,
+ ...attributes,
+ type: LENS_EMBEDDABLE_TYPE,
+ };
+
+ if (attributeService.inputIsRefType(initialInput)) {
+ chrome.recentlyAccessed.add(
+ getFullPath(initialInput.savedObjectId),
+ attributes.title,
+ initialInput.savedObjectId
+ );
+ }
+ const indexPatternIds = uniq(
+ doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
+ );
+ const { indexPatterns } = await getAllIndexPatterns(indexPatternIds, data.indexPatterns);
+
+ // Don't overwrite any pinned filters
+ data.query.filterManager.setAppFilters(
+ injectFilterReferences(doc.state.filters, doc.references)
+ );
+ return {
+ doc,
+ indexPatterns,
+ };
+ } catch (e) {
+ notifications.toasts.addDanger(
+ i18n.translate('xpack.lens.app.docLoadingError', {
+ defaultMessage: 'Error loading saved document',
+ })
+ );
+ }
+};
+
+// eslint-disable-next-line import/no-default-export
+export default SaveModalContainer;
diff --git a/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx b/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx
new file mode 100644
index 00000000000000..f1a537fe65928a
--- /dev/null
+++ b/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx
@@ -0,0 +1,66 @@
+/*
+ * 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, { Suspense, useEffect, useState } from 'react';
+
+import { EuiLoadingSpinner, EuiOverlayMask } from '@elastic/eui';
+import { CoreStart } from 'kibana/public';
+import type { SaveModalContainerProps } from '../save_modal_container';
+import type { LensAttributeService } from '../../lens_attribute_service';
+import type { LensPluginStartDependencies } from '../../plugin';
+import type { LensAppServices } from '../types';
+const SaveModal = React.lazy(() => import('../save_modal_container'));
+
+function LoadingSpinnerWithOverlay() {
+ return (
+
+
+
+ );
+}
+
+const LensSavedModalLazy = (props: SaveModalContainerProps) => {
+ return (
+ }>
+
+
+ );
+};
+
+export function getSaveModalComponent(
+ coreStart: CoreStart,
+ startDependencies: LensPluginStartDependencies,
+ attributeService: () => Promise
+) {
+ return (props: Omit) => {
+ const [lensServices, setLensServices] = useState();
+
+ useEffect(() => {
+ async function loadLensService() {
+ const { getLensServices } = await import('../../async_services');
+ const lensServicesT = await getLensServices(coreStart, startDependencies, attributeService);
+
+ setLensServices(lensServicesT);
+ }
+ loadLensService();
+ }, []);
+
+ if (!lensServices) {
+ return ;
+ }
+
+ const { ContextProvider: PresentationUtilContext } = lensServices.presentationUtil;
+
+ return (
+
+
+
+
+
+ );
+ };
+}
diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts
index 72850552723f33..d1e2d1cbdfc637 100644
--- a/x-pack/plugins/lens/public/app_plugin/types.ts
+++ b/x-pack/plugins/lens/public/app_plugin/types.ts
@@ -18,6 +18,7 @@ import {
SavedObjectsStart,
} from '../../../../../src/core/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
+import { DashboardStart } from '../../../../../src/plugins/dashboard/public';
import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddable';
import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';
import { LensAttributeService } from '../lens_attribute_service';
@@ -33,6 +34,7 @@ import {
EmbeddableStateTransfer,
} from '../../../../../src/plugins/embeddable/public';
import { EditorFrameInstance } from '../types';
+import { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public';
export interface RedirectToOriginProps {
input?: LensEmbeddableInput;
isCopied?: boolean;
@@ -45,7 +47,6 @@ export interface LensAppProps {
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
redirectTo: (savedObjectId?: string) => void;
redirectToOrigin?: (props?: RedirectToOriginProps) => void;
- redirectToDashboard?: (input: LensEmbeddableInput, dashboardId: string) => void;
// The initial input passed in by the container when editing. Can be either by reference or by value.
initialInput?: LensEmbeddableInput;
@@ -91,6 +92,7 @@ export interface LensAppServices {
chrome: ChromeStart;
overlays: OverlayStart;
storage: IStorageWrapper;
+ dashboard: DashboardStart;
data: DataPublicPluginStart;
uiSettings: IUiSettingsClient;
application: ApplicationStart;
@@ -101,6 +103,7 @@ export interface LensAppServices {
savedObjectsClient: SavedObjectsStart['client'];
savedObjectsTagging?: SavedObjectTaggingPluginStart;
getOriginatingAppName: () => string | undefined;
+ presentationUtil: PresentationUtilPluginStart;
// Temporarily required until the 'by value' paradigm is default.
dashboardFeatureFlag: DashboardFeatureFlagConfig;
diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts
index b0ecc412c357ff..e7be2959556154 100644
--- a/x-pack/plugins/lens/public/async_services.ts
+++ b/x-pack/plugins/lens/public/async_services.ts
@@ -27,3 +27,4 @@ export * from './editor_frame_service/embeddable';
export * from './app_plugin/mounter';
export * from './lens_attribute_service';
export * from './lens_ui_telemetry';
+export * from './app_plugin/save_modal_container';
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
index a4be46f61990b6..7e3c8c3342e4ca 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
@@ -126,7 +126,11 @@ exports[`DatatableComponent it renders actions column when there are row actions
},
},
"cellActions": undefined,
- "display": "a",
+ "display":
+ a
+
,
"displayAsText": "a",
"id": "a",
},
@@ -163,7 +167,11 @@ exports[`DatatableComponent it renders actions column when there are row actions
},
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
@@ -200,7 +208,11 @@ exports[`DatatableComponent it renders actions column when there are row actions
},
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
@@ -360,7 +372,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "a",
+ "display":
+ a
+
,
"displayAsText": "a",
"id": "a",
},
@@ -397,7 +413,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
@@ -434,7 +454,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
@@ -463,7 +487,7 @@ exports[`DatatableComponent it renders the title and value 1`] = `
`;
-exports[`DatatableComponent it should not render actions on header when it is in read only mode 1`] = `
+exports[`DatatableComponent it should render hide and reset actions on header even when it is in read only mode 1`] = `
+ a
+ ,
"displayAsText": "a",
"id": "a",
},
Object {
"actions": Object {
- "additional": Array [],
+ "additional": Array [
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableResetWidth",
+ "iconType": "empty",
+ "isDisabled": true,
+ "label": "Reset width",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ ],
"showHide": false,
"showMoveLeft": false,
"showMoveRight": false,
@@ -580,13 +646,36 @@ exports[`DatatableComponent it should not render actions on header when it is in
"showSortDesc": false,
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
Object {
"actions": Object {
- "additional": Array [],
+ "additional": Array [
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableResetWidth",
+ "iconType": "empty",
+ "isDisabled": true,
+ "label": "Reset width",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ ],
"showHide": false,
"showMoveLeft": false,
"showMoveRight": false,
@@ -594,7 +683,11 @@ exports[`DatatableComponent it should not render actions on header when it is in
"showSortDesc": false,
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
index 5c53d40f999b77..4372e2cd9e9640 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
@@ -35,7 +35,8 @@ export const createGridColumns = (
visibleColumns: string[],
formatFactory: FormatFactory,
onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void,
- onColumnHide: (eventData: { columnId: string }) => void
+ onColumnHide: (eventData: { columnId: string }) => void,
+ alignments: Record
) => {
const columnsReverseLookup = table.columns.reduce<
Record
@@ -151,31 +152,33 @@ export const createGridColumns = (
const additionalActions: EuiListGroupItemProps[] = [];
- if (!isReadOnly) {
+ additionalActions.push({
+ color: 'text',
+ size: 'xs',
+ onClick: () => onColumnResize({ columnId: originalColumnId || field, width: undefined }),
+ iconType: 'empty',
+ label: i18n.translate('xpack.lens.table.resize.reset', {
+ defaultMessage: 'Reset width',
+ }),
+ 'data-test-subj': 'lensDatatableResetWidth',
+ isDisabled: initialWidth == null,
+ });
+ if (!isTransposed) {
additionalActions.push({
color: 'text',
size: 'xs',
- onClick: () => onColumnResize({ columnId: originalColumnId || field, width: undefined }),
- iconType: 'empty',
- label: i18n.translate('xpack.lens.table.resize.reset', {
- defaultMessage: 'Reset width',
+ onClick: () => onColumnHide({ columnId: originalColumnId || field }),
+ iconType: 'eyeClosed',
+ label: i18n.translate('xpack.lens.table.hide.hideLabel', {
+ defaultMessage: 'Hide',
}),
- 'data-test-subj': 'lensDatatableResetWidth',
- isDisabled: initialWidth == null,
+ 'data-test-subj': 'lensDatatableHide',
+ isDisabled: !isHidden && visibleColumns.length <= 1,
});
- if (!isTransposed) {
- additionalActions.push({
- color: 'text',
- size: 'xs',
- onClick: () => onColumnHide({ columnId: originalColumnId || field }),
- iconType: 'eyeClosed',
- label: i18n.translate('xpack.lens.table.hide.hideLabel', {
- defaultMessage: 'Hide',
- }),
- 'data-test-subj': 'lensDatatableHide',
- isDisabled: !isHidden && visibleColumns.length <= 1,
- });
- } else if (columnArgs?.bucketValues) {
+ }
+
+ if (!isReadOnly) {
+ if (isTransposed && columnArgs?.bucketValues) {
const bucketValues = columnArgs?.bucketValues;
additionalActions.push({
color: 'text',
@@ -200,11 +203,13 @@ export const createGridColumns = (
});
}
}
+ const currentAlignment = alignments && alignments[field];
+ const alignmentClassName = `lnsTableCell--${currentAlignment}`;
const columnDefinition: EuiDataGridColumn = {
id: field,
cellActions,
- display: name,
+ display: {name}
,
displayAsText: name,
actions: {
showHide: false,
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
index 49003af28f3f1b..3479a9e964d53e 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui';
-import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../types';
+import { FramePublicAPI, Operation, VisualizationDimensionEditorProps } from '../../types';
import { DatatableVisualizationState } from '../visualization';
import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks';
import { mountWithIntl } from '@kbn/test/jest';
@@ -213,6 +213,22 @@ describe('data table dimension editor', () => {
expect(instance.find(PalettePanelContainer).exists()).toBe(true);
});
+ it('should not show the dynamic coloring option for a bucketed operation', () => {
+ frame.activeData!.first.columns[0].meta.type = 'number';
+ frame.datasourceLayers.first.getOperationForColumnId = jest.fn(
+ () => ({ isBucketed: true } as Operation)
+ );
+ state.columns[0].colorMode = 'cell';
+ const instance = mountWithIntl( );
+
+ expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_groups"]').exists()).toBe(
+ false
+ );
+ expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_palette"]').exists()).toBe(
+ false
+ );
+ });
+
it('should show the summary field for non numeric columns', () => {
const instance = mountWithIntl( );
expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_function"]').exists()).toBe(
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
index 6c39a04ae1504c..cf15df07ec72ca 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
@@ -104,6 +104,11 @@ export function TableDimensionEditor(
currentData
);
+ const datasource = frame.datasourceLayers[state.layerId];
+ const showDynamicColoringFeature = Boolean(
+ isNumeric && !datasource?.getOperationForColumnId(accessor)?.isBucketed
+ );
+
const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length;
const hasTransposedColumn = state.columns.some(({ isTransposed }) => isTransposed);
@@ -260,7 +265,7 @@ export function TableDimensionEditor(
)}
>
)}
- {isNumeric && (
+ {showDynamicColoringFeature && (
<>
{
).toMatchSnapshot();
});
- test('it should not render actions on header when it is in read only mode', () => {
+ test('it should render hide and reset actions on header even when it is in read only mode', () => {
const { data, args } = sampleArgs();
expect(
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
index cd990149fdaf55..b48cb94563d3b0 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
@@ -180,34 +180,6 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
[onEditAction, setColumnConfig, columnConfig]
);
- const columns: EuiDataGridColumn[] = useMemo(
- () =>
- createGridColumns(
- bucketColumns,
- firstLocalTable,
- handleFilterClick,
- handleTransposedColumnClick,
- isReadOnlySorted,
- columnConfig,
- visibleColumns,
- formatFactory,
- onColumnResize,
- onColumnHide
- ),
- [
- bucketColumns,
- firstLocalTable,
- handleFilterClick,
- handleTransposedColumnClick,
- isReadOnlySorted,
- columnConfig,
- visibleColumns,
- formatFactory,
- onColumnResize,
- onColumnHide,
- ]
- );
-
const isNumericMap: Record = useMemo(() => {
const numericMap: Record = {};
for (const column of firstLocalTable.columns) {
@@ -237,6 +209,36 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
);
}, [firstTable, isNumericMap, columnConfig]);
+ const columns: EuiDataGridColumn[] = useMemo(
+ () =>
+ createGridColumns(
+ bucketColumns,
+ firstLocalTable,
+ handleFilterClick,
+ handleTransposedColumnClick,
+ isReadOnlySorted,
+ columnConfig,
+ visibleColumns,
+ formatFactory,
+ onColumnResize,
+ onColumnHide,
+ alignments
+ ),
+ [
+ bucketColumns,
+ firstLocalTable,
+ handleFilterClick,
+ handleTransposedColumnClick,
+ isReadOnlySorted,
+ columnConfig,
+ visibleColumns,
+ formatFactory,
+ onColumnResize,
+ onColumnHide,
+ alignments,
+ ]
+ );
+
const trailingControlColumns: EuiDataGridControlColumn[] = useMemo(() => {
if (!hasAtLeastOneRowClickAction || !onRowContextMenuClick) {
return [];
@@ -278,9 +280,13 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
[formatters, columnConfig, props.uiSettings]
);
- const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns: () => {} }), [
- visibleColumns,
- ]);
+ const columnVisibility = useMemo(
+ () => ({
+ visibleColumns,
+ setVisibleColumns: () => {},
+ }),
+ [visibleColumns]
+ );
const sorting = useMemo(
() => createGridSortingConfig(sortBy, sortDirection as LensGridDirection, onEditAction),
diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts
index 0fdd3bf4262325..98e0198b9d0faa 100644
--- a/x-pack/plugins/lens/public/index.ts
+++ b/x-pack/plugins/lens/public/index.ts
@@ -55,6 +55,8 @@ export type {
DerivativeIndexPatternColumn,
MovingAverageIndexPatternColumn,
} from './indexpattern_datasource/types';
+export type { LensEmbeddableInput } from './editor_frame_service/embeddable';
+
export { LensPublicStart } from './plugin';
export const plugin = () => new LensPlugin();
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index ba3bee415f3f49..1ae2f4421a0bc9 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -1797,12 +1797,14 @@ describe('state_helpers', () => {
it('should promote the inner references when switching away from reference to field-based operation (case a2)', () => {
const expectedCol = {
- label: 'Count of records',
+ label: 'Count of records -3h',
dataType: 'number' as const,
isBucketed: false,
operationType: 'count' as const,
sourceField: 'Records',
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
};
const layer: IndexPatternLayer = {
indexPatternId: '1',
@@ -1817,6 +1819,8 @@ describe('state_helpers', () => {
// @ts-expect-error not a valid type
operationType: 'testReference',
references: ['col1'],
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
},
},
};
@@ -1845,6 +1849,8 @@ describe('state_helpers', () => {
isBucketed: false,
sourceField: 'bytes',
operationType: 'average' as const,
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
};
const layer: IndexPatternLayer = {
@@ -1858,6 +1864,8 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'differences',
references: ['metric'],
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
},
},
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index b650a2818b2d48..4e3bcec4b6ca20 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -414,11 +414,21 @@ export function replaceColumn({
indexPattern,
});
+ const column = copyCustomLabel({ ...referenceColumn }, previousColumn);
+ // do not forget to move over also any filter/shift/anything (if compatible)
+ // from the reference definition to the new operation.
+ if (referencedOperation.filterable) {
+ column.filter = (previousColumn as ReferenceBasedIndexPatternColumn).filter;
+ }
+ if (referencedOperation.shiftable) {
+ column.timeShift = (previousColumn as ReferenceBasedIndexPatternColumn).timeShift;
+ }
+
tempLayer = {
...tempLayer,
columns: {
...tempLayer.columns,
- [columnId]: copyCustomLabel({ ...referenceColumn }, previousColumn),
+ [columnId]: column,
},
};
return updateDefaultLabels(
diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx
index 07935bb2f241b8..b35353b98a5859 100644
--- a/x-pack/plugins/lens/public/mocks.tsx
+++ b/x-pack/plugins/lens/public/mocks.tsx
@@ -21,6 +21,7 @@ import { navigationPluginMock } from '../../../../src/plugins/navigation/public/
import { LensAppServices } from './app_plugin/types';
import { DOC_TYPE } from '../common';
import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public';
+import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks';
import {
LensByValueInput,
LensSavedObjectAttributes,
@@ -35,6 +36,7 @@ import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/publ
import { makeConfigureStore, getPreloadedState, LensAppState } from './state_management/index';
import { getResolvedDateRange } from './utils';
+import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks';
export type Start = jest.Mocked;
@@ -43,6 +45,9 @@ const createStartContract = (): Start => {
EmbeddableComponent: jest.fn(() => {
return Lens Embeddable Component ;
}),
+ SaveModalComponent: jest.fn(() => {
+ return Lens Save Modal Component ;
+ }),
canUseEditor: jest.fn(() => true),
navigateToPrefilledEditor: jest.fn(),
getXyVisTypes: jest.fn().mockReturnValue(new Promise((resolve) => resolve(visualizationTypes))),
@@ -228,6 +233,8 @@ export function makeDefaultServices(
navigation: navigationStartMock,
notifications: core.notifications,
attributeService: makeAttributeService(),
+ dashboard: dashboardPluginMock.createStartContract(),
+ presentationUtil: presentationUtilPluginMock.createStartContract(core),
savedObjectsClient: core.savedObjects.client,
dashboardFeatureFlag: { allowByValueEmbeddables: false },
stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer,
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts
index fe225dba6f2561..6d90691e2173ae 100644
--- a/x-pack/plugins/lens/public/plugin.ts
+++ b/x-pack/plugins/lens/public/plugin.ts
@@ -55,6 +55,8 @@ import {
getEmbeddableComponent,
} from './editor_frame_service/embeddable/embeddable_component';
import { HeatmapVisualization } from './heatmap_visualization';
+import { getSaveModalComponent } from './app_plugin/shared/saved_modal_lazy';
+import { SaveModalContainerProps } from './app_plugin/save_modal_container';
export interface LensPluginSetupDependencies {
urlForwarding: UrlForwardingSetup;
@@ -91,6 +93,15 @@ export interface LensPublicStart {
* @experimental
*/
EmbeddableComponent: React.ComponentType;
+ /**
+ * React component which can be used to embed a Lens Visualization Save Modal Component.
+ * See `x-pack/examples/embedded_lens_example` for exemplary usage.
+ *
+ * This API might undergo breaking changes even in minor versions.
+ *
+ * @experimental
+ */
+ SaveModalComponent: React.ComponentType>;
/**
* Method which navigates to the Lens editor, loading the state specified by the `input` parameter.
* See `x-pack/examples/embedded_lens_example` for exemplary usage.
@@ -185,11 +196,6 @@ export class LensPlugin {
visualizations.registerAlias(getLensAliasConfig());
- const getByValueFeatureFlag = async () => {
- const [, deps] = await core.getStartServices();
- return deps.dashboard.dashboardFeatureFlagConfig;
- };
-
const getPresentationUtilContext = async () => {
const [, deps] = await core.getStartServices();
const { ContextProvider } = deps.presentationUtil;
@@ -214,7 +220,6 @@ export class LensPlugin {
return mountApp(core, params, {
createEditorFrame: this.createEditorFrame!,
attributeService: this.attributeService!,
- getByValueFeatureFlag,
getPresentationUtilContext,
});
},
@@ -251,6 +256,7 @@ export class LensPlugin {
return {
EmbeddableComponent: getEmbeddableComponent(startDependencies.embeddable),
+ SaveModalComponent: getSaveModalComponent(core, startDependencies, this.attributeService!),
navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => {
// for openInNewTab, we set the time range in url via getEditPath below
if (input.timeRange && !openInNewTab) {
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
index 8033f8d187fd51..edadc20a595ce8 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
@@ -374,11 +374,19 @@ export function clamp(val: number, min: number, max: number): number {
export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent {
const width = bounds.maxLon - bounds.minLon;
const height = bounds.maxLat - bounds.minLat;
+
+ const newMinLon = bounds.minLon - width * scaleFactor;
+ const nexMaxLon = bounds.maxLon + width * scaleFactor;
+
+ const lonDelta = nexMaxLon - newMinLon;
+ const left = lonDelta > 360 ? -180 : newMinLon;
+ const right = lonDelta > 360 ? 180 : nexMaxLon;
+
return {
- minLon: bounds.minLon - width * scaleFactor,
- minLat: bounds.minLat - height * scaleFactor,
- maxLon: bounds.maxLon + width * scaleFactor,
- maxLat: bounds.maxLat + height * scaleFactor,
+ minLon: left,
+ minLat: clampToLatBounds(bounds.minLat - height * scaleFactor),
+ maxLon: right,
+ maxLat: clampToLonBounds(bounds.maxLat + height * scaleFactor),
};
}
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
index d828aca4a1a001..7aef32dfb4f8a5 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
@@ -23,11 +23,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -89,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-89, 39],
- bottom_right: [-83, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-89, 39],
+ bottom_right: [-83, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -39,11 +59,31 @@ describe('createExtentFilter', () => {
minLat: -100,
minLon: -190,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-180, 89],
- bottom_right: [180, -89],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-180, 89],
+ bottom_right: [180, -89],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -55,11 +95,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: 100,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [100, 39],
- bottom_right: [-160, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [100, 39],
+ bottom_right: [-160, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -71,11 +131,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -200,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [160, 39],
- bottom_right: [-100, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [160, 39],
+ bottom_right: [-100, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -87,11 +167,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -191,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-180, 39],
- bottom_right: [180, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-180, 39],
+ bottom_right: [180, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -184,23 +284,36 @@ describe('createSpatialFilterWithGeometry', () => {
negate: false,
type: 'spatial_filter',
},
- geo_shape: {
- 'geo.coordinates': {
- relation: 'INTERSECTS',
- shape: {
- coordinates: [
- [
- [-101.21639, 48.1413],
- [-101.21639, 41.84905],
- [-90.95149, 41.84905],
- [-90.95149, 48.1413],
- [-101.21639, 48.1413],
- ],
- ],
- type: 'Polygon',
- },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'geo.coordinates',
+ },
+ },
+ {
+ geo_shape: {
+ 'geo.coordinates': {
+ relation: 'INTERSECTS',
+ shape: {
+ coordinates: [
+ [
+ [-101.21639, 48.1413],
+ [-101.21639, 41.84905],
+ [-90.95149, 41.84905],
+ [-90.95149, 48.1413],
+ [-101.21639, 48.1413],
+ ],
+ ],
+ type: 'Polygon',
+ },
+ },
+ ignore_unmapped: true,
+ },
+ },
+ ],
},
- ignore_unmapped: true,
},
});
});
@@ -318,9 +431,22 @@ describe('createDistanceFilterWithMeta', () => {
negate: false,
type: 'spatial_filter',
},
- geo_distance: {
- distance: '1000km',
- 'geo.coordinates': [120, 30],
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'geo.coordinates',
+ },
+ },
+ {
+ geo_distance: {
+ distance: '1000km',
+ 'geo.coordinates': [120, 30],
+ },
+ },
+ ],
+ },
},
});
});
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
index 70df9e9646f50f..9a2b2c21136dfb 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
@@ -31,13 +31,23 @@ function createMultiGeoFieldFilter(
}
if (geoFieldNames.length === 1) {
- const geoFilter = createGeoFilter(geoFieldNames[0]);
return {
meta: {
...meta,
key: geoFieldNames[0],
},
- ...geoFilter,
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: geoFieldNames[0],
+ },
+ },
+ createGeoFilter(geoFieldNames[0]),
+ ],
+ },
+ },
};
}
@@ -201,8 +211,9 @@ export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] {
}
} else {
const geoFieldName = filter.meta.key;
- if (geoFieldName) {
- geometry = extractGeometryFromFilter(geoFieldName, filter);
+ const spatialClause = filter?.query?.bool?.must?.[1];
+ if (geoFieldName && spatialClause) {
+ geometry = extractGeometryFromFilter(geoFieldName, spatialClause);
}
}
diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js
index 77ce23594447f8..fa69dad6167478 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.test.js
+++ b/x-pack/plugins/maps/public/actions/map_actions.test.js
@@ -33,7 +33,9 @@ describe('map_actions', () => {
describe('store mapState is empty', () => {
beforeEach(() => {
require('../selectors/map_selectors').getDataFilters = () => {
- return {};
+ return {
+ zoom: 5,
+ };
};
require('../selectors/map_selectors').getLayerList = () => {
@@ -61,11 +63,13 @@ describe('map_actions', () => {
minLat: 5,
minLon: 95,
},
+ zoom: 5,
});
await action(dispatchMock, getStoreMock);
expect(dispatchMock).toHaveBeenCalledWith({
mapState: {
+ zoom: 5,
extent: {
maxLat: 10,
maxLon: 100,
@@ -73,10 +77,10 @@ describe('map_actions', () => {
minLon: 95,
},
buffer: {
- maxLat: 12.5,
- maxLon: 102.5,
- minLat: 2.5,
- minLon: 92.5,
+ maxLat: 21.94305,
+ maxLon: 112.5,
+ minLat: 0,
+ minLon: 90,
},
},
type: 'MAP_EXTENT_CHANGED',
@@ -154,10 +158,10 @@ describe('map_actions', () => {
minLon: 85,
},
buffer: {
- maxLat: 7.5,
- maxLon: 92.5,
- minLat: -2.5,
- minLon: 82.5,
+ maxLat: 7.71099,
+ maxLon: 92.8125,
+ minLat: -2.81137,
+ minLon: 82.26563,
},
},
type: 'MAP_EXTENT_CHANGED',
@@ -186,10 +190,10 @@ describe('map_actions', () => {
minLon: 96,
},
buffer: {
- maxLat: 13.5,
- maxLon: 103.5,
- minLat: 3.5,
- minLon: 93.5,
+ maxLat: 13.58192,
+ maxLon: 103.53516,
+ minLat: 3.33795,
+ minLon: 93.33984,
},
},
type: 'MAP_EXTENT_CHANGED',
diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts
index 42ce96d102d7e0..3cdc5bf05ccee7 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.ts
+++ b/x-pack/plugins/maps/public/actions/map_actions.ts
@@ -56,6 +56,7 @@ import {
import { INITIAL_LOCATION } from '../../common/constants';
import { scaleBounds } from '../../common/elasticsearch_util';
import { cleanTooltipStateForLayer } from './tooltip_actions';
+import { expandToTileBoundaries } from '../../common/geo_tile_utils';
export interface MapExtentState {
zoom: number;
@@ -158,7 +159,9 @@ export function mapExtentChanged(mapExtentState: MapExtentState) {
}
if (!doesBufferContainExtent || currentZoom !== newZoom) {
- dataFilters.buffer = scaleBounds(extent, 0.5);
+ const expandedExtent = scaleBounds(extent, 0.5);
+ // snap to the smallest tile-bounds, to avoid jitter in the bounds
+ dataFilters.buffer = expandToTileBoundaries(expandedExtent, Math.ceil(newZoom));
}
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
index ad413419a289b1..1043ed87783040 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
@@ -205,13 +205,31 @@ describe('ESGeoGridSource', () => {
expect(getProperty('query')).toEqual(undefined);
expect(getProperty('filter')).toEqual([
{
- geo_bounding_box: { bar: { bottom_right: [180, -82.67628], top_left: [-180, 82.67628] } },
meta: {
alias: null,
disabled: false,
key: 'bar',
negate: false,
},
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'bar',
+ },
+ },
+ {
+ geo_bounding_box: {
+ bar: {
+ top_left: [-180, 82.67628],
+ bottom_right: [180, -82.67628],
+ },
+ },
+ },
+ ],
+ },
+ },
},
]);
expect(getProperty('aggs')).toEqual({
@@ -262,20 +280,33 @@ describe('ESGeoGridSource', () => {
});
describe('ITiledSingleLayerVectorSource', () => {
+ const mvtGeogridSource = new ESGeoGridSource(
+ {
+ id: 'foobar',
+ indexPatternId: 'fooIp',
+ geoField: geoFieldName,
+ metrics: [],
+ resolution: GRID_RESOLUTION.SUPER_FINE,
+ type: SOURCE_TYPES.ES_GEO_GRID,
+ requestType: RENDER_AS.HEATMAP,
+ },
+ {}
+ );
+
it('getLayerName', () => {
- expect(geogridSource.getLayerName()).toBe('source_layer');
+ expect(mvtGeogridSource.getLayerName()).toBe('source_layer');
});
it('getMinZoom', () => {
- expect(geogridSource.getMinZoom()).toBe(0);
+ expect(mvtGeogridSource.getMinZoom()).toBe(0);
});
it('getMaxZoom', () => {
- expect(geogridSource.getMaxZoom()).toBe(24);
+ expect(mvtGeogridSource.getMaxZoom()).toBe(24);
});
it('getUrlTemplateWithMeta', async () => {
- const urlTemplateWithMeta = await geogridSource.getUrlTemplateWithMeta(
+ const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta(
vectorSourceRequestMeta
);
@@ -283,19 +314,19 @@ describe('ESGeoGridSource', () => {
expect(urlTemplateWithMeta.minSourceZoom).toBe(0);
expect(urlTemplateWithMeta.maxSourceZoom).toBe(24);
expect(urlTemplateWithMeta.urlTemplate).toEqual(
- "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
+ "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
);
});
it('should include searchSourceId in urlTemplateWithMeta', async () => {
- const urlTemplateWithMeta = await geogridSource.getUrlTemplateWithMeta({
+ const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta({
...vectorSourceRequestMeta,
searchSessionId: '1',
});
expect(
urlTemplateWithMeta.urlTemplate.startsWith(
- "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
+ "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1"
)
).toBe(true);
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
index 7ed24b4805997a..86dd63be4b67b8 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
@@ -142,6 +142,20 @@ export class ESPewPewSource extends AbstractESAggSource {
},
});
+ // pewpew source is often used with security solution index-pattern
+ // Some underlying indices may not contain geo fields
+ // Filter out documents without geo fields to avoid shard failures for those indices
+ searchSource.setField('filter', [
+ ...searchSource.getField('filter'),
+ // destGeoField exists ensured by buffer filter
+ // so only need additional check for sourceGeoField
+ {
+ exists: {
+ field: this._descriptor.sourceGeoField,
+ },
+ },
+ ]);
+
const esResponse = await this._runEsQuery({
requestId: this.getId(),
requestName: layerName,
diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts
index 776d316440a561..82d2162986503b 100644
--- a/x-pack/plugins/maps/server/mvt/get_tile.ts
+++ b/x-pack/plugins/maps/server/mvt/get_tile.ts
@@ -24,6 +24,7 @@ import {
} from '../../common/constants';
import {
+ createExtentFilter,
convertRegularRespToGeoJson,
hitsToGeoJson,
isTotalHitsGreaterThan,
@@ -254,21 +255,14 @@ export async function getTile({
}
function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown {
- return {
- geo_shape: {
- [geometryFieldName]: {
- shape: {
- type: 'envelope',
- // upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]]
- coordinates: [
- [tileBounds.top_left.lon, tileBounds.top_left.lat],
- [tileBounds.bottom_right.lon, tileBounds.bottom_right.lat],
- ],
- },
- relation: 'INTERSECTS',
- },
- },
+ const tileExtent = {
+ minLon: tileBounds.top_left.lon,
+ minLat: tileBounds.bottom_right.lat,
+ maxLon: tileBounds.bottom_right.lon,
+ maxLat: tileBounds.top_left.lat,
};
+ const tileExtentFilter = createExtentFilter(tileExtent, [geometryFieldName]);
+ return tileExtentFilter.query;
}
function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon {
diff --git a/x-pack/plugins/ml/server/saved_objects/checks.ts b/x-pack/plugins/ml/server/saved_objects/checks.ts
index 6b24ef000b6951..48ceaefb239394 100644
--- a/x-pack/plugins/ml/server/saved_objects/checks.ts
+++ b/x-pack/plugins/ml/server/saved_objects/checks.ts
@@ -24,7 +24,7 @@ interface JobSavedObjectStatus {
};
}
-interface JobStatus {
+export interface JobStatus {
jobId: string;
datafeedId?: string | null;
checks: {
@@ -68,7 +68,9 @@ export function checksFactory(
if (type === 'anomaly-detector') {
jobExists = adJobs.jobs.some((j) => j.job_id === jobId);
- datafeedExists = datafeeds.datafeeds.some((d) => d.job_id === jobId);
+ datafeedExists = datafeeds.datafeeds.some(
+ (d) => d.datafeed_id === datafeedId && d.job_id === jobId
+ );
} else {
jobExists = dfaJobs.data_frame_analytics.some((j) => j.id === jobId);
}
diff --git a/x-pack/plugins/ml/server/saved_objects/sync.ts b/x-pack/plugins/ml/server/saved_objects/sync.ts
index a59687b9c3cbf4..d6fa887d1d68be 100644
--- a/x-pack/plugins/ml/server/saved_objects/sync.ts
+++ b/x-pack/plugins/ml/server/saved_objects/sync.ts
@@ -14,6 +14,7 @@ import {
InitializeSavedObjectResponse,
} from '../../common/types/saved_objects';
import { checksFactory } from './checks';
+import type { JobStatus } from './checks';
import { getSavedObjectClientError } from './util';
import { Datafeed } from '../../common/types/anomaly_detection_jobs';
@@ -45,6 +46,12 @@ export function syncSavedObjectsFactory(
const tasks: Array<() => Promise> = [];
const status = await checkStatus();
+
+ const adJobsById = status.jobs['anomaly-detector'].reduce((acc, j) => {
+ acc[j.jobId] = j;
+ return acc;
+ }, {} as Record);
+
for (const job of status.jobs['anomaly-detector']) {
if (job.checks.savedObjectExits === false) {
if (simulate === true) {
@@ -141,8 +148,16 @@ export function syncSavedObjectsFactory(
}
for (const job of status.savedObjects['anomaly-detector']) {
- if (job.checks.datafeedExists === true && job.datafeedId === null) {
+ if (
+ (job.checks.datafeedExists === true && job.datafeedId === null) ||
+ (job.checks.datafeedExists === false &&
+ job.datafeedId === null &&
+ job.checks.datafeedExists === false &&
+ adJobsById[job.jobId] &&
+ adJobsById[job.jobId].datafeedId !== job.datafeedId)
+ ) {
// add datafeed id for jobs where the datafeed exists but the id is missing from the saved object
+ // or if the datafeed id in the saved object is not the same as the one attached to the job in es
if (simulate === true) {
results.datafeedsAdded[job.jobId] = { success: true };
} else {
diff --git a/x-pack/plugins/observability/public/application/types.ts b/x-pack/plugins/observability/public/application/types.ts
new file mode 100644
index 00000000000000..09c5de1e694c82
--- /dev/null
+++ b/x-pack/plugins/observability/public/application/types.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 {
+ ApplicationStart,
+ ChromeStart,
+ HttpStart,
+ IUiSettingsClient,
+ NotificationsStart,
+ OverlayStart,
+ SavedObjectsStart,
+} from 'kibana/public';
+import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
+import { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
+import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public';
+import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
+import { LensPublicStart } from '../../../lens/public';
+import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
+
+export interface ObservabilityAppServices {
+ http: HttpStart;
+ chrome: ChromeStart;
+ overlays: OverlayStart;
+ storage: IStorageWrapper;
+ data: DataPublicPluginStart;
+ uiSettings: IUiSettingsClient;
+ application: ApplicationStart;
+ notifications: NotificationsStart;
+ stateTransfer: EmbeddableStateTransfer;
+ navigation: NavigationPublicPluginStart;
+ savedObjectsClient: SavedObjectsStart['client'];
+
+ triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
+ lens: LensPublicStart;
+}
diff --git a/x-pack/plugins/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx
index ddfd8ebed4f8f7..191a1b2890ada3 100644
--- a/x-pack/plugins/observability/public/components/app/section/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/section/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiAccordion, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle, EuiButton } from '@elastic/eui';
import React from 'react';
import { ErrorPanel } from './error_panel';
import { usePluginContext } from '../../../hooks/use_plugin_context';
@@ -25,36 +25,29 @@ interface Props {
export function SectionContainer({ title, appLink, children, hasError }: Props) {
const { core } = usePluginContext();
return (
-
- {title}
-
- }
- extraAction={
- appLink?.href && (
-
- {appLink.label}
-
- )
- }
- >
- <>
-
-
- {hasError ? (
-
- ) : (
- <>
-
- {children}
- >
- )}
-
- >
-
+
+
+ {title}
+
+ }
+ extraAction={
+ appLink?.href && (
+ {appLink.label}
+ )
+ }
+ >
+ <>
+
+
+ {hasError ? : <>{children}>}
+
+ >
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index 3265287a7f915c..3e02207e262725 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -5,13 +5,13 @@
* 2.0.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
-import { TypedLensByValueInput } from '../../../../../../lens/public';
+import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
-import { ObservabilityPublicPluginsStart } from '../../../../plugin';
import { DataViewLabels } from '../configurations/constants';
+import { ObservabilityAppServices } from '../../../../application/types';
import { useSeriesStorage } from '../hooks/use_series_storage';
interface Props {
@@ -20,57 +20,88 @@ interface Props {
}
export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
- const {
- services: { lens },
- } = useKibana();
+ const kServices = useKibana().services;
+
+ const { lens } = kServices;
const { getSeries } = useSeriesStorage();
const series = getSeries(seriesId);
+ const [isSaveOpen, setIsSaveOpen] = useState(false);
+
+ const LensSaveModalComponent = lens.SaveModalComponent;
+
return (
-
-
-
-
- {DataViewLabels[series.reportType] ??
- i18n.translate('xpack.observability.expView.heading.label', {
- defaultMessage: 'Analyze data',
- })}{' '}
-
-
-
-
-
- {
- if (lensAttributes) {
- lens.navigateToPrefilledEditor(
- {
- id: '',
- timeRange: series.time,
- attributes: lensAttributes,
- },
- true
- );
- }
- }}
- >
- {i18n.translate('xpack.observability.expView.heading.openInLens', {
- defaultMessage: 'Open in Lens',
- })}
-
-
-
+ <>
+
+
+
+
+ {DataViewLabels[series.reportType] ??
+ i18n.translate('xpack.observability.expView.heading.label', {
+ defaultMessage: 'Analyze data',
+ })}{' '}
+
+
+
+
+
+ {
+ if (lensAttributes) {
+ lens.navigateToPrefilledEditor(
+ {
+ id: '',
+ timeRange: series.time,
+ attributes: lensAttributes,
+ },
+ true
+ );
+ }
+ }}
+ >
+ {i18n.translate('xpack.observability.expView.heading.openInLens', {
+ defaultMessage: 'Open in Lens',
+ })}
+
+
+
+ {
+ if (lensAttributes) {
+ setIsSaveOpen(true);
+ }
+ }}
+ >
+ {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
+ defaultMessage: 'Save',
+ })}
+
+
+
+
+ {isSaveOpen && lensAttributes && (
+ setIsSaveOpen(false)}
+ onSave={() => {}}
+ />
+ )}
+ >
);
}
diff --git a/x-pack/plugins/osquery/common/types.ts b/x-pack/plugins/osquery/common/types.ts
index 11c418a51fc7cb..ab9e36ec335db6 100644
--- a/x-pack/plugins/osquery/common/types.ts
+++ b/x-pack/plugins/osquery/common/types.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { PackagePolicy, PackagePolicyInput, PackagePolicyInputStream } from '../../fleet/common';
+
export const savedQuerySavedObjectType = 'osquery-saved-query';
export const packSavedObjectType = 'osquery-pack';
export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack';
@@ -25,3 +27,31 @@ export type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infe
? { [K in keyof U]: U[K][0] }
: never
: never;
+
+export interface OsqueryManagerPackagePolicyConfigRecordEntry {
+ type: string;
+ value: string;
+ frozen?: boolean;
+}
+
+export interface OsqueryManagerPackagePolicyConfigRecord {
+ id: OsqueryManagerPackagePolicyConfigRecordEntry;
+ query: OsqueryManagerPackagePolicyConfigRecordEntry;
+ interval: OsqueryManagerPackagePolicyConfigRecordEntry;
+ platform?: OsqueryManagerPackagePolicyConfigRecordEntry;
+ version?: OsqueryManagerPackagePolicyConfigRecordEntry;
+}
+
+export interface OsqueryManagerPackagePolicyInputStream
+ extends Omit {
+ config?: OsqueryManagerPackagePolicyConfigRecord;
+ vars?: OsqueryManagerPackagePolicyConfigRecord;
+}
+
+export interface OsqueryManagerPackagePolicyInput extends Omit {
+ streams: OsqueryManagerPackagePolicyInputStream[];
+}
+
+export interface OsqueryManagerPackagePolicy extends Omit {
+ inputs: OsqueryManagerPackagePolicyInput[];
+}
diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx
index ffa86c547656cb..23277976968a98 100644
--- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx
+++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx
@@ -23,6 +23,7 @@ import {
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
+import { PLUGIN_ID } from '../../../fleet/common';
import { pagePathGetters } from '../../../fleet/public';
import { useActionResults } from './use_action_results';
import { useAllResults } from '../results/use_all_results';
@@ -130,7 +131,7 @@ const ActionResultsSummaryComponent: React.FC = ({
(agentId) => (
= ({ policyId }
const href = useMemo(
() =>
- getUrlForApp('fleet', {
+ getUrlForApp(PLUGIN_ID, {
path: `#` + pagePathGetters.policy_details({ policyId }),
}),
[getUrlForApp, policyId]
@@ -36,7 +37,7 @@ const AgentsPolicyLinkComponent: React.FC = ({ policyId }
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
event.preventDefault();
- return navigateToApp('fleet', {
+ return navigateToApp(PLUGIN_ID, {
path: `#` + pagePathGetters.policy_details({ policyId }),
});
}
diff --git a/x-pack/plugins/osquery/public/components/manage_integration_link.tsx b/x-pack/plugins/osquery/public/components/manage_integration_link.tsx
index 8419003f57715b..b28471a907e04c 100644
--- a/x-pack/plugins/osquery/public/components/manage_integration_link.tsx
+++ b/x-pack/plugins/osquery/public/components/manage_integration_link.tsx
@@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
+import { INTEGRATIONS_PLUGIN_ID } from '../../../fleet/common';
import { pagePathGetters } from '../../../fleet/public';
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
@@ -22,12 +23,12 @@ const ManageIntegrationLinkComponent = () => {
const integrationHref = useMemo(() => {
if (osqueryIntegration) {
- return getUrlForApp('fleet', {
+ return getUrlForApp(INTEGRATIONS_PLUGIN_ID, {
path:
'#' +
pagePathGetters.integration_details_policies({
pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`,
- }),
+ })[1],
});
}
}, [getUrlForApp, osqueryIntegration]);
@@ -37,12 +38,12 @@ const ManageIntegrationLinkComponent = () => {
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
event.preventDefault();
if (osqueryIntegration) {
- return navigateToApp('fleet', {
+ return navigateToApp(INTEGRATIONS_PLUGIN_ID, {
path:
'#' +
pagePathGetters.integration_details_policies({
pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`,
- }),
+ })[1],
});
}
}
diff --git a/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx b/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx
index 808718c55d1994..d8169c25ad929c 100644
--- a/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx
+++ b/x-pack/plugins/osquery/public/fleet_integration/navigation_buttons.tsx
@@ -9,16 +9,17 @@ import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo } from 'react';
+import { PLUGIN_ID } from '../../common';
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
interface NavigationButtonsProps {
isDisabled?: boolean;
- integrationPolicyId?: string;
- agentPolicyId?: string;
+ integrationPolicyId?: string | undefined;
+ agentPolicyId?: string | undefined;
}
const NavigationButtonsComponent: React.FC = ({
- isDisabled,
+ isDisabled = false,
integrationPolicyId,
agentPolicyId,
}) => {
@@ -28,7 +29,7 @@ const NavigationButtonsComponent: React.FC = ({
const liveQueryHref = useMemo(
() =>
- getUrlForApp('osquery', {
+ getUrlForApp(PLUGIN_ID, {
path: agentPolicyId
? `/live_queries/new?agentPolicyId=${agentPolicyId}`
: ' `/live_queries/new',
@@ -40,7 +41,7 @@ const NavigationButtonsComponent: React.FC = ({
(event) => {
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
event.preventDefault();
- navigateToApp('osquery', {
+ navigateToApp(PLUGIN_ID, {
path: agentPolicyId
? `/live_queries/new?agentPolicyId=${agentPolicyId}`
: ' `/live_queries/new',
@@ -50,7 +51,7 @@ const NavigationButtonsComponent: React.FC = ({
[agentPolicyId, navigateToApp]
);
- const scheduleQueryGroupsHref = getUrlForApp('osquery', {
+ const scheduleQueryGroupsHref = getUrlForApp(PLUGIN_ID, {
path: integrationPolicyId
? `/scheduled_query_groups/${integrationPolicyId}/edit`
: `/scheduled_query_groups`,
@@ -60,7 +61,7 @@ const NavigationButtonsComponent: React.FC = ({
(event) => {
if (!isModifiedEvent(event) && isLeftClickEvent(event)) {
event.preventDefault();
- navigateToApp('osquery', {
+ navigateToApp(PLUGIN_ID, {
path: integrationPolicyId
? `/scheduled_query_groups/${integrationPolicyId}/edit`
: `/scheduled_query_groups`,
diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx
index 6dfbc086c394a5..2305df807f1c84 100644
--- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx
+++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx
@@ -15,8 +15,10 @@ import { i18n } from '@kbn/i18n';
import {
agentRouteService,
agentPolicyRouteService,
- PackagePolicy,
AgentPolicy,
+ PLUGIN_ID,
+ INTEGRATIONS_PLUGIN_ID,
+ NewPackagePolicy,
} from '../../../fleet/common';
import {
pagePathGetters,
@@ -27,6 +29,7 @@ import {
import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_groups/scheduled_query_group_queries_table';
import { useKibana } from '../common/lib/kibana';
import { NavigationButtons } from './navigation_buttons';
+import { OsqueryManagerPackagePolicy } from '../../common/types';
/**
* Exports Osquery-specific package policy instructions
@@ -51,7 +54,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
const agentsLinkHref = useMemo(() => {
if (!policy?.policy_id) return '#';
- return getUrlForApp('fleet', {
+ return getUrlForApp(PLUGIN_ID, {
path:
`#` +
pagePathGetters.policy_details({ policyId: policy?.policy_id }) +
@@ -128,13 +131,13 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
replace({
state: {
onSaveNavigateTo: (newPackagePolicy) => [
- 'fleet',
+ INTEGRATIONS_PLUGIN_ID,
{
path:
'#' +
pagePathGetters.integration_policy_edit({
packagePolicyId: newPackagePolicy.id,
- }),
+ })[1],
state: {
forceRefresh: true,
},
@@ -146,7 +149,11 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
}, [editMode, replace]);
const scheduledQueryGroupTableData = useMemo(() => {
- const policyWithoutEmptyQueries = produce(newPolicy, (draft) => {
+ const policyWithoutEmptyQueries = produce<
+ NewPackagePolicy,
+ OsqueryManagerPackagePolicy,
+ OsqueryManagerPackagePolicy
+ >(newPolicy, (draft) => {
draft.inputs[0].streams = filter(['compiled_stream.id', null], draft.inputs[0].streams);
return draft;
});
@@ -205,7 +212,9 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
{editMode && scheduledQueryGroupTableData.inputs[0].streams.length ? (
-
+
) : null}
diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx
index a1203542613058..960de043eac6e7 100644
--- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx
+++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx
@@ -125,7 +125,7 @@ const ScheduledQueryGroupDetailsPageComponent = () => {
return (
- {data && }
+ {data && }
);
};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx
deleted file mode 100644
index 3879a375b857c6..00000000000000
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/add_query_flyout.tsx
+++ /dev/null
@@ -1,128 +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 {
- EuiFlyout,
- EuiTitle,
- EuiSpacer,
- EuiFlyoutBody,
- EuiFlyoutHeader,
- EuiFlyoutFooter,
- EuiPortal,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiButton,
-} from '@elastic/eui';
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
-import { CodeEditorField } from '../../queries/form/code_editor_field';
-import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
-import { Form, useForm, FormData, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
-
-const FORM_ID = 'addQueryFlyoutForm';
-
-const CommonUseField = getUseField({ component: Field });
-
-interface AddQueryFlyoutProps {
- onSave: (payload: FormData) => Promise;
- onClose: () => void;
-}
-
-const AddQueryFlyoutComponent: React.FC = ({ onSave, onClose }) => {
- const { form } = useForm({
- id: FORM_ID,
- // @ts-expect-error update types
- onSubmit: (payload, isValid) => {
- if (isValid) {
- onSave(payload);
- onClose();
- }
- },
- schema: {
- id: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
- defaultMessage: 'ID',
- }),
- validations: idFieldValidations.map((validator) => ({ validator })),
- },
- query: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
- defaultMessage: 'Query',
- }),
- validations: [{ validator: queryFieldValidation }],
- },
- interval: {
- type: FIELD_TYPES.NUMBER,
- label: i18n.translate(
- 'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel',
- {
- defaultMessage: 'Interval (s)',
- }
- ),
- validations: [{ validator: intervalFieldValidation }],
- },
- },
- });
-
- const { submit } = form;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export const AddQueryFlyout = React.memo(AddQueryFlyoutComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx
deleted file mode 100644
index f44b5e45a26e54..00000000000000
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/edit_query_flyout.tsx
+++ /dev/null
@@ -1,140 +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 {
- EuiFlyout,
- EuiTitle,
- EuiSpacer,
- EuiFlyoutBody,
- EuiFlyoutHeader,
- EuiFlyoutFooter,
- EuiPortal,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiButton,
-} from '@elastic/eui';
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
-import { PackagePolicyInputStream } from '../../../../fleet/common';
-import { CodeEditorField } from '../../queries/form/code_editor_field';
-import { Form, useForm, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
-import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
-
-const FORM_ID = 'editQueryFlyoutForm';
-
-const CommonUseField = getUseField({ component: Field });
-
-interface EditQueryFlyoutProps {
- defaultValue: PackagePolicyInputStream;
- onSave: (payload: FormData) => void;
- onClose: () => void;
-}
-
-export const EditQueryFlyout: React.FC = ({
- defaultValue,
- onSave,
- onClose,
-}) => {
- const { form } = useForm({
- id: FORM_ID,
- // @ts-expect-error update types
- onSubmit: (payload, isValid) => {
- if (isValid) {
- // @ts-expect-error update types
- onSave(payload);
- onClose();
- }
- return;
- },
- defaultValue,
- deserializer: (payload) => ({
- id: payload.vars.id.value,
- query: payload.vars.query.value,
- interval: payload.vars.interval.value,
- }),
- schema: {
- id: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
- defaultMessage: 'ID',
- }),
- validations: idFieldValidations.map((validator) => ({ validator })),
- },
- query: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
- defaultMessage: 'Query',
- }),
- validations: [{ validator: queryFieldValidation }],
- },
- interval: {
- type: FIELD_TYPES.NUMBER,
- label: i18n.translate(
- 'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel',
- {
- defaultMessage: 'Interval (s)',
- }
- ),
- validations: [{ validator: intervalFieldValidation }],
- },
- },
- });
-
- const { submit } = form;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx
index 8924a61d181b6a..64efdf61fc7359 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx
@@ -24,13 +24,22 @@ import { produce } from 'immer';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
+import { PLUGIN_ID } from '../../../common';
+import { OsqueryManagerPackagePolicy } from '../../../common/types';
import {
AgentPolicy,
- PackagePolicy,
PackagePolicyPackage,
packagePolicyRouteService,
} from '../../../../fleet/common';
-import { Form, useForm, useFormData, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
+import {
+ Form,
+ useForm,
+ useFormData,
+ getUseField,
+ Field,
+ FIELD_TYPES,
+ fieldValidators,
+} from '../../shared_imports';
import { useKibana, useRouterNavigate } from '../../common/lib/kibana';
import { PolicyIdComboBoxField } from './policy_id_combobox_field';
import { QueriesField } from './queries_field';
@@ -44,7 +53,7 @@ const FORM_ID = 'scheduledQueryForm';
const CommonUseField = getUseField({ component: Field });
interface ScheduledQueryGroupFormProps {
- defaultValue?: PackagePolicy;
+ defaultValue?: OsqueryManagerPackagePolicy;
packageInfo?: PackagePolicyPackage;
editMode?: boolean;
}
@@ -89,7 +98,7 @@ const ScheduledQueryGroupFormComponent: React.FC =
{
onSuccess: (data) => {
if (!editMode) {
- navigateToApp('osquery', { path: `scheduled_query_groups/${data.item.id}` });
+ navigateToApp(PLUGIN_ID, { path: `scheduled_query_groups/${data.item.id}` });
toasts.addSuccess(
i18n.translate('xpack.osquery.scheduledQueryGroup.form.createSuccessToastMessageText', {
defaultMessage: 'Successfully scheduled {scheduledQueryGroupName}',
@@ -101,7 +110,7 @@ const ScheduledQueryGroupFormComponent: React.FC =
return;
}
- navigateToApp('osquery', { path: `scheduled_query_groups/${data.item.id}` });
+ navigateToApp(PLUGIN_ID, { path: `scheduled_query_groups/${data.item.id}` });
toasts.addSuccess(
i18n.translate('xpack.osquery.scheduledQueryGroup.form.updateSuccessToastMessageText', {
defaultMessage: 'Successfully updated {scheduledQueryGroupName}',
@@ -118,7 +127,15 @@ const ScheduledQueryGroupFormComponent: React.FC =
}
);
- const { form } = useForm({
+ const { form } = useForm<
+ Omit & {
+ policy_id: string;
+ },
+ Omit & {
+ policy_id: string[];
+ namespace: string[];
+ }
+ >({
id: FORM_ID,
schema: {
name: {
@@ -126,6 +143,18 @@ const ScheduledQueryGroupFormComponent: React.FC =
label: i18n.translate('xpack.osquery.scheduledQueryGroup.form.nameFieldLabel', {
defaultMessage: 'Name',
}),
+ validations: [
+ {
+ validator: fieldValidators.emptyField(
+ i18n.translate(
+ 'xpack.osquery.scheduledQueryGroup.form.nameFieldRequiredErrorMessage',
+ {
+ defaultMessage: 'Name is a required field',
+ }
+ )
+ ),
+ },
+ ],
},
description: {
type: FIELD_TYPES.TEXT,
@@ -144,19 +173,35 @@ const ScheduledQueryGroupFormComponent: React.FC =
label: i18n.translate('xpack.osquery.scheduledQueryGroup.form.agentPolicyFieldLabel', {
defaultMessage: 'Agent policy',
}),
+ validations: [
+ {
+ validator: fieldValidators.emptyField(
+ i18n.translate(
+ 'xpack.osquery.scheduledQueryGroup.form.policyIdFieldRequiredErrorMessage',
+ {
+ defaultMessage: 'Agent policy is a required field',
+ }
+ )
+ ),
+ },
+ ],
},
},
- onSubmit: (payload) => {
+ onSubmit: (payload, isValid) => {
+ if (!isValid) return Promise.resolve();
const formData = produce(payload, (draft) => {
- // @ts-expect-error update types
- draft.inputs[0].streams.forEach((stream) => {
- delete stream.compiled_stream;
+ if (draft.inputs?.length) {
+ draft.inputs[0].streams?.forEach((stream) => {
+ delete stream.compiled_stream;
+
+ // we don't want to send id as null when creating the policy
+ if (stream.id == null) {
+ // @ts-expect-error update types
+ delete stream.id;
+ }
+ });
+ }
- // we don't want to send id as null when creating the policy
- if (stream.id == null) {
- delete stream.id;
- }
- });
return draft;
});
return mutateAsync(formData);
@@ -164,7 +209,6 @@ const ScheduledQueryGroupFormComponent: React.FC =
options: {
stripEmptyFields: false,
},
- // @ts-expect-error update types
deserializer: (payload) => ({
...payload,
policy_id: payload.policy_id.length ? [payload.policy_id] : [],
@@ -172,9 +216,7 @@ const ScheduledQueryGroupFormComponent: React.FC =
}),
serializer: (payload) => ({
...payload,
- // @ts-expect-error update types
policy_id: payload.policy_id[0],
- // @ts-expect-error update types
namespace: payload.namespace[0],
}),
defaultValue: merge(
@@ -182,10 +224,11 @@ const ScheduledQueryGroupFormComponent: React.FC =
name: '',
description: '',
enabled: true,
- policy_id: [],
+ policy_id: '',
namespace: 'default',
output_id: '',
- package: packageInfo,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ package: packageInfo!,
inputs: [
{
type: 'osquery',
@@ -205,7 +248,15 @@ const ScheduledQueryGroupFormComponent: React.FC =
[defaultValue, agentPolicyOptions]
);
- const [{ policy_id: policyId }] = useFormData({ form, watch: ['policy_id'] });
+ const [
+ {
+ package: { version: integrationPackageVersion } = { version: undefined },
+ policy_id: policyId,
+ },
+ ] = useFormData({
+ form,
+ watch: ['package', 'policy_id'],
+ });
const currentPolicy = useMemo(() => {
if (!policyId) {
@@ -288,6 +339,7 @@ const ScheduledQueryGroupFormComponent: React.FC =
path="inputs"
component={QueriesField}
scheduledQueryGroupId={defaultValue?.id ?? null}
+ integrationPackageVersion={integrationPackageVersion}
/>
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx
index 34c6eaea1c2656..0718ff028e0022 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx
@@ -11,16 +11,20 @@ import { produce } from 'immer';
import React, { useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { PackagePolicyInput, PackagePolicyInputStream } from '../../../../fleet/common';
+import {
+ OsqueryManagerPackagePolicyInputStream,
+ OsqueryManagerPackagePolicyInput,
+} from '../../../common/types';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { FieldHook } from '../../shared_imports';
import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_group_queries_table';
-import { AddQueryFlyout } from './add_query_flyout';
-import { EditQueryFlyout } from './edit_query_flyout';
+import { QueryFlyout } from '../queries/query_flyout';
import { OsqueryPackUploader } from './pack_uploader';
+import { getSupportedPlatforms } from '../queries/platforms/helpers';
interface QueriesFieldProps {
- field: FieldHook;
+ field: FieldHook;
+ integrationPackageVersion?: string | undefined;
scheduledQueryGroupId: string;
}
@@ -28,29 +32,53 @@ interface GetNewStreamProps {
id: string;
interval: string;
query: string;
+ platform?: string | undefined;
+ version?: string | undefined;
scheduledQueryGroupId?: string;
}
-const getNewStream = ({ id, interval, query, scheduledQueryGroupId }: GetNewStreamProps) => ({
- data_stream: { type: 'logs', dataset: `${OSQUERY_INTEGRATION_NAME}.result` },
- enabled: true,
- id: scheduledQueryGroupId
- ? `osquery-${OSQUERY_INTEGRATION_NAME}.result-${scheduledQueryGroupId}`
- : null,
- vars: {
- id: { type: 'text', value: id },
- interval: {
- type: 'integer',
- value: interval,
+interface GetNewStreamReturn extends Omit {
+ id?: string | null;
+}
+
+const getNewStream = (payload: GetNewStreamProps) =>
+ produce(
+ {
+ data_stream: { type: 'logs', dataset: `${OSQUERY_INTEGRATION_NAME}.result` },
+ enabled: true,
+ id: payload.scheduledQueryGroupId
+ ? `osquery-${OSQUERY_INTEGRATION_NAME}.result-${payload.scheduledQueryGroupId}`
+ : null,
+ vars: {
+ id: { type: 'text', value: payload.id },
+ interval: {
+ type: 'integer',
+ value: payload.interval,
+ },
+ query: { type: 'text', value: payload.query },
+ },
},
- query: { type: 'text', value: query },
- },
-});
+ (draft) => {
+ if (payload.platform && draft.vars) {
+ draft.vars.platform = { type: 'text', value: payload.platform };
+ }
+ if (payload.version && draft.vars) {
+ draft.vars.version = { type: 'text', value: payload.version };
+ }
+ return draft;
+ }
+ );
-const QueriesFieldComponent: React.FC = ({ field, scheduledQueryGroupId }) => {
+const QueriesFieldComponent: React.FC = ({
+ field,
+ integrationPackageVersion,
+ scheduledQueryGroupId,
+}) => {
const [showAddQueryFlyout, setShowAddQueryFlyout] = useState(false);
const [showEditQueryFlyout, setShowEditQueryFlyout] = useState(-1);
- const [tableSelectedItems, setTableSelectedItems] = useState([]);
+ const [tableSelectedItems, setTableSelectedItems] = useState<
+ OsqueryManagerPackagePolicyInputStream[]
+ >([]);
const handleShowAddFlyout = useCallback(() => setShowAddQueryFlyout(true), []);
const handleHideAddFlyout = useCallback(() => setShowAddQueryFlyout(false), []);
@@ -59,7 +87,7 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
const { setValue } = field;
const handleDeleteClick = useCallback(
- (stream: PackagePolicyInputStream) => {
+ (stream: OsqueryManagerPackagePolicyInputStream) => {
const streamIndex = findIndex(field.value[0].streams, [
'vars.id.value',
stream.vars?.id.value,
@@ -79,7 +107,7 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
);
const handleEditClick = useCallback(
- (stream: PackagePolicyInputStream) => {
+ (stream: OsqueryManagerPackagePolicyInputStream) => {
const streamIndex = findIndex(field.value[0].streams, [
'vars.id.value',
stream.vars?.id.value,
@@ -91,39 +119,61 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
);
const handleEditQuery = useCallback(
- (updatedQuery) => {
- if (showEditQueryFlyout >= 0) {
- setValue(
- produce((draft) => {
- draft[0].streams[showEditQueryFlyout].vars.id.value = updatedQuery.id;
- draft[0].streams[showEditQueryFlyout].vars.interval.value = updatedQuery.interval;
- draft[0].streams[showEditQueryFlyout].vars.query.value = updatedQuery.query;
+ (updatedQuery) =>
+ new Promise((resolve) => {
+ if (showEditQueryFlyout >= 0) {
+ setValue(
+ produce((draft) => {
+ draft[0].streams[showEditQueryFlyout].vars.id.value = updatedQuery.id;
+ draft[0].streams[showEditQueryFlyout].vars.interval.value = updatedQuery.interval;
+ draft[0].streams[showEditQueryFlyout].vars.query.value = updatedQuery.query;
- return draft;
- })
- );
- }
+ if (updatedQuery.platform?.length) {
+ draft[0].streams[showEditQueryFlyout].vars.platform = {
+ type: 'text',
+ value: updatedQuery.platform,
+ };
+ } else {
+ delete draft[0].streams[showEditQueryFlyout].vars.platform;
+ }
- handleHideEditFlyout();
- },
+ if (updatedQuery.version?.length) {
+ draft[0].streams[showEditQueryFlyout].vars.version = {
+ type: 'text',
+ value: updatedQuery.version,
+ };
+ } else {
+ delete draft[0].streams[showEditQueryFlyout].vars.version;
+ }
+
+ return draft;
+ })
+ );
+ }
+
+ handleHideEditFlyout();
+ resolve();
+ }),
[handleHideEditFlyout, setValue, showEditQueryFlyout]
);
const handleAddQuery = useCallback(
- (newQuery) => {
- setValue(
- produce((draft) => {
- draft[0].streams.push(
- getNewStream({
- ...newQuery,
- scheduledQueryGroupId,
- })
- );
- return draft;
- })
- );
- handleHideAddFlyout();
- },
+ (newQuery) =>
+ new Promise((resolve) => {
+ setValue(
+ produce((draft) => {
+ draft[0].streams.push(
+ getNewStream({
+ ...newQuery,
+ scheduledQueryGroupId,
+ })
+ );
+ return draft;
+ })
+ );
+ handleHideAddFlyout();
+ resolve();
+ }),
[handleHideAddFlyout, scheduledQueryGroupId, setValue]
);
@@ -148,6 +198,8 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
id: newQueryId,
interval: newQuery.interval,
query: newQuery.query,
+ version: newQuery.version,
+ platform: getSupportedPlatforms(newQuery.platform),
scheduledQueryGroupId,
})
);
@@ -160,7 +212,9 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
[scheduledQueryGroupId, setValue]
);
- const tableData = useMemo(() => ({ inputs: field.value }), [field.value]);
+ const tableData = useMemo(() => (field.value.length ? field.value[0].streams : []), [
+ field.value,
+ ]);
return (
<>
@@ -201,12 +255,16 @@ const QueriesFieldComponent: React.FC = ({ field, scheduledQu
{ }
{showAddQueryFlyout && (
- // @ts-expect-error update types
-
+
)}
{showEditQueryFlyout != null && showEditQueryFlyout >= 0 && (
-
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts
new file mode 100644
index 00000000000000..3345c18d07b2cb
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+export const ALL_OSQUERY_VERSIONS_OPTIONS = [
+ {
+ label: '4.7.0',
+ },
+ {
+ label: '4.6.0',
+ },
+ {
+ label: '4.5.1',
+ },
+ {
+ label: '4.5.0',
+ },
+ {
+ label: '4.4.0',
+ },
+ {
+ label: '4.3.0',
+ },
+ {
+ label: '4.2.0',
+ },
+ {
+ label: '4.1.2',
+ },
+ {
+ label: '4.1.1',
+ },
+ {
+ label: '4.0.2',
+ },
+ {
+ label: '3.3.2',
+ },
+ {
+ label: '3.3.0',
+ },
+ {
+ label: '3.2.6',
+ },
+ {
+ label: '3.2.4',
+ },
+ {
+ label: '2.9.0',
+ },
+ {
+ label: '2.8.0',
+ },
+ {
+ label: '2.7.0',
+ },
+ {
+ label: '2.11.2',
+ },
+ {
+ label: '2.11.0',
+ },
+ {
+ label: '2.10.2',
+ },
+ {
+ label: '2.10.0',
+ },
+];
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx
new file mode 100644
index 00000000000000..4e433e9e240b1a
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platform_checkbox_group_field.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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 { isEmpty, pickBy } from 'lodash';
+import React, { useCallback, useMemo, useState } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiCheckboxGroup,
+ EuiCheckboxGroupOption,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { FieldHook, getFieldValidityAndErrorMessage } from '../../shared_imports';
+import { PlatformIcon } from './platforms/platform_icon';
+
+interface Props {
+ field: FieldHook;
+ euiFieldProps?: Record;
+ idAria?: string;
+ [key: string]: unknown;
+}
+
+export const PlatformCheckBoxGroupField = ({
+ field,
+ euiFieldProps = {},
+ idAria,
+ ...rest
+}: Props) => {
+ const options = useMemo(
+ () => [
+ {
+ id: 'linux',
+ label: (
+
+
+
+
+
+
+
+
+ ),
+ },
+ {
+ id: 'darwin',
+ label: (
+
+
+
+
+
+
+
+
+ ),
+ },
+ {
+ id: 'windows',
+ label: (
+
+
+
+
+
+
+
+
+ ),
+ },
+ ],
+ []
+ );
+
+ const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
+ const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState>(
+ () =>
+ (options as EuiCheckboxGroupOption[]).reduce((acc, option) => {
+ acc[option.id] = isEmpty(field.value) ? true : field.value?.includes(option.id) ?? false;
+ return acc;
+ }, {} as Record)
+ );
+
+ const onChange = useCallback(
+ (optionId: string) => {
+ const newCheckboxIdToSelectedMap = {
+ ...checkboxIdToSelectedMap,
+ [optionId]: !checkboxIdToSelectedMap[optionId],
+ };
+ setCheckboxIdToSelectedMap(newCheckboxIdToSelectedMap);
+
+ field.setValue(() =>
+ Object.keys(pickBy(newCheckboxIdToSelectedMap, (value) => value === true)).join(',')
+ );
+ },
+ [checkboxIdToSelectedMap, field]
+ );
+
+ const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
+
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts
new file mode 100644
index 00000000000000..4f81ed73e1e7a7
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/constants.ts
@@ -0,0 +1,10 @@
+/*
+ * 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 { PlatformType } from './types';
+
+export const SUPPORTED_PLATFORMS = [PlatformType.darwin, PlatformType.linux, PlatformType.windows];
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx
new file mode 100644
index 00000000000000..362fa5c67e6f90
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/helpers.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 { uniq } from 'lodash';
+import { SUPPORTED_PLATFORMS } from './constants';
+
+import linuxSvg from './logos/linux.svg';
+import windowsSvg from './logos/windows.svg';
+import macosSvg from './logos/macos.svg';
+import { PlatformType } from './types';
+
+export const getPlatformIconModule = (platform: string) => {
+ switch (platform) {
+ case 'darwin':
+ return macosSvg;
+ case 'linux':
+ return linuxSvg;
+ case 'windows':
+ return windowsSvg;
+ default:
+ return `${platform}`;
+ }
+};
+
+export const getSupportedPlatforms = (payload: string) => {
+ let platformArray: string[];
+ try {
+ platformArray = payload?.split(',').map((platformString) => platformString.trim());
+ } catch (e) {
+ return undefined;
+ }
+
+ if (!platformArray) return;
+
+ return uniq(
+ platformArray.reduce((acc, nextPlatform) => {
+ if (!SUPPORTED_PLATFORMS.includes(nextPlatform as PlatformType)) {
+ if (nextPlatform === 'posix') {
+ acc.push(PlatformType.darwin);
+ acc.push(PlatformType.linux);
+ }
+ if (nextPlatform === 'ubuntu') {
+ acc.push(PlatformType.linux);
+ }
+ } else {
+ acc.push(nextPlatform);
+ }
+ return acc;
+ }, [] as string[])
+ ).join(',');
+};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx
new file mode 100644
index 00000000000000..b8af2790c6f368
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/index.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import React, { useEffect, useState, useMemo } from 'react';
+
+import { SUPPORTED_PLATFORMS } from './constants';
+import { PlatformIcon } from './platform_icon';
+
+interface PlatformIconsProps {
+ platform: string;
+}
+
+const PlatformIconsComponent: React.FC = ({ platform }) => {
+ const [platforms, setPlatforms] = useState(SUPPORTED_PLATFORMS);
+
+ useEffect(() => {
+ setPlatforms((prevValue) => {
+ if (platform) {
+ let platformArray: string[];
+ try {
+ platformArray = platform?.split(',').map((platformString) => platformString.trim());
+ } catch (e) {
+ return prevValue;
+ }
+ return platformArray;
+ } else {
+ return SUPPORTED_PLATFORMS;
+ }
+ });
+ }, [platform]);
+
+ const content = useMemo(
+ () =>
+ platforms.map((platformString) => (
+
+
+
+ )),
+ [platforms]
+ );
+
+ return {content} ;
+};
+
+export const PlatformIcons = React.memo(PlatformIconsComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg
new file mode 100644
index 00000000000000..47358292e08a80
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/linux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg
new file mode 100644
index 00000000000000..baa5930800aa9c
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/macos.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg
new file mode 100644
index 00000000000000..0872225da3a117
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/logos/windows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx
new file mode 100644
index 00000000000000..1126dfd690c193
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/platform_icon.tsx
@@ -0,0 +1,21 @@
+/*
+ * 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 { EuiIcon } from '@elastic/eui';
+import React from 'react';
+import { getPlatformIconModule } from './helpers';
+
+interface PlatformIconProps {
+ platform: string;
+}
+
+const PlatformIconComponent: React.FC = ({ platform }) => {
+ const platformIconModule = getPlatformIconModule(platform);
+ return ;
+};
+
+export const PlatformIcon = React.memo(PlatformIconComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts
new file mode 100644
index 00000000000000..94953a6a854ea2
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/platforms/types.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+export enum PlatformType {
+ darwin = 'darwin',
+ windows = 'windows',
+ linux = 'linux',
+}
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx
new file mode 100644
index 00000000000000..62ac3a46a2d773
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx
@@ -0,0 +1,176 @@
+/*
+ * 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 {
+ EuiCallOut,
+ EuiFlyout,
+ EuiTitle,
+ EuiSpacer,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiFlyoutFooter,
+ EuiPortal,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiButton,
+} from '@elastic/eui';
+import React, { useMemo } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { satisfies } from 'semver';
+
+import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types';
+import { CodeEditorField } from '../../queries/form/code_editor_field';
+import { Form, getUseField, Field } from '../../shared_imports';
+import { PlatformCheckBoxGroupField } from './platform_checkbox_group_field';
+import { ALL_OSQUERY_VERSIONS_OPTIONS } from './constants';
+import {
+ UseScheduledQueryGroupQueryFormProps,
+ useScheduledQueryGroupQueryForm,
+} from './use_scheduled_query_group_query_form';
+import { ManageIntegrationLink } from '../../components/manage_integration_link';
+
+const CommonUseField = getUseField({ component: Field });
+
+interface QueryFlyoutProps {
+ defaultValue?: UseScheduledQueryGroupQueryFormProps['defaultValue'] | undefined;
+ integrationPackageVersion?: string | undefined;
+ onSave: (payload: OsqueryManagerPackagePolicyConfigRecord) => Promise;
+ onClose: () => void;
+}
+
+const QueryFlyoutComponent: React.FC = ({
+ defaultValue,
+ integrationPackageVersion,
+ onSave,
+ onClose,
+}) => {
+ const { form } = useScheduledQueryGroupQueryForm({
+ defaultValue,
+ handleSubmit: (payload, isValid) =>
+ new Promise((resolve) => {
+ if (isValid) {
+ onSave(payload);
+ onClose();
+ }
+ resolve();
+ }),
+ });
+
+ /* Platform and version fields are supported since osquer_manger@0.3.0 */
+ const isFieldSupported = useMemo(
+ () => (integrationPackageVersion ? satisfies(integrationPackageVersion, '>=0.3.0') : false),
+ [integrationPackageVersion]
+ );
+
+ const { submit } = form;
+
+ return (
+
+
+
+
+
+ {defaultValue ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {!isFieldSupported ? (
+
+ }
+ iconType="pin"
+ >
+
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const QueryFlyout = React.memo(QueryFlyoutComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx
new file mode 100644
index 00000000000000..344c33b419dd6c
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { FIELD_TYPES } from '../../shared_imports';
+
+import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
+
+export const formSchema = {
+ id: {
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
+ defaultMessage: 'ID',
+ }),
+ validations: idFieldValidations.map((validator) => ({ validator })),
+ },
+ query: {
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.queryFieldLabel', {
+ defaultMessage: 'Query',
+ }),
+ validations: [{ validator: queryFieldValidation }],
+ },
+ interval: {
+ defaultValue: 3600,
+ type: FIELD_TYPES.NUMBER,
+ label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.intervalFieldLabel', {
+ defaultMessage: 'Interval (s)',
+ }),
+ validations: [{ validator: intervalFieldValidation }],
+ },
+ platform: {
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.platformFieldLabel', {
+ defaultMessage: 'Platform',
+ }),
+ validations: [],
+ },
+ version: {
+ defaultValue: [],
+ type: FIELD_TYPES.COMBO_BOX,
+ label: ((
+
+
+
+
+
+
+
+
+
+
+ ) as unknown) as string,
+ validations: [],
+ },
+};
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx
new file mode 100644
index 00000000000000..bcde5f4b970d43
--- /dev/null
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/use_scheduled_query_group_query_form.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 { isArray } from 'lodash';
+import uuid from 'uuid';
+import { produce } from 'immer';
+
+import { FormConfig, useForm } from '../../shared_imports';
+import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types';
+import { formSchema } from './schema';
+
+const FORM_ID = 'editQueryFlyoutForm';
+
+export interface UseScheduledQueryGroupQueryFormProps {
+ defaultValue?: OsqueryManagerPackagePolicyConfigRecord | undefined;
+ handleSubmit: FormConfig<
+ OsqueryManagerPackagePolicyConfigRecord,
+ ScheduledQueryGroupFormData
+ >['onSubmit'];
+}
+
+export interface ScheduledQueryGroupFormData {
+ id: string;
+ query: string;
+ interval: number;
+ platform?: string | undefined;
+ version?: string[] | undefined;
+}
+
+export const useScheduledQueryGroupQueryForm = ({
+ defaultValue,
+ handleSubmit,
+}: UseScheduledQueryGroupQueryFormProps) =>
+ useForm({
+ id: FORM_ID + uuid.v4(),
+ onSubmit: handleSubmit,
+ options: {
+ stripEmptyFields: false,
+ },
+ defaultValue,
+ // @ts-expect-error update types
+ serializer: (payload) =>
+ produce(payload, (draft) => {
+ if (draft.platform?.split(',').length === 3) {
+ // if all platforms are checked then use undefined
+ delete draft.platform;
+ }
+ if (isArray(draft.version)) {
+ if (!draft.version.length) {
+ delete draft.version;
+ } else {
+ // @ts-expect-error update types
+ draft.version = draft.version[0];
+ }
+ }
+ return draft;
+ }),
+ deserializer: (payload) => {
+ if (!payload) return {} as ScheduledQueryGroupFormData;
+
+ return {
+ id: payload.id.value,
+ query: payload.query.value,
+ interval: parseInt(payload.interval.value, 10),
+ platform: payload.platform?.value,
+ version: payload.version?.value ? [payload.version?.value] : [],
+ };
+ },
+ schema: formSchema,
+ });
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/validations.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/validations.ts
similarity index 100%
rename from x-pack/plugins/osquery/public/scheduled_query_groups/form/validations.ts
rename to x-pack/plugins/osquery/public/scheduled_query_groups/queries/validations.ts
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx
index 6f78f2c086edf4..36d15587086f2d 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx
@@ -22,9 +22,10 @@ import {
PersistedIndexPatternLayer,
PieVisualizationState,
} from '../../../lens/public';
-import { PackagePolicy, PackagePolicyInputStream } from '../../../fleet/common';
import { FilterStateStore } from '../../../../../src/plugins/data/common';
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
+import { PlatformIcons } from './queries/platforms';
+import { OsqueryManagerPackagePolicyInputStream } from '../../common/types';
export enum ViewResultsActionButtonType {
icon = 'icon',
@@ -303,12 +304,12 @@ const ViewResultsInDiscoverActionComponent: React.FC;
+ data: OsqueryManagerPackagePolicyInputStream[];
editMode?: boolean;
- onDeleteClick?: (item: PackagePolicyInputStream) => void;
- onEditClick?: (item: PackagePolicyInputStream) => void;
- selectedItems?: PackagePolicyInputStream[];
- setSelectedItems?: (selection: PackagePolicyInputStream[]) => void;
+ onDeleteClick?: (item: OsqueryManagerPackagePolicyInputStream) => void;
+ onEditClick?: (item: OsqueryManagerPackagePolicyInputStream) => void;
+ selectedItems?: OsqueryManagerPackagePolicyInputStream[];
+ setSelectedItems?: (selection: OsqueryManagerPackagePolicyInputStream[]) => void;
}
const ScheduledQueryGroupQueriesTableComponent: React.FC = ({
@@ -320,7 +321,7 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC {
const renderDeleteAction = useCallback(
- (item: PackagePolicyInputStream) => (
+ (item: OsqueryManagerPackagePolicyInputStream) => (
(
+ (item: OsqueryManagerPackagePolicyInputStream) => (
,
+ []
+ );
+
+ const renderVersionColumn = useCallback(
+ (version: string) =>
+ version
+ ? `${version}`
+ : i18n.translate('xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', {
+ defaultMessage: 'ALL',
+ }),
+ []
+ );
+
const renderDiscoverResultsAction = useCallback(
(item) => (
({
sort: {
- field: 'vars.id.value' as keyof PackagePolicyInputStream,
+ field: 'vars.id.value' as keyof OsqueryManagerPackagePolicyInputStream,
direction: 'asc' as const,
},
}),
[]
);
- const itemId = useCallback((item: PackagePolicyInputStream) => get('vars.id.value', item), []);
+ const itemId = useCallback(
+ (item: OsqueryManagerPackagePolicyInputStream) => get('vars.id.value', item),
+ []
+ );
const selection = useMemo(
() => ({
@@ -477,8 +512,8 @@ const ScheduledQueryGroupQueriesTableComponent: React.FC
- items={data.inputs[0].streams}
+
+ items={data}
itemId={itemId}
columns={columns}
sorting={sorting}
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts
index e0f892d0302c0c..93d552b3f71f3a 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts
@@ -8,11 +8,8 @@
import { useQuery } from 'react-query';
import { useKibana } from '../common/lib/kibana';
-import {
- GetOnePackagePolicyResponse,
- PackagePolicy,
- packagePolicyRouteService,
-} from '../../../fleet/common';
+import { GetOnePackagePolicyResponse, packagePolicyRouteService } from '../../../fleet/common';
+import { OsqueryManagerPackagePolicy } from '../../common/types';
interface UseScheduledQueryGroup {
scheduledQueryGroupId: string;
@@ -25,7 +22,11 @@ export const useScheduledQueryGroup = ({
}: UseScheduledQueryGroup) => {
const { http } = useKibana().services;
- return useQuery(
+ return useQuery<
+ Omit & { item: OsqueryManagerPackagePolicy },
+ unknown,
+ OsqueryManagerPackagePolicy
+ >(
['scheduledQueryGroup', { scheduledQueryGroupId }],
() => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)),
{
diff --git a/x-pack/plugins/osquery/public/shared_imports.ts b/x-pack/plugins/osquery/public/shared_imports.ts
index 737b4d47357772..8a569a07616567 100644
--- a/x-pack/plugins/osquery/public/shared_imports.ts
+++ b/x-pack/plugins/osquery/public/shared_imports.ts
@@ -12,6 +12,7 @@ export {
FieldValidateResponse,
FIELD_TYPES,
Form,
+ FormConfig,
FormData,
FormDataProvider,
FormHook,
diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json
index 291b0f7c607cf1..76e26c770cfe02 100644
--- a/x-pack/plugins/osquery/tsconfig.json
+++ b/x-pack/plugins/osquery/tsconfig.json
@@ -12,7 +12,8 @@
"common/**/*",
"public/**/*",
"scripts/**/*",
- "server/**/*"
+ "server/**/*",
+ "../../../typings/**/*"
],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
index c91732019f79fe..209c224618f78e 100644
--- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
+++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
@@ -181,6 +181,10 @@ describe(' ', () => {
expect(exists('remoteClusterCreateButton')).toBe(true);
});
+ test('should have link to documentation', () => {
+ expect(exists('documentationLink')).toBe(true);
+ });
+
test('should list the remote clusters in the table', () => {
expect(tableCellsValues.length).toEqual(remoteClusters.length);
expect(tableCellsValues).toEqual([
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
index 1706be8cfbe2f6..2b2e7338fb6b05 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
@@ -5,62 +5,38 @@
* 2.0.
*/
-import React, { Fragment } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import { remoteClustersUrl } from '../../../services/documentation';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiText,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
export const RemoteClusterPageTitle = ({ title, description }) => (
-
-
-
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
-
-
- {description ? (
- <>
-
-
-
- {description}
-
- >
- ) : null}
-
-
-
+ <>
+ {title}}
+ rightSideItems={[
+
+
+ ,
+ ]}
+ description={description}
+ />
+
+
+ >
);
RemoteClusterPageTitle.propTypes = {
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
index 124d2d42afb789..f62550ca5aa107 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
@@ -9,8 +9,6 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageContent } from '@elastic/eui';
-
import { extractQueryParams } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
@@ -58,11 +56,7 @@ export class RemoteClusterAdd extends PureComponent {
const { isAddingCluster, addClusterError } = this.props;
return (
-
+ <>
-
+ >
);
}
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
index 18ee2e2b3875dd..1f3388d06e54c6 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
@@ -5,27 +5,17 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiButtonEmpty,
- EuiCallOut,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPageContent,
- EuiSpacer,
- EuiText,
- EuiTextColor,
-} from '@elastic/eui';
+import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiPageContent, EuiSpacer } from '@elastic/eui';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
-import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components';
+import { RemoteClusterPageTitle, RemoteClusterForm } from '../components';
export class RemoteClusterEdit extends Component {
static propTypes = {
@@ -92,56 +82,50 @@ export class RemoteClusterEdit extends Component {
}
};
- renderContent() {
+ render() {
const { clusterName } = this.state;
const { isLoading, cluster, isEditingCluster, getEditClusterError } = this.props;
if (isLoading) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
if (!cluster) {
return (
-
-
+
+
+
+
}
- color="danger"
- iconType="alert"
- >
-
-
-
-
-
-
+
+
+ }
+ actions={
+
@@ -149,10 +133,10 @@ export class RemoteClusterEdit extends Component {
id="xpack.remoteClusters.edit.viewRemoteClustersButtonLabel"
defaultMessage="View remote clusters"
/>
-
-
-
-
+
+ }
+ />
+
);
}
@@ -160,23 +144,50 @@ export class RemoteClusterEdit extends Component {
if (isConfiguredByNode) {
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+ }
+ body={
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+
);
}
return (
<>
+
+ }
+ />
+
{hasDeprecatedProxySetting ? (
<>
>
) : null}
+
);
}
-
- render() {
- return (
-
-
- }
- />
-
- {this.renderContent()}
-
- );
- }
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
index ccf4c7568f7ad9..b94ae8f7edbc04 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
@@ -5,32 +5,24 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
+ EuiButtonEmpty,
EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
EuiLoadingKibana,
- EuiLoadingSpinner,
EuiOverlayMask,
- EuiPageBody,
EuiPageContent,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
EuiSpacer,
- EuiText,
- EuiTextColor,
- EuiTitle,
- EuiCallOut,
+ EuiPageHeader,
} from '@elastic/eui';
+import { remoteClustersUrl } from '../../services/documentation';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { setBreadcrumbs } from '../../services/breadcrumb';
import { RemoteClusterTable } from './remote_cluster_table';
@@ -82,41 +74,6 @@ export class RemoteClusterList extends Component {
clearInterval(this.interval);
}
- getHeaderSection(isAuthorized) {
- return (
-
-
-
-
-
-
-
-
-
-
- {isAuthorized && (
-
-
-
-
-
- )}
-
-
-
- );
- }
-
renderBlockingAction() {
const { isCopyingCluster, isRemovingCluster } = this.props;
@@ -132,16 +89,28 @@ export class RemoteClusterList extends Component {
}
renderNoPermission() {
- const title = i18n.translate('xpack.remoteClusters.remoteClusterList.noPermissionTitle', {
- defaultMessage: 'Permission error',
- });
return (
-
-
+
+
+
+ }
+ body={
+
+
+
+ }
/>
-
+
);
}
@@ -150,80 +119,84 @@ export class RemoteClusterList extends Component {
// handle unexpected error shapes in the API action.
const { statusCode, error: errorString } = error.body;
- const title = i18n.translate('xpack.remoteClusters.remoteClusterList.loadingErrorTitle', {
- defaultMessage: 'Error loading remote clusters',
- });
return (
-
- {statusCode} {errorString}
-
+
+
+
+
+ }
+ body={
+
+ {statusCode} {errorString}
+
+ }
+ />
+
);
}
renderEmpty() {
return (
-
-
-
- }
- body={
-
+
+
+
+
+ }
+ body={
-
- }
- actions={
-
-
-
- }
- />
+ }
+ actions={
+
+
+
+ }
+ />
+
);
}
renderLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
}
@@ -231,10 +204,35 @@ export class RemoteClusterList extends Component {
const { clusters } = this.props;
return (
-
+ <>
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
-
+ >
);
}
@@ -242,7 +240,6 @@ export class RemoteClusterList extends Component {
const { isLoading, clusters, clusterLoadError } = this.props;
const isEmpty = !isLoading && !clusters.length;
const isAuthorized = !clusterLoadError || clusterLoadError.status !== 403;
- const isHeaderVisible = clusterLoadError || !isEmpty;
let content;
@@ -261,13 +258,10 @@ export class RemoteClusterList extends Component {
}
return (
-
-
- {isHeaderVisible && this.getHeaderSection(isAuthorized)}
- {content}
- {this.renderBlockingAction()}
-
-
+ <>
+ {content}
+ {this.renderBlockingAction()}
+ >
);
}
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
index 3da8bb505fc543..1404e51d98a6d4 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
@@ -330,6 +330,19 @@ export class RemoteClusterTable extends Component {
)}
) : undefined,
+ toolsRight: (
+
+
+
+ ),
onChange: this.onSearch,
box: {
incremental: true,
diff --git a/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js b/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
index 343237e70a120e..1a6459627c9a13 100644
--- a/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
+++ b/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
@@ -5,9 +5,7 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-
-import { loadClusters as sendLoadClustersRequest, showApiError } from '../../services';
+import { loadClusters as sendLoadClustersRequest } from '../../services';
import { LOAD_CLUSTERS_START, LOAD_CLUSTERS_SUCCESS, LOAD_CLUSTERS_FAILURE } from '../action_types';
@@ -20,17 +18,10 @@ export const loadClusters = () => async (dispatch) => {
try {
clusters = await sendLoadClustersRequest();
} catch (error) {
- dispatch({
+ return dispatch({
type: LOAD_CLUSTERS_FAILURE,
payload: { error },
});
-
- return showApiError(
- error,
- i18n.translate('xpack.remoteClusters.loadAction.errorTitle', {
- defaultMessage: 'Error loading remote clusters',
- })
- );
}
dispatch({
diff --git a/x-pack/plugins/remote_clusters/public/shared_imports.ts b/x-pack/plugins/remote_clusters/public/shared_imports.ts
index fd281753186665..c8d7f1d9f13f3d 100644
--- a/x-pack/plugins/remote_clusters/public/shared_imports.ts
+++ b/x-pack/plugins/remote_clusters/public/shared_imports.ts
@@ -5,4 +5,8 @@
* 2.0.
*/
-export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public';
+export {
+ extractQueryParams,
+ indices,
+ SectionLoading,
+} from '../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
index 04190fbf5eacdd..b1bc14cc79e641 100644
--- a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
+++ b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: expected value of type [boolean] but got [string]"`;
+exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [2]: expected value of type [boolean] but got [string]"`;
exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`;
diff --git a/x-pack/plugins/security/server/authorization/validate_es_response.ts b/x-pack/plugins/security/server/authorization/validate_es_response.ts
index d941b2d777c4fc..223aee37a90471 100644
--- a/x-pack/plugins/security/server/authorization/validate_es_response.ts
+++ b/x-pack/plugins/security/server/authorization/validate_es_response.ts
@@ -9,6 +9,11 @@ import { schema } from '@kbn/config-schema';
import type { HasPrivilegesResponse } from './types';
+const anyBoolean = schema.boolean();
+const anyBooleanArray = schema.arrayOf(anyBoolean);
+const anyString = schema.string();
+const anyObject = schema.object({}, { unknowns: 'allow' });
+
/**
* Validates an Elasticsearch "Has privileges" response against the expected application, actions, and resources.
*
@@ -31,7 +36,6 @@ export function validateEsPrivilegeResponse(
}
function buildValidationSchema(application: string, actions: string[], resources: string[]) {
- const actionValidationSchema = schema.boolean();
const actionsValidationSchema = schema.object(
{},
{
@@ -45,9 +49,7 @@ function buildValidationSchema(application: string, actions: string[], resources
throw new Error('Payload did not match expected actions');
}
- Object.values(value).forEach((actionResult) => {
- actionValidationSchema.validate(actionResult);
- });
+ anyBooleanArray.validate(Object.values(value));
},
}
);
@@ -73,12 +75,12 @@ function buildValidationSchema(application: string, actions: string[], resources
);
return schema.object({
- username: schema.string(),
- has_all_requested: schema.boolean(),
- cluster: schema.object({}, { unknowns: 'allow' }),
+ username: anyString,
+ has_all_requested: anyBoolean,
+ cluster: anyObject,
application: schema.object({
[application]: resourcesValidationSchema,
}),
- index: schema.object({}, { unknowns: 'allow' }),
+ index: anyObject,
});
}
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
index 740437646f61a4..2fdb7e99d860e8 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
@@ -8,14 +8,10 @@
import React from 'react';
import { FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n/react';
-export const FormattedDateAndTime: React.FC<{ date: Date; showRelativeTime?: boolean }> = ({
- date,
- showRelativeTime = false,
-}) => {
+export const FormattedDateAndTime: React.FC<{ date: Date }> = ({ date }) => {
// If date is greater than or equal to 1h (ago), then show it as a date
- // and if showRelativeTime is false
// else, show it as relative to "now"
- return Date.now() - date.getTime() >= 3.6e6 && !showRelativeTime ? (
+ return Date.now() - date.getTime() >= 3.6e6 ? (
<>
{' @'}
diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
index fc5c19e95fb770..d677a4a9fd662e 100644
--- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
@@ -140,6 +140,22 @@ describe('when using PaginatedContent', () => {
});
});
+ it('should call onChange when page is empty', () => {
+ render({
+ pagination: {
+ pageIndex: 1,
+ pageSizeOptions: [5, 10, 20],
+ pageSize: 10,
+ totalItemCount: 10,
+ },
+ });
+ expect(onChangeHandler).toHaveBeenCalledWith({
+ pageIndex: 0,
+ pageSize: 10,
+ });
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
+ });
+
it('should ignore items, error, noItemsMessage when `children` is used', () => {
render({ children: {'children being used here'}
});
expect(renderResult.getByTestId('custom-content')).not.toBeNull();
diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
index 890b21624eaf67..a6b2683316efe5 100644
--- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
@@ -16,6 +16,7 @@ import React, {
useCallback,
useMemo,
useState,
+ useEffect,
} from 'react';
import {
CommonProps,
@@ -152,6 +153,12 @@ export const PaginatedContent = memo(
[pagination?.pageSize, pagination?.totalItemCount]
);
+ useEffect(() => {
+ if (pageCount > 0 && pageCount < (pagination?.pageIndex || 0) + 1) {
+ onChange({ pageIndex: pageCount - 1, pageSize: pagination?.pageSize || 0 });
+ }
+ }, [pageCount, onChange, pagination]);
+
const handleItemsPerPageChange: EuiTablePaginationProps['onChangeItemsPerPage'] = useCallback(
(pageSize) => {
onChange({ pageSize, pageIndex: pagination?.pageIndex || 0 });
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
index 7ecbad54dbbece..356d44a8105287 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
@@ -70,6 +70,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
['View host details', 'hostLink'],
['View agent policy', 'agentPolicyLink'],
['View agent details', 'agentDetailsLink'],
+ ['Reassign agent policy', 'agentPolicyReassignLink'],
])('should display %s action', async (_, dataTestSubj) => {
await render();
expect(renderResult.getByTestId(dataTestSubj)).not.toBeNull();
@@ -80,6 +81,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
['View host details', 'hostLink'],
['View agent policy', 'agentPolicyLink'],
['View agent details', 'agentDetailsLink'],
+ ['Reassign agent policy', 'agentPolicyReassignLink'],
])(
'should navigate via kibana `navigateToApp()` when %s is clicked',
async (_, dataTestSubj) => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
index f8574da7f0a03d..c431cd682d25ba 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
@@ -10,7 +10,7 @@ import styled from 'styled-components';
import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui';
import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types';
-import { FormattedDateAndTime } from '../../../../../../common/components/endpoint/formatted_date_time';
+import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date';
import { LogEntryTimelineIcon } from './log_entry_timeline_icon';
import * as i18 from '../../translations';
@@ -144,9 +144,8 @@ export const LogEntry = memo(({ logEntry }: { logEntry: Immutable{displayResponseEvent ? responseEventTitle : actionEventTitle}}
timelineIcon={
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
index 16cae79d42c0f2..38404a5c6c11ff 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
@@ -8,10 +8,8 @@
import styled from 'styled-components';
import {
EuiDescriptionList,
- EuiHorizontalRule,
EuiListGroup,
EuiListGroupItem,
- EuiIcon,
EuiText,
EuiFlexGroup,
EuiFlexItem,
@@ -23,17 +21,14 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { isPolicyOutOfDate } from '../../utils';
import { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types';
-import { useEndpointSelector, useAgentDetailsIngestUrl } from '../hooks';
-import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
+import { useEndpointSelector } from '../hooks';
import { policyResponseStatus, uiQueryParams } from '../../store/selectors';
import { POLICY_STATUS_TO_BADGE_COLOR, HOST_STATUS_TO_BADGE_COLOR } from '../host_constants';
import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
-import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
import { getEndpointDetailsPath } from '../../../../common/routing';
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
-import { AgentDetailsReassignPolicyAction } from '../../../../../../../fleet/public';
import { EndpointPolicyLink } from '../components/endpoint_policy_link';
import { OutOfDate } from '../components/out_of_date';
@@ -44,8 +39,6 @@ const HostIds = styled(EuiListGroupItem)`
}
`;
-const openReassignFlyoutSearch = '?openReassignFlyout=true';
-
export const EndpointDetails = memo(
({
details,
@@ -56,19 +49,34 @@ export const EndpointDetails = memo(
policyInfo?: HostInfo['policy_info'];
hostStatus: HostStatus;
}) => {
- const agentId = details.elastic.agent.id;
- const {
- url: agentDetailsUrl,
- appId: ingestAppId,
- appPath: agentDetailsAppPath,
- } = useAgentDetailsIngestUrl(agentId);
const queryParams = useEndpointSelector(uiQueryParams);
const policyStatus = useEndpointSelector(
policyResponseStatus
) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR;
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
- const detailsResultsUpper = useMemo(() => {
+ const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const { selected_endpoint, show, ...currentUrlParams } = queryParams;
+ return [
+ formatUrl(
+ getEndpointDetailsPath({
+ name: 'endpointPolicyResponse',
+ ...currentUrlParams,
+ selected_endpoint: details.agent.id,
+ })
+ ),
+ getEndpointDetailsPath({
+ name: 'endpointPolicyResponse',
+ ...currentUrlParams,
+ selected_endpoint: details.agent.id,
+ }),
+ ];
+ }, [details.agent.id, formatUrl, queryParams]);
+
+ const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
+
+ const detailsResults = useMemo(() => {
return [
{
title: i18n.translate('xpack.securitySolution.endpoint.details.os', {
@@ -106,55 +114,9 @@ export const EndpointDetails = memo(
),
},
- ];
- }, [details, hostStatus]);
-
- const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const { selected_endpoint, show, ...currentUrlParams } = queryParams;
- return [
- formatUrl(
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
- ...currentUrlParams,
- selected_endpoint: details.agent.id,
- })
- ),
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
- ...currentUrlParams,
- selected_endpoint: details.agent.id,
- }),
- ];
- }, [details.agent.id, formatUrl, queryParams]);
-
- const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`;
- const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`;
- const handleReassignEndpointsClick = useNavigateToAppEventHandler(
- ingestAppId,
- {
- path: agentDetailsWithFlyoutPath,
- state: {
- onDoneNavigateTo: [
- 'securitySolution:administration',
- {
- path: getEndpointDetailsPath({
- name: 'endpointDetails',
- selected_endpoint: details.agent.id,
- }),
- },
- ],
- },
- }
- );
-
- const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
-
- const detailsResultsPolicy = useMemo(() => {
- return [
{
title: i18n.translate('xpack.securitySolution.endpoint.details.policy', {
- defaultMessage: 'Integration Policy',
+ defaultMessage: 'Policy',
}),
description: (
@@ -198,7 +160,7 @@ export const EndpointDetails = memo(
},
{
title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', {
- defaultMessage: 'Policy Response',
+ defaultMessage: 'Policy Status',
}),
description: (
// https://github.com/elastic/eui/issues/4530
@@ -210,7 +172,7 @@ export const EndpointDetails = memo(
onClick={policyStatusClickHandler}
onClickAriaLabel={i18n.translate(
'xpack.securitySolution.endpoint.details.policyStatus',
- { defaultMessage: 'Policy Response' }
+ { defaultMessage: 'Policy Status' }
)}
>
@@ -223,10 +185,12 @@ export const EndpointDetails = memo(
),
},
- ];
- }, [details, policyResponseUri, policyStatus, policyStatusClickHandler, policyInfo]);
- const detailsResultsLower = useMemo(() => {
- return [
+ {
+ title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', {
+ defaultMessage: 'Endpoint Version',
+ }),
+ description: {details.agent.version} ,
+ },
{
title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', {
defaultMessage: 'IP Address',
@@ -241,70 +205,23 @@ export const EndpointDetails = memo(
),
},
- {
- title: i18n.translate('xpack.securitySolution.endpoint.details.hostname', {
- defaultMessage: 'Hostname',
- }),
- description: {details.host.hostname} ,
- },
- {
- title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', {
- defaultMessage: 'Endpoint Version',
- }),
- description: {details.agent.version} ,
- },
];
- }, [details.agent.version, details.host.hostname, details.host.ip]);
+ }, [
+ details,
+ hostStatus,
+ policyResponseUri,
+ policyStatus,
+ policyStatusClickHandler,
+ policyInfo,
+ ]);
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
>
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
index c39a17e98c76ab..7892c56fef8069 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
@@ -166,14 +166,14 @@ export const EndpointDetailsFlyout = memo(() => {
style={{ zIndex: 4001 }}
data-test-subj="endpointDetailsFlyout"
size="m"
- paddingSize="m"
+ paddingSize="l"
>
-
+
{hostDetailsLoading ? (
) : (
-
+
),
},
+ {
+ icon: 'gear',
+ key: 'agentPolicyReassignLink',
+ 'data-test-subj': 'agentPolicyReassignLink',
+ navigateAppId: 'fleet',
+ navigateOptions: {
+ path: `#${
+ pagePathGetters.fleet_agent_details({
+ agentId: fleetAgentId,
+ })[1]
+ }/activity?openReassignFlyout=true`,
+ },
+ href: `${getUrlForApp('fleet')}#${
+ pagePathGetters.fleet_agent_details({
+ agentId: fleetAgentId,
+ })[1]
+ }/activity?openReassignFlyout=true`,
+ children: (
+
+ ),
+ },
];
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index aa1c47a3102d90..86f1e32e751eeb 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -516,7 +516,6 @@ describe('when on the endpoint list page', () => {
describe('when there is a selected host in the url', () => {
let hostDetails: HostInfo;
- let elasticAgentId: string;
let renderAndWaitForData: () => Promise>;
const mockEndpointListApi = (mockedPolicyResponse?: HostPolicyResponse) => {
const {
@@ -546,8 +545,6 @@ describe('when on the endpoint list page', () => {
query_strategy_version,
};
- elasticAgentId = hostDetails.metadata.elastic.agent.id;
-
const policy = docGenerator.generatePolicyPackagePolicy();
policy.id = hostDetails.metadata.Endpoint.policy.applied.id;
@@ -738,37 +735,11 @@ describe('when on the endpoint list page', () => {
);
});
- it('should include the link to reassignment in Ingest', async () => {
- coreStart.application.getUrlForApp.mockReturnValue('/app/fleet');
- const renderResult = await renderAndWaitForData();
- const linkToReassign = await renderResult.findByTestId('endpointDetailsLinkToIngest');
- expect(linkToReassign).not.toBeNull();
- expect(linkToReassign.textContent).toEqual('Reassign Policy');
- expect(linkToReassign.getAttribute('href')).toEqual(
- `/app/fleet#/fleet/agents/${elasticAgentId}/activity?openReassignFlyout=true`
- );
- });
-
it('should show the Take Action button', async () => {
const renderResult = await renderAndWaitForData();
expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull();
});
- describe('when link to reassignment in Ingest is clicked', () => {
- beforeEach(async () => {
- coreStart.application.getUrlForApp.mockReturnValue('/app/fleet');
- const renderResult = await renderAndWaitForData();
- const linkToReassign = await renderResult.findByTestId('endpointDetailsLinkToIngest');
- reactTestingLibrary.act(() => {
- reactTestingLibrary.fireEvent.click(linkToReassign);
- });
- });
-
- it('should navigate to Ingest without full page refresh', () => {
- expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1);
- });
- });
-
describe('when showing host Policy Response panel', () => {
let renderResult: ReturnType;
beforeEach(async () => {
@@ -1139,5 +1110,12 @@ describe('when on the endpoint list page', () => {
const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink');
expect(agentDetailsLink.getAttribute('href')).toEqual(`/app/fleet#/fleet/agents/${agentId}`);
});
+
+ it('navigates to the Ingest Agent Details page with policy reassign', async () => {
+ const agentPolicyReassignLink = await renderResult.findByTestId('agentPolicyReassignLink');
+ expect(agentPolicyReassignLink.getAttribute('href')).toEqual(
+ `/app/fleet#/fleet/agents/${agentId}/activity?openReassignFlyout=true`
+ );
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
index d4e81fd8126687..fef6ccb99a17a2 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
@@ -59,6 +59,13 @@ export const getListItems: EventFiltersSelector<
return apiResponseData?.data || [];
});
+export const getTotalCountListItems: EventFiltersSelector> = createSelector(
+ getListApiSuccessResponse,
+ (apiResponseData) => {
+ return apiResponseData?.total || 0;
+ }
+);
+
/**
* Will return the query that was used with the currently displayed list of content. If a new page
* of content is being loaded, this selector will then attempt to use the previousState to return
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
index ac2b16e51603c9..9d2d3c394c4166 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
@@ -17,6 +17,7 @@ import {
getCurrentListPageDataState,
getListApiSuccessResponse,
getListItems,
+ getTotalCountListItems,
getCurrentListItemsQuery,
getListPagination,
getListFetchError,
@@ -120,6 +121,19 @@ describe('event filters selectors', () => {
});
});
+ describe('getTotalCountListItems()', () => {
+ it('should return the list items from api response', () => {
+ setToLoadedState();
+ expect(getTotalCountListItems(initialState)).toEqual(
+ getLastLoadedResourceState(initialState.listPage.data)?.data.content.total
+ );
+ });
+
+ it('should return empty array if no api response', () => {
+ expect(getTotalCountListItems(initialState)).toEqual(0);
+ });
+ });
+
describe('getCurrentListItemsQuery()', () => {
it('should return empty object if Uninitialized', () => {
expect(getCurrentListItemsQuery(initialState)).toEqual({});
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
index 00ee80c5d70223..0975104f02297f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
@@ -32,6 +32,7 @@ import {
getActionError,
getFormEntry,
showDeleteModal,
+ getTotalCountListItems,
} from '../store/selector';
import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content';
import { Immutable, ListPageRouteState } from '../../../../../common/endpoint/types';
@@ -66,6 +67,7 @@ export const EventFiltersListPage = memo(() => {
const isActionError = useEventFiltersSelector(getActionError);
const formEntry = useEventFiltersSelector(getFormEntry);
const listItems = useEventFiltersSelector(getListItems);
+ const totalCountListItems = useEventFiltersSelector(getTotalCountListItems);
const pagination = useEventFiltersSelector(getListPagination);
const isLoading = useEventFiltersSelector(getListIsLoading);
const fetchError = useEventFiltersSelector(getListFetchError);
@@ -235,7 +237,7 @@ export const EventFiltersListPage = memo(() => {
diff --git a/x-pack/plugins/snapshot_restore/public/application/app.tsx b/x-pack/plugins/snapshot_restore/public/application/app.tsx
index 0c064d448105fc..a7993300079ab2 100644
--- a/x-pack/plugins/snapshot_restore/public/application/app.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/app.tsx
@@ -10,14 +10,16 @@ import { Redirect, Route, Switch } from 'react-router-dom';
import { EuiPageContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { APP_WRAPPER_CLASS } from '../../../../../src/core/public';
+
import { APP_REQUIRED_CLUSTER_PRIVILEGES } from '../../common';
import {
useAuthorizationContext,
- SectionError,
+ PageError,
WithPrivileges,
NotAuthorizedSection,
} from '../shared_imports';
-import { SectionLoading } from './components';
+import { PageLoading } from './components';
import { DEFAULT_SECTION, Section } from './constants';
import {
RepositoryAdd,
@@ -42,7 +44,7 @@ export const App: React.FunctionComponent = () => {
const sectionsRegex = sections.join('|');
return apiError ? (
- {
`cluster.${name}`)}>
{({ isLoading, hasPrivileges, privilegesMissing }) =>
isLoading ? (
-
+
-
+
) : hasPrivileges ? (
-
+
@@ -84,7 +86,7 @@ export const App: React.FunctionComponent = () => {
) : (
-
+
= ({ children, ...rest }) => {
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export const SectionLoading: React.FunctionComponent = ({ children }) => {
+ return (
+ }
+ body={{children} }
+ data-test-subj="sectionLoading"
+ />
+ );
+};
+
+/*
+ * Loading component used for full page loads.
+ * For tabbed sections, or within the context of a wizard,
+ * the component may be more appropriate
+ */
+export const PageLoading: React.FunctionComponent = ({ children }) => {
+ return (
+
+ }
+ body={{children} }
+ data-test-subj="sectionLoading"
+ />
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx
index 6443d774c9ac7b..06c65a17136927 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx
@@ -30,7 +30,7 @@ import { useCore, useServices } from '../../../app_context';
import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants';
import { useLoadRepositories } from '../../../services/http';
import { linkToAddRepository } from '../../../services/navigation';
-import { SectionLoading } from '../../';
+import { InlineLoading } from '../../';
import { StepProps } from './';
import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public';
@@ -174,12 +174,12 @@ export const PolicyStepLogistics: React.FunctionComponent = ({
const renderRepositorySelect = () => {
if (isLoadingRepositories) {
return (
-
+
-
+
);
}
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/section_loading.tsx b/x-pack/plugins/snapshot_restore/public/application/components/section_loading.tsx
deleted file mode 100644
index c1548ad960bb08..00000000000000
--- a/x-pack/plugins/snapshot_restore/public/application/components/section_loading.tsx
+++ /dev/null
@@ -1,48 +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 {
- EuiEmptyPrompt,
- EuiLoadingSpinner,
- EuiText,
- EuiFlexGroup,
- EuiFlexItem,
- EuiTextColor,
-} from '@elastic/eui';
-
-interface Props {
- inline?: boolean;
- children: React.ReactNode;
- [key: string]: any;
-}
-
-export const SectionLoading: React.FunctionComponent = ({ inline, children, ...rest }) => {
- if (inline) {
- return (
-
-
-
-
-
-
- {children}
-
-
-
- );
- }
-
- return (
- }
- body={{children} }
- data-test-subj="sectionLoading"
- />
- );
-};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx
index e4a23bac636d8f..211d30181c25c6 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx
@@ -9,18 +9,7 @@ import React, { useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPageBody,
- EuiPageContent,
- EuiSpacer,
- EuiTab,
- EuiTabs,
- EuiTitle,
- EuiText,
-} from '@elastic/eui';
+import { EuiButtonEmpty, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { BASE_PATH, Section } from '../../constants';
import { useConfig, useCore } from '../../app_context';
@@ -100,79 +89,65 @@ export const SnapshotRestoreHome: React.FunctionComponent
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
-
-
-
-
-
-
- {tabs.map((tab) => (
- onSectionChange(tab.id)}
- isSelected={tab.id === section}
- key={tab.id}
- data-test-subj={tab.id.toLowerCase() + '_tab'}
- >
- {tab.name}
-
- ))}
-
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ description={
+
+ }
+ tabs={tabs.map((tab) => ({
+ onClick: () => onSectionChange(tab.id),
+ isSelected: tab.id === section,
+ key: tab.id,
+ 'data-test-subj': tab.id.toLowerCase() + '_tab',
+ label: tab.name,
+ }))}
+ />
-
+
-
-
- {/* We have two separate SnapshotList routes because repository names could have slashes in
- * them. This would break a route with a path like snapshots/:repositoryName?/:snapshotId*
- */}
-
-
-
-
-
-
-
+
+
+ {/* We have two separate SnapshotList routes because repository names could have slashes in
+ * them. This would break a route with a path like snapshots/:repositoryName?/:snapshotId*
+ */}
+
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
index 2bad30b95081d2..0a283d406e5aad 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
@@ -40,6 +40,7 @@ import { linkToEditPolicy, linkToSnapshot } from '../../../../services/navigatio
import {
SectionLoading,
+ InlineLoading,
PolicyExecuteProvider,
PolicyDeleteProvider,
} from '../../../../components';
@@ -318,7 +319,7 @@ export const PolicyDetails: React.FunctionComponent = ({
{policyDetails && policyDetails.policy && policyDetails.policy.inProgress ? (
<>
-
+
= ({
values={{ snapshotName: policyDetails.policy.inProgress.snapshotName }}
/>
-
+
>
) : null}
{renderTabs()}
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
index 7b1c10ec59e8ab..3927b73abf0936 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
@@ -9,13 +9,13 @@ import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { RouteComponentProps } from 'react-router-dom';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui';
import { SlmPolicyPayload } from '../../../../common/types';
import { TIME_UNITS } from '../../../../common';
-import { SectionError, Error } from '../../../shared_imports';
+import { SectionError, PageError } from '../../../shared_imports';
-import { PolicyForm, SectionLoading } from '../../components';
+import { PolicyForm, PageLoading } from '../../components';
import { BASE_PATH, DEFAULT_POLICY_SCHEDULE } from '../../constants';
import { breadcrumbService, docTitleService } from '../../services/navigation';
import { addPolicy, useLoadIndices } from '../../services/http';
@@ -87,49 +87,57 @@ export const PolicyAdd: React.FunctionComponent = ({
setSaveError(null);
};
+ if (isLoadingIndices) {
+ return (
+
+
+
+ );
+ }
+
+ if (errorLoadingIndices) {
+ return (
+
+ }
+ error={errorLoadingIndices}
+ />
+ );
+ }
+
return (
-
-
-
-
+
+
-
-
-
- {isLoadingIndices ? (
-
-
-
- ) : errorLoadingIndices ? (
-
- }
- error={errorLoadingIndices as Error}
- />
- ) : (
-
- )}
-
-
+
+ }
+ />
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx
index 0ad19028457706..4ab0f15cc55230 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx
@@ -9,12 +9,12 @@ import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { RouteComponentProps } from 'react-router-dom';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle, EuiCallOut } from '@elastic/eui';
+import { EuiPageContentBody, EuiPageHeader, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { SlmPolicyPayload } from '../../../../common/types';
-import { SectionError, Error } from '../../../shared_imports';
+import { SectionError, Error, PageError } from '../../../shared_imports';
import { useDecodedParams } from '../../lib';
import { TIME_UNITS } from '../../../../common/constants';
-import { SectionLoading, PolicyForm } from '../../components';
+import { PageLoading, PolicyForm } from '../../components';
import { BASE_PATH } from '../../constants';
import { useServices } from '../../app_context';
import { breadcrumbService, docTitleService } from '../../services/navigation';
@@ -106,21 +106,39 @@ export const PolicyEdit: React.FunctionComponent {
+ return saveError ? (
+
+ }
+ error={saveError}
+ />
+ ) : null;
+ };
+
+ const clearSaveError = () => {
+ setSaveError(null);
+ };
+
const renderLoading = () => {
- return errorLoadingPolicy ? (
-
+ return isLoadingPolicy ? (
+
-
+
) : (
-
+
-
+
);
};
@@ -139,8 +157,9 @@ export const PolicyEdit: React.FunctionComponent
- }
- error={errorLoadingIndices as Error}
- />
- );
- }
- };
-
- const renderSaveError = () => {
- return saveError ? (
-
}
- error={saveError}
+ error={errorLoadingIndices as Error}
/>
- ) : null;
- };
-
- const clearSaveError = () => {
- setSaveError(null);
+ );
};
- const renderContent = () => {
- if (isLoadingPolicy || isLoadingIndices) {
- return renderLoading();
- }
- if (errorLoadingPolicy || errorLoadingIndices) {
- return renderError();
- }
+ if (isLoadingPolicy || isLoadingIndices) {
+ return renderLoading();
+ }
- return (
- <>
- {policy.isManagedPolicy ? (
- <>
-
- }
- />
-
- >
- ) : null}
-
- >
- );
- };
+ if (errorLoadingPolicy || errorLoadingIndices) {
+ return renderError();
+ }
return (
-
-
-
-
+
+
-
-
-
- {renderContent()}
-
-
+
+ }
+ />
+
+
+ {policy.isManagedPolicy ? (
+ <>
+
+ }
+ />
+
+ >
+ ) : null}
+
+
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx
index 343c0b60a2253d..100d345a49c4d2 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx
@@ -10,7 +10,7 @@ import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { RouteComponentProps } from 'react-router-dom';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui';
import { Repository, EmptyRepository } from '../../../../common/types';
import { SectionError } from '../../../shared_imports';
@@ -79,25 +79,27 @@ export const RepositoryAdd: React.FunctionComponent = ({
};
return (
-
-
-
-
+
+
-
-
-
-
-
-
+
+ }
+ />
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx
index e27dd255f3bdf2..9ecd1d0e3fafe9 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx
@@ -5,15 +5,15 @@
* 2.0.
*/
-import React, { useEffect, useState, Fragment } from 'react';
+import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { RouteComponentProps } from 'react-router-dom';
-import { EuiCallOut, EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiCallOut, EuiPageContentBody, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { Repository, EmptyRepository } from '../../../../common/types';
-import { SectionError, Error } from '../../../shared_imports';
-import { RepositoryForm, SectionLoading } from '../../components';
+import { PageError, SectionError, Error } from '../../../shared_imports';
+import { RepositoryForm, PageLoading } from '../../components';
import { BASE_PATH, Section } from '../../constants';
import { useServices } from '../../app_context';
import { breadcrumbService, docTitleService } from '../../services/navigation';
@@ -79,12 +79,12 @@ export const RepositoryEdit: React.FunctionComponent {
return (
-
+
-
+
);
};
@@ -106,7 +106,7 @@ export const RepositoryEdit: React.FunctionComponent {
+ setSaveError(null);
+ };
+
const renderSaveError = () => {
return saveError ? (
{
- setSaveError(null);
- };
-
- const renderContent = () => {
- if (loadingRepository) {
- return renderLoading();
- }
- if (repositoryError) {
- return renderError();
- }
-
- const { isManagedRepository } = repositoryData;
-
- return (
-
- {isManagedRepository ? (
-
-
- }
- />
-
-
- ) : null}
-
-
- );
- };
-
return (
-
-
-
-
+
+
-
-
-
- {renderContent()}
-
-
+
+ }
+ />
+
+
+
+ {isManagedRepository ? (
+ <>
+
+ }
+ />
+
+ >
+ ) : null}
+
+
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx
index 685f3c9346f49c..0f950ef3234ba2 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx
@@ -8,12 +8,12 @@
import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { RouteComponentProps } from 'react-router-dom';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { SnapshotDetails, RestoreSettings } from '../../../../common/types';
-import { SectionError, Error } from '../../../shared_imports';
+import { SectionError, Error, PageError } from '../../../shared_imports';
import { BASE_PATH } from '../../constants';
-import { SectionLoading, RestoreSnapshotForm } from '../../components';
+import { PageLoading, RestoreSnapshotForm } from '../../components';
import { useServices } from '../../app_context';
import { breadcrumbService, docTitleService } from '../../services/navigation';
import { useLoadSnapshot, executeRestore } from '../../services/http';
@@ -76,12 +76,12 @@ export const RestoreSnapshot: React.FunctionComponent {
return (
-
+
-
+
);
};
@@ -103,8 +103,9 @@ export const RestoreSnapshot: React.FunctionComponent {
- if (loadingSnapshot) {
- return renderLoading();
- }
- if (snapshotError) {
- return renderError();
- }
+ if (loadingSnapshot) {
+ return renderLoading();
+ }
- return (
-
- );
- };
+ if (snapshotError) {
+ return renderError();
+ }
return (
-
-
-
-
+
+
-
-
-
- {renderContent()}
-
-
+
+ }
+ />
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts
index c38f0daedf9964..759453edaba5db 100644
--- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts
+++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts
@@ -12,6 +12,7 @@ export {
Frequency,
NotAuthorizedSection,
SectionError,
+ PageError,
sendRequest,
SendRequestConfig,
SendRequestResponse,
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 0c419982bdfbc8..c59b1c85869d99 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8930,15 +8930,6 @@
"xpack.fleet.agentStatus.offlineLabel": "オフライン",
"xpack.fleet.agentStatus.unhealthyLabel": "異常",
"xpack.fleet.agentStatus.updatingLabel": "更新中",
- "xpack.fleet.alphaMessageDescription": "Fleet は本番環境用ではありません。",
- "xpack.fleet.alphaMessageLinkText": "詳細を参照してください。",
- "xpack.fleet.alphaMessageTitle": "ベータリリース",
- "xpack.fleet.alphaMessaging.docsLink": "ドキュメンテーション",
- "xpack.fleet.alphaMessaging.feedbackText": "{docsLink}をご覧ください。質問やフィードバックについては、{forumLink}にアクセスしてください。",
- "xpack.fleet.alphaMessaging.flyoutTitle": "このリリースについて",
- "xpack.fleet.alphaMessaging.forumLink": "ディスカッションフォーラム",
- "xpack.fleet.alphaMessaging.introText": "Fleet は開発中であり、本番環境用ではありません。このベータリリースは、ユーザーが Fleet と新しい Elastic エージェントをテストしてフィードバックを提供することを目的としています。このプラグインには、サポート SLA が適用されません。",
- "xpack.fleet.alphaMessging.closeFlyoutLabel": "閉じる",
"xpack.fleet.appNavigation.agentsLinkText": "エージェント",
"xpack.fleet.appNavigation.dataStreamsLinkText": "データストリーム",
"xpack.fleet.appNavigation.overviewLinkText": "概要",
@@ -11965,7 +11956,6 @@
"xpack.ingestPipelines.deleteModal.multipleErrorsNotificationMessageText": "{count}件のパイプラインの削除エラー",
"xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText": "パイプライン'{pipelineName}'を削除しました",
"xpack.ingestPipelines.edit.docsButtonLabel": "パイプラインドキュメントを編集",
- "xpack.ingestPipelines.edit.fetchPipelineError": "パイプラインの読み込みエラー",
"xpack.ingestPipelines.edit.loadingPipelinesDescription": "パイプラインを読み込んでいます…",
"xpack.ingestPipelines.edit.pageTitle": "パイプライン'{name}'を編集",
"xpack.ingestPipelines.form.cancelButtonLabel": "キャンセル",
@@ -11988,8 +11978,6 @@
"xpack.ingestPipelines.form.versionFieldLabel": "バージョン (任意) ",
"xpack.ingestPipelines.form.versionToggleDescription": "バージョン番号を追加",
"xpack.ingestPipelines.list.listTitle": "Ingestノードパイプライン",
- "xpack.ingestPipelines.list.loadErrorReloadLinkLabel": "再試行してください。",
- "xpack.ingestPipelines.list.loadErrorTitle": "パイプラインを読み込めません。{reloadLink}",
"xpack.ingestPipelines.list.loadingMessage": "パイプラインを読み込み中...",
"xpack.ingestPipelines.list.notFoundFlyoutMessage": "パイプラインが見つかりません",
"xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel": "クローンを作成",
@@ -17685,7 +17673,6 @@
"xpack.remoteClusters.form.errors.serverNameMissing": "サーバー名が必要です。",
"xpack.remoteClusters.licenseCheckErrorMessage": "ライセンス確認失敗",
"xpack.remoteClusters.listBreadcrumbTitle": "リモートクラスター",
- "xpack.remoteClusters.loadAction.errorTitle": "リモートクラスターの読み込み中にエラーが発生",
"xpack.remoteClusters.readDocsButtonLabel": "リモートクラスタードキュメント",
"xpack.remoteClusters.refreshAction.errorTitle": "リモートクラスターの更新中にエラーが発生",
"xpack.remoteClusters.remoteClusterForm.actions.savingText": "保存中",
@@ -20144,10 +20131,8 @@
"xpack.securitySolution.endpoint.details.endpointVersion": "エンドポイントバージョン",
"xpack.securitySolution.endpoint.details.errorBody": "フライアウトを終了して、利用可能なホストを選択してください。",
"xpack.securitySolution.endpoint.details.errorTitle": "ホストが見つかりませんでした",
- "xpack.securitySolution.endpoint.details.hostname": "ホスト名",
"xpack.securitySolution.endpoint.details.ipAddress": "IPアドレス",
"xpack.securitySolution.endpoint.details.lastSeen": "前回の認識",
- "xpack.securitySolution.endpoint.details.linkToIngestTitle": "ポリシーの再割り当て",
"xpack.securitySolution.endpoint.details.noPolicyResponse": "ポリシー応答がありません",
"xpack.securitySolution.endpoint.details.os": "OS",
"xpack.securitySolution.endpoint.details.policy": "統合ポリシー",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 59046e98a8f3e9..529a24042d209c 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9010,15 +9010,6 @@
"xpack.fleet.agentStatus.offlineLabel": "脱机",
"xpack.fleet.agentStatus.unhealthyLabel": "运行不正常",
"xpack.fleet.agentStatus.updatingLabel": "正在更新",
- "xpack.fleet.alphaMessageDescription": "不推荐在生产环境中使用 Fleet。",
- "xpack.fleet.alphaMessageLinkText": "查看更多详情。",
- "xpack.fleet.alphaMessageTitle": "公测版",
- "xpack.fleet.alphaMessaging.docsLink": "文档",
- "xpack.fleet.alphaMessaging.feedbackText": "阅读我们的{docsLink}或前往我们的{forumLink},以了解问题或提供反馈。",
- "xpack.fleet.alphaMessaging.flyoutTitle": "关于本版本",
- "xpack.fleet.alphaMessaging.forumLink": "讨论论坛",
- "xpack.fleet.alphaMessaging.introText": "Fleet 仍处于开发状态,不适用于生产环境。此公测版用于用户测试 Fleet 和新 Elastic 代理并提供相关反馈。此插件不受支持 SLA 的约束。",
- "xpack.fleet.alphaMessging.closeFlyoutLabel": "关闭",
"xpack.fleet.appNavigation.agentsLinkText": "代理",
"xpack.fleet.appNavigation.dataStreamsLinkText": "数据流",
"xpack.fleet.appNavigation.overviewLinkText": "概览",
@@ -12126,7 +12117,6 @@
"xpack.ingestPipelines.deleteModal.successDeleteMultipleNotificationMessageText": "已删除 {numSuccesses, plural, other {# 个管道}}",
"xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText": "已删除管道“{pipelineName}”",
"xpack.ingestPipelines.edit.docsButtonLabel": "编辑管道文档",
- "xpack.ingestPipelines.edit.fetchPipelineError": "加载管道时出错",
"xpack.ingestPipelines.edit.loadingPipelinesDescription": "正在加载管道……",
"xpack.ingestPipelines.edit.pageTitle": "编辑管道“{name}”",
"xpack.ingestPipelines.form.cancelButtonLabel": "取消",
@@ -12151,8 +12141,6 @@
"xpack.ingestPipelines.form.versionFieldLabel": "版本 (可选) ",
"xpack.ingestPipelines.form.versionToggleDescription": "添加版本号",
"xpack.ingestPipelines.list.listTitle": "采集节点管道",
- "xpack.ingestPipelines.list.loadErrorReloadLinkLabel": "请重试。",
- "xpack.ingestPipelines.list.loadErrorTitle": "无法加载管道。{reloadLink}",
"xpack.ingestPipelines.list.loadingMessage": "正在加载管道……",
"xpack.ingestPipelines.list.notFoundFlyoutMessage": "未找到管道",
"xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel": "克隆",
@@ -17924,7 +17912,6 @@
"xpack.remoteClusters.form.errors.serverNameMissing": "服务器名必填。",
"xpack.remoteClusters.licenseCheckErrorMessage": "许可证检查失败",
"xpack.remoteClusters.listBreadcrumbTitle": "远程集群",
- "xpack.remoteClusters.loadAction.errorTitle": "加载远程集群时出错",
"xpack.remoteClusters.readDocsButtonLabel": "远程集群文档",
"xpack.remoteClusters.refreshAction.errorTitle": "刷新远程集群时出错",
"xpack.remoteClusters.remoteClusterForm.actions.savingText": "正在保存",
@@ -20442,10 +20429,8 @@
"xpack.securitySolution.endpoint.details.endpointVersion": "终端版本",
"xpack.securitySolution.endpoint.details.errorBody": "请退出浮出控件并选择可用主机。",
"xpack.securitySolution.endpoint.details.errorTitle": "找不到主机",
- "xpack.securitySolution.endpoint.details.hostname": "主机名",
"xpack.securitySolution.endpoint.details.ipAddress": "IP 地址",
"xpack.securitySolution.endpoint.details.lastSeen": "最后看到时间",
- "xpack.securitySolution.endpoint.details.linkToIngestTitle": "重新分配策略",
"xpack.securitySolution.endpoint.details.noPolicyResponse": "没有可用策略响应",
"xpack.securitySolution.endpoint.details.os": "OS",
"xpack.securitySolution.endpoint.details.policy": "集成策略",
diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts
index c34da1bc097bc0..01fa320bbc789c 100644
--- a/x-pack/plugins/uptime/public/apps/plugin.ts
+++ b/x-pack/plugins/uptime/public/apps/plugin.ts
@@ -111,7 +111,7 @@ export class UptimePlugin
return [
{
label: 'Uptime',
- sortKey: 200,
+ sortKey: 500,
entries: [
{
label: i18n.translate('xpack.uptime.overview.heading', {
diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/sync.ts b/x-pack/test/api_integration/apis/ml/saved_objects/sync.ts
index e861dc528b2375..e8c940d6b29b63 100644
--- a/x-pack/test/api_integration/apis/ml/saved_objects/sync.ts
+++ b/x-pack/test/api_integration/apis/ml/saved_objects/sync.ts
@@ -6,6 +6,7 @@
*/
import expect from '@kbn/expect';
+import { cloneDeep } from 'lodash';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { USER } from '../../../../functional/services/ml/security_common';
import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api';
@@ -23,7 +24,7 @@ export default ({ getService }: FtrProviderContext) => {
async function runRequest(user: USER, expectedStatusCode: number) {
const { body } = await supertest
- .get(`/s/space1/api/ml/saved_objects/sync`)
+ .get(`/s/${idSpace1}/api/ml/saved_objects/sync`)
.auth(user, ml.securityCommon.getPasswordForUser(user))
.set(COMMON_REQUEST_HEADERS)
.expect(expectedStatusCode);
@@ -32,9 +33,19 @@ export default ({ getService }: FtrProviderContext) => {
}
describe('GET saved_objects/sync', () => {
- before(async () => {
+ beforeEach(async () => {
await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] });
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ });
+ afterEach(async () => {
+ await spacesService.delete(idSpace1);
+ await ml.api.cleanMlIndices();
+ await ml.testResources.cleanMLSavedObjects();
+ });
+
+ it('should sync datafeeds and saved objects', async () => {
+ // prepare test data
await ml.api.createAnomalyDetectionJob(
ml.commonConfig.getADFqSingleMetricJobConfig(adJobId1),
idSpace1
@@ -51,18 +62,6 @@ export default ({ getService }: FtrProviderContext) => {
ml.commonConfig.getADFqSingleMetricJobConfig(adJobIdES)
);
- await ml.testResources.setKibanaTimeZoneToUTC();
- });
-
- after(async () => {
- await spacesService.delete(idSpace1);
- await ml.api.cleanMlIndices();
- await ml.testResources.cleanMLSavedObjects();
- });
-
- it('should sync datafeeds and saved objects', async () => {
- // prepare test data
-
// datafeed should be added with the request
const datafeedConfig2 = ml.commonConfig.getADFqDatafeedConfig(adJobId2);
await ml.api.createDatafeedES(datafeedConfig2);
@@ -89,7 +88,73 @@ export default ({ getService }: FtrProviderContext) => {
});
});
+ it('should sync datafeeds after recreation in ES with different name', async () => {
+ // prepare test data
+ const jobConfig1 = ml.commonConfig.getADFqSingleMetricJobConfig(adJobId1);
+ await ml.api.createAnomalyDetectionJob(jobConfig1, idSpace1);
+
+ // datafeed should be added with the request
+ const datafeedConfig1 = ml.commonConfig.getADFqDatafeedConfig(adJobId1);
+ await ml.api.createDatafeedES(datafeedConfig1);
+
+ // run the sync request and verify the response
+ const body = await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
+
+ // expect datafeed to be added
+ expect(body).to.eql({
+ datafeedsAdded: { [adJobId1]: { success: true } },
+ datafeedsRemoved: {},
+ savedObjectsCreated: {},
+ savedObjectsDeleted: {},
+ });
+
+ // delete the datafeed but do not sync
+ await ml.api.deleteDatafeedES(datafeedConfig1.datafeed_id);
+
+ // create a new datafeed with a different id
+ const datafeedConfig2 = cloneDeep(datafeedConfig1);
+ datafeedConfig2.datafeed_id = `different_${datafeedConfig2.datafeed_id}`;
+ await ml.api.createDatafeedES(datafeedConfig2);
+
+ const body2 = await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
+
+ // previous datafeed should be removed on first sync
+ expect(body2).to.eql({
+ datafeedsAdded: {},
+ datafeedsRemoved: { [adJobId1]: { success: true } },
+ savedObjectsCreated: {},
+ savedObjectsDeleted: {},
+ });
+
+ const body3 = await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
+
+ // new datafeed will be added on second sync
+ expect(body3).to.eql({
+ datafeedsAdded: { [adJobId1]: { success: true } },
+ datafeedsRemoved: {},
+ savedObjectsCreated: {},
+ savedObjectsDeleted: {},
+ });
+ });
+
it('should not sync anything if all objects are already synced', async () => {
+ await ml.api.createAnomalyDetectionJob(
+ ml.commonConfig.getADFqSingleMetricJobConfig(adJobId1),
+ idSpace1
+ );
+ await ml.api.createAnomalyDetectionJob(
+ ml.commonConfig.getADFqSingleMetricJobConfig(adJobId2),
+ idSpace1
+ );
+ await ml.api.createAnomalyDetectionJob(
+ ml.commonConfig.getADFqSingleMetricJobConfig(adJobId3),
+ idSpace1
+ );
+ await ml.api.createAnomalyDetectionJobES(
+ ml.commonConfig.getADFqSingleMetricJobConfig(adJobIdES)
+ );
+
+ await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
const body = await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
expect(body).to.eql({
diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/blended_vector_layer.js
index e207410eb22818..3cc7d8e07d623f 100644
--- a/x-pack/test/functional/apps/maps/blended_vector_layer.js
+++ b/x-pack/test/functional/apps/maps/blended_vector_layer.js
@@ -29,7 +29,7 @@ export default function ({ getPageObjects, getService }) {
it('should request documents when zoomed to smaller regions showing less data', async () => {
const { rawResponse: response } = await PageObjects.maps.getResponse();
// Allow a range of hits to account for variances in browser window size.
- expect(response.hits.hits.length).to.be.within(30, 40);
+ expect(response.hits.hits.length).to.be.within(35, 45);
});
it('should request clusters when zoomed to larger regions showing lots of data', async () => {
diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts
index 52ec81b8bf7dbe..0872abfcaa4f86 100644
--- a/x-pack/test/functional/apps/uptime/synthetics_integration.ts
+++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts
@@ -253,7 +253,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
]);
});
- it('allows configuring http advanced options', async () => {
+ it.skip('allows configuring http advanced options', async () => {
// This test ensures that updates made to the Synthetics Policy are carried all the way through
// to the generated Agent Policy that is dispatch down to the Elastic Agent.
const config = generateHTTPConfig('http://elastic.co');
diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts
index 63326448ec1e51..777e6fd598f454 100644
--- a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts
@@ -67,7 +67,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
let testJobId = '';
- describe('anomaly detection alert', function () {
+ // Failing: See https://github.com/elastic/kibana/issues/102012
+ describe.skip('anomaly detection alert', function () {
this.tags('ciGroup13');
before(async () => {
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts
index e55307ed5ef66f..1a5158adbd6951 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts
@@ -124,8 +124,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
describe('when the hostname is clicked on,', () => {
it('display the details flyout', async () => {
await (await testSubjects.find('hostnameCellLink')).click();
- await testSubjects.existOrFail('endpointDetailsUpperList');
- await testSubjects.existOrFail('endpointDetailsLowerList');
+ await testSubjects.existOrFail('endpointDetailsList');
});
it('updates the details flyout when a new hostname is selected from the list', async () => {
diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
index b678e88bcf0dff..502c950d2b1131 100644
--- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
+++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
@@ -15,6 +15,8 @@ const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat');
export default function ({ getService, getPageObjects, updateBaselines }) {
const screenshot = getService('screenshots');
const browser = getService('browser');
+ const find = getService('find');
+ const log = getService('log');
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']);
@@ -28,7 +30,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
'dashboard',
'view/Metricbeat-system-overview-ecs?_g=(filters:!(),refreshInterval:(pause:!t,value:0),' +
'time:(from:%272020-09-29T19:02:37.902Z%27,to:%272020-09-29T19:06:43.218Z%27))&_a=' +
- '(description:%27Overview%20of%20system%20metrics%27,filters:!(),fullScreenMode:!t,' +
+ '(description:%27Overview%20of%20system%20metrics%27,filters:!(),' +
'options:(darkTheme:!f),query:(language:kuery,query:%27%27),timeRestore:!f,' +
'title:%27%5BMetricbeat%20System%5D%20Overview%20ECS%27,viewMode:view)',
{
@@ -45,6 +47,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
// await PageObjects.dashboard.clickFullScreenMode();
await PageObjects.common.sleep(2000);
+ await find.clickByButtonText('Dismiss');
await PageObjects.dashboard.waitForRenderComplete();
await browser.setScreenshotSize(1000, 1000);
});
@@ -61,7 +64,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
);
expect(percentDifference).to.be.lessThan(0.01);
} finally {
- await PageObjects.dashboard.clickExitFullScreenLogoButton();
+ log.debug('### Screenshot taken');
}
});
});
diff --git a/yarn.lock b/yarn.lock
index 9d78a16dd3ee58..0418c2e2c39713 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9293,17 +9293,16 @@ chrome-trace-event@^1.0.2:
dependencies:
tslib "^1.9.0"
-chromedriver@^90.0.0:
- version "90.0.0"
- resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-90.0.0.tgz#1b18960a31a12884981bdc270b43c4356ce7a65a"
- integrity sha512-k+GMmNb7cmuCCctQvUIeNxDGSq8DJauO+UKQS2qLT8aA36CPEcv8rpFepf6lRkNaIlfwdCUt/0B5bZDw3wY2yw==
+chromedriver@^91.0.1:
+ version "91.0.1"
+ resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-91.0.1.tgz#4d70a569901e356c978a41de3019c464f2a8ebd0"
+ integrity sha512-9LktpHiUxM4UWUsr+jI1K1YKx2GENt6BKKJ2mibPj1Wc6ODzX/3fFIlr8CZ4Ftuyga+dHTTbAyPWKwKvybEbKA==
dependencies:
"@testim/chrome-version" "^1.0.7"
axios "^0.21.1"
del "^6.0.0"
extract-zip "^2.0.1"
https-proxy-agent "^5.0.0"
- mkdirp "^1.0.4"
proxy-from-env "^1.1.0"
tcp-port-used "^1.0.1"