diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 85bfd4a7a4d26..f6378d9940bba 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -25,5 +25,6 @@ export const storybookAliases = { embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', security_solution: 'x-pack/plugins/security_solution/scripts/storybook.js', + tags: 'x-pack/plugins/tags/scripts/storybook.js', ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/scripts/storybook.js', }; diff --git a/src/plugins/bfetch/common/api.ts b/src/plugins/bfetch/common/api.ts new file mode 100644 index 0000000000000..c329cc0dc9626 --- /dev/null +++ b/src/plugins/bfetch/common/api.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type ApiMethod = (payload: Payload) => Promise; + +export interface ApiMethodRequest { + name: string; + payload: Payload; +} diff --git a/src/plugins/bfetch/common/index.ts b/src/plugins/bfetch/common/index.ts index 085b8e7c58a67..067c8bdf16056 100644 --- a/src/plugins/bfetch/common/index.ts +++ b/src/plugins/bfetch/common/index.ts @@ -21,3 +21,4 @@ export * from './util'; export * from './streaming'; export * from './buffer'; export * from './batch'; +export * from './api'; diff --git a/src/plugins/bfetch/public/mocks.ts b/src/plugins/bfetch/public/mocks.ts index f457b9ae5d671..c7c13159b809e 100644 --- a/src/plugins/bfetch/public/mocks.ts +++ b/src/plugins/bfetch/public/mocks.ts @@ -28,6 +28,7 @@ const createSetupContract = (): Setup => { const setupContract: Setup = { fetchStreaming: jest.fn(), batchedFunction: jest.fn(), + createApi: jest.fn(), }; return setupContract; }; @@ -36,6 +37,7 @@ const createStartContract = (): Start => { const startContract: Start = { fetchStreaming: jest.fn(), batchedFunction: jest.fn(), + createApi: jest.fn(), }; return startContract; diff --git a/src/plugins/bfetch/public/plugin.ts b/src/plugins/bfetch/public/plugin.ts index 5f01957c0908e..800f45cff6884 100644 --- a/src/plugins/bfetch/public/plugin.ts +++ b/src/plugins/bfetch/public/plugin.ts @@ -18,8 +18,9 @@ */ import { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public'; +import { Ensure } from '@kbn/utility-types'; import { fetchStreaming as fetchStreamingStatic, FetchStreamingParams } from './streaming'; -import { removeLeadingSlash } from '../common'; +import { removeLeadingSlash, ApiMethod } from '../common'; import { createStreamingBatchedFunction, BatchedFunc, @@ -37,6 +38,12 @@ export interface BfetchPublicContract { batchedFunction: ( params: StreamingBatchedFunctionParams ) => BatchedFunc; + createApi: ( + url: string + ) => ( + name: K, + payload: Parameters>[0] + ) => ReturnType>; } export type BfetchPublicSetup = BfetchPublicContract; @@ -61,9 +68,15 @@ export class BfetchPublicPlugin const fetchStreaming = this.fetchStreaming(version, basePath); const batchedFunction = this.batchedFunction(fetchStreaming); + const createApi: BfetchPublicContract['createApi'] = ((url: string) => { + const fn = batchedFunction({ url }); + return async (name, payload) => await fn({ name, payload } as unknown); + }) as BfetchPublicContract['createApi']; + this.contract = { fetchStreaming, batchedFunction, + createApi, }; return this.contract; diff --git a/src/plugins/bfetch/server/mocks.ts b/src/plugins/bfetch/server/mocks.ts index 5a772d641493d..80b404e61d85f 100644 --- a/src/plugins/bfetch/server/mocks.ts +++ b/src/plugins/bfetch/server/mocks.ts @@ -29,6 +29,7 @@ const createSetupContract = (): Setup => { addBatchProcessingRoute: jest.fn(), addStreamingResponseRoute: jest.fn(), createStreamingRequestHandler: jest.fn(), + createApiRoute: jest.fn(), }; return setupContract; }; diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index c27f34b8233cb..12b628db2247d 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -26,6 +26,7 @@ import { KibanaRequest, RouteMethod, RequestHandler, + RequestHandlerContext, } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { Subject } from 'rxjs'; @@ -39,6 +40,7 @@ import { } from '../common'; import { StreamingRequestHandler } from './types'; import { createNDJSONStream } from './streaming'; +import { ApiMethod, ApiMethodRequest } from '../common'; // eslint-disable-next-line export interface BfetchServerSetupDependencies {} @@ -54,12 +56,20 @@ export interface BatchProcessingRouteParams { export interface BfetchServerSetup { addBatchProcessingRoute: ( path: string, - handler: (request: KibanaRequest) => BatchProcessingRouteParams + handler: ( + request: KibanaRequest, + context: RequestHandlerContext + ) => BatchProcessingRouteParams ) => void; + addStreamingResponseRoute: ( path: string, - params: (request: KibanaRequest) => StreamingResponseHandler + params: ( + request: KibanaRequest, + context: RequestHandlerContext + ) => StreamingResponseHandler ) => void; + /** * Create a streaming request handler to be able to use an Observable to return chunked content to the client. * This is meant to be used with the `fetchStreaming` API of the `bfetch` client-side plugin. @@ -83,7 +93,6 @@ export interface BfetchServerSetup { * return results$; * }) * )} - * * ``` * * @param streamHandler @@ -91,6 +100,24 @@ export interface BfetchServerSetup { createStreamingRequestHandler: ( streamHandler: StreamingRequestHandler ) => RequestHandler; + + /** + * Creates a batch processing route that can route between a map of API methods. + * + * @example + * ```ts + * plugins.bfetch.createApiRoute('/calculator', { + * add: async ({a, b}) => ({ result: a + b }), + * subtract: async ({a, b}) => ({ result: a - b }), + * multiply: async ({a, b}) => ({ result: a * b }), + * divide: async ({a, b}) => ({ result: a / b }), + * }); + * ``` + * + * @param path + * @param api + */ + createApiRoute: (path: string, api: Record>) => void; } // eslint-disable-next-line @@ -119,10 +146,22 @@ export class BfetchServerPlugin const addBatchProcessingRoute = this.addBatchProcessingRoute(addStreamingResponseRoute); const createStreamingRequestHandler = this.createStreamingRequestHandler({ logger }); + const createApiRoute: BfetchServerSetup['createApiRoute'] = (path, api) => { + addBatchProcessingRoute(path, (request) => ({ + onBatchItem: async ({ name, payload }: ApiMethodRequest) => { + if (typeof name !== 'string') throw new Error('Api method name should be a string.'); + if (!api.hasOwnProperty(name)) + throw new Error(`Api method [name = ${name}] does not exist.`); + return await api[name](payload); + }, + })); + }; + return { addBatchProcessingRoute, addStreamingResponseRoute, createStreamingRequestHandler, + createApiRoute, }; } @@ -147,7 +186,7 @@ export class BfetchServerPlugin }, }, async (context, request, response) => { - const handlerInstance = handler(request); + const handlerInstance = handler(request, context); const data = request.body; return response.ok({ headers: streamingHeaders, @@ -181,13 +220,16 @@ export class BfetchServerPlugin E extends ErrorLike = ErrorLike >( path: string, - handler: (request: KibanaRequest) => BatchProcessingRouteParams + handler: ( + request: KibanaRequest, + context: RequestHandlerContext + ) => BatchProcessingRouteParams ) => { addStreamingResponseRoute< BatchRequestData, BatchResponseItem - >(path, (request) => { - const handlerInstance = handler(request); + >(path, (request, context) => { + const handlerInstance = handler(request, context); return { getResponseStream: ({ batch }) => { const subject = new Subject>(); diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx index e7534fa09aa3f..44cf548985728 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx @@ -62,6 +62,7 @@ beforeEach(async () => { overlays: coreStart.overlays, savedObjectMetaData: {} as any, uiActions: {} as any, + getRenderBeforeDashboard: () => () => null, }; const input = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index 08eeb19dcda93..8c83134fd490b 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -47,6 +47,8 @@ import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/pub import 'angular-sanitize'; // required for ngRoute import 'angular-route'; +import { DashboardContainer } from './embeddable'; +import { TagPickerType } from '../plugin'; export interface RenderDeps { pluginInitializerContext: PluginInitializerContext; @@ -74,6 +76,9 @@ export interface RenderDeps { scopedHistory: () => ScopedHistory; savedObjects: SavedObjectsStart; restorePreviousUrl: () => void; + renderBeforeDashboard: (dashboard: DashboardContainer) => React.ReactNode; + renderTags: (kid: string) => React.ReactNode; + getTagPicker: () => TagPickerType; } let angularModuleInstance: IModule | null = null; diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index f1ecd0f221926..750a11d3557a5 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -89,6 +89,7 @@ export interface DashboardContainerOptions { SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; uiActions: UiActionsStart; + getRenderBeforeDashboard: () => (dashboard: DashboardContainer) => React.ReactNode; } export type DashboardReactContextValue = KibanaReactContextValue; @@ -115,6 +116,7 @@ export class DashboardContainer extends Container + {this.options.getRenderBeforeDashboard()(this)} ; ExitFullScreenButton: React.ComponentType; uiActions: UiActionsStart; + getRenderBeforeDashboard: () => (dashboard: DashboardContainer) => React.ReactNode; } export type DashboardContainerFactory = EmbeddableFactory< diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index 8b8fdcb7a76ac..7e7005722c2b6 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -41,6 +41,8 @@ export function initDashboardApp(app, deps) { app.directive('dashboardListing', function (reactDirective) { return reactDirective(DashboardListing, [ ['core', { watchDepth: 'reference' }], + ['renderTags', { watchDepth: 'reference' }], + ['tagPicker', { watchDepth: 'reference' }], ['createItem', { watchDepth: 'reference' }], ['getViewUrl', { watchDepth: 'reference' }], ['editItem', { watchDepth: 'reference' }], @@ -138,6 +140,12 @@ export function initDashboardApp(app, deps) { addHelpMenuToAppChrome(deps.chrome, deps.core.docLinks); $scope.core = deps.core; + $scope.renderTags = (data) => { + const kid = `kid:::so:saved_objects/dashboard/${data.id}`; + return deps.renderTags(kid); + }; + $scope.tagPicker = deps.getTagPicker(); + $scope.$on('$destroy', () => { stopSyncingQueryServiceStateWithUrl(); }); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.js b/src/plugins/dashboard/public/application/listing/dashboard_listing.js index c8cb551fbe561..280ec4e2d6b46 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.js +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.js @@ -42,6 +42,7 @@ export class DashboardListing extends React.Component { return ( { + return this.props.renderTags(data); + }, + }, ]; return tableColumns; } } DashboardListing.propTypes = { + renderTags: PropTypes.func.isRequired, createItem: PropTypes.func.isRequired, + tagPicker: PropTypes.func.isRequired, findItems: PropTypes.func.isRequired, deleteItems: PropTypes.func.isRequired, editItem: PropTypes.func.isRequired, diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html b/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html index ba05c138a0cba..3dcf076770ad1 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html @@ -1,6 +1,8 @@ void; +}>; + +export interface DashboardSetup { + setRenderBeforeDashboard: (renderer: (dashboard: DashboardContainer) => React.ReactNode) => void; + setRenderTags: (renderer: (kid: string) => React.ReactNode) => void; + setTagPicker: (TagPicker: TagPickerType) => void; +} export interface DashboardStart { getSavedDashboardLoader: () => SavedObjectLoader; @@ -136,7 +146,7 @@ declare module '../../../plugins/ui_actions/public' { } export class DashboardPlugin - implements Plugin { + implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} private appStateUpdater = new BehaviorSubject(() => ({})); @@ -146,10 +156,18 @@ export class DashboardPlugin private dashboardUrlGenerator?: DashboardUrlGenerator; + private renderBeforeDashboard: (dashboard: DashboardContainer) => React.ReactNode = () => null; + private getRenderBeforeDashboard: () => (dashboard: DashboardContainer) => React.ReactNode = () => + this.renderBeforeDashboard; + + private renderTags: (kid: string) => React.ReactNode = (kid) => kid; + + private TagPicker: TagPickerType = () => null; + public setup( core: CoreSetup, { share, uiActions, embeddable, home, kibanaLegacy, data, usageCollection }: SetupDependencies - ): Setup { + ): DashboardSetup { const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id); @@ -203,6 +221,7 @@ export class DashboardPlugin SavedObjectFinder: getSavedObjectFinder(coreStart.savedObjects, coreStart.uiSettings), ExitFullScreenButton, uiActions: deps.uiActions, + getRenderBeforeDashboard: this.getRenderBeforeDashboard, }; }; @@ -296,6 +315,9 @@ export class DashboardPlugin scopedHistory: () => this.currentHistory!, savedObjects, restorePreviousUrl, + renderBeforeDashboard: this.renderBeforeDashboard, + renderTags: this.renderTags, + getTagPicker: () => this.TagPicker, }; // make sure the index pattern list is up to date await dataStart.indexPatterns.clearCache(); @@ -354,6 +376,18 @@ export class DashboardPlugin category: FeatureCatalogueCategory.DATA, }); } + + return { + setRenderBeforeDashboard: (renderer) => { + this.renderBeforeDashboard = renderer; + }, + setRenderTags: (renderer) => { + this.renderTags = renderer; + }, + setTagPicker: (TagPicker) => { + this.TagPicker = TagPicker; + }, + }; } private addEmbeddableToDashboard( diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index f3bdfd8e17f0a..62fec6a2d8f11 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -63,6 +63,12 @@ export function createSavedDashboardClass( timeRestore: 'boolean', timeTo: 'keyword', timeFrom: 'keyword', + _tags: { + type: 'object', + properties: { + tagId: { type: 'keyword' }, + }, + }, refreshInterval: { type: 'object', properties: { diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index 14d2c822a421e..b34c9bbe202cd 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -56,6 +56,12 @@ export const dashboardSavedObjectType: SavedObjectsType = { value: { type: 'integer' }, }, }, + _tags: { + type: 'object', + properties: { + tagId: { type: 'keyword' }, + }, + }, timeFrom: { type: 'keyword' }, timeRestore: { type: 'boolean' }, timeTo: { type: 'keyword' }, diff --git a/src/plugins/extensions/kibana.json b/src/plugins/extensions/kibana.json new file mode 100644 index 0000000000000..8db62662c1d57 --- /dev/null +++ b/src/plugins/extensions/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "extensions", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [] +} diff --git a/src/plugins/extensions/public/index.ts b/src/plugins/extensions/public/index.ts new file mode 100644 index 0000000000000..cff60b81e850c --- /dev/null +++ b/src/plugins/extensions/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExtensionsPlugin } from './plugin'; + +export const plugin = () => new ExtensionsPlugin(); + +export { ExtensionsPlugin, ExtensionsPluginSetup, ExtensionsPluginStart } from './plugin'; diff --git a/src/plugins/extensions/public/plugin.ts b/src/plugins/extensions/public/plugin.ts new file mode 100644 index 0000000000000..878ccd1afc17f --- /dev/null +++ b/src/plugins/extensions/public/plugin.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Plugin } from 'kibana/public'; + +export interface ExtensionsTags { + TagList?: React.ComponentType<{ kid: string }>; + TagListEditable?: React.ComponentType<{ kid: string }>; +} + +export interface ExtensionsPluginSetup { + readonly tags: ExtensionsTags; +} + +export interface ExtensionsPluginStart { + readonly tags: ExtensionsTags; +} + +export class ExtensionsPlugin + implements Plugin { + private readonly tags: ExtensionsTags = {}; + + setup(): ExtensionsPluginSetup { + return { + tags: this.tags, + }; + } + + start(): ExtensionsPluginStart { + return { + tags: this.tags, + }; + } +} diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index 7f8bf6c04cecc..c85d74b53f292 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -28,6 +28,7 @@ export * from './split_panel'; export * from './react_router_navigate'; export { ValidatedDualRange, Value } from './validated_range'; export * from './notifications'; +export * from './toasts'; export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 2fa1debf51b5c..c2e867f1dbabb 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -70,6 +70,7 @@ export interface TableListViewProps { * If the table is not empty, this component renders its own h1 element using the same id. */ headingId?: string; + TagPicker?: React.FC<{ selected: string[]; onChange: (selected: string[]) => void }>; } export interface TableListViewState { @@ -83,6 +84,7 @@ export interface TableListViewState { filter: string; selectedIds: string[]; totalItems: number; + tags: string[]; } // saved object client does not support sorting by title because title is only mapped as analyzed @@ -112,6 +114,7 @@ class TableListView extends React.Component { try { - const response = await this.props.findItems(filter); + let tagFilter = ''; + if (this.state.tags.length) { + tagFilter = `attributes._tags.tagId:("${this.state.tags.join('" | "')}")`; + } + const query = filter + ? `(title:(${filter}*) | description:(${filter}*)) + (${tagFilter})` + : tagFilter; + const response = await this.props.findItems(query); if (!this._isMounted) { return; @@ -459,18 +469,28 @@ class TableListView extends React.Component ); return ( - >} // EuiBasicTableColumn is stricter than Column - pagination={this.pagination} - loading={this.state.isFetchingItems} - message={noItemsMessage} - selection={selection} - search={search} - sorting={true} - data-test-subj="itemsInMemTable" - /> + <> + {!!this.props.TagPicker && ( + { + this.setState({ tags }, this.fetchItems); + }} + /> + )} + >} // EuiBasicTableColumn is stricter than Column + pagination={this.pagination} + loading={this.state.isFetchingItems} + message={noItemsMessage} + selection={selection} + search={search} + sorting={true} + data-test-subj="itemsInMemTable" + /> + ); } @@ -540,11 +560,7 @@ class TableListView extends React.Component + diff --git a/src/plugins/kibana_react/public/toasts/context.tsx b/src/plugins/kibana_react/public/toasts/context.tsx new file mode 100644 index 0000000000000..4f33f19c70a95 --- /dev/null +++ b/src/plugins/kibana_react/public/toasts/context.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { createContext, useContext, useMemo } from 'react'; +import { CoreStart, Toast } from '../../../../core/public'; +import { toMountPoint } from '../util'; + +const context = createContext(undefined); + +export const ToastsProvider = context.Provider; + +export interface ToastsParams { + title?: React.ReactNode; + text?: React.ReactNode; + color?: Toast['color']; + iconType?: Toast['iconType']; + toastLifeTimeMs?: Toast['toastLifeTimeMs']; + onClose?: Toast['onClose']; +} + +export const useToasts = () => { + const toasts = useContext(context)!; + const api = useMemo( + () => ({ + add: ({ title, text, ...rest }: ToastsParams) => + toasts.add({ + ...rest, + title: title ? toMountPoint(<>{title}) : undefined, + text: text ? toMountPoint(<>{text || null}) : undefined, + }), + addDanger: ({ title, text, ...rest }: ToastsParams) => + toasts.addDanger({ + ...rest, + title: title ? toMountPoint(<>{title}) : undefined, + text: text ? toMountPoint(<>{text || null}) : undefined, + }), + addInfo: ({ title, text, ...rest }: ToastsParams) => + toasts.addInfo({ + ...rest, + title: title ? toMountPoint(<>{title}) : undefined, + text: text ? toMountPoint(<>{text || null}) : undefined, + }), + addSuccess: ({ title, text, ...rest }: ToastsParams) => + toasts.addSuccess({ + ...rest, + title: title ? toMountPoint(<>{title}) : undefined, + text: text ? toMountPoint(<>{text || null}) : undefined, + }), + addWarning: ({ title, text, ...rest }: ToastsParams) => + toasts.addWarning({ + ...rest, + title: title ? toMountPoint(<>{title}) : undefined, + text: text ? toMountPoint(<>{text || null}) : undefined, + }), + addError: toasts.addError.bind(toasts), + }), + [toasts] + ); + return api; +}; diff --git a/src/plugins/kibana_react/public/toasts/index.tsx b/src/plugins/kibana_react/public/toasts/index.tsx new file mode 100644 index 0000000000000..7c4b4957913c0 --- /dev/null +++ b/src/plugins/kibana_react/public/toasts/index.tsx @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ToastsProvider, ToastsParams, useToasts } from './context'; diff --git a/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts b/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts index 9e7346f3b673c..d7373684f9ec1 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts @@ -129,11 +129,11 @@ export class SavedObjectLoader { return this.savedObjectsClient .find>({ type: this.lowercaseType, - search: search ? `${search}*` : undefined, + search: search ? `${search}` : undefined, perPage: size, page: 1, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', + searchFields: ['title^3', 'description', '_tags.tagId'], + // defaultSearchOperator: 'AND', fields, } as SavedObjectsFindOptions) .then((resp) => { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/json_flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/json_flyout.tsx new file mode 100644 index 0000000000000..8a0083fb50a20 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/json_flyout.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { + EuiTitle, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiIcon, + EuiToolTip, + EuiCodeBlock, +} from '@elastic/eui'; +import { IBasePath } from 'src/core/public'; +import { SavedObjectsClientContract, SavedObject } from 'src/core/public'; +import useMountedState from 'react-use/lib/useMountedState'; +import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; + +export interface JsonFlyoutProps { + basePath: IBasePath; + savedObject: SavedObject; + savedObjectsClient: SavedObjectsClientContract; + close: () => void; +} + +export const JsonFlyout: React.FC = ({ + close, + savedObject, + savedObjectsClient, +}) => { + const [so, setSo] = React.useState>(savedObject); + const isMounted = useMountedState(); + React.useEffect(() => { + savedObjectsClient.get(savedObject.type, savedObject.id).then((newSo) => { + if (!isMounted()) return; + setSo(newSo); + }); + }, [savedObjectsClient, savedObject, isMounted]); + + return ( + + + +

+ + + +    + {(savedObject as any).meta.title || getDefaultTitle(savedObject)} +

+
+
+ + + + {JSON.stringify( + { + ...{ + id: so.id, + type: so.type, + references: so.references, + attributes: so.attributes, + }, + ...(so.namespaces ? { namespaces: so.namespaces } : {}), + }, + null, + 4 + )} + + +
+ ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx index 6b209a62e1b98..48d0c93376cad 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -79,6 +79,7 @@ const defaultProps: TableProps = { onTableChange: () => {}, isSearching: false, onShowRelationships: () => {}, + onShowJSON: () => {}, canDelete: true, }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 719729cee2602..cb183ad5849ac 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -65,6 +65,7 @@ export interface TableProps { onTableChange: (table: any) => void; isSearching: boolean; onShowRelationships: (object: SavedObjectWithMetadata) => void; + onShowJSON: (object: SavedObjectWithMetadata) => void; canGoInApp: (obj: SavedObjectWithMetadata) => boolean; } @@ -143,6 +144,7 @@ export class Table extends PureComponent { onTableChange, goInspectObject, onShowRelationships, + onShowJSON, basePath, actionRegistry, } = this.props; @@ -162,7 +164,7 @@ export class Table extends PureComponent { defaultMessage: 'Type', }), multiSelect: 'or', - options: filterOptions, + options: [...filterOptions].sort((a, b) => (a.value > b.value ? 1 : -1)), }, // Add this back in once we have tag support // { @@ -261,6 +263,14 @@ export class Table extends PureComponent { onClick: (object) => onShowRelationships(object), 'data-test-subj': 'savedObjectsTableAction-relationships', }, + { + name: 'Raw JSON', + description: 'View saved object raw JSON', + type: 'icon', + icon: 'editorCodeBlock', + onClick: (object) => onShowJSON(object), + 'data-test-subj': 'savedObjectsTableAction-preview', + }, ...actionRegistry.getAll().map((action) => { return { ...action.euiAction, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 340c0e3237f91..a82924a79d526 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -75,6 +75,7 @@ import { } from '../../services'; import { Header, Table, Flyout, Relationships } from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { JsonFlyout } from './components/json_flyout'; interface ExportAllOption { id: string; @@ -109,6 +110,7 @@ export interface SavedObjectsTableState { isSearching: boolean; filteredItemCount: number; isShowingRelationships: boolean; + isShowingJSON: boolean; relationshipObject?: SavedObjectWithMetadata; isShowingDeleteConfirmModal: boolean; isShowingExportAllOptionsModal: boolean; @@ -139,6 +141,7 @@ export class SavedObjectsTable extends Component { + this.setState({ + isShowingJSON: true, + relationshipObject: object, + }); + }; + onHideRelationships = () => { this.setState({ isShowingRelationships: false, @@ -312,6 +322,13 @@ export class SavedObjectsTable extends Component { + this.setState({ + isShowingJSON: false, + relationshipObject: undefined, + }); + }; + onExport = async (includeReferencesDeep: boolean) => { const { selectedSavedObjects } = this.state; const { notifications, http } = this.props; @@ -494,6 +511,21 @@ export class SavedObjectsTable extends Component + ); + } + renderDeleteConfirmModal() { const { isShowingDeleteConfirmModal, isDeleting, selectedSavedObjects } = this.state; @@ -726,6 +758,7 @@ export class SavedObjectsTable extends Component {this.renderFlyout()} {this.renderRelationships()} + {this.renderJSON()} {this.renderDeleteConfirmModal()} {this.renderExportAllOptionsModal()}
diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index c27cfec24b332..ef1785e519ad2 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -9,7 +9,8 @@ "navigation", "savedObjects", "visualizations", - "embeddable" + "embeddable", + "extensions" ], "optionalPlugins": ["home", "share"] } diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index c571a5fb078bc..220fe68dd6e8d 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -80,6 +80,13 @@ export const VisualizeEditor = () => { return (
+
+ {!!visualizationIdFromUrl && !!services.extensions.tags.TagListEditable && ( + + )} +
{savedVisInstance && appState && currentAppState && ( { savedObjectsPublic, uiSettings, visualizeCapabilities, + extensions, }, } = useKibana(); const { pathname } = useLocation(); @@ -95,7 +96,11 @@ export const VisualizeListing = () => { ); const noItemsFragment = useMemo(() => getNoItemsMessage(createNewVis), [createNewVis]); - const tableColumns = useMemo(() => getTableColumns(application, history), [application, history]); + const tableColumns = useMemo(() => getTableColumns(application, history, extensions), [ + application, + history, + extensions, + ]); const fetchItems = useCallback( (filter) => { diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index a6adaf1f3c62b..7c1bcd0f22a6e 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -44,6 +44,7 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; +import { ExtensionsPluginSetup } from 'src/plugins/extensions/public'; import { ConfigSchema } from '../../config'; export type PureVisState = SavedVisState; @@ -93,6 +94,7 @@ export interface EditorRenderProps { export interface VisualizeServices extends CoreStart { embeddable: EmbeddableStart; + extensions: ExtensionsPluginSetup; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; kibanaLegacy: KibanaLegacyStart; diff --git a/src/plugins/visualize/public/application/utils/get_table_columns.tsx b/src/plugins/visualize/public/application/utils/get_table_columns.tsx index 805c039d9773b..b6ba92aaad30e 100644 --- a/src/plugins/visualize/public/application/utils/get_table_columns.tsx +++ b/src/plugins/visualize/public/application/utils/get_table_columns.tsx @@ -25,6 +25,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ApplicationStart } from 'kibana/public'; import { VisualizationListItem } from 'src/plugins/visualizations/public'; +import { ExtensionsPluginSetup } from 'src/plugins/extensions/public'; const getBadge = (item: VisualizationListItem) => { if (item.stage === 'beta') { @@ -80,7 +81,11 @@ const renderItemTypeIcon = (item: VisualizationListItem) => { return icon; }; -export const getTableColumns = (application: ApplicationStart, history: History) => [ +export const getTableColumns = ( + application: ApplicationStart, + history: History, + extensions: ExtensionsPluginSetup +) => [ { field: 'title', name: i18n.translate('visualize.listing.table.titleColumnName', { @@ -116,6 +121,20 @@ export const getTableColumns = (application: ApplicationStart, history: History) ), }, + ...(extensions.tags.TagList + ? [ + { + field: '', + name: 'Tags', + sortable: false, + render: (record: VisualizationListItem) => + !!extensions.tags.TagList ? ( + + ) : null, + }, + ] + : ([] as any)), + , { field: 'description', name: i18n.translate('visualize.listing.table.descriptionColumnName', { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index fd9a67599414f..662fab5b716f7 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -31,6 +31,7 @@ import { ScopedHistory, } from 'kibana/public'; +import { ExtensionsPluginSetup, ExtensionsPluginStart } from 'src/plugins/extensions/public'; import { Storage, createKbnUrlTracker, createKbnUrlStateStorage } from '../../kibana_utils/public'; import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; @@ -52,12 +53,14 @@ export interface VisualizePluginStartDependencies { embeddable: EmbeddableStart; kibanaLegacy: KibanaLegacyStart; savedObjects: SavedObjectsStart; + extensions: ExtensionsPluginSetup; } export interface VisualizePluginSetupDependencies { home?: HomePublicPluginSetup; kibanaLegacy: KibanaLegacySetup; data: DataPublicPluginSetup; + extensions: ExtensionsPluginStart; } export interface FeatureFlagConfig { @@ -75,7 +78,7 @@ export class VisualizePlugin public async setup( core: CoreSetup, - { home, kibanaLegacy, data }: VisualizePluginSetupDependencies + { home, kibanaLegacy, data, extensions }: VisualizePluginSetupDependencies ) { const { appMounted, @@ -146,6 +149,7 @@ export class VisualizePlugin const services: VisualizeServices = { ...coreStart, + extensions, history, kbnUrlStateStorage: createKbnUrlStateStorage({ history, diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 596ba17d343c0..120d08b7f8c62 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -44,6 +44,7 @@ "xpack.securitySolution": "plugins/security_solution", "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], + "xpack.tags": ["plugins/tags"], "xpack.taskManager": "legacy/plugins/task_manager", "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", diff --git a/x-pack/examples/tags_examples/README.md b/x-pack/examples/tags_examples/README.md new file mode 100644 index 0000000000000..42d4c111c4bc3 --- /dev/null +++ b/x-pack/examples/tags_examples/README.md @@ -0,0 +1,2 @@ +# `tags` plugin examples + diff --git a/x-pack/examples/tags_examples/kibana.json b/x-pack/examples/tags_examples/kibana.json new file mode 100644 index 0000000000000..1035ac5fd2ac7 --- /dev/null +++ b/x-pack/examples/tags_examples/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "tagsExamples", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["tags_examples"], + "server": false, + "ui": true, + "requiredPlugins": ["tags", "developerExamples"], + "optionalPlugins": [] +} diff --git a/x-pack/examples/tags_examples/package.json b/x-pack/examples/tags_examples/package.json new file mode 100644 index 0000000000000..0f283830f3aca --- /dev/null +++ b/x-pack/examples/tags_examples/package.json @@ -0,0 +1,17 @@ +{ + "name": "tags_examples", + "version": "1.0.0", + "main": "target/examples/tags_examples", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} diff --git a/x-pack/examples/tags_examples/public/application/components/index.ts b/x-pack/examples/tags_examples/public/application/components/index.ts new file mode 100644 index 0000000000000..224217e860e94 --- /dev/null +++ b/x-pack/examples/tags_examples/public/application/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './page'; diff --git a/x-pack/examples/tags_examples/public/application/components/page/index.tsx b/x-pack/examples/tags_examples/public/application/components/page/index.tsx new file mode 100644 index 0000000000000..aba2acd660369 --- /dev/null +++ b/x-pack/examples/tags_examples/public/application/components/page/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +export interface PageProps { + title?: React.ReactNode; +} + +export const Page: React.FC = ({ title = 'Untitled', children }) => { + return ( + + + + + +

{title}

+
+
+
+ + + {children} + + +
+
+ ); +}; diff --git a/x-pack/examples/tags_examples/public/application/containers/app/app.tsx b/x-pack/examples/tags_examples/public/application/containers/app/app.tsx new file mode 100644 index 0000000000000..7be3170c400e7 --- /dev/null +++ b/x-pack/examples/tags_examples/public/application/containers/app/app.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Page } from '../../components'; +import { TagListExample } from '../tag_list_example'; + +// eslint-disable-next-line +export interface Props {} + +export const App: React.FC = () => { + return ( + + + + ); +}; diff --git a/x-pack/examples/tags_examples/public/application/containers/app/index.ts b/x-pack/examples/tags_examples/public/application/containers/app/index.ts new file mode 100644 index 0000000000000..1460fdfef37e6 --- /dev/null +++ b/x-pack/examples/tags_examples/public/application/containers/app/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './app'; diff --git a/x-pack/examples/tags_examples/public/application/containers/app/lazy.tsx b/x-pack/examples/tags_examples/public/application/containers/app/lazy.tsx new file mode 100644 index 0000000000000..f12e16cbab9e5 --- /dev/null +++ b/x-pack/examples/tags_examples/public/application/containers/app/lazy.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { lazy, Suspense } from 'react'; +import { Props } from './app'; + +const AppLazy = lazy(() => + import('./app').then((module) => ({ + default: module.App, + })) +); + +export const App: React.FC = (props) => ( + }> + + +); diff --git a/x-pack/examples/tags_examples/public/application/containers/tag_list_example/index.ts b/x-pack/examples/tags_examples/public/application/containers/tag_list_example/index.ts new file mode 100644 index 0000000000000..8966175373cc3 --- /dev/null +++ b/x-pack/examples/tags_examples/public/application/containers/tag_list_example/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag_list_example'; diff --git a/x-pack/examples/tags_examples/public/application/containers/tag_list_example/tag_list_example.tsx b/x-pack/examples/tags_examples/public/application/containers/tag_list_example/tag_list_example.tsx new file mode 100644 index 0000000000000..5c8d37a3a65cf --- /dev/null +++ b/x-pack/examples/tags_examples/public/application/containers/tag_list_example/tag_list_example.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiTitle, EuiCode, EuiBadge } from '@elastic/eui'; +import { TagList, TagPicker, TagListEditable } from '../../../../../../plugins/tags/public'; + +export const TagListExample: React.FC = () => { + const [selected, setSelected] = useState([]); + + return ( +
+
+ +

Tag

+
+
+ {""} +
+
+ Hello world +
+
+
+ +
+ +

TagList

+
+
+ {""} +
+
+ foo + bar +
+
+
+ +
+ +

TagPicker

+
+
+ {''} +
+
+ +
+
+
+ +
+ +

TagListEditable

+
+
+ {""} +
+
+ +
+
+
+
+ ); +}; diff --git a/x-pack/examples/tags_examples/public/index.ts b/x-pack/examples/tags_examples/public/index.ts new file mode 100644 index 0000000000000..33a6a6cca5531 --- /dev/null +++ b/x-pack/examples/tags_examples/public/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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TagsExamplesPlugin } from './plugin'; + +export const plugin = () => new TagsExamplesPlugin(); diff --git a/x-pack/examples/tags_examples/public/mount.tsx b/x-pack/examples/tags_examples/public/mount.tsx new file mode 100644 index 0000000000000..eee2495f9c5f8 --- /dev/null +++ b/x-pack/examples/tags_examples/public/mount.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { CoreSetup, AppMountParameters } from 'kibana/public'; +import { StartDependencies } from './plugin'; +import { App } from './application/containers/app/lazy'; + +export const mount = (coreSetup: CoreSetup) => async ({ + element, +}: AppMountParameters) => { + const [, plugins] = await coreSetup.getStartServices(); + const reactElement = ( + + + + ); + render(reactElement, element); + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/examples/tags_examples/public/plugin.ts b/x-pack/examples/tags_examples/public/plugin.ts new file mode 100644 index 0000000000000..6f3a93b3baed5 --- /dev/null +++ b/x-pack/examples/tags_examples/public/plugin.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, CoreStart, AppNavLinkStatus } from '../../../../src/core/public'; +import { TagsPluginSetup, TagsPluginStart } from '../../../plugins/tags/public'; +import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; +import { mount } from './mount'; + +export interface SetupDependencies { + tags: TagsPluginSetup; + developerExamples: DeveloperExamplesSetup; +} + +export interface StartDependencies { + tags: TagsPluginStart; +} + +export class TagsExamplesPlugin + implements Plugin { + public setup(core: CoreSetup, { tags, developerExamples }: SetupDependencies) { + core.application.register({ + id: 'tags-examples', + title: 'Tags Examples', + navLinkStatus: AppNavLinkStatus.hidden, + mount: mount(core), + }); + + developerExamples.register({ + appId: 'tags-examples', + title: 'Tags', + description: 'Examples showcasing tags plugin', + links: [ + { + label: 'README', + href: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/tags', + iconType: 'logoGithub', + size: 's', + target: '_blank', + }, + ], + }); + } + + public start(core: CoreStart, plugins: StartDependencies) {} + + public stop() {} +} diff --git a/x-pack/examples/tags_examples/tsconfig.json b/x-pack/examples/tags_examples/tsconfig.json new file mode 100644 index 0000000000000..d508076b33199 --- /dev/null +++ b/x-pack/examples/tags_examples/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*", + ], + "exclude": [] +} diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index 3a95419d2f2fe..40812c29d1d1f 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"], + "requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share", "tags"], "configPath": ["xpack", "dashboardEnhanced"] } diff --git a/x-pack/plugins/dashboard_enhanced/public/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts index 854a4964ffe15..d635cfb263ef3 100644 --- a/x-pack/plugins/dashboard_enhanced/public/plugin.ts +++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts @@ -5,17 +5,21 @@ */ import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; +import { createElement as h } from 'react'; import { SharePluginStart, SharePluginSetup } from '../../../../src/plugins/share/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { DashboardDrilldownsService } from './services'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../ui_actions_enhanced/public'; -import { DashboardStart } from '../../../../src/plugins/dashboard/public'; +import { TagsPluginSetup, TagsPluginStart } from '../../tags/public'; +import { DashboardSetup, DashboardStart } from '../../../../src/plugins/dashboard/public'; export interface SetupDependencies { uiActionsEnhanced: AdvancedUiActionsSetup; embeddable: EmbeddableSetup; share: SharePluginSetup; + dashboard: DashboardSetup; + tags: TagsPluginSetup; } export interface StartDependencies { @@ -24,6 +28,7 @@ export interface StartDependencies { embeddable: EmbeddableStart; share: SharePluginStart; dashboard: DashboardStart; + tags: TagsPluginStart; } // eslint-disable-next-line @@ -43,6 +48,19 @@ export class DashboardEnhancedPlugin enableDrilldowns: true, }); + plugins.dashboard.setRenderBeforeDashboard((dashboard) => + h( + 'div', + { style: { padding: 8 } }, + h(plugins.tags.ui.TagListEditable, { + kid: `kid:::so:saved_objects/dashboard/${dashboard.getInput().id}`, + }) + ) + ); + + plugins.dashboard.setRenderTags((kid) => h(plugins.tags.ui.TagList, { kid })); + plugins.dashboard.setTagPicker(plugins.tags.ui.TagPicker); + return {}; } diff --git a/x-pack/plugins/tags/README.md b/x-pack/plugins/tags/README.md new file mode 100644 index 0000000000000..92d922ec1d1a2 --- /dev/null +++ b/x-pack/plugins/tags/README.md @@ -0,0 +1,3 @@ +# Kibana tags + +The Kibana tags plugin provides a common way to manage and attach tags to objects. diff --git a/x-pack/plugins/tags/common/constants.ts b/x-pack/plugins/tags/common/constants.ts new file mode 100644 index 0000000000000..c349b1ab4d0e1 --- /dev/null +++ b/x-pack/plugins/tags/common/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const TAGS_API_PATH = '/api/tags'; diff --git a/x-pack/plugins/tags/common/index.ts b/x-pack/plugins/tags/common/index.ts new file mode 100644 index 0000000000000..22a6320666278 --- /dev/null +++ b/x-pack/plugins/tags/common/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './constants'; +export * from './tags'; +export * from './tag_attachments'; +export * from './key_value'; diff --git a/x-pack/plugins/tags/common/key_value.test.ts b/x-pack/plugins/tags/common/key_value.test.ts new file mode 100644 index 0000000000000..b0dd84f8c60b2 --- /dev/null +++ b/x-pack/plugins/tags/common/key_value.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseTag } from './key_value'; + +describe('parseTag', () => { + test('simpleg non-key-value tag key contains the whole tag and value is empty', () => { + const result = parseTag('Staging'); + expect(result).toEqual({ + key: 'Staging', + value: '', + }); + }); + + test('parses key-value tag into key and value', () => { + const result = parseTag('Team:👍 AppArch'); + expect(result).toEqual({ + key: 'Team', + value: '👍 AppArch', + }); + }); + + test('trims whitespace in a key-value tag', () => { + const result = parseTag(' Team : Canvas '); + expect(result).toEqual({ + key: 'Team', + value: 'Canvas', + }); + }); + + test('key-value tag can have empty value when it has extra whitespace', () => { + const result = parseTag(' Team : '); + expect(result).toEqual({ + key: 'Team', + value: '', + }); + }); + + test('key-value tag without extra whitespace can have empty value', () => { + const result = parseTag('Environment:'); + expect(result).toEqual({ + key: 'Environment', + value: '', + }); + }); + + test('in key-value tag with multiple colons, the first colon is used as key separator', () => { + const result = parseTag('Environment:Production:123'); + expect(result).toEqual({ + key: 'Environment', + value: 'Production:123', + }); + }); +}); diff --git a/x-pack/plugins/tags/common/key_value.ts b/x-pack/plugins/tags/common/key_value.ts new file mode 100644 index 0000000000000..bb23b0acb111e --- /dev/null +++ b/x-pack/plugins/tags/common/key_value.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface TagKeyValue { + key: string; + value: string; +} + +/** + * Tag title can contain a colon ":", in that case we treat it as a key-value + * tag. The text before the first colon is assumed to be the key and the rest + * is the value. If tag is not a key-value tag (it has no colon) then the key + * is assumed to be the whole tag and the value is empty string. + * + * @example + * + * Tag title | Key-value + * ------------ | --------------------------------- + * Team:AppArch | { key: 'Team', value: 'AppArch' } + * Production | { key: 'Production', value: '' } + * + * @param title Plain text tag title as entered by user. + * @return Object representing tag parsed into key-value. + */ +export const parseTag = (title: string): TagKeyValue => { + const colonIndex = title.indexOf(':'); + + if (colonIndex < 0) + return { + key: title, + value: '', + }; + + return { + key: title.substr(0, colonIndex).trim(), + value: title.substr(colonIndex + 1).trim(), + }; +}; diff --git a/x-pack/plugins/tags/common/kid.test.ts b/x-pack/plugins/tags/common/kid.test.ts new file mode 100644 index 0000000000000..571764557c784 --- /dev/null +++ b/x-pack/plugins/tags/common/kid.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseKID, formatKID } from './kid'; + +describe('parseKID()', () => { + test('parses correct fully specified KID', () => { + const kid = parseKID('kid:default:expressions:expr:functions/kibana_context'); + expect(kid).toEqual({ + space: 'default', + plugin: 'expressions', + service: 'expr', + path: ['functions', 'kibana_context'], + }); + }); + + test('parses partially specified KIDs', () => { + const kid1 = parseKID('kid::canvas::elements/123'); + expect(kid1).toEqual({ + space: '', + plugin: 'canvas', + service: '', + path: ['elements', '123'], + }); + + const kid2 = parseKID('kid:::so:saved_object/dashboard/123'); + expect(kid2).toEqual({ + space: '', + plugin: '', + service: 'so', + path: ['saved_object', 'dashboard', '123'], + }); + }); + + test('throws on invalid kid protocol', () => { + expect(() => + parseKID('kidz:default:expressions:expr:functions/kibana_context') + ).toThrowErrorMatchingInlineSnapshot(`"Expected KID protocol to be \\"kid\\"."`); + }); + + test('throws when KID to few parts', () => { + expect(() => parseKID('kid:default:expressions:expr')).toThrowErrorMatchingInlineSnapshot( + `"Invalid number of parts in KID."` + ); + expect(() => parseKID('kid:default')).toThrowErrorMatchingInlineSnapshot( + `"Invalid number of parts in KID."` + ); + }); + + test('throws if KID is not a string', () => { + expect(() => parseKID(123 as any)).toThrowErrorMatchingInlineSnapshot( + `"KID must be a string."` + ); + }); + + test('throws if KID string is too long', () => { + expect(() => + parseKID('kid:default:expressions:expr:functions' + '/kibana_context'.repeat(1000)) + ).toThrowErrorMatchingInlineSnapshot(`"KID stirng too long."`); + }); +}); + +describe('formatKID()', () => { + test('formats fully specified KID', () => { + const kid = formatKID({ + space: 'test', + plugin: 'data', + service: 'query', + path: ['saved_query', 'bar'], + }); + expect(kid).toBe('kid:test:data:query:saved_query/bar'); + }); +}); diff --git a/x-pack/plugins/tags/common/kid.ts b/x-pack/plugins/tags/common/kid.ts new file mode 100644 index 0000000000000..16c349e91871e --- /dev/null +++ b/x-pack/plugins/tags/common/kid.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; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Utilities to work with KIDs (Kibana IDs). Kibana ID is a global resource + * identifier without a Kibana deployment. + * + * KID format: + * + * ``` + * kid:::: + * ``` + * + * Consider an index pattern with ID 123, its ID in `indexPatterns` service in `data` plugin: + * + * ``` + * kid:default:data:ip:index_pattern/123 + * ``` + * + * Index pattern is actually stored in saved object service, the KID + * of that resource in saved object service: + * + * ``` + * kid:default::so:saved_object/index_pattern/123 + * ``` + */ +export interface KID { + space: string; + plugin: string; + service: string; + path: string[]; +} + +export const parseKID = (str: string) => { + if (typeof str !== 'string') throw new TypeError('KID must be a string.'); + if (str.length < 8) throw new Error('KID string too short.'); + if (str.length > 2048) throw new Error('KID stirng too long.'); + + const parts = str.split(/[\/\:]/); + + if (parts.length < 5) throw new Error('Invalid number of parts in KID.'); + + const [protocol, space, plugin, service, ...path] = parts; + + if (protocol !== 'kid') throw new Error('Expected KID protocol to be "kid".'); + + return { + space, + plugin, + service, + path, + }; +}; + +export const formatKID = ({ space = '', plugin = '', service = '', path = [] }: KID): string => { + return `kid:${space}:${plugin}:${service}:${path.join('/')}`; +}; diff --git a/x-pack/plugins/tags/common/tag_attachments.ts b/x-pack/plugins/tags/common/tag_attachments.ts new file mode 100644 index 0000000000000..95b46b9d85be2 --- /dev/null +++ b/x-pack/plugins/tags/common/tag_attachments.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RawTagWithId } from './tags'; + +/** + * Represents a tag attachemnt to KID (Kibana ID) as stored in + * Saved Object client. + */ +export interface RawTagAttachment { + tagId: string; + kid: string; + createdBy: string | null; + createdAt: string; +} + +/** Tag attachment together with saved object ID. */ +export interface RawTagAttachmentWithId extends RawTagAttachment { + id: string; +} + +export interface TagAttachmentClientCreateParams { + attachments: Array>; +} + +export interface TagAttachmentClientCreateResult { + attachments: RawTagAttachmentWithId[]; +} + +export interface TagAttachmentClientDeleteParams { + kid: string; + tagId: string; +} + +export interface TagAttachmentClientSetParams { + kid: string; + tagIds: string[]; +} + +export interface TagAttachmentClientSetResult { + attachments: RawTagAttachmentWithId[]; +} + +export interface TagAttachmentClientGetResourceTagsParams { + kid: string; +} + +export interface TagAttachmentClientGetResourceTagsResult { + attachments: RawTagAttachmentWithId[]; + tags: RawTagWithId[]; +} + +export interface TagAttachmentClientFindResourcesParams { + tagIds: string[]; + kidPrefix: string; + perPage: number; + page: number; +} + +export interface TagAttachmentClientFindResourcesResult { + attachments: RawTagAttachmentWithId[]; +} + +/** + * CRUD + List/Find API for tag attachments. + */ +export interface ITagAttachmentsClient { + create(params: TagAttachmentClientCreateParams): Promise; + set(params: TagAttachmentClientSetParams): Promise; + del(params: TagAttachmentClientDeleteParams): Promise; + getAttachedTags( + params: TagAttachmentClientGetResourceTagsParams + ): Promise; + findResources( + params: TagAttachmentClientFindResourcesParams + ): Promise; +} diff --git a/x-pack/plugins/tags/common/tags.ts b/x-pack/plugins/tags/common/tags.ts new file mode 100644 index 0000000000000..471f2fa974afe --- /dev/null +++ b/x-pack/plugins/tags/common/tags.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface RawTag { + enabled: boolean; + title: string; + description: string; + key: string; + value: string; + color: '' | string; + createdBy: string | null; + updatedBy: string | null; + createdAt: string; + updatedAt: string; +} + +export interface RawTagWithId extends RawTag { + id: string; +} + +export interface TagsClientCreateParams { + tag: Pick; +} + +export interface TagsClientCreateResult { + tag: RawTagWithId; +} + +export interface TagsClientReadParams { + id: string; +} + +export interface TagsClientReadResult { + tag: RawTagWithId; +} + +export interface TagsClientUpdateParams { + patch: Pick; +} + +export interface TagsClientUpdateResult { + patch: Partial; +} + +export interface TagsClientDeleteParams { + id: string; +} + +export interface TagsClientGetAllResult { + tags: RawTagWithId[]; +} + +/** + * CRUD + List/Find API for tags. + */ +export interface ITagsClient { + create(params: TagsClientCreateParams): Promise; + read(params: TagsClientReadParams): Promise; + update(params: TagsClientUpdateParams): Promise; + del(params: TagsClientDeleteParams): Promise; + getAll(): Promise; +} diff --git a/x-pack/plugins/tags/kibana.json b/x-pack/plugins/tags/kibana.json new file mode 100644 index 0000000000000..863f8d76ae55b --- /dev/null +++ b/x-pack/plugins/tags/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "tags", + "server": true, + "ui": true, + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "tags"], + "requiredPlugins": ["management", "bfetch", "extensions"], + "optionalPlugins": ["security"] +} diff --git a/x-pack/plugins/tags/public/application/containers/tags_app/index.ts b/x-pack/plugins/tags/public/application/containers/tags_app/index.ts new file mode 100644 index 0000000000000..a6a89f7d06650 --- /dev/null +++ b/x-pack/plugins/tags/public/application/containers/tags_app/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tags_app'; diff --git a/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/empty.tsx b/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/empty.tsx new file mode 100644 index 0000000000000..fe6ed7ff90c73 --- /dev/null +++ b/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/empty.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +export const Empty: React.FC = () => { + return ( + Find resources by tag} + body={ + <> +

Select tags on the left to find attached resources

+ + } + /> + ); +}; diff --git a/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/index.ts b/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/index.ts new file mode 100644 index 0000000000000..9c8a7155fd66f --- /dev/null +++ b/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './landing_page'; diff --git a/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/landing_page.tsx b/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/landing_page.tsx new file mode 100644 index 0000000000000..cbb1b7aa609f4 --- /dev/null +++ b/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/landing_page.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSideNav, EuiPanel, EuiHorizontalRule } from '@elastic/eui'; +import { useTagsApp } from '../../../../context'; +import { Tag as TagUi } from '../../../../../containers/tag'; +import { TagPicker } from '../../../../../containers/tag_picker'; +import { Tag } from '../../../../../services/tags_service/tag_manager/tag'; +import { ResultsList } from './results_list'; + +export const LandinPage: React.FC = () => { + const { manager, useQueryParam } = useTagsApp(); + const tags = manager.useTagsList(); + const [uriTags, setUriTags] = useQueryParam('tags'); + const selected = useMemo(() => (uriTags ? uriTags.split('_') : []), [uriTags]); + const setSelected = (newSelection: string[]) => + newSelection.length ? setUriTags(newSelection.join('_')) : setUriTags(null); + + const grouped = useMemo(() => { + const categories: Record = {}; + const remaining: Tag[] = []; + for (const tag of tags) { + if (tag.data.value) { + if (!categories[tag.data.key]) categories[tag.data.key] = []; + categories[tag.data.key].push(tag); + } else remaining.push(tag); + } + return { + categories: Object.entries(categories).sort((a, b) => (a[0] > b[0] ? 1 : -1)), + remaining, + }; + }, [tags]); + + return ( +
+ + + ({ + name, + id: name, + items: tagsList.map((tag) => ({ + name: tag.data.title, + id: tag.id, + renderItem: () => ( +
+ +
+ ), + })), + }))} + /> + +
+ ({ + name: tag.data.title, + id: tag.id, + renderItem: () => ( +
+ +
+ ), + })), + }, + ]} + /> +
+
+ +
+ +
+ + + +
+
+
+ ); +}; diff --git a/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/results_list.tsx b/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/results_list.tsx new file mode 100644 index 0000000000000..93b2c8dc2f77a --- /dev/null +++ b/x-pack/plugins/tags/public/application/containers/tags_app/pages/landing_page/results_list.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; +import useMountedState from 'react-use/lib/useMountedState'; +import { useTagsApp } from '../../../../context'; +import { Empty } from './empty'; + +export interface Props { + tagIds: string[]; +} + +export const ResultsList: React.FC = ({ tagIds }) => { + const { tags, kid } = useTagsApp(); + const isMounted = useMountedState(); + const [kids, setKids] = useState(null); + const [error, setError] = useState(null); + useEffect(() => { + tags + .attachments!.findResources({ + tagIds, + kidPrefix: 'kid:', + page: 1, + perPage: 100, + }) + .then( + (response) => { + if (!isMounted()) return; + setKids(response.attachments.map((item) => item.kid)); + }, + (newError) => { + if (!isMounted()) return; + setError(newError); + } + ); + }, [isMounted, tags.attachments, tagIds]); + + const grouped = useMemo<{ + dashboards: string[]; + visualizations: string[]; + other: string[]; + }>(() => { + const dashboards: string[] = []; + const visualizations: string[] = []; + const other: string[] = []; + if (!kids || !kids.length) return { dashboards, visualizations, other }; + for (const item of kids) { + if (item.indexOf('kid:::so:saved_objects/dashboard/') > -1) dashboards.push(item); + else if (item.indexOf('kid:::so:saved_objects/visualization/') > -1) + visualizations.push(item); + else other.push(item); + } + return { dashboards, visualizations, other }; + }, [kids]); + + if (error || !kids) return null; + + return ( + <> + {!!kids.length ? ( + <> + {!!grouped.dashboards.length && ( + <> + + + +
Dashboards
+
+
+
+ + {grouped.dashboards.map((resuldKid) => ( + + + + ))} + + + + )} + {!!grouped.visualizations.length && ( + <> + + + +
Visualizations
+
+
+
+ + {grouped.visualizations.map((resuldKid) => ( + + + + ))} + + + + )} + {!!grouped.other.length && + grouped.other.map((resuldKid) => ( + + +
Other
+
+ + + + +
+ ))} + + ) : ( + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/tags/public/application/containers/tags_app/tags_app.tsx b/x-pack/plugins/tags/public/application/containers/tags_app/tags_app.tsx new file mode 100644 index 0000000000000..e864509c91108 --- /dev/null +++ b/x-pack/plugins/tags/public/application/containers/tags_app/tags_app.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { LandinPage } from './pages/landing_page'; +import { TagsAppServices } from '../../services'; +import { TagsAppProvider } from '../../context'; + +export interface Props { + services: TagsAppServices; +} + +export const TagsApp: React.FC = ({ services }) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/tags/public/application/context/index.ts b/x-pack/plugins/tags/public/application/context/index.ts new file mode 100644 index 0000000000000..536d901c632be --- /dev/null +++ b/x-pack/plugins/tags/public/application/context/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { useTagsApp } from './tags_app_context'; +export { TagsAppProvider } from './provider'; diff --git a/x-pack/plugins/tags/public/application/context/provider.tsx b/x-pack/plugins/tags/public/application/context/provider.tsx new file mode 100644 index 0000000000000..7c85bee38abc6 --- /dev/null +++ b/x-pack/plugins/tags/public/application/context/provider.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Router } from 'react-router-dom'; +import { context } from './tags_app_context'; +import { TagsAppServices } from '../services'; +import { TagsProvider } from '../../context'; + +export interface Props { + services: TagsAppServices; +} + +export const TagsAppProvider: React.FC = ({ services, children }) => { + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/tags/public/application/context/tags_app_context.ts b/x-pack/plugins/tags/public/application/context/tags_app_context.ts new file mode 100644 index 0000000000000..bd7f1f82574db --- /dev/null +++ b/x-pack/plugins/tags/public/application/context/tags_app_context.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createContext, useContext } from 'react'; +import { TagsAppServices } from '../services'; + +export const context = createContext(undefined); +export const useTagsApp = () => useContext(context)!; diff --git a/x-pack/plugins/tags/public/application/index.ts b/x-pack/plugins/tags/public/application/index.ts new file mode 100644 index 0000000000000..68dd889852444 --- /dev/null +++ b/x-pack/plugins/tags/public/application/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './lazy'; diff --git a/x-pack/plugins/tags/public/application/lazy.tsx b/x-pack/plugins/tags/public/application/lazy.tsx new file mode 100644 index 0000000000000..d79a233efa02d --- /dev/null +++ b/x-pack/plugins/tags/public/application/lazy.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { lazy, Suspense } from 'react'; +import { Props } from './containers/tags_app'; + +const TagsAppLazy = lazy(() => + import('./containers/tags_app').then((module) => ({ + default: module.TagsApp, + })) +); + +export const TagsApp: React.FC = (props) => ( + }> + + +); diff --git a/x-pack/plugins/tags/public/application/services/index.ts b/x-pack/plugins/tags/public/application/services/index.ts new file mode 100644 index 0000000000000..18efb3d09ce2c --- /dev/null +++ b/x-pack/plugins/tags/public/application/services/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tags_app_services'; diff --git a/x-pack/plugins/tags/public/application/services/tags_app_services.ts b/x-pack/plugins/tags/public/application/services/tags_app_services.ts new file mode 100644 index 0000000000000..2ec4d8619d546 --- /dev/null +++ b/x-pack/plugins/tags/public/application/services/tags_app_services.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ScopedHistory } from 'kibana/public'; +import React from 'react'; +import { TagsServiceContract, TagManager, KidService } from '../../services'; + +/* eslint-disable react-hooks/rules-of-hooks */ + +export interface Params { + readonly tags: TagsServiceContract; + readonly kid: KidService; + readonly history: ScopedHistory; +} + +export class TagsAppServices { + public readonly tags: TagsServiceContract; + public readonly manager: TagManager; + public readonly kid: KidService; + public readonly history: ScopedHistory; + + constructor(params: Params) { + this.tags = params.tags; + this.manager = params.tags.manager!; + this.kid = params.kid; + this.history = params.history; + } + + public readonly useQueryParam = ( + key: string + ): [string | null, (value: string | null) => void] => { + const [value, setValue] = React.useState( + new URLSearchParams(location.search).get(key) || null + ); + + React.useEffect(() => { + setValue(new URLSearchParams(location.search).get(key) || null); + const unregister = this.history.listen((location) => { + setValue(new URLSearchParams(location.search).get(key)); + }); + return () => unregister(); + }, [key]); + + const set = React.useCallback( + (newValue: string | null) => { + const params = new URLSearchParams(this.history.location.search); + if (newValue === null) params.delete(key); + else params.set(key, newValue); + const search = params.toString(); + this.history.push({ + search, + }); + }, + [key] + ); + + return [value, set]; + }; +} diff --git a/x-pack/plugins/tags/public/components/create_new_tag_form/create_new_tag_form.tsx b/x-pack/plugins/tags/public/components/create_new_tag_form/create_new_tag_form.tsx new file mode 100644 index 0000000000000..d9792333416bb --- /dev/null +++ b/x-pack/plugins/tags/public/components/create_new_tag_form/create_new_tag_form.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiSpacer, + EuiColorPicker, + EuiTextArea, +} from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { Link } from 'react-router-dom'; +import { EuiHorizontalRule } from '@elastic/eui'; +import { EuiDescribedFormGroup } from '@elastic/eui'; +import { EuiCode } from '@elastic/eui'; +import { txtTitle, txtColor, txtDescription, txtSubmit, txtCancel } from './i18n'; +import { Tag } from '../../components/tag'; +import { parseTag } from '../../../common'; + +export interface Props { + title: string; + color?: string; + description?: string; + disabled?: boolean; + submitText?: string; + onTitleChange: (title: string) => void; + onColorChange?: (color: string) => void; + onDescriptionChange?: (description: string) => void; + onSubmit: () => void; +} + +export const CreateNewTagForm: React.FC = ({ + title, + color, + description, + disabled, + submitText = txtSubmit, + onTitleChange, + onColorChange, + onDescriptionChange, + onSubmit, +}) => { + const { key, value } = useMemo(() => parseTag(title), [title]); + + return ( + { + e.preventDefault(); + onSubmit(); + }} + > + Display} + description={ + <> +
This is how your tag will look like.
+ + {!!key && } + + } + > + + Use colon : to create a key-value tag. Max. 256 + characters. + + } + > + onTitleChange(e.target.value)} + autoFocus + aria-label={txtTitle} + disabled={disabled} + /> + + + {!!onColorChange && ( + + onColorChange(newColor)} + aria-label={txtColor} + disabled={disabled} + /> + + )} +
+ + {!!onDescriptionChange && ( + Extra} + description={<>Describe how your tag should be used.} + > + + onDescriptionChange(e.target.value)} + aria-label={txtDescription} + disabled={disabled} + /> + + + )} + + + + + + + {submitText} + + + + + {txtCancel} + + + +
+ ); +}; diff --git a/x-pack/plugins/tags/public/components/create_new_tag_form/i18n.ts b/x-pack/plugins/tags/public/components/create_new_tag_form/i18n.ts new file mode 100644 index 0000000000000..fee695e0e6ba3 --- /dev/null +++ b/x-pack/plugins/tags/public/components/create_new_tag_form/i18n.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtTitle = i18n.translate('xpack.tags.creanteNewTagForm.title', { + defaultMessage: 'Title', +}); + +export const txtColor = i18n.translate('xpack.tags.creanteNewTagForm.color', { + defaultMessage: 'Color', +}); + +export const txtDescription = i18n.translate('xpack.tags.creanteNewTagForm.description', { + defaultMessage: 'Description', +}); + +export const txtSubmit = i18n.translate('xpack.tags.creanteNewTagForm.submit', { + defaultMessage: 'Save', +}); + +export const txtCancel = i18n.translate('xpack.tags.creanteNewTagForm.cancel', { + defaultMessage: 'Cancel', +}); diff --git a/x-pack/plugins/tags/public/components/create_new_tag_form/index.ts b/x-pack/plugins/tags/public/components/create_new_tag_form/index.ts new file mode 100644 index 0000000000000..0c8e80bb2ee4b --- /dev/null +++ b/x-pack/plugins/tags/public/components/create_new_tag_form/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_new_tag_form'; diff --git a/x-pack/plugins/tags/public/components/kid_card/index.ts b/x-pack/plugins/tags/public/components/kid_card/index.ts new file mode 100644 index 0000000000000..752d5ce23d183 --- /dev/null +++ b/x-pack/plugins/tags/public/components/kid_card/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './kid_card'; diff --git a/x-pack/plugins/tags/public/components/kid_card/kid_card.tsx b/x-pack/plugins/tags/public/components/kid_card/kid_card.tsx new file mode 100644 index 0000000000000..7fd78dda4b84a --- /dev/null +++ b/x-pack/plugins/tags/public/components/kid_card/kid_card.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiCard, EuiIcon } from '@elastic/eui'; +import { KidInfo } from '../../services'; + +export interface Props { + info: KidInfo; +} + +export const KidCard: React.FC = ({ info }) => { + return ( + : undefined} + title={info.name || 'Untitled'} + description={info.description || ''} + onClick={() => (info.goto ? info.goto() : undefined)} + /> + ); +}; diff --git a/x-pack/plugins/tags/public/components/page/index.ts b/x-pack/plugins/tags/public/components/page/index.ts new file mode 100644 index 0000000000000..224217e860e94 --- /dev/null +++ b/x-pack/plugins/tags/public/components/page/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './page'; diff --git a/x-pack/plugins/tags/public/components/page/page.examples.tsx b/x-pack/plugins/tags/public/components/page/page.examples.tsx new file mode 100644 index 0000000000000..29f2642c4cf33 --- /dev/null +++ b/x-pack/plugins/tags/public/components/page/page.examples.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { storiesOf } from '@storybook/react'; +import { Page } from './page'; + +storiesOf('management/Page', module).add('default', () => ); diff --git a/x-pack/plugins/tags/public/components/page/page.tsx b/x-pack/plugins/tags/public/components/page/page.tsx new file mode 100644 index 0000000000000..cac79b5f8939f --- /dev/null +++ b/x-pack/plugins/tags/public/components/page/page.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiPageContent, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiSpacer, + EuiHorizontalRule, +} from '@elastic/eui'; + +export interface Props { + id: string; + title: React.ReactNode; + subtitle?: React.ReactNode; + callToAction?: React.ReactNode; + separator?: boolean; +} + +export const Page: React.FC = ({ + id, + title, + subtitle, + callToAction, + separator, + children, +}) => { + return ( + + + + +

{title}

+
+ {!!subtitle && ( + + {subtitle} + + )} +
+ {!!callToAction && {callToAction}} +
+ {!!separator && } + +
{children}
+
+ ); +}; diff --git a/x-pack/plugins/tags/public/components/tag/index.ts b/x-pack/plugins/tags/public/components/tag/index.ts new file mode 100644 index 0000000000000..3848bb9ec9854 --- /dev/null +++ b/x-pack/plugins/tags/public/components/tag/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag'; diff --git a/x-pack/plugins/tags/public/components/tag/tag.tsx b/x-pack/plugins/tags/public/components/tag/tag.tsx new file mode 100644 index 0000000000000..6a2c0805dcd68 --- /dev/null +++ b/x-pack/plugins/tags/public/components/tag/tag.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { RawTagWithId } from '../../../common'; + +export type TagView = Pick & + Partial>; + +export interface Props { + tag: TagView; +} + +export const Tag: React.FC = React.memo(({ tag }) => { + const content = tag.key ? ( + <> + {tag.key} + {!!tag.value ? ':' : null} + {!!tag.value ? {tag.value} : null} + + ) : ( + tag.title + ); + + return {content}; +}); diff --git a/x-pack/plugins/tags/public/components/tag_list/index.ts b/x-pack/plugins/tags/public/components/tag_list/index.ts new file mode 100644 index 0000000000000..b4a7ce47f572d --- /dev/null +++ b/x-pack/plugins/tags/public/components/tag_list/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag_list'; diff --git a/x-pack/plugins/tags/public/components/tag_list/tag_list.tsx b/x-pack/plugins/tags/public/components/tag_list/tag_list.tsx new file mode 100644 index 0000000000000..468010a662b98 --- /dev/null +++ b/x-pack/plugins/tags/public/components/tag_list/tag_list.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Tag, TagView } from '../tag/tag'; + +export interface Props { + tags: TagView[]; +} + +export const TagList: React.FC = React.memo(({ tags }) => { + return ( + + {tags.map((tag) => ( + + + + ))} + + ); +}); diff --git a/x-pack/plugins/tags/public/components/tag_picker/i18n.ts b/x-pack/plugins/tags/public/components/tag_picker/i18n.ts new file mode 100644 index 0000000000000..fa70e07eba22d --- /dev/null +++ b/x-pack/plugins/tags/public/components/tag_picker/i18n.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtPlaceholder = i18n.translate('xpack.tags.components.tagPicker.placeholder', { + defaultMessage: 'Select tags', +}); diff --git a/x-pack/plugins/tags/public/components/tag_picker/index.ts b/x-pack/plugins/tags/public/components/tag_picker/index.ts new file mode 100644 index 0000000000000..abbd2486f6a82 --- /dev/null +++ b/x-pack/plugins/tags/public/components/tag_picker/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag_picker'; diff --git a/x-pack/plugins/tags/public/components/tag_picker/tag_picker.tsx b/x-pack/plugins/tags/public/components/tag_picker/tag_picker.tsx new file mode 100644 index 0000000000000..063cf5fc0dd96 --- /dev/null +++ b/x-pack/plugins/tags/public/components/tag_picker/tag_picker.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useCallback } from 'react'; +import { + EuiHighlight, + EuiHealth, + EuiComboBox, + EuiComboBoxProps, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import { TagView } from '../tag/tag'; +import { txtPlaceholder } from './i18n'; + +export interface PickerTagView extends TagView { + id: string; +} + +export interface TagPickerProps extends Pick, 'fullWidth'> { + isDisabled?: boolean; + tags: PickerTagView[]; + selected: string[]; + onChange: (selected: string[]) => void; +} + +export const TagPicker: React.FC = React.memo( + ({ isDisabled, tags, selected, onChange, ...rest }) => { + const options = useMemo>>(() => { + return tags.map((value) => ({ + key: value.id, + label: value.title, + color: value.color, + value, + })); + }, [tags]); + + const selectedOptions = useMemo>>(() => { + return options.filter((option) => selected.indexOf(option.value!.id) > -1); + }, [options, selected]); + + const handleChange = useCallback( + (newSelection: Array>) => { + onChange(newSelection.map(({ value }) => value!.id)); + }, + [onChange] + ); + + return ( + + {...rest} + isDisabled={isDisabled} + placeholder={txtPlaceholder} + options={options} + selectedOptions={selectedOptions} + onChange={handleChange} + renderOption={({ label, color }, searchValue, contentClassName) => { + return ( + + + {label} + + + ); + }} + /> + ); + } +); diff --git a/x-pack/plugins/tags/public/containers/create_new_tag_form/create_new_tag_form.tsx b/x-pack/plugins/tags/public/containers/create_new_tag_form/create_new_tag_form.tsx new file mode 100644 index 0000000000000..dcbf6c2b57480 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/create_new_tag_form/create_new_tag_form.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { takeUntil } from 'rxjs/operators'; +import { useToasts } from '../../../../../../src/plugins/kibana_react/public'; +import { CreateNewTagForm as CreateNewTagFormUi } from '../../components/create_new_tag_form'; +import { txtTagCreated, txtCouldNotCreate } from './i18n'; +import { useUnmount$ } from '../../hooks/use_unmount'; +import { useTags } from '../../context'; + +const defaultColor = '#548034'; + +export interface Props { + onCreate?: () => void; +} + +export const CreateNewTagForm: React.FC = ({ onCreate }) => { + const { manager } = useTags(); + const unmount$ = useUnmount$(); + const toasts = useToasts(); + const [title, setTitle] = useState(''); + const [color, setColor] = useState(defaultColor); + const [description, setDescription] = useState(''); + const [disabled, setDisabled] = useState(false); + + const handleSubmit = async () => { + setDisabled(true); + manager + .create$({ + title, + color, + description, + }) + .pipe(takeUntil(unmount$)) + .subscribe( + () => {}, + (error) => { + toasts.addError(error, { title: txtCouldNotCreate }); + setDisabled(false); + } + ); + toasts.addSuccess({ + title: txtTagCreated, + }); + if (onCreate) onCreate(); + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/tags/public/containers/create_new_tag_form/i18n.ts b/x-pack/plugins/tags/public/containers/create_new_tag_form/i18n.ts new file mode 100644 index 0000000000000..6b2f2007874d6 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/create_new_tag_form/i18n.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtTagCreated = i18n.translate('xpack.tags.creanteNewTagForm.tagCreated', { + defaultMessage: 'Tag created', +}); + +export const txtCouldNotCreate = i18n.translate('xpack.tags.creanteNewTagForm.couldNotCreate', { + defaultMessage: 'Could not create tag', +}); diff --git a/x-pack/plugins/tags/public/containers/create_new_tag_form/index.ts b/x-pack/plugins/tags/public/containers/create_new_tag_form/index.ts new file mode 100644 index 0000000000000..0c8e80bb2ee4b --- /dev/null +++ b/x-pack/plugins/tags/public/containers/create_new_tag_form/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_new_tag_form'; diff --git a/x-pack/plugins/tags/public/containers/tag/index.ts b/x-pack/plugins/tags/public/containers/tag/index.ts new file mode 100644 index 0000000000000..3848bb9ec9854 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag'; diff --git a/x-pack/plugins/tags/public/containers/tag/tag.tsx b/x-pack/plugins/tags/public/containers/tag/tag.tsx new file mode 100644 index 0000000000000..66c45ca001125 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag/tag.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useTags } from '../../context'; +import { Tag as TagUi } from '../../components/tag'; + +export interface TagProps { + id: string; +} + +export const Tag: React.FC = React.memo(({ id }) => { + const { manager } = useTags(); + const tag = manager!.useTag(id); + + if (!tag) return null; + + return ; +}); diff --git a/x-pack/plugins/tags/public/containers/tag_list/index.ts b/x-pack/plugins/tags/public/containers/tag_list/index.ts new file mode 100644 index 0000000000000..b4a7ce47f572d --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_list/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag_list'; diff --git a/x-pack/plugins/tags/public/containers/tag_list/tag_list.tsx b/x-pack/plugins/tags/public/containers/tag_list/tag_list.tsx new file mode 100644 index 0000000000000..da2a0919471dd --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_list/tag_list.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useTags } from '../../context'; +import { Tag } from '../tag'; + +export interface TagListProps { + kid: string; + onEditClick?: () => void; +} + +export const TagList: React.FC = React.memo(({ kid, onEditClick }) => { + const { manager } = useTags(); + const attachments = manager!.useResource(kid); + + return ( + <> + + {attachments.map(({ data }) => ( + + + + ))} + + {!!onEditClick && ( + + {'...'} + + )} + + ); +}); diff --git a/x-pack/plugins/tags/public/containers/tag_list_editable/index.ts b/x-pack/plugins/tags/public/containers/tag_list_editable/index.ts new file mode 100644 index 0000000000000..1c4befbecf3d0 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_list_editable/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag_list_editable'; diff --git a/x-pack/plugins/tags/public/containers/tag_list_editable/tag_list_editable.tsx b/x-pack/plugins/tags/public/containers/tag_list_editable/tag_list_editable.tsx new file mode 100644 index 0000000000000..356d2d099dba8 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_list_editable/tag_list_editable.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { TagList } from '../tag_list'; +import { TagPickerForResource } from '../tag_picker_for_resource'; + +export interface TagListEditableProps { + kid: string; +} + +export const TagListEditable: React.FC = React.memo(({ kid }) => { + const [edit, setEdit] = useState(false); + + if (!edit) { + return setEdit(true)} />; + } + + return ( + setEdit(false)} onCancel={() => setEdit(false)} /> + ); +}); diff --git a/x-pack/plugins/tags/public/containers/tag_picker/index.ts b/x-pack/plugins/tags/public/containers/tag_picker/index.ts new file mode 100644 index 0000000000000..abbd2486f6a82 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_picker/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag_picker'; diff --git a/x-pack/plugins/tags/public/containers/tag_picker/tag_picker.tsx b/x-pack/plugins/tags/public/containers/tag_picker/tag_picker.tsx new file mode 100644 index 0000000000000..89be26cdd67cf --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_picker/tag_picker.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { + TagPicker as TagPickerUi, + TagPickerProps as TagPickerPropsUi, +} from '../../components/tag_picker'; +import { useTags } from '../../context'; + +export type TagPickerProps = Omit; + +export const TagPicker: React.FC = (props) => { + const { manager } = useTags(); + const initializing = manager!.useInitializing(); + const tags = manager!.useTags(); + const rawTags = useMemo( + () => + Object.values(tags) + .map(({ data }) => data) + .sort((a, b) => (a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1)), + [tags] + ); + + if (initializing) return null; + + if (!rawTags.length) { + return
Not tags setup! Go to mangement app to add tags.
; + } + + return ; +}; diff --git a/x-pack/plugins/tags/public/containers/tag_picker_for_resource/i18n.ts b/x-pack/plugins/tags/public/containers/tag_picker_for_resource/i18n.ts new file mode 100644 index 0000000000000..b0a8190cc85c3 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_picker_for_resource/i18n.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtSave = i18n.translate('xpack.tags.containers.tagPickerForResource.save', { + defaultMessage: 'Save', +}); + +export const txtCancel = i18n.translate('xpack.tags.containers.tagPickerForResource.cancel', { + defaultMessage: 'Cancel', +}); diff --git a/x-pack/plugins/tags/public/containers/tag_picker_for_resource/index.ts b/x-pack/plugins/tags/public/containers/tag_picker_for_resource/index.ts new file mode 100644 index 0000000000000..e4bd52eda9e59 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_picker_for_resource/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tag_picker_for_resource'; diff --git a/x-pack/plugins/tags/public/containers/tag_picker_for_resource/tag_picker_for_resource.tsx b/x-pack/plugins/tags/public/containers/tag_picker_for_resource/tag_picker_for_resource.tsx new file mode 100644 index 0000000000000..aa489276c9b0e --- /dev/null +++ b/x-pack/plugins/tags/public/containers/tag_picker_for_resource/tag_picker_for_resource.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; +import { takeUntil } from 'rxjs/operators'; +import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { TagPicker } from '../tag_picker'; +import { useTags } from '../../context'; +import { RawTagAttachmentWithId } from '../../../common'; +import { useUnmount$ } from '../../hooks/use_unmount'; +import { txtSave, txtCancel } from './i18n'; + +export interface TagPickerForResourceProps { + kid: string; + onSave: (selected: string[]) => void; + onCancel: () => void; +} + +export const TagPickerForResource: React.FC = ({ + kid, + onSave, + onCancel, + ...rest +}) => { + const unmount$ = useUnmount$(); + const { manager } = useTags(); + const resource = manager!.useResource(kid); + const [attachments, setAttachments] = useState(null); + const [error, setError] = useState(null); + const [selected, setSelected] = useState([]); + + useEffect(() => { + manager! + .getResourceDataAttachments$(kid) + .pipe(takeUntil(unmount$)) + .subscribe((response) => { + const loadedAttachments = response.map(({ data }) => data); + setAttachments(loadedAttachments); + setSelected(loadedAttachments.map(({ tagId }) => tagId)); + }, setError); + }, [unmount$, manager, kid]); + + if (!attachments && !error) { + return ( + data.tagId)} onChange={() => {}} /> + ); + } + + if (error) { + return
could not load resource tag attachments: {error.message}
; + } + + const handleSave = () => { + manager!.setAttachments$(kid, selected); + onSave(selected); + }; + + return ( + <> + + + + + + {txtSave} + + + + + {txtCancel} + + + + + ); +}; diff --git a/x-pack/plugins/tags/public/containers/update_tag_form/i18n.ts b/x-pack/plugins/tags/public/containers/update_tag_form/i18n.ts new file mode 100644 index 0000000000000..691b30179ddd0 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/update_tag_form/i18n.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtTagUpdated = i18n.translate('xpack.tags.creanteNewTagForm.tagUpdated', { + defaultMessage: 'Tag updated', +}); + +export const txtCouldNotUpdate = i18n.translate('xpack.tags.creanteNewTagForm.couldNotCreate', { + defaultMessage: 'Could not update tag', +}); diff --git a/x-pack/plugins/tags/public/containers/update_tag_form/index.ts b/x-pack/plugins/tags/public/containers/update_tag_form/index.ts new file mode 100644 index 0000000000000..a8d98aa18ca8b --- /dev/null +++ b/x-pack/plugins/tags/public/containers/update_tag_form/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './update_tag_form'; +export * from './update_tag_by_id_form'; diff --git a/x-pack/plugins/tags/public/containers/update_tag_form/update_tag_by_id_form.tsx b/x-pack/plugins/tags/public/containers/update_tag_form/update_tag_by_id_form.tsx new file mode 100644 index 0000000000000..d62a482049ad2 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/update_tag_form/update_tag_by_id_form.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { UpdateTagForm, UpdateTagFormProps } from './update_tag_form'; +import { useTags } from '../../context'; + +export interface UpdateTagByIdFormProps extends Omit { + id: string; +} + +export const UpdateTagByIdForm: React.FC = ({ id, ...rest }) => { + const { manager } = useTags(); + const tag = manager.useTag(id); + + if (!tag) return null; + + return ; +}; diff --git a/x-pack/plugins/tags/public/containers/update_tag_form/update_tag_form.tsx b/x-pack/plugins/tags/public/containers/update_tag_form/update_tag_form.tsx new file mode 100644 index 0000000000000..a65bdbd049b67 --- /dev/null +++ b/x-pack/plugins/tags/public/containers/update_tag_form/update_tag_form.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { takeUntil } from 'rxjs/operators'; +import { RawTagWithId } from '../../../common'; +import { useToasts } from '../../../../../../src/plugins/kibana_react/public'; +import { CreateNewTagForm as CreateNewTagFormUi } from '../../components/create_new_tag_form'; +import { txtTagUpdated, txtCouldNotUpdate } from './i18n'; +import { useUnmount$ } from '../../hooks/use_unmount'; +import { useTags } from '../../context'; + +export interface UpdateTagFormProps { + tag: RawTagWithId; + onDone?: () => void; +} + +export const UpdateTagForm: React.FC = ({ tag, onDone }) => { + const { manager } = useTags(); + const unmount$ = useUnmount$(); + const toasts = useToasts(); + const [title, setTitle] = useState(tag.title); + const [color, setColor] = useState(tag.color); + const [description, setDescription] = useState(tag.description); + const [disabled, setDisabled] = useState(false); + + const handleSubmit = async () => { + setDisabled(true); + manager + .update$({ id: tag.id, title, color, description }) + .pipe(takeUntil(unmount$)) + .subscribe( + () => {}, + (error) => { + toasts.addError(error, { title: txtCouldNotUpdate }); + setDisabled(false); + } + ); + + toasts.addSuccess({ + title: txtTagUpdated, + }); + + if (onDone) onDone(); + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/tags/public/context/index.ts b/x-pack/plugins/tags/public/context/index.ts new file mode 100644 index 0000000000000..5b741825cc3ab --- /dev/null +++ b/x-pack/plugins/tags/public/context/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tags_provider'; diff --git a/x-pack/plugins/tags/public/context/tags_provider.ts b/x-pack/plugins/tags/public/context/tags_provider.ts new file mode 100644 index 0000000000000..29cb40650942e --- /dev/null +++ b/x-pack/plugins/tags/public/context/tags_provider.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createElement as h, createContext, useContext } from 'react'; +import { TagsServiceContract } from '../services'; + +type ContextValue = TagsServiceContract; + +const context = createContext(undefined); + +export const TagsProvider = context.Provider; +export const useTags = () => useContext(context)!; +export const createTagsProvider = (value: ContextValue): React.FC => ({ children }) => + h(TagsProvider, { value, children }); diff --git a/x-pack/plugins/tags/public/hooks/use_unmount.ts b/x-pack/plugins/tags/public/hooks/use_unmount.ts new file mode 100644 index 0000000000000..f7ec1732cb0dc --- /dev/null +++ b/x-pack/plugins/tags/public/hooks/use_unmount.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo, useEffect } from 'react'; +import { Observable, Subject } from 'rxjs'; + +export const useUnmount$ = (): Observable => { + const observable = useMemo(() => new Subject(), []); + + useEffect(() => { + return () => { + observable.next(true); + observable.complete(); + }; + }); + + return observable; +}; diff --git a/x-pack/plugins/tags/public/index.ts b/x-pack/plugins/tags/public/index.ts new file mode 100644 index 0000000000000..3515f86d72fa2 --- /dev/null +++ b/x-pack/plugins/tags/public/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TagsPlugin } from './plugin'; +import { PluginInitializerContext } from '../../../../src/core/public'; + +export { RawTag, RawTagWithId, ITagsClient, TagsClientCreateParams } from '../common'; + +export const plugin = (initContext: PluginInitializerContext) => new TagsPlugin(initContext); + +export { + TagsPluginSetup, + TagsPluginStart, + TagsPluginSetupDependencies, + TagsPluginStartDependencies, +} from './plugin'; + +export { Tag, TagProps } from './containers/tag'; +export { TagList, TagListProps } from './containers/tag_list'; +export { TagPicker, TagPickerProps } from './containers/tag_picker'; +export { TagListEditable, TagListEditableProps } from './containers/tag_list_editable'; diff --git a/x-pack/plugins/tags/public/management/containers/create_new_page/create_new_page.tsx b/x-pack/plugins/tags/public/management/containers/create_new_page/create_new_page.tsx new file mode 100644 index 0000000000000..5cf5f48120b6d --- /dev/null +++ b/x-pack/plugins/tags/public/management/containers/create_new_page/create_new_page.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Link } from 'react-router-dom'; +import { EuiButtonToggle } from '@elastic/eui'; +import { Page } from '../page'; +import { txtTitle, txtSubtitle, txtGoBack } from './i18n'; +import { CreateNewTagForm } from '../../../containers/create_new_tag_form'; +import { useServices } from '../../context'; + +export const CreateNewPage: React.FC = () => { + const { params } = useServices(); + + return ( + {txtSubtitle}

} + breadcrumbs={[{ text: txtTitle }]} + separator + callToAction={ + + + + } + > + params.history.push('/')} /> +
+ ); +}; diff --git a/x-pack/plugins/tags/public/management/containers/create_new_page/i18n.ts b/x-pack/plugins/tags/public/management/containers/create_new_page/i18n.ts new file mode 100644 index 0000000000000..c2eb60ac07571 --- /dev/null +++ b/x-pack/plugins/tags/public/management/containers/create_new_page/i18n.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtTitle = i18n.translate('xpack.tags.creanteNewPage.title', { + defaultMessage: 'Create a tag', +}); + +export const txtSubtitle = i18n.translate('xpack.tags.creanteNewPage.subtitle', { + defaultMessage: 'Create a descriptive colorful tag to better organize your content.', +}); + +export const txtGoBack = i18n.translate('xpack.tags.creanteNewPage.goBack', { + defaultMessage: 'Back to Tags', +}); diff --git a/x-pack/plugins/tags/public/management/containers/create_new_page/index.tsx b/x-pack/plugins/tags/public/management/containers/create_new_page/index.tsx new file mode 100644 index 0000000000000..c065ca4c049e6 --- /dev/null +++ b/x-pack/plugins/tags/public/management/containers/create_new_page/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_new_page'; diff --git a/x-pack/plugins/tags/public/management/containers/landing_page/empty.tsx b/x-pack/plugins/tags/public/management/containers/landing_page/empty.tsx new file mode 100644 index 0000000000000..f7bb5b3ddae8c --- /dev/null +++ b/x-pack/plugins/tags/public/management/containers/landing_page/empty.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import { Link } from 'react-router-dom'; +import { txtSubtitle, txtCreateATag } from './i18n'; + +export const Empty: React.FC = () => { + return ( + Create your first tag} + body={ + <> +

