-
+ {field ? (
+
+ ) : (
+
+ )}
|
{isCollapsible && (
)}
{displayUnderscoreWarning && }
+ {field ? null : {key}: }
void;
+ /**
+ * Counter how often data was fetched (used for testing)
+ */
+ fetchCounter: number;
+ /**
+ * Error in case of a failing document fetch
+ */
+ fetchError?: Error;
+ /**
+ * Statistics by fields calculated using the fetched documents
+ */
+ fieldCounts: Record ;
+ /**
+ * Histogram aggregation data
+ */
+ histogramData?: Chart;
+ /**
+ * Number of documents found by recent fetch
+ */
+ hits: number;
+ /**
+ * Current IndexPattern
+ */
+ indexPattern: IndexPattern;
+ /**
+ * Value needed for legacy "infinite" loading functionality
+ * Determins how much records are rendered using the legacy table
+ * Increased when scrolling down
+ */
+ minimumVisibleRows: number;
+ /**
+ * Function to add a column to state
+ */
+ onAddColumn: (column: string) => void;
+ /**
+ * Function to add a filter to state
+ */
+ onAddFilter: DocViewFilterFn;
+ /**
+ * Function to change the used time interval of the date histogram
+ */
+ onChangeInterval: (interval: string) => void;
+ /**
+ * Function to move a given column to a given index, used in legacy table
+ */
+ onMoveColumn: (columns: string, newIdx: number) => void;
+ /**
+ * Function to remove a given column from state
+ */
+ onRemoveColumn: (column: string) => void;
+ /**
+ * Function to replace columns in state
+ */
+ onSetColumns: (columns: string[]) => void;
+ /**
+ * Function to scroll down the legacy table to the bottom
+ */
+ onSkipBottomButtonClick: () => void;
+ /**
+ * Function to change sorting of the table, triggers a fetch
+ */
+ onSort: (sort: string[][]) => void;
+ opts: {
+ /**
+ * Date histogram aggregation config
+ */
+ chartAggConfigs?: AggConfigs;
+ /**
+ * Client of uiSettings
+ */
+ config: IUiSettingsClient;
+ /**
+ * Data plugin
+ */
+ data: DataPublicPluginStart;
+ /**
+ * Data plugin filter manager
+ */
+ filterManager: FilterManager;
+ /**
+ * List of available index patterns
+ */
+ indexPatternList: Array>;
+ /**
+ * The number of documents that can be displayed in the table/grid
+ */
+ sampleSize: number;
+ /**
+ * Current instance of SavedSearch
+ */
+ savedSearch: SavedSearch;
+ /**
+ * Function to set the header menu
+ */
+ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void;
+ /**
+ * Timefield of the currently used index pattern
+ */
+ timefield: string;
+ /**
+ * Function to set the current state
+ */
+ setAppState: (state: Partial) => void;
+ };
+ /**
+ * Function to reset the current query
+ */
+ resetQuery: () => void;
+ /**
+ * Current state of the actual query, one of 'uninitialized', 'loading' ,'ready', 'none'
+ */
+ resultState: string;
+ /**
+ * Array of document of the recent successful search request
+ */
+ rows: ElasticSearchHit[];
+ /**
+ * Instance of SearchSource, the high level search API
+ */
+ searchSource: ISearchSource;
+ /**
+ * Function to change the current index pattern
+ */
+ setIndexPattern: (id: string) => void;
+ /**
+ * Current app state of URL
+ */
+ state: AppState;
+ /**
+ * Function to update the time filter
+ */
+ timefilterUpdateHandler: (ranges: { from: number; to: number }) => void;
+ /**
+ * Currently selected time range
+ */
+ timeRange?: { from: string; to: string };
+ /**
+ * Menu data of top navigation (New, save ...)
+ */
+ topNavMenu: TopNavMenuData[];
+ /**
+ * Function to update the actual query
+ */
+ updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
+ /**
+ * Function to update the actual savedQuery id
+ */
+ updateSavedQueryId: (savedQueryId?: string) => void;
+}
diff --git a/src/plugins/discover/public/application/helpers/columns.test.ts b/src/plugins/discover/public/application/helpers/columns.test.ts
new file mode 100644
index 0000000000000..d455fd1f42c6d
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/columns.test.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { getDisplayedColumns } from './columns';
+import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield';
+import { indexPatternMock } from '../../__mocks__/index_pattern';
+
+describe('getDisplayedColumns', () => {
+ test('returns default columns given a index pattern without timefield', async () => {
+ const result = getDisplayedColumns([], indexPatternMock);
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ "_source",
+ ]
+ `);
+ });
+ test('returns default columns given a index pattern with timefield', async () => {
+ const result = getDisplayedColumns([], indexPatternWithTimefieldMock);
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ "_source",
+ ]
+ `);
+ });
+ test('returns default columns when just timefield is in state', async () => {
+ const result = getDisplayedColumns(['timestamp'], indexPatternWithTimefieldMock);
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ "_source",
+ ]
+ `);
+ });
+ test('returns columns given by argument, no fallback ', async () => {
+ const result = getDisplayedColumns(['test'], indexPatternWithTimefieldMock);
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ "test",
+ ]
+ `);
+ });
+});
diff --git a/src/plugins/discover/public/application/helpers/columns.ts b/src/plugins/discover/public/application/helpers/columns.ts
new file mode 100644
index 0000000000000..d2d47c932b7bd
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/columns.ts
@@ -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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+import { IndexPattern } from '../../../../data/common';
+
+/**
+ * Function to provide fallback when
+ * 1) no columns are given
+ * 2) Just one column is given, which is the configured timefields
+ */
+export function getDisplayedColumns(stateColumns: string[] = [], indexPattern: IndexPattern) {
+ return stateColumns &&
+ stateColumns.length > 0 &&
+ // check if all columns where removed except the configured timeField (this can't be removed)
+ !(stateColumns.length === 1 && stateColumns[0] === indexPattern.timeFieldName)
+ ? stateColumns
+ : ['_source'];
+}
diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts
index 1394ceab1dd18..ea16b81615e42 100644
--- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts
+++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts
@@ -6,7 +6,8 @@
* Public License, v 1.
*/
-import { getSharingData } from './get_sharing_data';
+import { Capabilities } from 'kibana/public';
+import { getSharingData, showPublicUrlSwitch } from './get_sharing_data';
import { IUiSettingsClient } from 'kibana/public';
import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks';
import { indexPatternMock } from '../../__mocks__/index_pattern';
@@ -68,3 +69,44 @@ describe('getSharingData', () => {
`);
});
});
+
+describe('showPublicUrlSwitch', () => {
+ test('returns false if "discover" app is not available', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(false);
+ });
+
+ test('returns false if "discover" app is not accessible', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ discover: {
+ show: false,
+ },
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(false);
+ });
+
+ test('returns true if "discover" app is not available an accessible', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ discover: {
+ show: true,
+ },
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(true);
+ });
+});
diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts
index 62478f1d2830f..1d780a5573e2a 100644
--- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts
+++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts
@@ -6,7 +6,7 @@
* Public License, v 1.
*/
-import { IUiSettingsClient } from 'kibana/public';
+import { Capabilities, IUiSettingsClient } from 'kibana/public';
import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
import { getSortForSearchSource } from '../angular/doc_table';
import { SearchSource } from '../../../../data/common';
@@ -76,3 +76,19 @@ export async function getSharingData(
indexPatternId: index.id,
};
}
+
+export interface DiscoverCapabilities {
+ createShortUrl?: boolean;
+ save?: boolean;
+ saveQuery?: boolean;
+ show?: boolean;
+ storeSearchSession?: boolean;
+}
+
+export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
+ if (!anonymousUserCapabilities.discover) return false;
+
+ const discover = (anonymousUserCapabilities.discover as unknown) as DiscoverCapabilities;
+
+ return !!discover.show;
+};
diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts
index b27426a6c0621..4eda742d967f4 100644
--- a/src/plugins/discover/public/get_inner_angular.ts
+++ b/src/plugins/discover/public/get_inner_angular.ts
@@ -42,7 +42,6 @@ import {
} from '../../kibana_legacy/public';
import { DiscoverStartPlugins } from './plugin';
import { getScopedHistory } from './kibana_services';
-import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive';
import { createDiscoverDirective } from './application/components/create_discover_directive';
/**
@@ -124,7 +123,6 @@ export function initializeInnerAngularModule(
.config(watchMultiDecorator)
.run(registerListenEventListener)
.directive('renderComplete', createRenderCompleteDirective)
- .directive('discoverLegacy', createDiscoverLegacyDirective)
.directive('discover', createDiscoverDirective);
}
diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md
index ec27895eed666..36c7d7119ffe5 100644
--- a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md
+++ b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md
@@ -96,11 +96,11 @@ setTimeout(() => {
}, 0);
```
-For cases, where granular control over URL updates is needed, `kbnUrlStateStorage` provides these advanced apis:
+For cases, where granular control over URL updates is needed, `kbnUrlStateStorage` exposes `kbnUrlStateStorage.kbnUrlControls` that exposes these advanced apis:
-- `kbnUrlStateStorage.flush({replace: boolean})` - allows to synchronously apply any pending updates.
- `replace` option allows to use `history.replace()` instead of `history.push()`. Returned boolean indicates if any update happened
-- `kbnUrlStateStorage.cancel()` - cancels any pending updates
+- `kbnUrlStateStorage.kbnUrlControls.flush({replace: boolean})` - allows to synchronously apply any pending updates.
+ `replace` option allows using `history.replace()` instead of `history.push()`.
+- `kbnUrlStateStorage.kbnUrlControls.cancel()` - cancels any pending updates.
### Sharing one `kbnUrlStateStorage` instance
diff --git a/src/plugins/kibana_utils/public/history/history_observable.test.ts b/src/plugins/kibana_utils/public/history/history_observable.test.ts
new file mode 100644
index 0000000000000..818c0d7739283
--- /dev/null
+++ b/src/plugins/kibana_utils/public/history/history_observable.test.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import {
+ createHistoryObservable,
+ createQueryParamObservable,
+ createQueryParamsObservable,
+} from './history_observable';
+import { createMemoryHistory, History } from 'history';
+import { ParsedQuery } from 'query-string';
+
+let history: History;
+
+beforeEach(() => {
+ history = createMemoryHistory();
+});
+
+test('createHistoryObservable', () => {
+ const obs$ = createHistoryObservable(history);
+ const emits: string[] = [];
+ obs$.subscribe(({ location }) => {
+ emits.push(location.pathname + location.search);
+ });
+
+ history.push('/test');
+ history.push('/');
+
+ expect(emits.length).toEqual(2);
+ expect(emits).toMatchInlineSnapshot(`
+ Array [
+ "/test",
+ "/",
+ ]
+ `);
+});
+
+test('createQueryParamsObservable', () => {
+ const obs$ = createQueryParamsObservable(history);
+ const emits: ParsedQuery[] = [];
+ obs$.subscribe((params) => {
+ emits.push(params);
+ });
+
+ history.push('/test');
+ history.push('/test?foo=bar');
+ history.push('/?foo=bar');
+ history.push('/test?foo=bar&foo1=bar1');
+
+ expect(emits.length).toEqual(2);
+ expect(emits).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "foo": "bar",
+ },
+ Object {
+ "foo": "bar",
+ "foo1": "bar1",
+ },
+ ]
+ `);
+});
+
+test('createQueryParamObservable', () => {
+ const obs$ = createQueryParamObservable(history, 'foo');
+ const emits: unknown[] = [];
+ obs$.subscribe((param) => {
+ emits.push(param);
+ });
+
+ history.push('/test');
+ history.push('/test?foo=bar');
+ history.push('/?foo=bar');
+ history.push('/test?foo=baaaar&foo1=bar1');
+ history.push('/test?foo1=bar1');
+
+ expect(emits.length).toEqual(3);
+ expect(emits).toMatchInlineSnapshot(`
+ Array [
+ "bar",
+ "baaaar",
+ null,
+ ]
+ `);
+});
diff --git a/src/plugins/kibana_utils/public/history/history_observable.ts b/src/plugins/kibana_utils/public/history/history_observable.ts
new file mode 100644
index 0000000000000..f02a5e340b1a0
--- /dev/null
+++ b/src/plugins/kibana_utils/public/history/history_observable.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { Action, History, Location } from 'history';
+import { Observable } from 'rxjs';
+import { ParsedQuery } from 'query-string';
+import deepEqual from 'fast-deep-equal';
+import { map } from 'rxjs/operators';
+import { getQueryParams } from './get_query_params';
+import { distinctUntilChangedWithInitialValue } from '../../common';
+
+/**
+ * Convert history.listen into an observable
+ * @param history - {@link History} instance
+ */
+export function createHistoryObservable(
+ history: History
+): Observable<{ location: Location; action: Action }> {
+ return new Observable((observer) => {
+ const unlisten = history.listen((location, action) => observer.next({ location, action }));
+ return () => {
+ unlisten();
+ };
+ });
+}
+
+/**
+ * Create an observable that emits every time any of query params change.
+ * Uses deepEqual check.
+ * @param history - {@link History} instance
+ */
+export function createQueryParamsObservable(history: History): Observable {
+ return createHistoryObservable(history).pipe(
+ map(({ location }) => ({ ...getQueryParams(location) })),
+ distinctUntilChangedWithInitialValue({ ...getQueryParams(history.location) }, deepEqual)
+ );
+}
+
+/**
+ * Create an observable that emits every time _paramKey_ changes
+ * @param history - {@link History} instance
+ * @param paramKey - query param key to observe
+ */
+export function createQueryParamObservable(
+ history: History,
+ paramKey: string
+): Observable {
+ return createQueryParamsObservable(history).pipe(
+ map((params) => (params[paramKey] ?? null) as Param | null),
+ distinctUntilChangedWithInitialValue(
+ (getQueryParams(history.location)[paramKey] ?? null) as Param | null,
+ deepEqual
+ )
+ );
+}
diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts
index 4b1b610d560e2..b2ac9ed6c739e 100644
--- a/src/plugins/kibana_utils/public/history/index.ts
+++ b/src/plugins/kibana_utils/public/history/index.ts
@@ -9,3 +9,4 @@
export { removeQueryParam } from './remove_query_param';
export { redirectWhenMissing } from './redirect_when_missing';
export { getQueryParams } from './get_query_params';
+export * from './history_observable';
diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts
index fa9cf5a52371d..29936da0117c1 100644
--- a/src/plugins/kibana_utils/public/index.ts
+++ b/src/plugins/kibana_utils/public/index.ts
@@ -68,7 +68,14 @@ export {
StopSyncStateFnType,
} from './state_sync';
export { Configurable, CollectConfigProps } from './ui';
-export { removeQueryParam, redirectWhenMissing, getQueryParams } from './history';
+export {
+ removeQueryParam,
+ redirectWhenMissing,
+ getQueryParams,
+ createQueryParamsObservable,
+ createHistoryObservable,
+ createQueryParamObservable,
+} from './history';
export { applyDiff } from './state_management/utils/diff_object';
export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter';
diff --git a/src/plugins/kibana_utils/public/state_sync/public.api.md b/src/plugins/kibana_utils/public/state_sync/public.api.md
index a4dfea82cdb59..5524563c034a8 100644
--- a/src/plugins/kibana_utils/public/state_sync/public.api.md
+++ b/src/plugins/kibana_utils/public/state_sync/public.api.md
@@ -22,14 +22,12 @@ export const createSessionStorageStateStorage: (storage?: Storage) => ISessionSt
// @public
export interface IKbnUrlStateStorage extends IStateStorage {
- cancel: () => void;
// (undocumented)
change$: (key: string) => Observable;
- flush: (opts?: {
- replace?: boolean;
- }) => boolean;
// (undocumented)
get: (key: string) => State | null;
+ // Warning: (ae-forgotten-export) The symbol "IKbnUrlControls" needs to be exported by the entry point index.d.ts
+ kbnUrlControls: IKbnUrlControls;
// (undocumented)
set: (key: string, state: State, opts?: {
replace: boolean;
diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
index c7f04bc9cdbe3..890de8f6ed6a1 100644
--- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
+++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
@@ -255,7 +255,7 @@ describe('state_sync', () => {
expect(history.length).toBe(startHistoryLength);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
- urlSyncStrategy.flush();
+ urlSyncStrategy.kbnUrlControls.flush();
expect(history.length).toBe(startHistoryLength + 1);
expect(getCurrentUrl()).toMatchInlineSnapshot(
@@ -290,7 +290,7 @@ describe('state_sync', () => {
expect(history.length).toBe(startHistoryLength);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
- urlSyncStrategy.cancel();
+ urlSyncStrategy.kbnUrlControls.cancel();
expect(history.length).toBe(startHistoryLength);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts
index fbd3c3f933791..037c6f9fc666d 100644
--- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts
+++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts
@@ -39,11 +39,11 @@ describe('KbnUrlStateStorage', () => {
const key = '_s';
urlStateStorage.set(key, state);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
- expect(urlStateStorage.flush()).toBe(true);
+ expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(true);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_s=(ok:1,test:test)"`);
expect(urlStateStorage.get(key)).toEqual(state);
- expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update
+ expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(false); // nothing to flush, not update
});
it('should cancel url updates', async () => {
@@ -51,7 +51,7 @@ describe('KbnUrlStateStorage', () => {
const key = '_s';
const pr = urlStateStorage.set(key, state);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
- urlStateStorage.cancel();
+ urlStateStorage.kbnUrlControls.cancel();
await pr;
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`);
expect(urlStateStorage.get(key)).toEqual(null);
@@ -215,11 +215,11 @@ describe('KbnUrlStateStorage', () => {
const key = '_s';
urlStateStorage.set(key, state);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`);
- expect(urlStateStorage.flush()).toBe(true);
+ expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(true);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/#?_s=(ok:1,test:test)"`);
expect(urlStateStorage.get(key)).toEqual(state);
- expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update
+ expect(!!urlStateStorage.kbnUrlControls.flush()).toBe(false); // nothing to flush, not update
});
it('should cancel url updates', async () => {
@@ -227,7 +227,7 @@ describe('KbnUrlStateStorage', () => {
const key = '_s';
const pr = urlStateStorage.set(key, state);
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`);
- urlStateStorage.cancel();
+ urlStateStorage.kbnUrlControls.cancel();
await pr;
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`);
expect(urlStateStorage.get(key)).toEqual(null);
diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts
index 700420447bf4f..0935ecd20111f 100644
--- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts
+++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts
@@ -13,6 +13,7 @@ import { IStateStorage } from './types';
import {
createKbnUrlControls,
getStateFromKbnUrl,
+ IKbnUrlControls,
setStateToKbnUrl,
} from '../../state_management/url';
@@ -39,16 +40,9 @@ export interface IKbnUrlStateStorage extends IStateStorage {
change$: (key: string) => Observable;
/**
- * cancels any pending url updates
+ * Lower level wrapper around history library that handles batching multiple URL updates into one history change
*/
- cancel: () => void;
-
- /**
- * Synchronously runs any pending url updates, returned boolean indicates if change occurred.
- * @param opts: {replace? boolean} - allows to specify if push or replace should be used for flushing update
- * @returns boolean - indicates if there was an update to flush
- */
- flush: (opts?: { replace?: boolean }) => boolean;
+ kbnUrlControls: IKbnUrlControls;
}
/**
@@ -114,11 +108,6 @@ export const createKbnUrlStateStorage = (
}),
share()
),
- flush: ({ replace = false }: { replace?: boolean } = {}) => {
- return !!url.flush(replace);
- },
- cancel() {
- url.cancel();
- },
+ kbnUrlControls: url,
};
};
diff --git a/src/plugins/presentation_util/README.md b/src/plugins/presentation_util/README.md
deleted file mode 100755
index 047423a0a9036..0000000000000
--- a/src/plugins/presentation_util/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# presentationUtil
-
-Utilities and components used by the presentation-related plugins
\ No newline at end of file
diff --git a/src/plugins/presentation_util/README.mdx b/src/plugins/presentation_util/README.mdx
new file mode 100755
index 0000000000000..35b80e3634534
--- /dev/null
+++ b/src/plugins/presentation_util/README.mdx
@@ -0,0 +1,211 @@
+---
+id: presentationUtilPlugin
+slug: /kibana-dev-docs/presentationPlugin
+title: Presentation Utility Plugin
+summary: Introduction to the Presentation Utility Plugin.
+date: 2020-01-12
+tags: ['kibana', 'presentation', 'services']
+related: []
+---
+
+## Introduction
+
+The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas).
+
+## Plugin Services Toolkit
+
+While Kibana provides a `useKibana` hook for use in a plugin, the number of services it provides is very large. This presents a set of difficulties:
+
+- a direct dependency upon the Kibana environment;
+- a requirement to mock the full Kibana environment when testing or using Storybook;
+- a lack of knowledge as to what services are being consumed at any given time.
+
+To mitigate these difficulties, the Presentation Team creates services within the plugin that then consume Kibana-provided (or other) services. This is a toolkit for creating simple services within a plugin.
+
+### Overview
+
+- A `PluginServiceFactory` is a function that will return a set of functions-- which comprise a `Service`-- given a set of parameters.
+- A `PluginServiceProvider` is an object that use a factory to start, stop or provide a `Service`.
+- A `PluginServiceRegistry` is a collection of providers for a given environment, (e.g. Kibana, Jest, Storybook, stub, etc).
+- A `PluginServices` object uses a registry to provide services throughout the plugin.
+
+### Defining Services
+
+To start, a plugin should define a set of services it wants to provide to itself or other plugins.
+
+
+```ts
+export interface PresentationDashboardsService {
+ findDashboards: (
+ query: string,
+ fields: string[]
+ ) => Promise>>;
+ findDashboardsByTitle: (title: string) => Promise>>;
+}
+
+export interface PresentationFooService {
+ getFoo: () => string;
+ setFoo: (bar: string) => void;
+}
+
+export interface PresentationUtilServices {
+ dashboards: PresentationDashboardsService;
+ foo: PresentationFooService;
+}
+```
+
+
+This definition will be used in the toolkit to ensure services are complete and as expected.
+
+### Plugin Services
+
+The `PluginServices` class hosts a registry of service providers from which a plugin can access its services. It uses the service definition as a generic.
+
+```ts
+export const pluginServices = new PluginServices();
+```
+
+This can be placed in the `index.ts` file of a `services` directory within your plugin.
+
+Once created, it simply requires a `PluginServiceRegistry` to be started and set.
+
+### Service Provider Registry
+
+Each environment in which components are used requires a `PluginServiceRegistry` to specify how the providers are started. For example, simple stubs of services require no parameters to start, (so the `StartParameters` generic remains unspecified)
+
+
+```ts
+export const providers: PluginServiceProviders = {
+ dashboards: new PluginServiceProvider(dashboardsServiceFactory),
+ foo: new PluginServiceProvider(fooServiceFactory),
+};
+
+export const serviceRegistry = new PluginServiceRegistry(providers);
+```
+
+
+By contrast, a registry that uses Kibana can provide `KibanaPluginServiceParams` to determine how to start its providers, so the `StartParameters` generic is given:
+
+
+```ts
+export const providers: PluginServiceProviders<
+ PresentationUtilServices,
+ KibanaPluginServiceParams
+> = {
+ dashboards: new PluginServiceProvider(dashboardsServiceFactory),
+ foo: new PluginServiceProvider(fooServiceFactory),
+};
+
+export const serviceRegistry = new PluginServiceRegistry<
+ PresentationUtilServices,
+ KibanaPluginServiceParams
+>(providers);
+```
+
+
+### Service Provider
+
+A `PluginServiceProvider` is a container for a Service Factory that is responsible for starting, stopping and providing a service implementation. A Service Provider doesn't change, rather the factory and the relevant `StartParameters` change.
+
+### Service Factories
+
+A Service Factory is nothing more than a function that uses `StartParameters` to return a set of functions that conforms to a portion of the `Services` specification. For each service, a factory is provided for each environment.
+
+Given a service definition:
+
+```ts
+export interface PresentationFooService {
+ getFoo: () => string;
+ setFoo: (bar: string) => void;
+}
+```
+
+a factory for a stubbed version might look like this:
+
+```ts
+type FooServiceFactory = PluginServiceFactory;
+
+export const fooServiceFactory: FooServiceFactory = () => ({
+ getFoo: () => 'bar',
+ setFoo: (bar) => { console.log(`${bar} set!`)},
+});
+```
+
+and a factory for a Kibana version might look like this:
+
+```ts
+export type FooServiceFactory = KibanaPluginServiceFactory<
+ PresentationFooService,
+ PresentationUtilPluginStart
+>;
+
+export const fooServiceFactory: FooServiceFactory = ({
+ coreStart,
+ startPlugins,
+}) => {
+ // ...do something with Kibana services...
+
+ return {
+ getFoo: //...
+ setFoo: //...
+ }
+}
+```
+
+### Using Services
+
+Once your services and providers are defined, and you have at least one set of factories, you can use `PluginServices` to provide the services to your React components:
+
+
+```ts
+// plugin.ts
+import { pluginServices } from './services';
+import { registry } from './services/kibana';
+
+ public async start(
+ coreStart: CoreStart,
+ startPlugins: StartDeps
+ ): Promise {
+ pluginServices.setRegistry(registry.start({ coreStart, startPlugins }));
+ return {};
+ }
+```
+
+
+and wrap your root React component with the `PluginServices` context:
+
+
+```ts
+import { pluginServices } from './services';
+
+const ContextProvider = pluginServices.getContextProvider(),
+
+return(
+
+
+ {application}
+
+
+)
+```
+
+
+and then, consume your services using provided hooks in a component:
+
+
+```ts
+// component.ts
+
+import { pluginServices } from '../services';
+
+export function MyComponent() {
+ // Retrieve all context hooks from `PluginServices`, destructuring for the one we're using
+ const { foo } = pluginServices.getHooks();
+
+ // Use the `useContext` hook to access the API.
+ const { getFoo } = foo.useService();
+
+ // ...
+}
+```
+
diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx
new file mode 100644
index 0000000000000..cb9991e216019
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+import { action } from '@storybook/addon-actions';
+
+import { DashboardPicker } from './dashboard_picker';
+
+export default {
+ component: DashboardPicker,
+ title: 'Dashboard Picker',
+ argTypes: {
+ isDisabled: {
+ control: 'boolean',
+ defaultValue: false,
+ },
+ },
+};
+
+export const Example = ({ isDisabled }: { isDisabled: boolean }) => (
+
+);
diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx
index 8aaf9be6ef5c6..b156ef4ae764c 100644
--- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx
+++ b/src/plugins/presentation_util/public/components/dashboard_picker.tsx
@@ -6,18 +6,16 @@
* Public License, v 1.
*/
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiComboBox } from '@elastic/eui';
-import { SavedObjectsClientContract } from '../../../../core/public';
-import { DashboardSavedObject } from '../../../../plugins/dashboard/public';
+import { pluginServices } from '../services';
export interface DashboardPickerProps {
onChange: (dashboard: { name: string; id: string } | null) => void;
isDisabled: boolean;
- savedObjectsClient: SavedObjectsClientContract;
}
interface DashboardOption {
@@ -26,34 +24,43 @@ interface DashboardOption {
}
export function DashboardPicker(props: DashboardPickerProps) {
- const [dashboards, setDashboards] = useState([]);
+ const [dashboardOptions, setDashboardOptions] = useState([]);
const [isLoadingDashboards, setIsLoadingDashboards] = useState(true);
const [selectedDashboard, setSelectedDashboard] = useState(null);
+ const [query, setQuery] = useState('');
- const { savedObjectsClient, isDisabled, onChange } = props;
+ const { isDisabled, onChange } = props;
+ const { dashboards } = pluginServices.getHooks();
+ const { findDashboardsByTitle } = dashboards.useService();
- const fetchDashboards = useCallback(
- async (query) => {
+ useEffect(() => {
+ // We don't want to manipulate the React state if the component has been unmounted
+ // while we wait for the saved objects to return.
+ let cleanedUp = false;
+
+ const fetchDashboards = async () => {
setIsLoadingDashboards(true);
- setDashboards([]);
-
- const { savedObjects } = await savedObjectsClient.find({
- type: 'dashboard',
- search: query ? `${query}*` : '',
- searchFields: ['title'],
- });
- if (savedObjects) {
- setDashboards(savedObjects.map((d) => ({ value: d.id, label: d.attributes.title })));
+ setDashboardOptions([]);
+
+ const objects = await findDashboardsByTitle(query ? `${query}*` : '');
+
+ if (cleanedUp) {
+ return;
+ }
+
+ if (objects) {
+ setDashboardOptions(objects.map((d) => ({ value: d.id, label: d.attributes.title })));
}
+
setIsLoadingDashboards(false);
- },
- [savedObjectsClient]
- );
+ };
- // Initial dashboard load
- useEffect(() => {
- fetchDashboards('');
- }, [fetchDashboards]);
+ fetchDashboards();
+
+ return () => {
+ cleanedUp = true;
+ };
+ }, [findDashboardsByTitle, query]);
return (
{
if (e.length) {
@@ -72,7 +79,7 @@ export function DashboardPicker(props: DashboardPickerProps) {
onChange(null);
}
}}
- onSearchChange={fetchDashboards}
+ onSearchChange={setQuery}
isDisabled={isDisabled}
isLoading={isLoadingDashboards}
compressed={true}
diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx
index 58a70c9db7dd5..7c7b12f52ab5f 100644
--- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx
+++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx
@@ -9,18 +9,6 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormRow,
- EuiRadio,
- EuiIconTip,
- EuiPanel,
- EuiSpacer,
-} from '@elastic/eui';
-import { SavedObjectsClientContract } from '../../../../core/public';
import {
OnSaveProps,
@@ -28,9 +16,9 @@ import {
SavedObjectSaveModal,
} from '../../../../plugins/saved_objects/public';
-import { DashboardPicker } from './dashboard_picker';
-
import './saved_object_save_modal_dashboard.scss';
+import { pluginServices } from '../services';
+import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector';
interface SaveModalDocumentInfo {
id?: string;
@@ -38,116 +26,50 @@ interface SaveModalDocumentInfo {
description?: string;
}
-export interface DashboardSaveModalProps {
+export interface SaveModalDashboardProps {
documentInfo: SaveModalDocumentInfo;
objectType: string;
onClose: () => void;
onSave: (props: OnSaveProps & { dashboardId: string | null }) => void;
- savedObjectsClient: SavedObjectsClientContract;
tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
}
-export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
- const { documentInfo, savedObjectsClient, tagOptions } = props;
- const initialCopyOnSave = !Boolean(documentInfo.id);
+export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) {
+ const { documentInfo, tagOptions, objectType, onClose } = props;
+ const { id: documentId } = documentInfo;
+ const initialCopyOnSave = !Boolean(documentId);
+
+ const { capabilities } = pluginServices.getHooks();
+ const {
+ canAccessDashboards,
+ canCreateNewDashboards,
+ canEditDashboards,
+ } = capabilities.useService();
+
+ const disableDashboardOptions =
+ !canAccessDashboards() || (!canCreateNewDashboards && !canEditDashboards);
const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>(
- documentInfo.id ? null : 'existing'
+ documentId || disableDashboardOptions ? null : 'existing'
);
const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>(
null
);
const [copyOnSave, setCopyOnSave] = useState(initialCopyOnSave);
- const renderDashboardSelect = (state: SaveModalState) => {
- const isDisabled = Boolean(!state.copyOnSave && documentInfo.id);
-
- return (
- <>
-
-
-
-
-
-
- }
- />
-
-
- }
- hasChildLabel={false}
- >
-
-
- setDashboardOption('existing')}
- disabled={isDisabled}
- />
-
-
- {
- setSelectedDashboard(dash);
- }}
- />
-
-
-
-
- setDashboardOption('new')}
- disabled={isDisabled}
- />
-
-
-
- setDashboardOption(null)}
- disabled={isDisabled}
- />
-
-
-
- >
- );
- };
+ const rightOptions = !disableDashboardOptions
+ ? () => (
+ {
+ setSelectedDashboard(dash);
+ }}
+ onChange={(option) => {
+ setDashboardOption(option);
+ }}
+ {...{ copyOnSave, documentId, dashboardOption }}
+ />
+ )
+ : null;
const onCopyOnSaveChange = (newCopyOnSave: boolean) => {
setDashboardOption(null);
@@ -159,7 +81,7 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
// Don't save with a dashboard ID if we're
// just updating an existing visualization
- if (!(!onSaveProps.newCopyOnSave && documentInfo.id)) {
+ if (!(!onSaveProps.newCopyOnSave && documentId)) {
if (dashboardOption === 'existing') {
dashboardId = selectedDashboard?.id || null;
} else {
@@ -171,13 +93,14 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
};
const saveLibraryLabel =
- !copyOnSave && documentInfo.id
+ !copyOnSave && documentId
? i18n.translate('presentationUtil.saveModalDashboard.saveLabel', {
defaultMessage: 'Save',
})
: i18n.translate('presentationUtil.saveModalDashboard.saveToLibraryLabel', {
defaultMessage: 'Save and add to library',
});
+
const saveDashboardLabel = i18n.translate(
'presentationUtil.saveModalDashboard.saveAndGoToDashboardLabel',
{
@@ -192,18 +115,20 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
return (
);
}
diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx
new file mode 100644
index 0000000000000..2044ecdd713e1
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React, { useState } from 'react';
+import { action } from '@storybook/addon-actions';
+
+import { StorybookParams } from '../services/storybook';
+import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector';
+
+export default {
+ component: SaveModalDashboardSelector,
+ title: 'Save Modal Dashboard Selector',
+ description: 'A selector for determining where an object will be saved after it is created.',
+ argTypes: {
+ hasDocumentId: {
+ control: 'boolean',
+ defaultValue: false,
+ },
+ copyOnSave: {
+ control: 'boolean',
+ defaultValue: false,
+ },
+ canCreateNewDashboards: {
+ control: 'boolean',
+ defaultValue: true,
+ },
+ canEditDashboards: {
+ control: 'boolean',
+ defaultValue: true,
+ },
+ },
+};
+
+export function Example({
+ copyOnSave,
+ hasDocumentId,
+}: {
+ copyOnSave: boolean;
+ hasDocumentId: boolean;
+} & StorybookParams) {
+ const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing');
+
+ return (
+
+ );
+}
diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx
new file mode 100644
index 0000000000000..b1bf9ed695842
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx
@@ -0,0 +1,132 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiRadio,
+ EuiIconTip,
+ EuiPanel,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import { pluginServices } from '../services';
+import { DashboardPicker, DashboardPickerProps } from './dashboard_picker';
+
+import './saved_object_save_modal_dashboard.scss';
+
+export interface SaveModalDashboardSelectorProps {
+ copyOnSave: boolean;
+ documentId?: string;
+ onSelectDashboard: DashboardPickerProps['onChange'];
+
+ dashboardOption: 'new' | 'existing' | null;
+ onChange: (dashboardOption: 'new' | 'existing' | null) => void;
+}
+
+export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProps) {
+ const { documentId, onSelectDashboard, dashboardOption, onChange, copyOnSave } = props;
+ const { capabilities } = pluginServices.getHooks();
+ const { canCreateNewDashboards, canEditDashboards } = capabilities.useService();
+
+ const isDisabled = !copyOnSave && !!documentId;
+
+ return (
+ <>
+
+
+
+
+
+
+ }
+ />
+
+
+ }
+ hasChildLabel={false}
+ >
+
+
+ {canEditDashboards() && (
+ <>
+ {' '}
+ onChange('existing')}
+ disabled={isDisabled}
+ />
+
+
+
+
+ >
+ )}
+ {canCreateNewDashboards() && (
+ <>
+ {' '}
+ onChange('new')}
+ disabled={isDisabled}
+ />
+
+ >
+ )}
+ onChange(null)}
+ disabled={isDisabled}
+ />
+
+
+
+ >
+ );
+}
diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts
index baf40a1ea0ae4..586ddd1320641 100644
--- a/src/plugins/presentation_util/public/index.ts
+++ b/src/plugins/presentation_util/public/index.ts
@@ -10,9 +10,11 @@ import { PresentationUtilPlugin } from './plugin';
export {
SavedObjectSaveModalDashboard,
- DashboardSaveModalProps,
+ SaveModalDashboardProps,
} from './components/saved_object_save_modal_dashboard';
+export { DashboardPicker } from './components/dashboard_picker';
+
export function plugin() {
return new PresentationUtilPlugin();
}
diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts
index cbc1d0eb04e27..5d3618b034656 100644
--- a/src/plugins/presentation_util/public/plugin.ts
+++ b/src/plugins/presentation_util/public/plugin.ts
@@ -7,16 +7,39 @@
*/
import { CoreSetup, CoreStart, Plugin } from '../../../core/public';
-import { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types';
+import { pluginServices } from './services';
+import { registry } from './services/kibana';
+import {
+ PresentationUtilPluginSetup,
+ PresentationUtilPluginStart,
+ PresentationUtilPluginSetupDeps,
+ PresentationUtilPluginStartDeps,
+} from './types';
export class PresentationUtilPlugin
- implements Plugin {
- public setup(core: CoreSetup): PresentationUtilPluginSetup {
+ implements
+ Plugin<
+ PresentationUtilPluginSetup,
+ PresentationUtilPluginStart,
+ PresentationUtilPluginSetupDeps,
+ PresentationUtilPluginStartDeps
+ > {
+ public setup(
+ _coreSetup: CoreSetup,
+ _setupPlugins: PresentationUtilPluginSetupDeps
+ ): PresentationUtilPluginSetup {
return {};
}
- public start(core: CoreStart): PresentationUtilPluginStart {
- return {};
+ public async start(
+ coreStart: CoreStart,
+ startPlugins: PresentationUtilPluginStartDeps
+ ): Promise {
+ pluginServices.setRegistry(registry.start({ coreStart, startPlugins }));
+
+ return {
+ ContextProvider: pluginServices.getContextProvider(),
+ };
}
public stop() {}
diff --git a/src/plugins/presentation_util/public/services/create/factory.ts b/src/plugins/presentation_util/public/services/create/factory.ts
new file mode 100644
index 0000000000000..01b143e612461
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/create/factory.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { BehaviorSubject } from 'rxjs';
+import { CoreStart, AppUpdater } from 'src/core/public';
+
+/**
+ * A factory function for creating a service.
+ *
+ * The `Service` generic determines the shape of the API being produced.
+ * The `StartParameters` generic determines what parameters are expected to
+ * create the service.
+ */
+export type PluginServiceFactory = (params: Parameters) => Service;
+
+/**
+ * Parameters necessary to create a Kibana-based service, (e.g. during Plugin
+ * startup or setup).
+ *
+ * The `Start` generic refers to the specific Plugin `TPluginsStart`.
+ */
+export interface KibanaPluginServiceParams {
+ coreStart: CoreStart;
+ startPlugins: Start;
+ appUpdater?: BehaviorSubject;
+}
+
+/**
+ * A factory function for creating a Kibana-based service.
+ *
+ * The `Service` generic determines the shape of the API being produced.
+ * The `Setup` generic refers to the specific Plugin `TPluginsSetup`.
+ * The `Start` generic refers to the specific Plugin `TPluginsStart`.
+ */
+export type KibanaPluginServiceFactory = (
+ params: KibanaPluginServiceParams
+) => Service;
diff --git a/src/plugins/presentation_util/public/services/create/index.ts b/src/plugins/presentation_util/public/services/create/index.ts
new file mode 100644
index 0000000000000..59f1f9fd7a43b
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/create/index.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { mapValues } from 'lodash';
+
+import { PluginServiceRegistry } from './registry';
+
+export { PluginServiceRegistry } from './registry';
+export { PluginServiceProvider, PluginServiceProviders } from './provider';
+export {
+ PluginServiceFactory,
+ KibanaPluginServiceFactory,
+ KibanaPluginServiceParams,
+} from './factory';
+
+/**
+ * `PluginServices` is a top-level class for specifying and accessing services within a plugin.
+ *
+ * A `PluginServices` object can be provided with a `PluginServiceRegistry` at any time, which will
+ * then be used to provide services to any component that accesses it.
+ *
+ * The `Services` generic determines the shape of all service APIs being produced.
+ */
+export class PluginServices {
+ private registry: PluginServiceRegistry | null = null;
+
+ /**
+ * Supply a `PluginServiceRegistry` for the class to use to provide services and context.
+ *
+ * @param registry A setup and started `PluginServiceRegistry`.
+ */
+ setRegistry(registry: PluginServiceRegistry | null) {
+ if (registry && !registry.isStarted()) {
+ throw new Error('Registry has not been started.');
+ }
+
+ this.registry = registry;
+ }
+
+ /**
+ * Returns true if a registry has been provided, false otherwise.
+ */
+ hasRegistry() {
+ return !!this.registry;
+ }
+
+ /**
+ * Private getter that will enforce proper setup throughout the class.
+ */
+ private getRegistry() {
+ if (!this.registry) {
+ throw new Error('No registry has been provided.');
+ }
+
+ return this.registry;
+ }
+
+ /**
+ * Return the React Context Provider that will supply services.
+ */
+ getContextProvider() {
+ return this.getRegistry().getContextProvider();
+ }
+
+ /**
+ * Return a map of React Hooks that can be used in React components.
+ */
+ getHooks(): { [K in keyof Services]: { useService: () => Services[K] } } {
+ const registry = this.getRegistry();
+ const providers = registry.getServiceProviders();
+
+ // @ts-expect-error Need to fix this; the type isn't fully understood when inferred.
+ return mapValues(providers, (provider) => ({
+ useService: provider.getUseServiceHook(),
+ }));
+ }
+}
diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx
new file mode 100644
index 0000000000000..981ff1527f981
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/create/provider.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React, { createContext, useContext } from 'react';
+import { PluginServiceFactory } from './factory';
+
+/**
+ * A collection of `PluginServiceProvider` objects, keyed by the `Services` API generic.
+ *
+ * The `Services` generic determines the shape of all service APIs being produced.
+ * The `StartParameters` generic determines what parameters are expected to
+ * start the service.
+ */
+export type PluginServiceProviders = {
+ [K in keyof Services]: PluginServiceProvider;
+};
+
+/**
+ * An object which uses a given factory to start, stop or provide a service.
+ *
+ * The `Service` generic determines the shape of the API being produced.
+ * The `StartParameters` generic determines what parameters are expected to
+ * start the service.
+ */
+export class PluginServiceProvider {
+ private factory: PluginServiceFactory;
+ private context = createContext(null);
+ private pluginService: Service | null = null;
+ public readonly Provider: React.FC = ({ children }) => {
+ return {children};
+ };
+
+ constructor(factory: PluginServiceFactory) {
+ this.factory = factory;
+ this.context.displayName = 'PluginServiceContext';
+ }
+
+ /**
+ * Private getter that will enforce proper setup throughout the class.
+ */
+ private getService() {
+ if (!this.pluginService) {
+ throw new Error('Service not started');
+ }
+ return this.pluginService;
+ }
+
+ /**
+ * Start the service.
+ *
+ * @param params Parameters used to start the service.
+ */
+ start(params: StartParameters) {
+ this.pluginService = this.factory(params);
+ }
+
+ /**
+ * Returns a function for providing a Context hook for the service.
+ */
+ getUseServiceHook() {
+ return () => {
+ const service = useContext(this.context);
+
+ if (!service) {
+ throw new Error('Provider is not set up correctly');
+ }
+
+ return service;
+ };
+ }
+
+ /**
+ * Stop the service.
+ */
+ stop() {
+ this.pluginService = null;
+ }
+}
diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx
new file mode 100644
index 0000000000000..5165380780fa9
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/create/registry.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+import { values } from 'lodash';
+import { PluginServiceProvider, PluginServiceProviders } from './provider';
+
+/**
+ * A `PluginServiceRegistry` maintains a set of service providers which can be collectively
+ * started, stopped or retreived.
+ *
+ * The `Services` generic determines the shape of all service APIs being produced.
+ * The `StartParameters` generic determines what parameters are expected to
+ * start the service.
+ */
+export class PluginServiceRegistry {
+ private providers: PluginServiceProviders;
+ private _isStarted = false;
+
+ constructor(providers: PluginServiceProviders) {
+ this.providers = providers;
+ }
+
+ /**
+ * Returns true if the registry has been started, false otherwise.
+ */
+ isStarted() {
+ return this._isStarted;
+ }
+
+ /**
+ * Returns a map of `PluginServiceProvider` objects.
+ */
+ getServiceProviders() {
+ if (!this._isStarted) {
+ throw new Error('Registry not started');
+ }
+ return this.providers;
+ }
+
+ /**
+ * Returns a React Context Provider for use in consuming applications.
+ */
+ getContextProvider() {
+ // Collect and combine Context.Provider elements from each Service Provider into a single
+ // Functional Component.
+ const provider: React.FC = ({ children }) => (
+ <>
+ {values>(this.getServiceProviders()).reduceRight(
+ (acc, serviceProvider) => {
+ return {acc};
+ },
+ children
+ )}
+ >
+ );
+
+ return provider;
+ }
+
+ /**
+ * Start the registry.
+ *
+ * @param params Parameters used to start the registry.
+ */
+ start(params: StartParameters) {
+ values>(this.providers).map((serviceProvider) =>
+ serviceProvider.start(params)
+ );
+ this._isStarted = true;
+ return this;
+ }
+
+ /**
+ * Stop the registry.
+ */
+ stop() {
+ values>(this.providers).map((serviceProvider) =>
+ serviceProvider.stop()
+ );
+ this._isStarted = false;
+ return this;
+ }
+}
diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts
new file mode 100644
index 0000000000000..732cc19e14763
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/index.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { SimpleSavedObject } from 'src/core/public';
+import { DashboardSavedObject } from 'src/plugins/dashboard/public';
+import { PluginServices } from './create';
+export interface PresentationDashboardsService {
+ findDashboards: (
+ query: string,
+ fields: string[]
+ ) => Promise>>;
+ findDashboardsByTitle: (title: string) => Promise>>;
+}
+
+export interface PresentationCapabilitiesService {
+ canAccessDashboards: () => boolean;
+ canCreateNewDashboards: () => boolean;
+ canEditDashboards: () => boolean;
+}
+
+export interface PresentationUtilServices {
+ dashboards: PresentationDashboardsService;
+ capabilities: PresentationCapabilitiesService;
+}
+
+export const pluginServices = new PluginServices();
diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts
new file mode 100644
index 0000000000000..f36b277979358
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/kibana/capabilities.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { PresentationUtilPluginStartDeps } from '../../types';
+import { KibanaPluginServiceFactory } from '../create';
+import { PresentationCapabilitiesService } from '..';
+
+export type CapabilitiesServiceFactory = KibanaPluginServiceFactory<
+ PresentationCapabilitiesService,
+ PresentationUtilPluginStartDeps
+>;
+
+export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => {
+ const { dashboard } = coreStart.application.capabilities;
+
+ return {
+ canAccessDashboards: () => Boolean(dashboard.show),
+ canCreateNewDashboards: () => Boolean(dashboard.createNew),
+ canEditDashboards: () => !Boolean(dashboard.hideWriteControls),
+ };
+};
diff --git a/src/plugins/presentation_util/public/services/kibana/dashboards.ts b/src/plugins/presentation_util/public/services/kibana/dashboards.ts
new file mode 100644
index 0000000000000..acfe4bd33e26a
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/kibana/dashboards.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { DashboardSavedObject } from 'src/plugins/dashboard/public';
+
+import { PresentationUtilPluginStartDeps } from '../../types';
+import { KibanaPluginServiceFactory } from '../create';
+import { PresentationDashboardsService } from '..';
+
+export type DashboardsServiceFactory = KibanaPluginServiceFactory<
+ PresentationDashboardsService,
+ PresentationUtilPluginStartDeps
+>;
+
+export const dashboardsServiceFactory: DashboardsServiceFactory = ({ coreStart }) => {
+ const findDashboards = async (query: string = '', fields: string[] = []) => {
+ const { find } = coreStart.savedObjects.client;
+
+ const { savedObjects } = await find({
+ type: 'dashboard',
+ search: `${query}*`,
+ searchFields: fields,
+ });
+
+ return savedObjects;
+ };
+
+ const findDashboardsByTitle = async (title: string = '') => findDashboards(title, ['title']);
+
+ return {
+ findDashboards,
+ findDashboardsByTitle,
+ };
+};
diff --git a/src/plugins/presentation_util/public/services/kibana/index.ts b/src/plugins/presentation_util/public/services/kibana/index.ts
new file mode 100644
index 0000000000000..a129b0d94479f
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/kibana/index.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { dashboardsServiceFactory } from './dashboards';
+import { capabilitiesServiceFactory } from './capabilities';
+import {
+ PluginServiceProviders,
+ KibanaPluginServiceParams,
+ PluginServiceProvider,
+ PluginServiceRegistry,
+} from '../create';
+import { PresentationUtilPluginStartDeps } from '../../types';
+import { PresentationUtilServices } from '..';
+
+export { dashboardsServiceFactory } from './dashboards';
+export { capabilitiesServiceFactory } from './capabilities';
+
+export const providers: PluginServiceProviders<
+ PresentationUtilServices,
+ KibanaPluginServiceParams
+> = {
+ dashboards: new PluginServiceProvider(dashboardsServiceFactory),
+ capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
+};
+
+export const registry = new PluginServiceRegistry<
+ PresentationUtilServices,
+ KibanaPluginServiceParams
+>(providers);
diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts
new file mode 100644
index 0000000000000..5048fe50cc025
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { PluginServiceFactory } from '../create';
+import { StorybookParams } from '.';
+import { PresentationCapabilitiesService } from '..';
+
+type CapabilitiesServiceFactory = PluginServiceFactory<
+ PresentationCapabilitiesService,
+ StorybookParams
+>;
+
+export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({
+ canAccessDashboards,
+ canCreateNewDashboards,
+ canEditDashboards,
+}) => {
+ const check = (value: boolean = true) => value;
+ return {
+ canAccessDashboards: () => check(canAccessDashboards),
+ canCreateNewDashboards: () => check(canCreateNewDashboards),
+ canEditDashboards: () => check(canEditDashboards),
+ };
+};
diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts
new file mode 100644
index 0000000000000..536cad3a9d131
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/storybook/index.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { PluginServices, PluginServiceProviders, PluginServiceProvider } from '../create';
+import { dashboardsServiceFactory } from '../stub/dashboards';
+import { capabilitiesServiceFactory } from './capabilities';
+import { PresentationUtilServices } from '..';
+
+export { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create';
+export { PresentationUtilServices } from '..';
+
+export interface StorybookParams {
+ canAccessDashboards?: boolean;
+ canCreateNewDashboards?: boolean;
+ canEditDashboards?: boolean;
+}
+
+export const providers: PluginServiceProviders = {
+ dashboards: new PluginServiceProvider(dashboardsServiceFactory),
+ capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
+};
+
+export const pluginServices = new PluginServices();
diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts
new file mode 100644
index 0000000000000..33c091022421c
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { PluginServiceFactory } from '../create';
+import { PresentationCapabilitiesService } from '..';
+
+type CapabilitiesServiceFactory = PluginServiceFactory;
+
+export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({
+ canAccessDashboards: () => true,
+ canCreateNewDashboards: () => true,
+ canEditDashboards: () => true,
+});
diff --git a/src/plugins/presentation_util/public/services/stub/dashboards.ts b/src/plugins/presentation_util/public/services/stub/dashboards.ts
new file mode 100644
index 0000000000000..862fa4f952c1e
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/stub/dashboards.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { PluginServiceFactory } from '../create';
+import { PresentationDashboardsService } from '..';
+
+// TODO (clint): Create set of dashboards to stub and return.
+
+type DashboardsServiceFactory = PluginServiceFactory;
+
+function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export const dashboardsServiceFactory: DashboardsServiceFactory = () => ({
+ findDashboards: async (query: string = '', _fields: string[] = []) => {
+ if (!query) {
+ return [];
+ }
+
+ await sleep(2000);
+ return [];
+ },
+ findDashboardsByTitle: async (title: string) => {
+ if (!title) {
+ return [];
+ }
+
+ await sleep(2000);
+ return [];
+ },
+});
diff --git a/src/plugins/presentation_util/public/services/stub/index.ts b/src/plugins/presentation_util/public/services/stub/index.ts
new file mode 100644
index 0000000000000..a2bde357fd4c0
--- /dev/null
+++ b/src/plugins/presentation_util/public/services/stub/index.ts
@@ -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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { dashboardsServiceFactory } from './dashboards';
+import { capabilitiesServiceFactory } from './capabilities';
+import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create';
+import { PresentationUtilServices } from '..';
+
+export { dashboardsServiceFactory } from './dashboards';
+export { capabilitiesServiceFactory } from './capabilities';
+
+export const providers: PluginServiceProviders = {
+ dashboards: new PluginServiceProvider(dashboardsServiceFactory),
+ capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
+};
+
+export const registry = new PluginServiceRegistry(providers);
diff --git a/src/plugins/presentation_util/public/types.ts b/src/plugins/presentation_util/public/types.ts
index ae5646bd9bbae..7371ebc6f736e 100644
--- a/src/plugins/presentation_util/public/types.ts
+++ b/src/plugins/presentation_util/public/types.ts
@@ -8,5 +8,12 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PresentationUtilPluginSetup {}
+
+export interface PresentationUtilPluginStart {
+ ContextProvider: React.FC;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface PresentationUtilPluginSetupDeps {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface PresentationUtilPluginStart {}
+export interface PresentationUtilPluginStartDeps {}
diff --git a/src/plugins/presentation_util/storybook/decorator.tsx b/src/plugins/presentation_util/storybook/decorator.tsx
new file mode 100644
index 0000000000000..5f56c70a2f849
--- /dev/null
+++ b/src/plugins/presentation_util/storybook/decorator.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+
+import { DecoratorFn } from '@storybook/react';
+import { I18nProvider } from '@kbn/i18n/react';
+import { pluginServices } from '../public/services';
+import { PresentationUtilServices } from '../public/services';
+import { providers, StorybookParams } from '../public/services/storybook';
+import { PluginServiceRegistry } from '../public/services/create';
+
+export const servicesContextDecorator: DecoratorFn = (story: Function, storybook) => {
+ const registry = new PluginServiceRegistry(providers);
+ pluginServices.setRegistry(registry.start(storybook.args));
+ const ContextProvider = pluginServices.getContextProvider();
+
+ return (
+
+ {story()}
+
+ );
+};
diff --git a/src/plugins/presentation_util/storybook/main.ts b/src/plugins/presentation_util/storybook/main.ts
new file mode 100644
index 0000000000000..d12b98f38a03f
--- /dev/null
+++ b/src/plugins/presentation_util/storybook/main.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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { Configuration } from 'webpack';
+import { defaultConfig } from '@kbn/storybook';
+import webpackConfig from '@kbn/storybook/target/webpack.config';
+
+module.exports = {
+ ...defaultConfig,
+ addons: ['@storybook/addon-essentials'],
+ webpackFinal: (config: Configuration) => {
+ return webpackConfig({ config });
+ },
+};
diff --git a/src/plugins/presentation_util/storybook/manager.ts b/src/plugins/presentation_util/storybook/manager.ts
new file mode 100644
index 0000000000000..e9b6a11242036
--- /dev/null
+++ b/src/plugins/presentation_util/storybook/manager.ts
@@ -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
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { addons } from '@storybook/addons';
+import { create } from '@storybook/theming';
+import { PANEL_ID } from '@storybook/addon-actions';
+
+addons.setConfig({
+ theme: create({
+ base: 'light',
+ brandTitle: 'Kibana Presentation Utility Storybook',
+ brandUrl: 'https://github.com/elastic/kibana/tree/master/src/plugins/presentation_util',
+ }),
+ showPanel: true.valueOf,
+ selectedPanel: PANEL_ID,
+});
diff --git a/src/plugins/presentation_util/storybook/preview.tsx b/src/plugins/presentation_util/storybook/preview.tsx
new file mode 100644
index 0000000000000..dfa8ad3be04e7
--- /dev/null
+++ b/src/plugins/presentation_util/storybook/preview.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import React from 'react';
+import { addDecorator } from '@storybook/react';
+import { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks';
+
+import { servicesContextDecorator } from './decorator';
+
+addDecorator(servicesContextDecorator);
+
+export const parameters = {
+ docs: {
+ page: () => (
+ <>
+
+
+
+
+
+ >
+ ),
+ },
+};
diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json
index 1e3756f45e953..a9657db288848 100644
--- a/src/plugins/presentation_util/tsconfig.json
+++ b/src/plugins/presentation_util/tsconfig.json
@@ -7,7 +7,7 @@
"declaration": true,
"declarationMap": true
},
- "include": ["common/**/*", "public/**/*"],
+ "include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"],
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../dashboard/tsconfig.json" },
diff --git a/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx
index 6702255ee2e2c..f87169d4b828a 100644
--- a/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx
+++ b/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx
@@ -31,7 +31,8 @@ interface MinimalSaveModalProps {
export function showSaveModal(
saveModal: React.ReactElement,
- I18nContext: I18nStart['Context']
+ I18nContext: I18nStart['Context'],
+ Wrapper?: React.FC
) {
const container = document.createElement('div');
const closeModal = () => {
@@ -55,5 +56,13 @@ export function showSaveModal(
onClose: closeModal,
});
- ReactDOM.render({element}, container);
+ const wrappedElement = Wrapper ? (
+
+ {element}
+
+ ) : (
+ {element}
+ );
+
+ ReactDOM.render(wrappedElement, container);
}
diff --git a/src/plugins/security_oss/public/plugin.mock.ts b/src/plugins/security_oss/public/plugin.mock.ts
index 53d96b9c7a303..8fe8d56ea6576 100644
--- a/src/plugins/security_oss/public/plugin.mock.ts
+++ b/src/plugins/security_oss/public/plugin.mock.ts
@@ -7,6 +7,7 @@
*/
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
+import { InsecureClusterServiceStart } from './insecure_cluster_service';
import { mockInsecureClusterService } from './insecure_cluster_service/insecure_cluster_service.mock';
import { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin';
@@ -18,7 +19,11 @@ export const mockSecurityOssPlugin = {
},
createStart: () => {
return {
- insecureCluster: mockInsecureClusterService.createStart(),
+ insecureCluster: mockInsecureClusterService.createStart() as jest.Mocked,
+ anonymousAccess: {
+ getAccessURLParameters: jest.fn().mockResolvedValue(null),
+ getCapabilities: jest.fn().mockResolvedValue({}),
+ },
} as DeeplyMockedKeys;
},
};
diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json
index 7760ea321992d..8b1d28b1606d4 100644
--- a/src/plugins/share/kibana.json
+++ b/src/plugins/share/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredBundles": ["kibanaUtils"]
+ "requiredBundles": ["kibanaUtils"],
+ "optionalPlugins": ["securityOss"]
}
diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap
index 9a7191519131c..e883b550fde04 100644
--- a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap
+++ b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap
@@ -115,49 +115,68 @@ exports[`share url panel content render 1`] = `
/>
+ }
labelType="label"
>
-
+
-
-
- }
- onChange={[Function]}
- />
-
-
-
- }
- position="bottom"
- />
-
-
+
+
+ }
+ onChange={[Function]}
+ />
+
+
+
+ }
+ position="bottom"
+ />
+
+
+
+ }
labelType="label"
>
-
+
-
-
- }
- onChange={[Function]}
- />
-
-
-
- }
- position="bottom"
- />
-
-
+
+
+ }
+ onChange={[Function]}
+ />
+
+
+
+ }
+ position="bottom"
+ />
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
@@ -569,49 +626,68 @@ exports[`should show url param extensions 1`] = `
/>
+ }
labelType="label"
>
-
+
-
-
- }
- onChange={[Function]}
- />
-
-
-
- }
- position="bottom"
- />
-
-
+
+
+ }
+ onChange={[Function]}
+ />
+
+
+
+ }
+ position="bottom"
+ />
+
+
+
boolean;
}
export class ShareContextMenu extends Component {
@@ -62,6 +66,8 @@ export class ShareContextMenu extends Component {
basePath={this.props.basePath}
post={this.props.post}
shareableUrl={this.props.shareableUrl}
+ anonymousAccess={this.props.anonymousAccess}
+ showPublicUrlSwitch={this.props.showPublicUrlSwitch}
/>
),
};
@@ -91,6 +97,8 @@ export class ShareContextMenu extends Component {
post={this.props.post}
shareableUrl={this.props.shareableUrl}
urlParamExtensions={this.props.embedUrlParamExtensions}
+ anonymousAccess={this.props.anonymousAccess}
+ showPublicUrlSwitch={this.props.showPublicUrlSwitch}
/>
),
};
diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx
index 5901d2452e9aa..ca9025f242b78 100644
--- a/src/plugins/share/public/components/url_panel_content.tsx
+++ b/src/plugins/share/public/components/url_panel_content.tsx
@@ -28,9 +28,11 @@ import { format as formatUrl, parse as parseUrl } from 'url';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { HttpStart } from 'kibana/public';
import { i18n } from '@kbn/i18n';
+import type { Capabilities } from 'src/core/public';
import { shortenUrl } from '../lib/url_shortener';
import { UrlParamExtension } from '../types';
+import type { SecurityOssPluginStart } from '../../../security_oss/public';
interface Props {
allowShortUrl: boolean;
@@ -41,6 +43,8 @@ interface Props {
basePath: string;
post: HttpStart['post'];
urlParamExtensions?: UrlParamExtension[];
+ anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
+ showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
}
export enum ExportUrlAsType {
@@ -57,10 +61,13 @@ interface UrlParams {
interface State {
exportUrlAs: ExportUrlAsType;
useShortUrl: boolean;
+ usePublicUrl: boolean;
isCreatingShortUrl: boolean;
url?: string;
shortUrlErrorMsg?: string;
urlParams?: UrlParams;
+ anonymousAccessParameters: Record | null;
+ showPublicUrlSwitch: boolean;
}
export class UrlPanelContent extends Component {
@@ -75,8 +82,11 @@ export class UrlPanelContent extends Component {
this.state = {
exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT,
useShortUrl: false,
+ usePublicUrl: false,
isCreatingShortUrl: false,
url: '',
+ anonymousAccessParameters: null,
+ showPublicUrlSwitch: false,
};
}
@@ -91,6 +101,41 @@ export class UrlPanelContent extends Component {
this.setUrl();
window.addEventListener('hashchange', this.resetUrl, false);
+
+ if (this.props.anonymousAccess) {
+ (async () => {
+ const anonymousAccessParameters = await this.props.anonymousAccess!.getAccessURLParameters();
+
+ if (!this.mounted) {
+ return;
+ }
+
+ if (!anonymousAccessParameters) {
+ return;
+ }
+
+ let showPublicUrlSwitch: boolean = false;
+
+ if (this.props.showPublicUrlSwitch) {
+ const anonymousUserCapabilities = await this.props.anonymousAccess!.getCapabilities();
+
+ if (!this.mounted) {
+ return;
+ }
+
+ try {
+ showPublicUrlSwitch = this.props.showPublicUrlSwitch!(anonymousUserCapabilities);
+ } catch {
+ showPublicUrlSwitch = false;
+ }
+ }
+
+ this.setState({
+ anonymousAccessParameters,
+ showPublicUrlSwitch,
+ });
+ })();
+ }
}
public render() {
@@ -99,7 +144,16 @@ export class UrlPanelContent extends Component |