{txtSubtitle}

+ + } + actions={ + + + {txtCreateATag} + + + } + /> + ); +}; diff --git a/x-pack/plugins/tags/public/management/containers/landing_page/footer.tsx b/x-pack/plugins/tags/public/management/containers/landing_page/footer.tsx new file mode 100644 index 0000000000000..90352ea38fac7 --- /dev/null +++ b/x-pack/plugins/tags/public/management/containers/landing_page/footer.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiHorizontalRule, EuiText, EuiLink } from '@elastic/eui'; +import { useServices } from '../../context'; + +const defaultTags: Array<{ color: string; title: string; description: string }> = [ + { color: '#B44B6E', title: 'Environment:Production', description: 'Production environment.' }, + { color: '#B44B6E', title: 'Environment:Staging', description: 'Staging environment.' }, + { + color: '#B44B6E', + title: 'Environment:QA', + description: 'Testing and qulity assurance environment.', + }, + { color: '#378400', title: 'Feature:Landing page', description: '' }, + { color: '#378400', title: 'Feature:Load balancer', description: '' }, + { color: '#378400', title: 'Feature:GraphQL', description: '' }, + { color: '#378400', title: 'Feature:Sing up form', description: '' }, + { color: '#378400', title: 'Feature:API', description: '' }, + { color: '#378400', title: 'Feature:Message Broker', description: '' }, + { color: '#378400', title: 'Feature:Analytics', description: '' }, + { color: '#D5BB0F', title: 'Team:Design', description: '' }, + { color: '#D5BB0F', title: 'Team:Marketing', description: '' }, + { color: '#378400', title: 'Feature:Reporting', description: '' }, + { color: '#D5BB0F', title: 'Team:Backend', description: '' }, + { color: '#378400', title: 'Feature:Payments', description: '' }, + { color: '#D5BB0F', title: 'Team:Frontend', description: '' }, + { color: '#D5BB0F', title: 'Team:Infrastructure', description: '' }, + { color: '#CA8EAE', title: 'chart', description: '' }, + { color: '#6092C0', title: 'filter', description: '' }, + { color: '#da205e', title: 'graphic', description: '' }, + { color: '#0b7f00', title: 'presentation', description: '' }, + { color: '#49009e', title: 'proportion', description: '' }, + { color: '#dcb400', title: 'report', description: '' }, + { color: '#6b6b6b', title: 'text', description: '' }, +]; + +export const Footer = () => { + const { manager } = useServices(); + + const handleSampleTagsClick = () => { + for (const tag of defaultTags) manager.create$(tag).subscribe(); + }; + + return ( + <> + + +

+ Install sample tags for this space. +

+
+ + ); +}; diff --git a/x-pack/plugins/tags/public/management/containers/landing_page/i18n.ts b/x-pack/plugins/tags/public/management/containers/landing_page/i18n.ts new file mode 100644 index 0000000000000..99f4694cb4f92 --- /dev/null +++ b/x-pack/plugins/tags/public/management/containers/landing_page/i18n.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtTitle = i18n.translate('xpack.tags.landingPage.title', { + defaultMessage: 'Tags', +}); + +export const txtSubtitle = i18n.translate('xpack.tags.landingPage.subtitle', { + defaultMessage: 'Tags help you organize content throughout Kibana.', +}); + +export const txtCreateATag = i18n.translate('xpack.tags.landingPage.createATag', { + defaultMessage: 'Create tag', +}); diff --git a/x-pack/plugins/tags/public/management/containers/landing_page/index.tsx b/x-pack/plugins/tags/public/management/containers/landing_page/index.tsx new file mode 100644 index 0000000000000..9c8a7155fd66f --- /dev/null +++ b/x-pack/plugins/tags/public/management/containers/landing_page/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './landing_page'; diff --git a/x-pack/plugins/tags/public/management/containers/landing_page/landing_page.tsx b/x-pack/plugins/tags/public/management/containers/landing_page/landing_page.tsx new file mode 100644 index 0000000000000..a5a226b27651f --- /dev/null +++ b/x-pack/plugins/tags/public/management/containers/landing_page/landing_page.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { Link } from 'react-router-dom'; +import { EuiButton } from '@elastic/eui'; +import { Page } from '../page'; +import { TagTable } from '../tag_table'; +import { txtTitle, txtSubtitle, txtCreateATag } from './i18n'; +import { useServices } from '../../context'; +import { Empty } from './empty'; +import { Footer } from './footer'; + +const callToAction = ( + + + {txtCreateATag} + + +); + +export const LandingPage: React.FC = () => { + const { manager } = useServices(); + const initializing = manager.useInitializing(); + const tagMap = manager.useTags(); + const tags = useMemo(() => Object.values(tagMap).map(({ data }) => data), [tagMap]); + + return ( + <> + {txtSubtitle}

: undefined} + callToAction={tags.length ? callToAction : undefined} + > + {!initializing && !!tags.length && } + {!initializing && !tags.length && } +
+