diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc index d9502e4cb47ee..6e814921d3f32 100644 --- a/docs/developer/architecture/code-exploration.asciidoc +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -313,9 +313,10 @@ To access an elasticsearch instance that has live data you have two options: WARNING: Missing README. -- {kib-repo}blob/{branch}/x-pack/plugins/beats_management[beats_management] +- {kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] -WARNING: Missing README. +Notes: +Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place - {kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 842f90b7047c8..85e1da08b00af 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -96,5 +96,6 @@ readonly links: { readonly dateMath: string; }; readonly management: Record; + readonly visualize: Record; }; ``` diff --git a/packages/kbn-monaco/src/xjson/grammar.ts b/packages/kbn-monaco/src/xjson/grammar.ts index fbd7b3d319c1d..e6599837dbb2e 100644 --- a/packages/kbn-monaco/src/xjson/grammar.ts +++ b/packages/kbn-monaco/src/xjson/grammar.ts @@ -22,6 +22,19 @@ export enum AnnoTypes { warning = 'warning', } +export type Parser = ReturnType; + +export interface Annotation { + name?: string; + type: AnnoTypes; + text: string; + at: number; +} + +export interface ParseResult { + annotations: Annotation[]; +} + /* eslint-disable */ export const createParser = () => { diff --git a/packages/kbn-monaco/src/xjson/index.ts b/packages/kbn-monaco/src/xjson/index.ts index 35fd35887bc56..8a4644a3792d2 100644 --- a/packages/kbn-monaco/src/xjson/index.ts +++ b/packages/kbn-monaco/src/xjson/index.ts @@ -17,8 +17,10 @@ * under the License. */ -import { registerGrammarChecker } from './language'; - +/** + * This import registers the XJSON monaco language contribution + */ +import './language'; import { ID } from './constants'; -export const XJsonLang = { registerGrammarChecker, ID }; +export const XJsonLang = { ID }; diff --git a/packages/kbn-monaco/src/xjson/language.ts b/packages/kbn-monaco/src/xjson/language.ts index 54b7004fecd8e..4ae7f2402ed2f 100644 --- a/packages/kbn-monaco/src/xjson/language.ts +++ b/packages/kbn-monaco/src/xjson/language.ts @@ -32,13 +32,16 @@ const wps = new WorkerProxyService(); registerLexerRules(monaco); // In future we will need to make this map languages to workers using "id" and/or "label" values -// that get passed in. +// that get passed in. Also this should not live inside the "xjson" dir directly. We can update this +// once we have another worker. // @ts-ignore window.MonacoEnvironment = { - getWorker: (id: any, label: any) => { - // In kibana we will probably build this once and then load with raw-loader - const blob = new Blob([workerSrc], { type: 'application/javascript' }); - return new Worker(URL.createObjectURL(blob)); + getWorker: (module: string, languageId: string) => { + if (languageId === ID) { + // In kibana we will probably build this once and then load with raw-loader + const blob = new Blob([workerSrc], { type: 'application/javascript' }); + return new Worker(URL.createObjectURL(blob)); + } }, }; @@ -47,15 +50,19 @@ monaco.languages.onLanguage(ID, async () => { }); const OWNER = 'XJSON_GRAMMAR_CHECKER'; -export const registerGrammarChecker = (editor: monaco.editor.IEditor) => { + +export const registerGrammarChecker = () => { const allDisposables: monaco.IDisposable[] = []; - const updateAnnos = async () => { - const { annotations } = await wps.getAnnos(); - const model = editor.getModel() as monaco.editor.ITextModel | null; - if (!model) { + const updateAnnotations = async (model: monaco.editor.IModel): Promise => { + if (model.isDisposed()) { return; } + const parseResult = await wps.getAnnos(model.uri); + if (!parseResult) { + return; + } + const { annotations } = parseResult; monaco.editor.setModelMarkers( model, OWNER, @@ -74,19 +81,21 @@ export const registerGrammarChecker = (editor: monaco.editor.IEditor) => { }; const onModelAdd = (model: monaco.editor.IModel) => { - allDisposables.push( - model.onDidChangeContent(async () => { - updateAnnos(); - }) - ); + if (model.getModeId() === ID) { + allDisposables.push( + model.onDidChangeContent(async () => { + updateAnnotations(model); + }) + ); - updateAnnos(); + updateAnnotations(model); + } }; - allDisposables.push(monaco.editor.onDidCreateModel(onModelAdd)); - monaco.editor.getModels().forEach(onModelAdd); return () => { wps.stop(); allDisposables.forEach((d) => d.dispose()); }; }; + +registerGrammarChecker(); diff --git a/packages/kbn-monaco/src/xjson/worker/xjson_worker.ts b/packages/kbn-monaco/src/xjson/worker/xjson_worker.ts index 501adcacb6990..c99f033d793a8 100644 --- a/packages/kbn-monaco/src/xjson/worker/xjson_worker.ts +++ b/packages/kbn-monaco/src/xjson/worker/xjson_worker.ts @@ -19,17 +19,19 @@ /* eslint-disable-next-line @kbn/eslint/module_migration */ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import { createParser } from '../grammar'; +import { createParser, Parser, ParseResult } from '../grammar'; export class XJsonWorker { constructor(private ctx: monaco.worker.IWorkerContext) {} - private parser: any; + private parser: Parser | undefined; - async parse() { + async parse(modelUri: string): Promise { if (!this.parser) { this.parser = createParser(); } - const [model] = this.ctx.getMirrorModels(); - return this.parser(model.getValue()); + const model = this.ctx.getMirrorModels().find((m) => m.uri.toString() === modelUri); + if (model) { + return this.parser(model.getValue()); + } } } diff --git a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts index 17d6d56e51e59..548a413a483d9 100644 --- a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts +++ b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts @@ -17,32 +17,21 @@ * under the License. */ -import { AnnoTypes } from './grammar'; +import { ParseResult } from './grammar'; import { monaco } from '../monaco'; import { XJsonWorker } from './worker'; import { ID } from './constants'; -export interface Annotation { - name?: string; - type: AnnoTypes; - text: string; - at: number; -} - -export interface AnnotationsResponse { - annotations: Annotation[]; -} - export class WorkerProxyService { private worker: monaco.editor.MonacoWebWorker | undefined; - public async getAnnos(): Promise { + public async getAnnos(modelUri: monaco.Uri): Promise { if (!this.worker) { throw new Error('Worker Proxy Service has not been setup!'); } - await this.worker.withSyncedResources(monaco.editor.getModels().map(({ uri }) => uri)); + await this.worker.withSyncedResources([modelUri]); const proxy = await this.worker.getProxy(); - return proxy.parse(); + return proxy.parse(modelUri.toString()); } public setup() { diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 50e47bdf71772..d0c2ac111eb1f 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -117,7 +117,7 @@ describe('#setup()', () => { expect.objectContaining({ id: 'app1', legacy: false, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) ); @@ -125,7 +125,7 @@ describe('#setup()', () => { expect.objectContaining({ id: 'app2', legacy: false, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) ); @@ -142,7 +142,7 @@ describe('#setup()', () => { expect.objectContaining({ id: 'app1', legacy: false, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.hidden, status: AppStatus.inaccessible, defaultPath: 'foo/bar', tooltip: 'App inaccessible due to reason', @@ -152,7 +152,7 @@ describe('#setup()', () => { expect.objectContaining({ id: 'app2', legacy: false, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) ); @@ -268,7 +268,7 @@ describe('#setup()', () => { expect.objectContaining({ id: 'app2', legacy: false, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, tooltip: 'App accessible', }) @@ -523,7 +523,7 @@ describe('#start()', () => { appRoute: '/app/app1', id: 'app1', legacy: false, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) ); @@ -532,7 +532,7 @@ describe('#start()', () => { appUrl: '/my-url', id: 'app2', legacy: true, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) ); diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts index b41945aa43682..4663ca2db21e7 100644 --- a/src/core/public/application/utils.test.ts +++ b/src/core/public/application/utils.test.ts @@ -18,15 +18,15 @@ */ import { of } from 'rxjs'; -import { LegacyApp, App, AppStatus, AppNavLinkStatus } from './types'; +import { App, AppNavLinkStatus, AppStatus, LegacyApp } from './types'; import { BasePath } from '../http/base_path'; import { - removeSlashes, appendAppPath, + getAppInfo, isLegacyApp, - relativeToAbsolute, parseAppUrl, - getAppInfo, + relativeToAbsolute, + removeSlashes, } from './utils'; describe('removeSlashes', () => { @@ -494,7 +494,7 @@ describe('getAppInfo', () => { id: 'some-id', title: 'some-title', status: AppStatus.accessible, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, legacy: false, }); @@ -509,8 +509,35 @@ describe('getAppInfo', () => { id: 'some-id', title: 'some-title', status: AppStatus.accessible, - navLinkStatus: AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.visible, legacy: true, }); }); + + it('computes the navLinkStatus depending on the app status', () => { + expect( + getAppInfo( + createApp({ + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.inaccessible, + }) + ) + ).toEqual( + expect.objectContaining({ + navLinkStatus: AppNavLinkStatus.hidden, + }) + ); + expect( + getAppInfo( + createApp({ + navLinkStatus: AppNavLinkStatus.default, + status: AppStatus.accessible, + }) + ) + ).toEqual( + expect.objectContaining({ + navLinkStatus: AppNavLinkStatus.visible, + }) + ); + }); }); diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts index 92d25fa468c4a..c5ed7b659f3ae 100644 --- a/src/core/public/application/utils.ts +++ b/src/core/public/application/utils.ts @@ -18,7 +18,15 @@ */ import { IBasePath } from '../http'; -import { App, LegacyApp, PublicAppInfo, PublicLegacyAppInfo, ParsedAppUrl } from './types'; +import { + App, + AppNavLinkStatus, + AppStatus, + LegacyApp, + ParsedAppUrl, + PublicAppInfo, + PublicLegacyAppInfo, +} from './types'; /** * Utility to remove trailing, leading or duplicate slashes. @@ -116,12 +124,18 @@ const removeBasePath = (url: string, basePath: IBasePath, origin: string): strin }; export function getAppInfo(app: App | LegacyApp): PublicAppInfo | PublicLegacyAppInfo { + const navLinkStatus = + app.navLinkStatus === AppNavLinkStatus.default + ? app.status === AppStatus.inaccessible + ? AppNavLinkStatus.hidden + : AppNavLinkStatus.visible + : app.navLinkStatus!; if (isLegacyApp(app)) { const { updater$, ...infos } = app; return { ...infos, status: app.status!, - navLinkStatus: app.navLinkStatus!, + navLinkStatus, legacy: true, }; } else { @@ -129,7 +143,7 @@ export function getAppInfo(app: App | LegacyApp): PublicAppInfo | Publi return { ...infos, status: app.status!, - navLinkStatus: app.navLinkStatus!, + navLinkStatus, appRoute: app.appRoute!, legacy: false, }; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 8853d95181994..bd279baa78d98 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -127,6 +127,10 @@ export class DocLinksService { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, }, + visualize: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/visualize.html`, + timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/timelion.html#timelion-deprecation`, + }, }, }); } @@ -225,5 +229,6 @@ export interface DocLinksStart { readonly dateMath: string; }; readonly management: Record; + readonly visualize: Record; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 0e879d16b4637..17626418cbeeb 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -591,6 +591,7 @@ export interface DocLinksStart { readonly dateMath: string; }; readonly management: Record; + readonly visualize: Record; }; } diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 3a4e49968626f..7a19514eebe17 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -468,9 +468,14 @@ export class DashboardAppController { const explicitInput = { savedVis: input, }; + const embeddableId = + 'embeddableId' in incomingEmbeddable + ? incomingEmbeddable.embeddableId + : undefined; container.addOrUpdateEmbeddable( incomingEmbeddable.type, - explicitInput + explicitInput, + embeddableId ); } } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index ff74580ba256b..036880a1d088b 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -171,6 +171,7 @@ export class DashboardContainer extends Container = IEmbeddable - >(type: string, explicitInput: Partial) { - if (explicitInput.id && this.input.panels[explicitInput.id]) { - this.replacePanel(this.input.panels[explicitInput.id], { + >(type: string, explicitInput: Partial, embeddableId?: string) { + const idToReplace = embeddableId || explicitInput.id; + if (idToReplace && this.input.panels[idToReplace]) { + this.replacePanel(this.input.panels[idToReplace], { type, explicitInput: { ...explicitInput, diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 594a7ad73c396..8c3d7ab9c30d0 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -56,12 +56,18 @@ test('is compatible when edit url is available, in edit mode and editable', asyn test('redirects to app using state transfer', async () => { applicationMock.currentAppId$ = of('superCoolCurrentApp'); const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock); - const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true); + const input = { id: '123', viewMode: ViewMode.EDIT }; + const embeddable = new EditableEmbeddable(input, true); embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' })); await action.execute({ embeddable }); expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', { path: '/123', - state: { originatingApp: 'superCoolCurrentApp' }, + state: { + originatingApp: 'superCoolCurrentApp', + byValueMode: true, + embeddableId: '123', + valueInput: input, + }, }); }); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 9177a77d547b0..8d12ddd0299e7 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -24,7 +24,12 @@ import { take } from 'rxjs/operators'; import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { IEmbeddable, EmbeddableEditorState, EmbeddableStateTransfer } from '../..'; +import { + IEmbeddable, + EmbeddableEditorState, + EmbeddableStateTransfer, + SavedObjectEmbeddableInput, +} from '../..'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -109,8 +114,17 @@ export class EditPanelAction implements Action { const app = embeddable ? embeddable.getOutput().editApp : undefined; const path = embeddable ? embeddable.getOutput().editPath : undefined; if (app && path) { - const state = this.currentAppId ? { originatingApp: this.currentAppId } : undefined; - return { app, path, state }; + if (this.currentAppId) { + const byValueMode = !(embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId; + const state: EmbeddableEditorState = { + originatingApp: this.currentAppId, + byValueMode, + valueInput: byValueMode ? embeddable.getInput() : undefined, + embeddableId: embeddable.id, + }; + return { app, path, state }; + } + return { app, path }; } } diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index a6721784302ac..3f3456d914728 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -27,6 +27,7 @@ export interface EmbeddableEditorState { originatingApp: string; byValueMode?: boolean; valueInput?: EmbeddableInput; + embeddableId?: string; } export function isEmbeddableEditorState(state: unknown): state is EmbeddableEditorState { @@ -49,6 +50,7 @@ export interface EmbeddablePackageByReferenceState { export interface EmbeddablePackageByValueState { type: string; input: EmbeddableInput; + embeddableId?: string; } export type EmbeddablePackageState = diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index f00beb470a9fc..eead90d2f75b7 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -17,9 +17,10 @@ * under the License. */ import React, { useEffect } from 'react'; +import { act } from 'react-dom/test-utils'; -import { registerTestBed } from '../shared_imports'; -import { OnUpdateHandler } from '../types'; +import { registerTestBed, TestBed } from '../shared_imports'; +import { FormHook, OnUpdateHandler, FieldConfig } from '../types'; import { useForm } from '../hooks/use_form'; import { Form } from './form'; import { UseField } from './use_field'; @@ -62,4 +63,91 @@ describe('', () => { lastName: 'Snow', }); }); + + describe('serializer(), deserializer(), formatter()', () => { + interface MyForm { + name: string; + } + + const serializer = jest.fn(); + const deserializer = jest.fn(); + const formatter = jest.fn(); + + const fieldConfig: FieldConfig = { + defaultValue: '', + serializer, + deserializer, + formatters: [formatter], + }; + + let formHook: FormHook | null = null; + + beforeEach(() => { + formHook = null; + serializer.mockReset().mockImplementation((value) => `${value}-serialized`); + deserializer.mockReset().mockImplementation((value) => `${value}-deserialized`); + formatter.mockReset().mockImplementation((value: string) => value.toUpperCase()); + }); + + const onFormHook = (_form: FormHook) => { + formHook = _form; + }; + + const TestComp = ({ onForm }: { onForm: (form: FormHook) => void }) => { + const { form } = useForm({ defaultValue: { name: 'John' } }); + + useEffect(() => { + onForm(form); + }, [form]); + + return ( +
+ + + ); + }; + + test('should call each handler at expected lifecycle', async () => { + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + defaultProps: { onForm: onFormHook }, + }); + + const testBed = setup() as TestBed; + + if (!formHook) { + throw new Error( + `formHook is not defined. Use the onForm() prop to update the reference to the form hook.` + ); + } + + const { form } = testBed; + + expect(deserializer).toBeCalled(); + expect(serializer).not.toBeCalled(); + expect(formatter).not.toBeCalled(); + + let formData = formHook.getFormData({ unflatten: false }); + expect(formData.name).toEqual('John-deserialized'); + + await act(async () => { + form.setInputValue('myField', 'Mike'); + }); + + expect(formatter).toBeCalled(); // Formatters are executed on each value change + expect(serializer).not.toBeCalled(); // Serializer are executed *only** when outputting the form data + + formData = formHook.getFormData(); + expect(serializer).toBeCalled(); + expect(formData.name).toEqual('MIKE-serialized'); + + // Make sure that when we reset the form values, we don't serialize the fields + serializer.mockReset(); + + await act(async () => { + formHook!.reset(); + }); + expect(serializer).not.toBeCalled(); + }); + }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 15ea99eb6cc3a..caf75b42598f5 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -118,15 +118,13 @@ export const useField = ( setIsChangingValue(true); } - const newValue = serializeOutput(value); - // Notify listener if (valueChangeListener) { - valueChangeListener(newValue as T); + valueChangeListener(value); } // Update the form data observable - __updateFormDataAt(path, newValue); + __updateFormDataAt(path, value); // Validate field(s) and update form.isValid state await __validateFields(fieldsToValidateOnChange ?? [path]); @@ -153,7 +151,6 @@ export const useField = ( } } }, [ - serializeOutput, valueChangeListener, errorDisplayDelay, path, @@ -442,13 +439,7 @@ export const useField = ( if (resetValue) { setValue(initialValue); - /** - * Having to call serializeOutput() is a current bug of the lib and will be fixed - * in a future PR. The serializer function should only be called when outputting - * the form data. If we need to continuously format the data while it changes, - * we need to use the field `formatter` config. - */ - return serializeOutput(initialValue); + return initialValue; } }, [setValue, serializeOutput, initialValue] diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 216c7974a9679..b2cc91152b571 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -22,7 +22,7 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, getRandomString, TestBed } from '../shared_imports'; import { Form, UseField } from '../components'; -import { FormSubmitHandler, OnUpdateHandler } from '../types'; +import { FormSubmitHandler, OnUpdateHandler, FormHook, ValidationFunc } from '../types'; import { useForm } from './use_form'; interface MyForm { @@ -123,6 +123,71 @@ describe('use_form() hook', () => { expect(formData).toEqual(expectedData); }); + + test('should not build the object if the form is not valid', async () => { + let formHook: FormHook | null = null; + + const onFormHook = (_form: FormHook) => { + formHook = _form; + }; + + const TestComp = ({ onForm }: { onForm: (form: FormHook) => void }) => { + const { form } = useForm({ defaultValue: { username: 'initialValue' } }); + const validator: ValidationFunc = ({ value }) => { + if (value === 'wrongValue') { + return { message: 'Error on the field' }; + } + }; + + useEffect(() => { + onForm(form); + }, [form]); + + return ( +
+ + + ); + }; + + const setup = registerTestBed(TestComp, { + defaultProps: { onForm: onFormHook }, + memoryRouter: { wrapComponent: false }, + }); + + const { + form: { setInputValue }, + } = setup() as TestBed; + + if (!formHook) { + throw new Error( + `formHook is not defined. Use the onForm() prop to update the reference to the form hook.` + ); + } + + let data; + let isValid; + + await act(async () => { + ({ data, isValid } = await formHook!.submit()); + }); + + expect(isValid).toBe(true); + expect(data).toEqual({ username: 'initialValue' }); + + setInputValue('myField', 'wrongValue'); // Validation will fail + + await act(async () => { + ({ data, isValid } = await formHook!.submit()); + }); + + expect(isValid).toBe(false); + expect(data).toEqual({}); // Don't build the object (and call the serializers()) when invalid + }); }); describe('form.subscribe()', () => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 46b8958491e56..1f51b75a80b21 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -140,7 +140,7 @@ export function useForm( return Object.entries(fieldsRefs.current).reduce( (acc, [key, field]) => ({ ...acc, - [key]: field.__serializeOutput(), + [key]: field.value, }), {} as T ); @@ -233,8 +233,7 @@ export function useForm( fieldsRefs.current[field.path] = field; if (!{}.hasOwnProperty.call(getFormData$().value, field.path)) { - const fieldValue = field.__serializeOutput(); - updateFormDataAt(field.path, fieldValue); + updateFormDataAt(field.path, field.value); } }, [getFormData$, updateFormDataAt] @@ -301,7 +300,7 @@ export function useForm( setSubmitting(true); const isFormValid = await validateAllFields(); - const formData = getFormData(); + const formData = isFormValid ? getFormData() : ({} as T); if (onSubmit) { await onSubmit(formData, isFormValid!); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index 1c5642f9b75b7..b058ef0de448b 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -46,6 +46,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { iconType: props.iconType, iconSide: props.iconSide, 'data-test-subj': props.testId, + className: props.className, }; const btn = props.emphasize ? ( diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js index 614a7539de44c..40fffe7a5a063 100644 --- a/src/plugins/timelion/public/app.js +++ b/src/plugins/timelion/public/app.js @@ -43,6 +43,7 @@ import { initTimelionOptionsSheetDirective } from './directives/timelion_options import { initSavedObjectSaveAsCheckBoxDirective } from './directives/saved_object_save_as_checkbox'; import { initSavedObjectFinderDirective } from './directives/saved_object_finder'; import { initTimelionTabsDirective } from './components/timelionhelp_tabs_directive'; +import { initTimelionTDeprecationDirective } from './components/timelion_deprecation_directive'; import { initInputFocusDirective } from './directives/input_focus'; import { Chart } from './directives/chart/chart'; import { TimelionInterval } from './directives/timelion_interval/timelion_interval'; @@ -84,6 +85,7 @@ export function initTimelionApp(app, deps) { initTimelionHelpDirective(app); initInputFocusDirective(app); initTimelionTabsDirective(app, deps); + initTimelionTDeprecationDirective(app, deps); initSavedObjectFinderDirective(app, savedSheetLoader, deps.core.uiSettings); initSavedObjectSaveAsCheckBoxDirective(app); initCellsDirective(app); diff --git a/src/plugins/timelion/public/components/timelion_deprecation.tsx b/src/plugins/timelion/public/components/timelion_deprecation.tsx new file mode 100644 index 0000000000000..f9f04d3504570 --- /dev/null +++ b/src/plugins/timelion/public/components/timelion_deprecation.tsx @@ -0,0 +1,52 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCallOut, EuiLink } from '@elastic/eui'; +import React from 'react'; +import { DocLinksStart } from '../../../../core/public'; + +export const TimelionDeprecation = ({ links }: DocLinksStart) => { + const timelionDeprecationLink = links.visualize.timelionDeprecation; + return ( + <> + + + + ), + }} + /> + } + color="warning" + iconType="alert" + size="s" + /> + + + ); +}; diff --git a/src/plugins/timelion/public/components/timelion_deprecation_directive.js b/src/plugins/timelion/public/components/timelion_deprecation_directive.js new file mode 100644 index 0000000000000..6a38161c7d40d --- /dev/null +++ b/src/plugins/timelion/public/components/timelion_deprecation_directive.js @@ -0,0 +1,41 @@ +/* + * 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 { TimelionDeprecation } from './timelion_deprecation'; + +export function initTimelionTDeprecationDirective(app, deps) { + app.directive('timelionDeprecation', function (reactDirective) { + return reactDirective( + () => { + return ( + + + + ); + }, + [], + { + restrict: 'E', + scope: { + docLinks: '=', + }, + } + ); + }); +} diff --git a/src/plugins/timelion/public/index.html b/src/plugins/timelion/public/index.html index 54efae7f81ba7..0cf64287a3bd5 100644 --- a/src/plugins/timelion/public/index.html +++ b/src/plugins/timelion/public/index.html @@ -28,6 +28,7 @@
+ diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 4f0f10703c5e9..0b1cca07de007 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -177,7 +177,7 @@ New fields added to the telemetry payload currently mean that telemetry cluster There are a few ways you can test that your usage collector is working properly. -1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. +1. The `/api/stats?extended=true&legacy=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. 2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 3158582438558..56fb15ea8354a 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -69,7 +69,6 @@ class DefaultEditorController { ] : visType.editorConfig.optionTabs), ]; - this.state = { vis, optionTabs, diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 45c750de05ae1..194deef82a5f0 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -41,7 +41,8 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe try { const visId = vis.id as string; - const editPath = visId ? savedVisualizations.urlFor(visId) : ''; + const editPath = visId ? savedVisualizations.urlFor(visId) : '#/edit_by_value'; + const editUrl = visId ? getHttp().basePath.prepend(`/app/visualize${savedVisualizations.urlFor(visId)}`) : ''; diff --git a/src/plugins/visualize/public/application/app.tsx b/src/plugins/visualize/public/application/app.tsx index 0e71d72a3d4c7..8dd6b2ace8413 100644 --- a/src/plugins/visualize/public/application/app.tsx +++ b/src/plugins/visualize/public/application/app.tsx @@ -24,7 +24,12 @@ import { Route, Switch, useLocation } from 'react-router-dom'; import { syncQueryStateWithUrl } from '../../../data/public'; import { useKibana } from '../../../kibana_react/public'; import { VisualizeServices } from './types'; -import { VisualizeEditor, VisualizeListing, VisualizeNoMatch } from './components'; +import { + VisualizeEditor, + VisualizeListing, + VisualizeNoMatch, + VisualizeByValueEditor, +} from './components'; import { VisualizeConstants } from './visualize_constants'; export const VisualizeApp = () => { @@ -48,6 +53,9 @@ export const VisualizeApp = () => { return ( + + + diff --git a/src/plugins/visualize/public/application/components/index.ts b/src/plugins/visualize/public/application/components/index.ts index a3a7fde1d6569..1666bae9b72e0 100644 --- a/src/plugins/visualize/public/application/components/index.ts +++ b/src/plugins/visualize/public/application/components/index.ts @@ -20,3 +20,4 @@ export { VisualizeListing } from './visualize_listing'; export { VisualizeEditor } from './visualize_editor'; export { VisualizeNoMatch } from './visualize_no_match'; +export { VisualizeByValueEditor } from './visualize_byvalue_editor'; diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx new file mode 100644 index 0000000000000..a78633d6841e5 --- /dev/null +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -0,0 +1,105 @@ +/* + * 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 './visualize_editor.scss'; +import React, { useEffect, useState } from 'react'; +import { EventEmitter } from 'events'; + +import { VisualizeInput } from 'src/plugins/visualizations/public'; +import { useKibana } from '../../../../kibana_react/public'; +import { + useChromeVisibility, + useVisByValue, + useVisualizeAppState, + useEditorUpdates, + useLinkedSearchUpdates, +} from '../utils'; +import { VisualizeServices } from '../types'; +import { VisualizeEditorCommon } from './visualize_editor_common'; + +export const VisualizeByValueEditor = () => { + const [originatingApp, setOriginatingApp] = useState(); + const { services } = useKibana(); + const [eventEmitter] = useState(new EventEmitter()); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(true); + const [embeddableId, setEmbeddableId] = useState(); + const [valueInput, setValueInput] = useState(); + + useEffect(() => { + const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = + services.embeddable + .getStateTransfer(services.scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'embeddableId', 'valueInput'] }) || + {}; + setOriginatingApp(value); + setValueInput(valueInputValue); + setEmbeddableId(embeddableIdValue); + if (!valueInputValue) { + history.back(); + } + }, [services]); + + const isChromeVisible = useChromeVisibility(services.chrome); + + const { byValueVisInstance, visEditorRef, visEditorController } = useVisByValue( + services, + eventEmitter, + isChromeVisible, + valueInput, + originatingApp + ); + const { appState, hasUnappliedChanges } = useVisualizeAppState( + services, + eventEmitter, + byValueVisInstance + ); + const { isEmbeddableRendered, currentAppState } = useEditorUpdates( + services, + eventEmitter, + setHasUnsavedChanges, + appState, + byValueVisInstance, + visEditorController + ); + useLinkedSearchUpdates(services, eventEmitter, appState, byValueVisInstance); + + useEffect(() => { + // clean up all registered listeners if any is left + return () => { + eventEmitter.removeAllListeners(); + }; + }, [eventEmitter]); + + return ( + + ); +}; diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index 516dcacfe5813..0bf5b26e1339f 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -21,8 +21,6 @@ import './visualize_editor.scss'; import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { EventEmitter } from 'events'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiScreenReaderOnly } from '@elastic/eui'; import { useKibana } from '../../../../kibana_react/public'; import { @@ -33,8 +31,7 @@ import { useLinkedSearchUpdates, } from '../utils'; import { VisualizeServices } from '../types'; -import { ExperimentalVisInfo } from './experimental_vis_info'; -import { VisualizeTopNav } from './visualize_top_nav'; +import { VisualizeEditorCommon } from './visualize_editor_common'; export const VisualizeEditor = () => { const { id: visualizationIdFromUrl } = useParams(); @@ -67,7 +64,9 @@ export const VisualizeEditor = () => { useEffect(() => { const { originatingApp: value } = - services.embeddable.getStateTransfer(services.scopedHistory).getIncomingEditorState() || {}; + services.embeddable + .getStateTransfer(services.scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; setOriginatingApp(value); }, [services]); @@ -79,38 +78,19 @@ export const VisualizeEditor = () => { }, [eventEmitter]); return ( -
- {savedVisInstance && appState && currentAppState && ( - - )} - {savedVisInstance?.vis?.type?.isExperimental && } - {savedVisInstance && ( - -

- -

-
- )} -
-
+ ); }; diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx new file mode 100644 index 0000000000000..b811936c63b14 --- /dev/null +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx @@ -0,0 +1,110 @@ +/* + * 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 './visualize_editor.scss'; +import React, { RefObject } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiScreenReaderOnly } from '@elastic/eui'; +import { VisualizeTopNav } from './visualize_top_nav'; +import { ExperimentalVisInfo } from './experimental_vis_info'; +import { + SavedVisInstance, + VisualizeAppState, + VisualizeAppStateContainer, + VisualizeEditorVisInstance, +} from '../types'; + +interface VisualizeEditorCommonProps { + visInstance?: VisualizeEditorVisInstance; + appState: VisualizeAppStateContainer | null; + currentAppState?: VisualizeAppState; + isChromeVisible?: boolean; + hasUnsavedChanges: boolean; + setHasUnsavedChanges: (value: boolean) => void; + hasUnappliedChanges: boolean; + isEmbeddableRendered: boolean; + visEditorRef: RefObject; + originatingApp?: string; + setOriginatingApp?: (originatingApp: string | undefined) => void; + visualizationIdFromUrl?: string; + embeddableId?: string; +} + +export const VisualizeEditorCommon = ({ + visInstance, + appState, + currentAppState, + isChromeVisible, + hasUnsavedChanges, + setHasUnsavedChanges, + hasUnappliedChanges, + isEmbeddableRendered, + originatingApp, + setOriginatingApp, + visualizationIdFromUrl, + embeddableId, + visEditorRef, +}: VisualizeEditorCommonProps) => { + return ( +
+ {visInstance && appState && currentAppState && ( + + )} + {visInstance?.vis?.type?.isExperimental && } + {visInstance && ( + +

+ {'savedVis' in visInstance && visInstance.savedVis.id ? ( + + ) : ( + + )} +

+
+ )} +
+
+ ); +}; diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index f2cb2d49f59b0..12a3d1cdf95b1 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -25,7 +25,7 @@ import { VisualizeServices, VisualizeAppState, VisualizeAppStateContainer, - SavedVisInstance, + VisualizeEditorVisInstance, } from '../types'; import { APP_NAME } from '../visualize_constants'; import { getTopNavConfig } from '../utils'; @@ -38,10 +38,11 @@ interface VisualizeTopNavProps { setHasUnsavedChanges: (value: boolean) => void; hasUnappliedChanges: boolean; originatingApp?: string; + visInstance: VisualizeEditorVisInstance; setOriginatingApp?: (originatingApp: string | undefined) => void; - savedVisInstance: SavedVisInstance; stateContainer: VisualizeAppStateContainer; visualizationIdFromUrl?: string; + embeddableId?: string; } const TopNav = ({ @@ -53,26 +54,26 @@ const TopNav = ({ hasUnappliedChanges, originatingApp, setOriginatingApp, - savedVisInstance, + visInstance, stateContainer, visualizationIdFromUrl, + embeddableId, }: VisualizeTopNavProps) => { const { services } = useKibana(); const { TopNavMenu } = services.navigation.ui; - const { embeddableHandler, vis } = savedVisInstance; + const { embeddableHandler, vis } = visInstance; const [inspectorSession, setInspectorSession] = useState(); const openInspector = useCallback(() => { const session = embeddableHandler.openInspector(); setInspectorSession(session); }, [embeddableHandler]); - const handleRefresh = useCallback( (_payload: any, isUpdate?: boolean) => { if (isUpdate === false) { - savedVisInstance.embeddableHandler.reload(); + visInstance.embeddableHandler.reload(); } }, - [savedVisInstance.embeddableHandler] + [visInstance.embeddableHandler] ); const config = useMemo(() => { @@ -85,9 +86,10 @@ const TopNav = ({ openInspector, originatingApp, setOriginatingApp, - savedVisInstance, + visInstance, stateContainer, visualizationIdFromUrl, + embeddableId, }, services ); @@ -99,11 +101,12 @@ const TopNav = ({ hasUnappliedChanges, openInspector, originatingApp, + visInstance, setOriginatingApp, - savedVisInstance, stateContainer, visualizationIdFromUrl, services, + embeddableId, ]); const [indexPattern, setIndexPattern] = useState(vis.data.indexPattern); const showDatePicker = () => { diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 02ae1cc155dd2..65b88485b2f06 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -121,6 +121,14 @@ export interface SavedVisInstance { embeddableHandler: VisualizeEmbeddableContract; } +export interface ByValueVisInstance { + vis: Vis; + savedSearch?: SavedObject; + embeddableHandler: VisualizeEmbeddableContract; +} + +export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; + export interface IEditorController { render(props: EditorRenderProps): void; destroy(): void; diff --git a/src/plugins/visualize/public/application/utils/breadcrumbs.ts b/src/plugins/visualize/public/application/utils/breadcrumbs.ts index a1e5a9e8912e1..a5c246c539c54 100644 --- a/src/plugins/visualize/public/application/utils/breadcrumbs.ts +++ b/src/plugins/visualize/public/application/utils/breadcrumbs.ts @@ -21,6 +21,18 @@ import { i18n } from '@kbn/i18n'; import { VisualizeConstants } from '../visualize_constants'; +const appPrefixes: Record = { + dashboards: { + text: i18n.translate('visualize.dashboard.prefix.breadcrumb', { + defaultMessage: 'Dashboard', + }), + }, +}; + +const defaultEditText = i18n.translate('visualize.editor.defaultEditBreadcrumbText', { + defaultMessage: 'Edit', +}); + export function getLandingBreadcrumbs() { return [ { @@ -43,7 +55,12 @@ export function getCreateBreadcrumbs() { ]; } -export function getEditBreadcrumbs(text: string) { +export function getBreadcrumbsPrefixedWithApp(originatingApp: string) { + const originatingAppBreadcrumb = appPrefixes[originatingApp]; + return [originatingAppBreadcrumb, ...getLandingBreadcrumbs(), { text: defaultEditText }]; +} + +export function getEditBreadcrumbs(text: string = defaultEditText) { return [ ...getLandingBreadcrumbs(), { diff --git a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts index 52b7e3ede298b..7a7e04d78354b 100644 --- a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts +++ b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts @@ -32,6 +32,7 @@ const STATE_STORAGE_KEY = '_a'; interface Arguments { kbnUrlStateStorage: IKbnUrlStateStorage; stateDefaults: VisualizeAppState; + byValue?: boolean; } function toObject(state: PureVisState): PureVisState { @@ -40,55 +41,67 @@ function toObject(state: PureVisState): PureVisState { }) as PureVisState; } -export function createVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { +const pureTransitions = { + set: (state) => (prop, value) => ({ ...state, [prop]: value }), + setVis: (state) => (vis) => ({ + ...state, + vis: { + ...state.vis, + ...vis, + }, + }), + unlinkSavedSearch: (state) => ({ query, parentFilters = [] }) => ({ + ...state, + query: query || state.query, + filters: union(state.filters, parentFilters), + linked: false, + }), + updateVisState: (state) => (newVisState) => ({ ...state, vis: toObject(newVisState) }), + updateSavedQuery: (state) => (savedQueryId) => { + const updatedState = { + ...state, + savedQuery: savedQueryId, + }; + + if (!savedQueryId) { + delete updatedState.savedQuery; + } + + return updatedState; + }, +} as VisualizeAppStateTransitions; + +function createVisualizeByValueAppState(stateDefaults: VisualizeAppState) { + const initialState = migrateAppState({ + ...stateDefaults, + ...stateDefaults, + }); + const stateContainer = createStateContainer( + initialState, + pureTransitions + ); + const stopStateSync = () => {}; + return { stateContainer, stopStateSync }; +} + +function createDefaultVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY); const initialState = migrateAppState({ ...stateDefaults, ...urlState, }); - /* - make sure url ('_a') matches initial state - Initializing appState does two things - first it translates the defaults into AppState, - second it updates appState based on the url (the url trumps the defaults). This means if - we update the state format at all and want to handle BWC, we must not only migrate the - data stored with saved vis, but also any old state in the url. - */ + make sure url ('_a') matches initial state + Initializing appState does two things - first it translates the defaults into AppState, + second it updates appState based on the url (the url trumps the defaults). This means if + we update the state format at all and want to handle BWC, we must not only migrate the + data stored with saved vis, but also any old state in the url. + */ kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); - const stateContainer = createStateContainer( initialState, - { - set: (state) => (prop, value) => ({ ...state, [prop]: value }), - setVis: (state) => (vis) => ({ - ...state, - vis: { - ...state.vis, - ...vis, - }, - }), - unlinkSavedSearch: (state) => ({ query, parentFilters = [] }) => ({ - ...state, - query: query || state.query, - filters: union(state.filters, parentFilters), - linked: false, - }), - updateVisState: (state) => (newVisState) => ({ ...state, vis: toObject(newVisState) }), - updateSavedQuery: (state) => (savedQueryId) => { - const updatedState = { - ...state, - savedQuery: savedQueryId, - }; - - if (!savedQueryId) { - delete updatedState.savedQuery; - } - - return updatedState; - }, - } + pureTransitions ); - const { start: startStateSync, stop: stopStateSync } = syncState({ storageKey: STATE_STORAGE_KEY, stateContainer: { @@ -102,9 +115,14 @@ export function createVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: A }, stateStorage: kbnUrlStateStorage, }); - // start syncing the appState with the ('_a') url startStateSync(); - return { stateContainer, stopStateSync }; } + +export function createVisualizeAppState({ stateDefaults, kbnUrlStateStorage, byValue }: Arguments) { + if (byValue) { + return createVisualizeByValueAppState(stateDefaults); + } + return createDefaultVisualizeAppState({ stateDefaults, kbnUrlStateStorage }); +} diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index da9ba66a914dd..87a6437192aa9 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -31,9 +31,14 @@ import { } from '../../../../saved_objects/public'; import { unhashUrl } from '../../../../kibana_utils/public'; -import { SavedVisInstance, VisualizeServices, VisualizeAppStateContainer } from '../types'; +import { + VisualizeServices, + VisualizeAppStateContainer, + VisualizeEditorVisInstance, +} from '../types'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from './breadcrumbs'; + interface TopNavConfigParams { hasUnsavedChanges: boolean; setHasUnsavedChanges: (value: boolean) => void; @@ -41,9 +46,10 @@ interface TopNavConfigParams { originatingApp?: string; setOriginatingApp?: (originatingApp: string | undefined) => void; hasUnappliedChanges: boolean; - savedVisInstance: SavedVisInstance; + visInstance: VisualizeEditorVisInstance; stateContainer: VisualizeAppStateContainer; visualizationIdFromUrl?: string; + embeddableId?: string; } export const getTopNavConfig = ( @@ -54,9 +60,10 @@ export const getTopNavConfig = ( originatingApp, setOriginatingApp, hasUnappliedChanges, - savedVisInstance: { embeddableHandler, savedVis, vis }, + visInstance, stateContainer, visualizationIdFromUrl, + embeddableId, }: TopNavConfigParams, { application, @@ -71,10 +78,15 @@ export const getTopNavConfig = ( featureFlagConfig, }: VisualizeServices ) => { + const { vis, embeddableHandler } = visInstance; + const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; /** * Called when the user clicks "Save" button. */ async function doSave(saveOptions: SavedObjectSaveOpts) { + if (!savedVis) { + return {}; + } const newlyCreated = !Boolean(savedVis.id) || savedVis.copyOnSave; // vis.title was not bound and it's needed to reflect title into visState stateContainer.transitions.setVis({ @@ -147,8 +159,26 @@ export const getTopNavConfig = ( } } + const createVisReference = () => { + if (!originatingApp) { + return; + } + const state = { + input: { + ...vis.serialize(), + id: embeddableId ? embeddableId : uuid.v4(), + }, + type: VISUALIZE_EMBEDDABLE_TYPE, + embeddableId: '', + }; + if (embeddableId) { + state.embeddableId = embeddableId; + } + embeddable.getStateTransfer().navigateToWithEmbeddablePackage(originatingApp, { state }); + }; + const topNavMenu: TopNavMenuData[] = [ - ...(originatingApp && savedVis.id + ...(originatingApp && ((savedVis && savedVis.id) || embeddableId) ? [ { id: 'saveAndReturn', @@ -180,27 +210,35 @@ export const getTopNavConfig = ( confirmOverwrite: false, returnToOrigin: true, }; + if ( + originatingApp === 'dashboards' && + featureFlagConfig.showNewVisualizeFlow && + !savedVis + ) { + return createVisReference(); + } return doSave(saveOptions); }, }, ] : []), - ...(visualizeCapabilities.save + ...(visualizeCapabilities.save && !embeddableId ? [ { id: 'save', label: - savedVis.id && originatingApp + savedVis?.id && originatingApp ? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', { defaultMessage: 'save as', }) : i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { defaultMessage: 'save', }), - emphasize: !savedVis.id || !originatingApp, + emphasize: (savedVis && !savedVis.id) || !originatingApp, description: i18n.translate('visualize.topNavMenu.saveVisualizationButtonAriaLabel', { defaultMessage: 'Save Visualization', }), + className: savedVis?.id && originatingApp ? 'saveAsButton' : '', testId: 'visualizeSaveButton', disableButton: hasUnappliedChanges, tooltip() { @@ -213,7 +251,7 @@ export const getTopNavConfig = ( ); } }, - run: () => { + run: (anchorElement: HTMLElement) => { const onSave = async ({ newTitle, newCopyOnSave, @@ -222,6 +260,9 @@ export const getTopNavConfig = ( newDescription, returnToOrigin, }: OnSaveProps & { returnToOrigin: boolean }) => { + if (!savedVis) { + return; + } const currentTitle = savedVis.title; savedVis.title = newTitle; savedVis.copyOnSave = newCopyOnSave; @@ -239,32 +280,23 @@ export const getTopNavConfig = ( } return response; }; - - const createVisReference = () => { - if (!originatingApp) { - return; - } - const input = { - ...vis.serialize(), - id: uuid.v4(), - }; - embeddable.getStateTransfer().navigateToWithEmbeddablePackage(originatingApp, { - state: { input, type: VISUALIZE_EMBEDDABLE_TYPE }, - }); - }; - const saveModal = ( {}} originatingApp={originatingApp} /> ); - if (originatingApp === 'dashboards' && featureFlagConfig.showNewVisualizeFlow) { + const isSaveAsButton = anchorElement.classList.contains('saveAsButton'); + if ( + originatingApp === 'dashboards' && + featureFlagConfig.showNewVisualizeFlow && + !isSaveAsButton + ) { createVisReference(); - } else { + } else if (savedVis) { showSaveModal(saveModal, I18nContext); } }, @@ -281,23 +313,24 @@ export const getTopNavConfig = ( }), testId: 'shareTopNavButton', run: (anchorElement) => { - if (share) { + if (share && !embeddableId) { + // TODO: support sharing in by-value mode share.toggleShareContextMenu({ anchorElement, allowEmbed: true, allowShortUrl: visualizeCapabilities.createShortUrl, shareableUrl: unhashUrl(window.location.href), - objectId: savedVis.id, + objectId: savedVis?.id, objectType: 'visualization', sharingData: { - title: savedVis.title, + title: savedVis?.title, }, isDirty: hasUnappliedChanges || hasUnsavedChanges, }); } }, // disable the Share button if no action specified - disableButton: !share, + disableButton: !share || !!embeddableId, }, { id: 'inspector', diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index a75c84cf0b71c..3ffca578f8052 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -18,46 +18,31 @@ */ import { i18n } from '@kbn/i18n'; -import { VisSavedObject, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { + SerializedVis, + Vis, + VisSavedObject, + VisualizeEmbeddableContract, + VisualizeInput, +} from 'src/plugins/visualizations/public'; import { SearchSourceFields } from 'src/plugins/data/public'; import { SavedObject } from 'src/plugins/saved_objects/public'; +import { cloneDeep } from 'lodash'; import { createSavedSearchesLoader } from '../../../../discover/public'; import { VisualizeServices } from '../types'; -export const getVisualizationInstance = async ( - { +const createVisualizeEmbeddableAndLinkSavedSearch = async ( + vis: Vis, + visualizeServices: VisualizeServices +) => { + const { chrome, data, overlays, - visualizations, createVisEmbeddableFromObject, savedObjects, - savedVisualizations, toastNotifications, - }: VisualizeServices, - /** - * opts can be either a saved visualization id passed as string, - * or an object of new visualization params. - * Both come from url search query - */ - opts?: Record | string -) => { - const savedVis: VisSavedObject = await savedVisualizations.get(opts); - - if (typeof opts !== 'string') { - savedVis.searchSourceFields = { index: opts?.indexPattern } as SearchSourceFields; - } - const serializedVis = visualizations.convertToSerializedVis(savedVis); - let vis = await visualizations.createVis(serializedVis.type, serializedVis); - - if (vis.type.setup) { - try { - vis = await vis.type.setup(vis); - } catch { - // skip this catch block - } - } - + } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -86,5 +71,67 @@ export const getVisualizationInstance = async ( }).get(vis.data.savedSearchId); } - return { vis, savedVis, savedSearch, embeddableHandler }; + return { savedSearch, embeddableHandler }; +}; + +export const getVisualizationInstanceFromInput = async ( + visualizeServices: VisualizeServices, + input: VisualizeInput +) => { + const { visualizations } = visualizeServices; + const visState = input.savedVis as SerializedVis; + let vis = await visualizations.createVis(visState.type, cloneDeep(visState)); + if (vis.type.setup) { + try { + vis = await vis.type.setup(vis); + } catch { + // skip this catch block + } + } + const { embeddableHandler, savedSearch } = await createVisualizeEmbeddableAndLinkSavedSearch( + vis, + visualizeServices + ); + return { + vis, + embeddableHandler, + savedSearch, + }; +}; + +export const getVisualizationInstance = async ( + visualizeServices: VisualizeServices, + /** + * opts can be either a saved visualization id passed as string, + * or an object of new visualization params. + * Both come from url search query + */ + opts?: Record | string +) => { + const { visualizations, savedVisualizations } = visualizeServices; + const savedVis: VisSavedObject = await savedVisualizations.get(opts); + + if (typeof opts !== 'string') { + savedVis.searchSourceFields = { index: opts?.indexPattern } as SearchSourceFields; + } + const serializedVis = visualizations.convertToSerializedVis(savedVis); + let vis = await visualizations.createVis(serializedVis.type, serializedVis); + if (vis.type.setup) { + try { + vis = await vis.type.setup(vis); + } catch { + // skip this catch block + } + } + + const { embeddableHandler, savedSearch } = await createVisualizeEmbeddableAndLinkSavedSearch( + vis, + visualizeServices + ); + return { + vis, + embeddableHandler, + savedSearch, + savedVis, + }; }; diff --git a/src/plugins/visualize/public/application/utils/use/index.ts b/src/plugins/visualize/public/application/utils/use/index.ts index 8bd9456b10572..98d1f11d81a8e 100644 --- a/src/plugins/visualize/public/application/utils/use/index.ts +++ b/src/plugins/visualize/public/application/utils/use/index.ts @@ -22,3 +22,4 @@ export { useEditorUpdates } from './use_editor_updates'; export { useSavedVisInstance } from './use_saved_vis_instance'; export { useVisualizeAppState } from './use_visualize_app_state'; export { useLinkedSearchUpdates } from './use_linked_search_updates'; +export { useVisByValue } from './use_vis_byvalue'; diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts index 0f4b2d34e8e87..c29f6337a6246 100644 --- a/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts +++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts @@ -25,8 +25,8 @@ import { VisualizeServices, VisualizeAppState, VisualizeAppStateContainer, - SavedVisInstance, IEditorController, + VisualizeEditorVisInstance, } from '../../types'; export const useEditorUpdates = ( @@ -34,21 +34,22 @@ export const useEditorUpdates = ( eventEmitter: EventEmitter, setHasUnsavedChanges: (value: boolean) => void, appState: VisualizeAppStateContainer | null, - savedVisInstance: SavedVisInstance | undefined, + visInstance: VisualizeEditorVisInstance | undefined, visEditorController: IEditorController | undefined ) => { const [isEmbeddableRendered, setIsEmbeddableRendered] = useState(false); const [currentAppState, setCurrentAppState] = useState(); useEffect(() => { - if (appState && savedVisInstance) { + if (appState && visInstance) { const { timefilter: { timefilter }, filterManager, queryString, state$, } = services.data.query; - const { embeddableHandler, savedVis, savedSearch, vis } = savedVisInstance; + const { embeddableHandler, savedSearch, vis } = visInstance; + const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; const initialState = appState.getState(); setCurrentAppState(initialState); @@ -79,15 +80,18 @@ export const useEditorUpdates = ( }); const handleLinkedSearch = (linked: boolean) => { - if (linked && !savedVis.savedSearchId && savedSearch) { + if (linked && savedVis && !savedVis.savedSearchId && savedSearch) { savedVis.savedSearchId = savedSearch.id; vis.data.savedSearchId = savedSearch.id; if (vis.data.searchSource) { vis.data.searchSource.setParent(savedSearch.searchSource); } - } else if (!linked && savedVis.savedSearchId) { + } else if (!linked && savedVis && savedVis.savedSearchId) { delete savedVis.savedSearchId; delete vis.data.savedSearchId; + } else if (!linked && !savedVis) { + // delete link when it's not a saved vis + delete vis.data.savedSearchId; } }; @@ -105,8 +109,7 @@ export const useEditorUpdates = ( const unsubscribeStateUpdates = appState.subscribe((state) => { setCurrentAppState(state); - - if (savedVis.id && !services.history.location.pathname.includes(savedVis.id)) { + if (savedVis && savedVis.id && !services.history.location.pathname.includes(savedVis.id)) { // this filters out the case when manipulating the browser history back/forward // and initializing different visualizations return; @@ -118,6 +121,7 @@ export const useEditorUpdates = ( // if the browser history was changed manually we need to reflect changes in the editor if ( + savedVis && !isEqual( { ...services.visualizations.convertFromSerializedVis(vis.serialize()).visState, @@ -160,14 +164,7 @@ export const useEditorUpdates = ( unsubscribeStateUpdates(); }; } - }, [ - appState, - eventEmitter, - savedVisInstance, - services, - setHasUnsavedChanges, - visEditorController, - ]); + }, [appState, eventEmitter, visInstance, services, setHasUnsavedChanges, visEditorController]); return { isEmbeddableRendered, currentAppState }; }; diff --git a/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.ts b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.ts index e257b72ee751b..7bc38ba6e2842 100644 --- a/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.ts +++ b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.ts @@ -22,24 +22,23 @@ import { i18n } from '@kbn/i18n'; import { EventEmitter } from 'events'; import { Filter } from 'src/plugins/data/public'; -import { VisualizeServices, VisualizeAppStateContainer, SavedVisInstance } from '../../types'; +import { + VisualizeServices, + VisualizeAppStateContainer, + VisualizeEditorVisInstance, +} from '../../types'; export const useLinkedSearchUpdates = ( services: VisualizeServices, eventEmitter: EventEmitter, appState: VisualizeAppStateContainer | null, - savedVisInstance: SavedVisInstance | undefined + visInstance: VisualizeEditorVisInstance | undefined ) => { useEffect(() => { - if ( - appState && - savedVisInstance && - savedVisInstance.savedSearch && - savedVisInstance.vis.data.searchSource - ) { - const { savedSearch } = savedVisInstance; + if (appState && visInstance && visInstance.savedSearch && visInstance.vis.data.searchSource) { + const { savedSearch } = visInstance; // SearchSource is a promise-based stream of search results that can inherit from other search sources. - const { searchSource } = savedVisInstance.vis.data; + const { searchSource } = visInstance.vis.data; const unlinkFromSavedSearch = () => { const searchSourceParent = savedSearch.searchSource; @@ -70,5 +69,5 @@ export const useLinkedSearchUpdates = ( eventEmitter.off('unlinkFromSavedSearch', unlinkFromSavedSearch); }; } - }, [appState, eventEmitter, savedVisInstance, services.toastNotifications]); + }, [appState, eventEmitter, visInstance, services.toastNotifications]); }; diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index 764bcb4a327c0..ec815b8cfcbee 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -59,7 +59,6 @@ export const useSavedVisInstance = ( const getSavedVisInstance = async () => { try { let savedVisInstance: SavedVisInstance; - if (history.location.pathname === '/create') { const searchParams = parse(history.location.search); const visTypes = services.visualizations.all(); diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts new file mode 100644 index 0000000000000..f2758d0cc01a4 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -0,0 +1,95 @@ +/* + * 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 { EventEmitter } from 'events'; +import { useEffect, useRef, useState } from 'react'; +import { VisualizeInput } from 'src/plugins/visualizations/public'; +import { ByValueVisInstance, IEditorController, VisualizeServices } from '../../types'; +import { getVisualizationInstanceFromInput } from '../get_visualization_instance'; +import { getBreadcrumbsPrefixedWithApp, getEditBreadcrumbs } from '../breadcrumbs'; +import { DefaultEditorController } from '../../../../../vis_default_editor/public'; + +export const useVisByValue = ( + services: VisualizeServices, + eventEmitter: EventEmitter, + isChromeVisible: boolean | undefined, + valueInput?: VisualizeInput, + originatingApp?: string +) => { + const [state, setState] = useState<{ + byValueVisInstance?: ByValueVisInstance; + visEditorController?: IEditorController; + }>({}); + const visEditorRef = useRef(null); + const loaded = useRef(false); + useEffect(() => { + const { chrome } = services; + const getVisInstance = async () => { + if (!valueInput || loaded.current) { + return; + } + const byValueVisInstance = await getVisualizationInstanceFromInput(services, valueInput); + const { embeddableHandler, vis } = byValueVisInstance; + const Editor = vis.type.editor || DefaultEditorController; + const visEditorController = new Editor( + visEditorRef.current, + vis, + eventEmitter, + embeddableHandler + ); + + if (chrome && originatingApp) { + chrome.setBreadcrumbs(getBreadcrumbsPrefixedWithApp(originatingApp)); + } else if (chrome) { + chrome.setBreadcrumbs(getEditBreadcrumbs()); + } + + loaded.current = true; + setState({ + byValueVisInstance, + visEditorController, + }); + }; + + getVisInstance(); + }, [ + eventEmitter, + isChromeVisible, + services, + state.byValueVisInstance, + state.visEditorController, + valueInput, + originatingApp, + ]); + + useEffect(() => { + return () => { + if (state.visEditorController) { + state.visEditorController.destroy(); + } else if (state.byValueVisInstance?.embeddableHandler) { + state.byValueVisInstance.embeddableHandler.destroy(); + } + }; + }, [state]); + + return { + ...state, + visEditorRef, + }; +}; diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts index 8bde9a049c492..39a2db12ffad1 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts @@ -90,6 +90,7 @@ describe('useVisualizeAppState', () => { expect(createVisualizeAppState).toHaveBeenCalledWith({ stateDefaults: visualizeAppStateStub, kbnUrlStateStorage: undefined, + byValue: false, }); expect(mockServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith( visualizeAppStateStub.filters diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx index c44f67df3729f..935d4b26c98c9 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx @@ -26,11 +26,14 @@ import { i18n } from '@kbn/i18n'; import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; import { migrateLegacyQuery } from '../../../../../kibana_legacy/public'; import { esFilters, connectToQueryState } from '../../../../../data/public'; -import { VisualizeServices, VisualizeAppStateContainer, SavedVisInstance } from '../../types'; +import { + VisualizeServices, + VisualizeAppStateContainer, + VisualizeEditorVisInstance, +} from '../../types'; import { visStateToEditorState } from '../utils'; import { createVisualizeAppState } from '../create_visualize_app_state'; import { VisualizeConstants } from '../../visualize_constants'; - /** * This effect is responsible for instantiating the visualize app state container, * which is in sync with "_a" url param @@ -38,7 +41,7 @@ import { VisualizeConstants } from '../../visualize_constants'; export const useVisualizeAppState = ( services: VisualizeServices, eventEmitter: EventEmitter, - instance?: SavedVisInstance + instance?: VisualizeEditorVisInstance ) => { const [hasUnappliedChanges, setHasUnappliedChanges] = useState(false); const [appState, setAppState] = useState(null); @@ -46,10 +49,11 @@ export const useVisualizeAppState = ( useEffect(() => { if (instance) { const stateDefaults = visStateToEditorState(instance, services); - + const byValue = !('savedVis' in instance); const { stateContainer, stopStateSync } = createVisualizeAppState({ stateDefaults, kbnUrlStateStorage: services.kbnUrlStateStorage, + byValue, }); const onDirtyStateChange = ({ isDirty }: { isDirty: boolean }) => { diff --git a/src/plugins/visualize/public/application/utils/utils.ts b/src/plugins/visualize/public/application/utils/utils.ts index 532d87985a0b6..3d8d443d714a5 100644 --- a/src/plugins/visualize/public/application/utils/utils.ts +++ b/src/plugins/visualize/public/application/utils/utils.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { ChromeStart, DocLinksStart } from 'kibana/public'; import { Filter } from '../../../../data/public'; -import { VisualizeServices, SavedVisInstance } from '../types'; +import { VisualizeServices, VisualizeEditorVisInstance } from '../types'; export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksStart) => { chrome.setHelpExtension({ @@ -54,15 +54,18 @@ export const getDefaultQuery = ({ data }: VisualizeServices) => { }; export const visStateToEditorState = ( - { vis, savedVis }: SavedVisInstance, + visInstance: VisualizeEditorVisInstance, services: VisualizeServices ) => { + const vis = visInstance.vis; const savedVisState = services.visualizations.convertFromSerializedVis(vis.serialize()); + const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; return { - uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : vis.uiState.toJSON(), + uiState: + savedVis && savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : vis.uiState.toJSON(), query: vis.data.searchSource?.getOwnField('query') || getDefaultQuery(services), filters: (vis.data.searchSource?.getOwnField('filter') as Filter[]) || [], vis: { ...savedVisState.visState, title: vis.title }, - linked: !!savedVis.savedSearchId, + linked: savedVis && savedVis.id ? !!savedVis.savedSearchId : !!savedVisState.savedSearchId, }; }; diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index adcf27f17dc25..1950fff2733d4 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -25,4 +25,5 @@ export const VisualizeConstants = { WIZARD_STEP_2_PAGE_PATH: '/new/configure', CREATE_PATH: '/create', EDIT_PATH: '/edit', + EDIT_BY_VALUE_PATH: '/edit_by_value', }; diff --git a/x-pack/index.js b/x-pack/index.js index 66fe05e8f035e..b984782df3986 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -7,9 +7,8 @@ import { xpackMain } from './legacy/plugins/xpack_main'; import { monitoring } from './legacy/plugins/monitoring'; import { security } from './legacy/plugins/security'; -import { beats } from './legacy/plugins/beats_management'; import { spaces } from './legacy/plugins/spaces'; module.exports = function (kibana) { - return [xpackMain(kibana), monitoring(kibana), spaces(kibana), security(kibana), beats(kibana)]; + return [xpackMain(kibana), monitoring(kibana), spaces(kibana), security(kibana)]; }; diff --git a/x-pack/legacy/plugins/beats_management/index.ts b/x-pack/legacy/plugins/beats_management/index.ts deleted file mode 100644 index 1f04f342f9ca0..0000000000000 --- a/x-pack/legacy/plugins/beats_management/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Joi from 'joi'; -import { PLUGIN } from './common/constants'; -import { CONFIG_PREFIX } from './common/constants/plugin'; -import { initServerWithKibana } from './server/kibana.index'; -import { KibanaLegacyServer } from './server/lib/adapters/framework/adapter_types'; - -const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes - -export const config = Joi.object({ - enabled: Joi.boolean().default(true), - defaultUserRoles: Joi.array().items(Joi.string()).default(['superuser']), - encryptionKey: Joi.string().default('xpack_beats_default_encryptionKey'), - enrollmentTokensTtlInSeconds: Joi.number() - .integer() - .min(1) - .max(10 * 60 * 14) // No more then 2 weeks for security reasons - .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), -}).default(); - -export function beats(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - require: ['kibana', 'elasticsearch', 'xpack_main'], - config: () => config, - configPrefix: CONFIG_PREFIX, - init(server: KibanaLegacyServer) { - initServerWithKibana(server); - }, - }); -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts deleted file mode 100644 index 3b29e50e4465b..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ResponseToolkit } from 'hapi'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { get } from 'lodash'; -import { isLeft } from 'fp-ts/lib/Either'; -import { KibanaRequest, LegacyRequest } from '../../../../../../../../src/core/server'; -// @ts-ignore -import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; -import { - BackendFrameworkAdapter, - FrameworkInfo, - FrameworkRequest, - FrameworkResponse, - FrameworkRouteOptions, - internalAuthData, - internalUser, - KibanaLegacyServer, - KibanaServerRequest, - KibanaUser, - RuntimeFrameworkInfo, - RuntimeKibanaUser, - XpackInfo, -} from './adapter_types'; - -export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { - public readonly internalUser = internalUser; - public info: null | FrameworkInfo = null; - - constructor( - private readonly PLUGIN_ID: string, - private readonly server: KibanaLegacyServer, - private readonly CONFIG_PREFIX?: string - ) { - const xpackMainPlugin = this.server.plugins.xpack_main; - const thisPlugin = this.server.plugins.beats_management; - - mirrorPluginStatus(xpackMainPlugin, thisPlugin); - - xpackMainPlugin.status.on('green', () => { - this.xpackInfoWasUpdatedHandler(xpackMainPlugin.info); - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info - .feature(this.PLUGIN_ID) - .registerLicenseCheckResultsGenerator(this.xpackInfoWasUpdatedHandler); - }); - } - - public on(event: 'xpack.status.green' | 'elasticsearch.status.green', cb: () => void) { - switch (event) { - case 'xpack.status.green': - this.server.plugins.xpack_main.status.on('green', cb); - case 'elasticsearch.status.green': - this.server.plugins.elasticsearch.status.on('green', cb); - } - } - - public getSetting(settingPath: string) { - return this.server.config().get(settingPath); - } - - public log(text: string) { - this.server.log(text); - } - - public registerRoute< - RouteRequest extends FrameworkRequest, - RouteResponse extends FrameworkResponse - >(route: FrameworkRouteOptions) { - this.server.route({ - handler: async (request: KibanaServerRequest, h: ResponseToolkit) => { - // Note, RuntimeKibanaServerRequest is avalaible to validate request, and its type *is* KibanaServerRequest - // but is not used here for perf reasons. It's value here is not high enough... - return await route.handler(await this.wrapRequest(request), h); - }, - method: route.method, - path: route.path, - config: route.config, - }); - } - - private async wrapRequest( - req: KibanaServerRequest - ): Promise> { - const { params, payload, query, headers, info } = req; - - let isAuthenticated = headers.authorization != null; - let user; - if (isAuthenticated) { - user = await this.getUser(req); - if (!user) { - isAuthenticated = false; - } - } - return { - user: - isAuthenticated && user - ? { - kind: 'authenticated', - [internalAuthData]: headers, - ...user, - } - : { - kind: 'unauthenticated', - }, - headers, - info, - params, - payload, - query, - }; - } - - private async getUser(request: KibanaServerRequest): Promise { - const user = this.server.newPlatform.setup.plugins.security?.authc.getCurrentUser( - KibanaRequest.from((request as unknown) as LegacyRequest) - ); - if (!user) { - return null; - } - const assertKibanaUser = RuntimeKibanaUser.decode(user); - if (isLeft(assertKibanaUser)) { - throw new Error( - `Error parsing user info in ${this.PLUGIN_ID}, ${ - PathReporter.report(assertKibanaUser)[0] - }` - ); - } - - return user; - } - - private xpackInfoWasUpdatedHandler = (xpackInfo: XpackInfo) => { - let xpackInfoUnpacked: FrameworkInfo; - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackInfo || !xpackInfo.isAvailable()) { - this.info = null; - return; - } - - try { - xpackInfoUnpacked = { - kibana: { - version: get(this.server, 'plugins.kibana.status.plugin.version', 'unknown'), - }, - license: { - type: xpackInfo.license.getType(), - expired: !xpackInfo.license.isActive(), - expiry_date_in_millis: - xpackInfo.license.getExpiryDateInMillis() !== undefined - ? xpackInfo.license.getExpiryDateInMillis() - : -1, - }, - security: { - enabled: !!xpackInfo.feature('security') && xpackInfo.feature('security').isEnabled(), - available: !!xpackInfo.feature('security'), - }, - watcher: { - enabled: !!xpackInfo.feature('watcher') && xpackInfo.feature('watcher').isEnabled(), - available: !!xpackInfo.feature('watcher'), - }, - }; - } catch (e) { - this.server.log(`Error accessing required xPackInfo in ${this.PLUGIN_ID} Kibana adapter`); - throw e; - } - - const assertData = RuntimeFrameworkInfo.decode(xpackInfoUnpacked); - if (isLeft(assertData)) { - throw new Error( - `Error parsing xpack info in ${this.PLUGIN_ID}, ${PathReporter.report(assertData)[0]}` - ); - } - this.info = xpackInfoUnpacked; - - return { - security: xpackInfoUnpacked.security, - settings: this.getSetting(this.CONFIG_PREFIX || this.PLUGIN_ID), - }; - }; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/framework.ts b/x-pack/legacy/plugins/beats_management/server/lib/framework.ts deleted file mode 100644 index 96a06929073e5..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/framework.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ResponseObject, ResponseToolkit } from 'hapi'; -import { difference } from 'lodash'; -import { BaseReturnType } from '../../common/return_types'; -import { - BackendFrameworkAdapter, - FrameworkRequest, - FrameworkResponse, -} from './adapters/framework/adapter_types'; - -export class BackendFrameworkLib { - public log = this.adapter.log; - public on = this.adapter.on.bind(this.adapter); - public internalUser = this.adapter.internalUser; - constructor(private readonly adapter: BackendFrameworkAdapter) { - this.validateConfig(); - } - - public registerRoute< - RouteRequest extends FrameworkRequest, - RouteResponse extends FrameworkResponse - >(route: { - path: string; - method: string | string[]; - licenseRequired?: string[]; - requiredRoles?: string[]; - handler: (request: FrameworkRequest) => Promise; - config?: {}; - }) { - this.adapter.registerRoute({ - ...route, - handler: this.wrapErrors( - this.wrapRouteWithSecurity(route.handler, route.licenseRequired || [], route.requiredRoles) - ), - }); - } - - public getSetting(setting: 'encryptionKey'): string; - public getSetting(setting: 'enrollmentTokensTtlInSeconds'): number; - public getSetting(setting: 'defaultUserRoles'): string[]; - public getSetting( - setting: 'encryptionKey' | 'enrollmentTokensTtlInSeconds' | 'defaultUserRoles' - ) { - return this.adapter.getSetting(`xpack.beats.${setting}`); - } - - /** - * Expired `null` happens when we have no xpack info - */ - public get license() { - return { - type: this.adapter.info ? this.adapter.info.license.type : 'unknown', - expired: this.adapter.info ? this.adapter.info.license.expired : null, - }; - } - - public get securityIsEnabled() { - return this.adapter.info ? this.adapter.info.security.enabled : false; - } - - private validateConfig() { - const encryptionKey = this.adapter.getSetting('xpack.beats.encryptionKey'); - - if (!encryptionKey) { - this.adapter.log( - 'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token' - ); - } - } - - private wrapRouteWithSecurity( - handler: (request: FrameworkRequest) => Promise, - requiredLicense: string[], - requiredRoles?: string[] - ): (request: FrameworkRequest) => Promise { - return async (request: FrameworkRequest) => { - if ( - requiredLicense.length > 0 && - (this.license.expired || !requiredLicense.includes(this.license.type)) - ) { - return { - error: { - message: `Your ${this.license.type} license does not support this API or is expired. Please upgrade your license.`, - code: 403, - }, - success: false, - }; - } - - if (requiredRoles) { - if (request.user.kind !== 'authenticated') { - return { - error: { - message: `Request must be authenticated`, - code: 403, - }, - success: false, - }; - } - - if ( - request.user.kind === 'authenticated' && - !request.user.roles.includes('superuser') && - difference(requiredRoles, request.user.roles).length !== 0 - ) { - return { - error: { - message: `Request must be authenticated by a user with one of the following user roles: ${requiredRoles.join( - ',' - )}`, - code: 403, - }, - success: false, - }; - } - } - return await handler(request); - }; - } - private wrapErrors( - handler: (request: FrameworkRequest) => Promise - ): (request: FrameworkRequest, h: ResponseToolkit) => Promise { - return async (request: FrameworkRequest, h: ResponseToolkit) => { - try { - const result = await handler(request); - if (!result.error) { - return h.response(result); - } - return h - .response({ - error: result.error, - success: false, - }) - .code(result.error.code || 400); - } catch (err) { - let statusCode = err.statusCode; - - // This is the only known non-status code error in the system, but just in case we have an else - if (!statusCode && (err.message as string).includes('Invalid user type')) { - statusCode = 403; - } else { - statusCode = 500; - } - - if (statusCode === 403) { - return h - .response({ - error: { - message: 'Insufficient user permissions for managing Beats configuration', - code: 403, - }, - success: false, - }) - .code(403); - } - - return h - .response({ - error: { - message: err.message, - code: statusCode, - }, - success: false, - }) - .code(statusCode); - } - }; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/management_server.ts b/x-pack/legacy/plugins/beats_management/server/management_server.ts deleted file mode 100644 index 1073251949028..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/management_server.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { INDEX_NAMES } from '../common/constants/index_names'; -import { beatsIndexTemplate } from './index_templates'; -import { CMServerLibs } from './lib/types'; -import { createGetBeatConfigurationRoute } from './rest_api/beats/configuration'; -import { createBeatEnrollmentRoute } from './rest_api/beats/enroll'; -import { beatEventsRoute } from './rest_api/beats/events'; -import { createGetBeatRoute } from './rest_api/beats/get'; -import { createListAgentsRoute } from './rest_api/beats/list'; -import { createTagAssignmentsRoute } from './rest_api/beats/tag_assignment'; -import { createTagRemovalsRoute } from './rest_api/beats/tag_removal'; -import { createBeatUpdateRoute } from './rest_api/beats/update'; -import { createDeleteConfidurationsRoute } from './rest_api/configurations/delete'; -import { createGetConfigurationBlocksRoute } from './rest_api/configurations/get'; -import { upsertConfigurationRoute } from './rest_api/configurations/upsert'; -import { createAssignableTagsRoute } from './rest_api/tags/assignable'; -import { createDeleteTagsWithIdsRoute } from './rest_api/tags/delete'; -import { createGetTagsWithIdsRoute } from './rest_api/tags/get'; -import { createListTagsRoute } from './rest_api/tags/list'; -import { createSetTagRoute } from './rest_api/tags/set'; -import { createTokensRoute } from './rest_api/tokens/create'; - -export const initManagementServer = (libs: CMServerLibs) => { - if (libs.database) { - libs.framework.on('elasticsearch.status.green', async () => { - await libs.database!.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate); - }); - } - - libs.framework.registerRoute(createGetBeatRoute(libs)); - libs.framework.registerRoute(createGetTagsWithIdsRoute(libs)); - libs.framework.registerRoute(createListTagsRoute(libs)); - libs.framework.registerRoute(createDeleteTagsWithIdsRoute(libs)); - libs.framework.registerRoute(createGetBeatConfigurationRoute(libs)); - libs.framework.registerRoute(createTagAssignmentsRoute(libs)); - libs.framework.registerRoute(createListAgentsRoute(libs)); - libs.framework.registerRoute(createTagRemovalsRoute(libs)); - libs.framework.registerRoute(createBeatEnrollmentRoute(libs)); - libs.framework.registerRoute(createSetTagRoute(libs)); - libs.framework.registerRoute(createTokensRoute(libs)); - libs.framework.registerRoute(createBeatUpdateRoute(libs)); - libs.framework.registerRoute(createDeleteConfidurationsRoute(libs)); - libs.framework.registerRoute(createGetConfigurationBlocksRoute(libs)); - libs.framework.registerRoute(upsertConfigurationRoute(libs)); - libs.framework.registerRoute(createAssignableTagsRoute(libs)); - libs.framework.registerRoute(beatEventsRoute(libs)); -}; diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/configuration.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/beats/configuration.ts deleted file mode 100644 index f279a51b2bc1b..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/configuration.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Joi from 'joi'; -import { ConfigurationBlock } from '../../../common/domain_types'; -import { BaseReturnType, ReturnTypeList } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createGetBeatConfigurationRoute = (libs: CMServerLibs) => ({ - method: 'GET', - path: '/api/beats/agent/{beatId}/configuration', - config: { - validate: { - headers: Joi.object({ - 'kbn-beats-access-token': Joi.string().required(), - }).options({ allowUnknown: true }), - }, - auth: false, - }, - handler: async ( - request: FrameworkRequest - ): Promise> => { - const beatId = request.params.beatId; - const accessToken = request.headers['kbn-beats-access-token']; - - let configurationBlocks: ConfigurationBlock[]; - const beat = await libs.beats.getById(libs.framework.internalUser, beatId); - if (beat === null) { - return { error: { message: `Beat "${beatId}" not found`, code: 404 }, success: false }; - } - - const isAccessTokenValid = beat.access_token === accessToken; - if (!isAccessTokenValid) { - return { error: { message: 'Invalid access token', code: 401 }, success: false }; - } - - await libs.beats.update(libs.framework.internalUser, beat.id, { - last_checkin: new Date(), - }); - - if (beat.tags) { - const result = await libs.configurationBlocks.getForTags( - libs.framework.internalUser, - beat.tags, - -1 - ); - - configurationBlocks = result.blocks; - } else { - configurationBlocks = []; - } - - return { - list: configurationBlocks, - success: true, - }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/enroll.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/beats/enroll.ts deleted file mode 100644 index 916cfad4102d0..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/enroll.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Joi from 'joi'; -import { omit } from 'lodash'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BaseReturnType, ReturnTypeCreate } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { BeatEnrollmentStatus, CMServerLibs } from '../../lib/types'; - -// TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 -export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ - method: 'POST', - path: '/api/beats/agent/{beatId}', - licenseRequired: REQUIRED_LICENSES, - config: { - auth: false, - validate: { - headers: Joi.object({ - 'kbn-beats-enrollment-token': Joi.string().required(), - }).options({ - allowUnknown: true, - }), - payload: Joi.object({ - host_name: Joi.string().required(), - name: Joi.string().required(), - type: Joi.string().required(), - version: Joi.string().required(), - }).required(), - }, - }, - handler: async ( - request: FrameworkRequest - ): Promise> => { - const { beatId } = request.params; - const enrollmentToken = request.headers['kbn-beats-enrollment-token']; - - const { status, accessToken } = await libs.beats.enrollBeat( - enrollmentToken, - beatId, - request.info.remoteAddress, - omit(request.payload, 'enrollment_token') - ); - - switch (status) { - case BeatEnrollmentStatus.ExpiredEnrollmentToken: - return { - error: { message: BeatEnrollmentStatus.ExpiredEnrollmentToken, code: 400 }, - success: false, - }; - - case BeatEnrollmentStatus.InvalidEnrollmentToken: - return { - error: { message: BeatEnrollmentStatus.InvalidEnrollmentToken, code: 400 }, - success: false, - }; - case BeatEnrollmentStatus.Success: - default: - return { - item: accessToken, - action: 'created', - success: true, - }; - } - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/events.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/beats/events.ts deleted file mode 100644 index 65d7e9979b9ca..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/events.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Joi from 'joi'; -import { BaseReturnType, ReturnTypeBulkAction } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const beatEventsRoute = (libs: CMServerLibs) => ({ - method: 'POST', - path: '/api/beats/{beatId}/events', - config: { - validate: { - headers: Joi.object({ - 'kbn-beats-access-token': Joi.string().required(), - }).options({ allowUnknown: true }), - }, - auth: false, - }, - handler: async (request: FrameworkRequest): Promise => { - const beatId = request.params.beatId; - const events = request.payload; - const accessToken = request.headers['kbn-beats-access-token']; - - const beat = await libs.beats.getById(libs.framework.internalUser, beatId); - if (beat === null) { - return { error: { message: `Beat "${beatId}" not found`, code: 400 }, success: false }; - } - - const isAccessTokenValid = beat.access_token === accessToken; - if (!isAccessTokenValid) { - return { error: { message: `Invalid access token`, code: 401 }, success: false }; - } - - const results = await libs.beatEvents.log(libs.framework.internalUser, beat.id, events); - - return { - results, - success: true, - }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/get.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/beats/get.ts deleted file mode 100644 index 874e66bb8a533..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/get.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CMBeat } from '../../../common/domain_types'; -import { BaseReturnType, ReturnTypeGet } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createGetBeatRoute = (libs: CMServerLibs) => ({ - method: 'GET', - path: '/api/beats/agent/{beatId}/{token?}', - requiredRoles: ['beats_admin'], - handler: async (request: FrameworkRequest): Promise> => { - const beatId = request.params.beatId; - - let beat: CMBeat | null; - if (beatId === 'unknown') { - beat = await libs.beats.getByEnrollmentToken(request.user, request.params.token); - if (beat === null) { - return { success: false }; - } - } else { - beat = await libs.beats.getById(request.user, beatId); - if (beat === null) { - return { error: { message: 'Beat not found', code: 404 }, success: false }; - } - } - - delete beat.access_token; - - return { - item: beat, - success: true, - }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/list.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/beats/list.ts deleted file mode 100644 index 74fb98fc877cc..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/list.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Joi from 'joi'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { CMBeat } from '../../../common/domain_types'; -import { ReturnTypeList } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createListAgentsRoute = (libs: CMServerLibs) => ({ - method: 'GET', - path: '/api/beats/agents/{listByAndValue*}', - requiredRoles: ['beats_admin'], - licenseRequired: REQUIRED_LICENSES, - - validate: { - headers: Joi.object({ - 'kbn-beats-enrollment-token': Joi.string().required(), - }).options({ - allowUnknown: true, - }), - query: Joi.object({ - ESQuery: Joi.string(), - }), - }, - handler: async (request: FrameworkRequest): Promise> => { - const listByAndValueParts = request.params.listByAndValue - ? request.params.listByAndValue.split('/') - : []; - let listBy: 'tag' | null = null; - let listByValue: string | null = null; - - if (listByAndValueParts.length === 2) { - listBy = listByAndValueParts[0]; - listByValue = listByAndValueParts[1]; - } - - let beats: CMBeat[]; - - switch (listBy) { - case 'tag': - beats = await libs.beats.getAllWithTag(request.user, listByValue || ''); - break; - - default: - beats = await libs.beats.getAll( - request.user, - request.query && request.query.ESQuery ? JSON.parse(request.query.ESQuery) : undefined - ); - - break; - } - - return { list: beats, success: true, page: -1, total: -1 }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/tag_assignment.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/beats/tag_assignment.ts deleted file mode 100644 index 974b2822fbd92..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/tag_assignment.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkAction } from '../../../common/return_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BeatsTagAssignment } from '../../../../../../plugins/beats_management/public/lib/adapters/beats/adapter_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -// TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 -export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ - method: 'POST', - path: '/api/beats/agents_tags/assignments', - licenseRequired: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - config: { - validate: { - payload: Joi.object({ - assignments: Joi.array().items( - Joi.object({ - beatId: Joi.string().required(), - tag: Joi.string().required(), - }) - ), - }).required(), - }, - }, - handler: async (request: FrameworkRequest): Promise => { - const { assignments }: { assignments: BeatsTagAssignment[] } = request.payload; - - const response = await libs.beats.assignTagsToBeats(request.user, assignments); - - return { - success: true, - results: response.assignments.map((assignment) => ({ - success: assignment.status && assignment.status >= 200 && assignment.status < 300, - error: - !assignment.status || assignment.status >= 300 - ? { - code: assignment.status || 400, - message: assignment.result, - } - : undefined, - result: - assignment.status && assignment.status >= 200 && assignment.status < 300 - ? { - message: assignment.result, - } - : undefined, - })), - } as ReturnTypeBulkAction; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/tag_removal.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/beats/tag_removal.ts deleted file mode 100644 index 3bbc32dc5748b..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/tag_removal.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkAction } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -// TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 -export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ - method: 'POST', - path: '/api/beats/agents_tags/removals', - licenseRequired: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - config: { - validate: { - payload: Joi.object({ - removals: Joi.array().items( - Joi.object({ - beatId: Joi.string().required(), - tag: Joi.string().required(), - }) - ), - }).required(), - }, - }, - handler: async (request: FrameworkRequest): Promise => { - const { removals } = request.payload; - - const response = await libs.beats.removeTagsFromBeats(request.user, removals); - - return { - success: true, - results: response.removals.map((removal) => ({ - success: removal.status && removal.status >= 200 && removal.status < 300, - error: - !removal.status || removal.status >= 300 - ? { - code: removal.status || 400, - message: removal.result, - } - : undefined, - result: - removal.status && removal.status >= 200 && removal.status < 300 - ? { - message: removal.result, - } - : undefined, - })), - } as ReturnTypeBulkAction; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/update.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/beats/update.ts deleted file mode 100644 index 2859083e83386..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/beats/update.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { CMBeat } from '../../../common/domain_types'; -import { BaseReturnType, ReturnTypeUpdate } from '../../../common/return_types'; -import { FrameworkRequest, internalUser } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -// TODO: write to Kibana audit log file (include who did the verification as well) https://github.com/elastic/kibana/issues/26024 -export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ - method: 'PUT', - path: '/api/beats/agent/{beatId}', - licenseRequired: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - config: { - validate: { - headers: Joi.object({ - 'kbn-beats-access-token': Joi.string(), - }).options({ - allowUnknown: true, - }), - params: Joi.object({ - beatId: Joi.string(), - }), - payload: Joi.object({ - active: Joi.bool(), - ephemeral_id: Joi.string(), - host_name: Joi.string(), - local_configuration_yml: Joi.string(), - metadata: Joi.object(), - name: Joi.string(), - type: Joi.string(), - version: Joi.string(), - }), - }, - }, - handler: async ( - request: FrameworkRequest - ): Promise> => { - const { beatId } = request.params; - const accessToken = request.headers['kbn-beats-access-token']; - const remoteAddress = request.info.remoteAddress; - const userOrToken = accessToken || request.user; - - if (request.user.kind === 'unauthenticated' && request.payload.active !== undefined) { - return { - error: { - message: 'access-token is not a valid auth type to change beat status', - code: 401, - }, - success: false, - }; - } - - const status = await libs.beats.update(userOrToken, beatId, { - ...request.payload, - host_ip: remoteAddress, - }); - - switch (status) { - case 'beat-not-found': - return { - error: { - message: 'Beat not found', - code: 404, - }, - success: false, - }; - case 'invalid-access-token': - return { - error: { - message: 'Invalid access token', - code: 401, - }, - success: false, - }; - } - - const beat = await libs.beats.getById(internalUser, beatId); - - if (!beat) { - return { - error: { - message: 'Beat not found', - code: 404, - }, - success: false, - }; - } - - return { - item: beat, - action: 'updated', - success: true, - }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/delete.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/delete.ts deleted file mode 100644 index b7d430fb18c01..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/delete.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkDelete } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createDeleteConfidurationsRoute = (libs: CMServerLibs) => ({ - method: 'DELETE', - path: '/api/beats/configurations/{ids}', - requiredRoles: ['beats_admin'], - licenseRequired: REQUIRED_LICENSES, - handler: async (request: FrameworkRequest): Promise => { - const idString: string = request.params.ids; - const ids = idString.split(',').filter((id: string) => id.length > 0); - - const results = await libs.configurationBlocks.delete(request.user, ids); - - return { - success: true, - results: results.map((result) => ({ - success: result.success, - action: 'deleted', - error: result.success ? undefined : { message: result.reason }, - })), - } as ReturnTypeBulkDelete; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/get.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/get.ts deleted file mode 100644 index df534f74239ed..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/get.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ConfigurationBlock } from '../../../common/domain_types'; -import { ReturnTypeList } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createGetConfigurationBlocksRoute = (libs: CMServerLibs) => ({ - method: 'GET', - path: '/api/beats/configurations/{tagIds}/{page?}', - requiredRoles: ['beats_admin'], - licenseRequired: REQUIRED_LICENSES, - handler: async (request: FrameworkRequest): Promise> => { - const tagIdString: string = request.params.tagIds; - const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); - - const result = await libs.configurationBlocks.getForTags( - request.user, - tagIds, - parseInt(request.params.page, 10), - 5 - ); - - return { page: result.page, total: result.total, list: result.blocks, success: true }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/upsert.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/upsert.ts deleted file mode 100644 index fb62800594d0a..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/configurations/upsert.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PathReporter } from 'io-ts/lib/PathReporter'; -/* - * 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 Joi from 'joi'; -import { isLeft } from 'fp-ts/lib/Either'; -import { REQUIRED_LICENSES } from '../../../common/constants'; -import { - ConfigurationBlock, - createConfigurationBlockInterface, -} from '../../../common/domain_types'; -import { ReturnTypeBulkUpsert } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -// TODO: write to Kibana audit log file -export const upsertConfigurationRoute = (libs: CMServerLibs) => ({ - method: 'PUT', - path: '/api/beats/configurations', - licenseRequired: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - config: { - validate: { - payload: Joi.array().items(Joi.object({}).unknown(true)), - }, - }, - handler: async (request: FrameworkRequest): Promise => { - const result = await Promise.all( - request.payload.map(async (block: ConfigurationBlock) => { - const assertData = createConfigurationBlockInterface().decode(block); - if (isLeft(assertData)) { - return { - error: `Error parsing block info, ${PathReporter.report(assertData)[0]}`, - }; - } - - const { blockID, success, error } = await libs.configurationBlocks.save( - request.user, - block - ); - if (error) { - return { success, error }; - } - - return { success, blockID }; - }) - ); - - return { - results: result.map((r) => ({ - success: r.success as boolean, - // TODO: we need to surface this data, not hard coded - action: 'created' as 'created' | 'updated', - })), - success: true, - }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/assignable.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/tags/assignable.ts deleted file mode 100644 index 88a322c03790f..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/assignable.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { flatten } from 'lodash'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BeatTag } from '../../../common/domain_types'; -import { ReturnTypeBulkGet } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createAssignableTagsRoute = (libs: CMServerLibs) => ({ - method: 'GET', - path: '/api/beats/tags/assignable/{beatIds}', - requiredRoles: ['beats_admin'], - licenseRequired: REQUIRED_LICENSES, - handler: async (request: FrameworkRequest): Promise> => { - const beatIdString: string = request.params.beatIds; - const beatIds = beatIdString.split(',').filter((id: string) => id.length > 0); - - const beats = await libs.beats.getByIds(request.user, beatIds); - const tags = await libs.tags.getNonConflictingTags( - request.user, - flatten(beats.map((beat) => beat.tags)) - ); - - return { - items: tags, - success: true, - }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/delete.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/tags/delete.ts deleted file mode 100644 index 3e65f271c0fb0..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/delete.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkDelete } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createDeleteTagsWithIdsRoute = (libs: CMServerLibs) => ({ - method: 'DELETE', - path: '/api/beats/tags/{tagIds}', - requiredRoles: ['beats_admin'], - licenseRequired: REQUIRED_LICENSES, - handler: async (request: FrameworkRequest): Promise => { - const tagIdString: string = request.params.tagIds; - const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); - - const success = await libs.tags.delete(request.user, tagIds); - - return { - results: tagIds.map(() => ({ - success, - action: 'deleted', - })), - success, - } as ReturnTypeBulkDelete; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/get.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/tags/get.ts deleted file mode 100644 index 37f8e1169fa6c..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/get.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BeatTag } from '../../../common/domain_types'; -import { ReturnTypeBulkGet } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createGetTagsWithIdsRoute = (libs: CMServerLibs) => ({ - method: 'GET', - path: '/api/beats/tags/{tagIds}', - requiredRoles: ['beats_admin'], - licenseRequired: REQUIRED_LICENSES, - handler: async (request: FrameworkRequest): Promise> => { - const tagIdString: string = request.params.tagIds; - const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); - - const tags = await libs.tags.getWithIds(request.user, tagIds); - - return { - items: tags, - success: true, - }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/list.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/tags/list.ts deleted file mode 100644 index eb5570273960f..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/list.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Joi from 'joi'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BeatTag } from '../../../common/domain_types'; -import { ReturnTypeList } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -export const createListTagsRoute = (libs: CMServerLibs) => ({ - method: 'GET', - path: '/api/beats/tags', - requiredRoles: ['beats_admin'], - licenseRequired: REQUIRED_LICENSES, - validate: { - headers: Joi.object({ - 'kbn-beats-enrollment-token': Joi.string().required(), - }).options({ - allowUnknown: true, - }), - query: Joi.object({ - ESQuery: Joi.string(), - }), - }, - handler: async (request: FrameworkRequest): Promise> => { - const tags = await libs.tags.getAll( - request.user, - request.query && request.query.ESQuery ? JSON.parse(request.query.ESQuery) : undefined - ); - - return { list: tags, success: true, page: -1, total: -1 }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/set.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/tags/set.ts deleted file mode 100644 index f2c4b17007f10..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/tags/set.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { get } from 'lodash'; -import { REQUIRED_LICENSES } from '../../../common/constants'; -import { BeatTag } from '../../../common/domain_types'; -import { ReturnTypeUpsert } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -// TODO: write to Kibana audit log file -export const createSetTagRoute = (libs: CMServerLibs) => ({ - method: 'PUT', - path: '/api/beats/tag/{tagId}', - licenseRequired: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - config: { - validate: { - params: Joi.object({ - tagId: Joi.string(), - }), - payload: Joi.object({ - color: Joi.string(), - name: Joi.string(), - }), - }, - }, - handler: async (request: FrameworkRequest): Promise> => { - const defaultConfig = { - id: request.params.tagId, - name: request.params.tagId, - color: '#DD0A73', - hasConfigurationBlocksTypes: [], - }; - const config = { ...defaultConfig, ...get(request, 'payload', {}) }; - - const id = await libs.tags.upsertTag(request.user, config); - const tag = await libs.tags.getWithIds(request.user, [id]); - - // TODO the action needs to be surfaced - return { success: true, item: tag[0], action: 'created' }; - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/tokens/create.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/tokens/create.ts deleted file mode 100644 index 571d2b4a4947c..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/tokens/create.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { get } from 'lodash'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BaseReturnType, ReturnTypeBulkCreate } from '../../../common/return_types'; -import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/types'; - -// TODO: write to Kibana audit log file -const DEFAULT_NUM_TOKENS = 1; -export const createTokensRoute = (libs: CMServerLibs) => ({ - method: 'POST', - path: '/api/beats/enrollment_tokens', - licenseRequired: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - config: { - validate: { - payload: Joi.object({ - num_tokens: Joi.number().optional().default(DEFAULT_NUM_TOKENS).min(1), - }).allow(null), - }, - }, - handler: async ( - request: FrameworkRequest - ): Promise> => { - const numTokens = get(request, 'payload.num_tokens', DEFAULT_NUM_TOKENS); - - try { - const tokens = await libs.tokens.createEnrollmentTokens(request.user, numTokens); - return { - results: tokens.map((token) => ({ - item: token, - success: true, - action: 'created', - })), - success: true, - }; - } catch (err) { - libs.framework.log(err.message); - return { - error: { - message: 'An error occured, please check your Kibana logs', - code: 500, - }, - success: false, - }; - } - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/tsconfig.json b/x-pack/legacy/plugins/beats_management/tsconfig.json deleted file mode 100644 index 7ade047bad32e..0000000000000 --- a/x-pack/legacy/plugins/beats_management/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "exclude": ["**/node_modules/**"], - "paths": { - "react": ["../../../node_modules/@types/react"] - } -} diff --git a/x-pack/legacy/plugins/beats_management/common/config_schemas.ts b/x-pack/plugins/beats_management/common/config_schemas.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/config_schemas.ts rename to x-pack/plugins/beats_management/common/config_schemas.ts diff --git a/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts b/x-pack/plugins/beats_management/common/config_schemas_translations_map.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts rename to x-pack/plugins/beats_management/common/config_schemas_translations_map.ts diff --git a/x-pack/legacy/plugins/beats_management/common/constants/configuration_blocks.ts b/x-pack/plugins/beats_management/common/constants/configuration_blocks.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/constants/configuration_blocks.ts rename to x-pack/plugins/beats_management/common/constants/configuration_blocks.ts diff --git a/x-pack/legacy/plugins/beats_management/common/constants/index.ts b/x-pack/plugins/beats_management/common/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/constants/index.ts rename to x-pack/plugins/beats_management/common/constants/index.ts diff --git a/x-pack/legacy/plugins/beats_management/common/constants/index_names.ts b/x-pack/plugins/beats_management/common/constants/index_names.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/constants/index_names.ts rename to x-pack/plugins/beats_management/common/constants/index_names.ts diff --git a/x-pack/legacy/plugins/beats_management/common/constants/plugin.ts b/x-pack/plugins/beats_management/common/constants/plugin.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/constants/plugin.ts rename to x-pack/plugins/beats_management/common/constants/plugin.ts diff --git a/x-pack/legacy/plugins/beats_management/common/constants/security.ts b/x-pack/plugins/beats_management/common/constants/security.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/constants/security.ts rename to x-pack/plugins/beats_management/common/constants/security.ts diff --git a/x-pack/legacy/plugins/beats_management/common/constants/table.ts b/x-pack/plugins/beats_management/common/constants/table.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/constants/table.ts rename to x-pack/plugins/beats_management/common/constants/table.ts diff --git a/x-pack/legacy/plugins/beats_management/common/domain_types.ts b/x-pack/plugins/beats_management/common/domain_types.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/domain_types.ts rename to x-pack/plugins/beats_management/common/domain_types.ts diff --git a/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts b/x-pack/plugins/beats_management/common/io_ts_types.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/io_ts_types.ts rename to x-pack/plugins/beats_management/common/io_ts_types.ts diff --git a/x-pack/legacy/plugins/beats_management/common/return_types.ts b/x-pack/plugins/beats_management/common/return_types.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/common/return_types.ts rename to x-pack/plugins/beats_management/common/return_types.ts diff --git a/x-pack/plugins/beats_management/kibana.json b/x-pack/plugins/beats_management/kibana.json index 1b431216ef992..3fd1ab6fd8701 100644 --- a/x-pack/plugins/beats_management/kibana.json +++ b/x-pack/plugins/beats_management/kibana.json @@ -1,5 +1,5 @@ { - "id": "beats_management", + "id": "beatsManagement", "configPath": ["xpack", "beats_management"], "ui": true, "server": true, diff --git a/x-pack/plugins/beats_management/public/components/config_list.tsx b/x-pack/plugins/beats_management/public/components/config_list.tsx index 5f200df1e3c65..285ce0afdf2fc 100644 --- a/x-pack/plugins/beats_management/public/components/config_list.tsx +++ b/x-pack/plugins/beats_management/public/components/config_list.tsx @@ -8,9 +8,9 @@ import { EuiBasicTable, EuiLink } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; -import { configBlockSchemas } from '../../../../legacy/plugins/beats_management/common/config_schemas'; -import { translateConfigSchema } from '../../../../legacy/plugins/beats_management/common/config_schemas_translations_map'; -import { ConfigurationBlock } from '../../../../legacy/plugins/beats_management/common/domain_types'; +import { configBlockSchemas } from '../../common/config_schemas'; +import { translateConfigSchema } from '../../common/config_schemas_translations_map'; +import { ConfigurationBlock } from '../../common/domain_types'; interface ComponentProps { configs: { diff --git a/x-pack/plugins/beats_management/public/components/enroll_beats.tsx b/x-pack/plugins/beats_management/public/components/enroll_beats.tsx index 5bf0f51f48355..38f4a9a307014 100644 --- a/x-pack/plugins/beats_management/public/components/enroll_beats.tsx +++ b/x-pack/plugins/beats_management/public/components/enroll_beats.tsx @@ -20,7 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { upperFirst } from 'lodash'; import React from 'react'; -import { CMBeat } from '../../../../legacy/plugins/beats_management/common/domain_types'; +import { CMBeat } from '../../common/domain_types'; interface ComponentProps { /** Such as kibanas basePath, for use to generate command */ diff --git a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx index c13ab69a19090..cd6435a52f294 100644 --- a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx +++ b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx @@ -5,7 +5,7 @@ */ import React, { Component } from 'react'; import { RouteProps } from 'react-router-dom'; -import { BASE_PATH } from '../../../../../../legacy/plugins/beats_management/common/constants'; +import { BASE_PATH } from '../../../../common/constants'; import { BreadcrumbConsumer } from './consumer'; import { Breadcrumb as BreadcrumbData, BreadcrumbContext } from './types'; diff --git a/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx b/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx index a99c8b9ef8f9f..550c57d11d79c 100644 --- a/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx @@ -13,7 +13,7 @@ import { EuiPopover, } from '@elastic/eui'; import React from 'react'; -import { TABLE_CONFIG } from '../../../../../../legacy/plugins/beats_management/common/constants/table'; +import { TABLE_CONFIG } from '../../../../common/constants/table'; import { TagBadge } from '../../tag/tag_badge'; import { AssignmentActionType } from '../index'; diff --git a/x-pack/plugins/beats_management/public/components/table/table.tsx b/x-pack/plugins/beats_management/public/components/table/table.tsx index 997ad13246d39..eecba050766b5 100644 --- a/x-pack/plugins/beats_management/public/components/table/table.tsx +++ b/x-pack/plugins/beats_management/public/components/table/table.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; -import { TABLE_CONFIG } from '../../../../../legacy/plugins/beats_management/common/constants'; +import { TABLE_CONFIG } from '../../../common/constants'; import { AutocompleteField } from '../autocomplete_field/index'; import { ControlSchema } from './action_schema'; import { OptionControl } from './controls/option_control'; diff --git a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx index 6bbf269711fbd..3c31a86ffd8c6 100644 --- a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx +++ b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -9,10 +9,7 @@ import { i18n } from '@kbn/i18n'; import { sortBy, uniqBy } from 'lodash'; import moment from 'moment'; import React from 'react'; -import { - BeatTag, - CMBeat, -} from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, CMBeat } from '../../../common/domain_types'; import { ConnectedLink } from '../navigation/connected_link'; import { TagBadge } from '../tag'; diff --git a/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx b/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx index 30c4d79d4c6bc..de06e676355d8 100644 --- a/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx +++ b/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx @@ -8,10 +8,7 @@ import { i18n } from '@kbn/i18n'; import Formsy from 'formsy-react'; import { get } from 'lodash'; import React from 'react'; -import { - ConfigBlockSchema, - ConfigurationBlock, -} from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { ConfigBlockSchema, ConfigurationBlock } from '../../../../common/domain_types'; import { FormsyEuiCodeEditor, FormsyEuiFieldText, diff --git a/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx b/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx index 773ca1719d5fa..183620dfa934f 100644 --- a/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx +++ b/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx @@ -22,9 +22,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { configBlockSchemas } from '../../../../../../legacy/plugins/beats_management/common/config_schemas'; -import { translateConfigSchema } from '../../../../../../legacy/plugins/beats_management/common/config_schemas_translations_map'; -import { ConfigurationBlock } from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { configBlockSchemas } from '../../../../common/config_schemas'; +import { translateConfigSchema } from '../../../../common/config_schemas_translations_map'; +import { ConfigurationBlock } from '../../../../common/domain_types'; import { ConfigForm } from './config_form'; interface ComponentProps { diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx b/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx index 5880871da9f13..7fa0231cf3409 100644 --- a/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx +++ b/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx @@ -6,7 +6,7 @@ import { EuiBadge, EuiBadgeProps } from '@elastic/eui'; import React from 'react'; -import { TABLE_CONFIG } from '../../../../../legacy/plugins/beats_management/common/constants'; +import { TABLE_CONFIG } from '../../../common/constants'; type TagBadgeProps = EuiBadgeProps & { maxIdRenderSize?: number; diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx index 9fca9d3add5e7..d76d5fcd476d5 100644 --- a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx +++ b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx @@ -24,11 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import 'brace/mode/yaml'; import 'brace/theme/github'; import React from 'react'; -import { - BeatTag, - CMBeat, - ConfigurationBlock, -} from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types'; import { ConfigList } from '../config_list'; import { AssignmentActionType, BeatsTableType, Table, tagConfigActions } from '../table'; import { ConfigView } from './config_view'; diff --git a/x-pack/plugins/beats_management/public/containers/beats.ts b/x-pack/plugins/beats_management/public/containers/beats.ts index 874447deec321..1454dacbf2a0f 100644 --- a/x-pack/plugins/beats_management/public/containers/beats.ts +++ b/x-pack/plugins/beats_management/public/containers/beats.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { Container } from 'unstated'; -import { CMBeat } from '../../../../legacy/plugins/beats_management/common/domain_types'; -import { BeatsTagAssignment } from '../../../../legacy/plugins/beats_management/server/lib/adapters/beats/adapter_types'; +import { CMBeat } from '../../common/domain_types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { BeatsTagAssignment } from '../../server/lib/adapters/beats/adapter_types'; import { FrontendLibs } from './../lib/types'; interface ContainerState { diff --git a/x-pack/plugins/beats_management/public/containers/tags.ts b/x-pack/plugins/beats_management/public/containers/tags.ts index e7c73d26a1d02..56161f35c79e5 100644 --- a/x-pack/plugins/beats_management/public/containers/tags.ts +++ b/x-pack/plugins/beats_management/public/containers/tags.ts @@ -5,7 +5,7 @@ */ import { Container } from 'unstated'; -import { BeatTag } from '../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag } from '../../common/domain_types'; import { FrontendLibs } from '../lib/types'; interface ContainerState { diff --git a/x-pack/plugins/beats_management/public/lib/__tests__/config_blocks.test.ts b/x-pack/plugins/beats_management/public/lib/__tests__/config_blocks.test.ts index 6851020cdf97e..6776fb0b6f5ad 100644 --- a/x-pack/plugins/beats_management/public/lib/__tests__/config_blocks.test.ts +++ b/x-pack/plugins/beats_management/public/lib/__tests__/config_blocks.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { configBlockSchemas } from '../../../../../legacy/plugins/beats_management/common/config_schemas'; -import { translateConfigSchema } from '../../../../../legacy/plugins/beats_management/common/config_schemas_translations_map'; +import { configBlockSchemas } from '../../../common/config_schemas'; +import { translateConfigSchema } from '../../../common/config_schemas_translations_map'; import { ConfigBlocksLib } from '../configuration_blocks'; import { MemoryConfigBlocksAdapter } from './../adapters/configuration_blocks/memory_config_blocks_adapter'; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts index 1366894f78ddb..815b80e55fa11 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CMBeat } from '../../../../../../legacy/plugins/beats_management/common/domain_types'; -import { ReturnTypeBulkAction } from '../../../../../../legacy/plugins/beats_management/common/return_types'; +import { CMBeat } from '../../../../common/domain_types'; +import { ReturnTypeBulkAction } from '../../../../common/return_types'; export interface CMBeatsAdapter { get(id: string): Promise; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts index 24a7e5c3af8fa..099c568b90f9e 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts @@ -5,8 +5,8 @@ */ import { omit } from 'lodash'; -import { CMBeat } from '../../../../../../legacy/plugins/beats_management/common/domain_types'; -import { ReturnTypeBulkAction } from '../../../../../../legacy/plugins/beats_management/common/return_types'; +import { CMBeat } from '../../../../common/domain_types'; +import { ReturnTypeBulkAction } from '../../../../common/return_types'; import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; export class MemoryBeatsAdapter implements CMBeatsAdapter { diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts index 880af83bfb3f2..d37ceeb5dfcdd 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CMBeat } from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { CMBeat } from '../../../../common/domain_types'; import { ReturnTypeBulkAction, ReturnTypeGet, ReturnTypeList, ReturnTypeUpdate, -} from '../../../../../../legacy/plugins/beats_management/common/return_types'; +} from '../../../../common/return_types'; import { RestAPIAdapter } from '../rest_api/adapter_types'; import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; + export class RestBeatsAdapter implements CMBeatsAdapter { constructor(private readonly REST: RestAPIAdapter) {} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/adapter_types.ts index 413cf32529b2f..e8e2d5cd6b430 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/adapter_types.ts @@ -3,11 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ConfigurationBlock } from '../../../../../../legacy/plugins/beats_management/common/domain_types'; -import { - ReturnTypeBulkUpsert, - ReturnTypeList, -} from '../../../../../../legacy/plugins/beats_management/common/return_types'; +import { ConfigurationBlock } from '../../../../common/domain_types'; +import { ReturnTypeBulkUpsert, ReturnTypeList } from '../../../../common/return_types'; export interface FrontendConfigBlocksAdapter { upsert(blocks: ConfigurationBlock[]): Promise; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/memory_config_blocks_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/memory_config_blocks_adapter.ts index ccb1f27bf67eb..455a31a0b4659 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/memory_config_blocks_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/memory_config_blocks_adapter.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConfigurationBlock } from '../../../../../../legacy/plugins/beats_management/common/domain_types'; -import { - ReturnTypeBulkUpsert, - ReturnTypeList, -} from '../../../../../../legacy/plugins/beats_management/common/return_types'; +import { ConfigurationBlock } from '../../../../common/domain_types'; +import { ReturnTypeBulkUpsert, ReturnTypeList } from '../../../../common/return_types'; import { FrontendConfigBlocksAdapter } from './adapter_types'; export class MemoryConfigBlocksAdapter implements FrontendConfigBlocksAdapter { diff --git a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/rest_config_blocks_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/rest_config_blocks_adapter.ts index 640da59ea572d..be501a5e951ba 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/rest_config_blocks_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/rest_config_blocks_adapter.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConfigurationBlock } from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { ConfigurationBlock } from '../../../../common/domain_types'; import { ReturnTypeBulkDelete, ReturnTypeBulkUpsert, ReturnTypeList, -} from '../../../../../../legacy/plugins/beats_management/common/return_types'; +} from '../../../../common/return_types'; import { RestAPIAdapter } from '../rest_api/adapter_types'; import { FrontendConfigBlocksAdapter } from './adapter_types'; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts index ce663650409fa..cb4fa830949bc 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import * as t from 'io-ts'; -import { LICENSES } from '../../../../../../legacy/plugins/beats_management/common/constants/security'; +import { LICENSES } from '../../../../common/constants/security'; import { RegisterManagementAppArgs } from '../../../../../../../src/plugins/management/public'; export interface FrameworkAdapter { diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts index b0ddf0309232a..5030b1704b1d7 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts @@ -3,10 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - BeatTag, - CMBeat, -} from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, CMBeat } from '../../../../common/domain_types'; export interface CMTagsAdapter { getTagsWithIds(tagIds: string[]): Promise; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts index 699880bd303ec..1e7cae5aed3d8 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - BeatTag, - CMBeat, -} from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, CMBeat } from '../../../../common/domain_types'; import { CMTagsAdapter } from './adapter_types'; export class MemoryTagsAdapter implements CMTagsAdapter { diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts index a31f81b8e5ab4..9a2cebefc8f8f 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts @@ -5,16 +5,13 @@ */ import { uniq } from 'lodash'; -import { - BeatTag, - CMBeat, -} from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, CMBeat } from '../../../../common/domain_types'; import { ReturnTypeBulkDelete, ReturnTypeBulkGet, ReturnTypeList, ReturnTypeUpsert, -} from '../../../../../../legacy/plugins/beats_management/common/return_types'; +} from '../../../../common/return_types'; import { RestAPIAdapter } from '../rest_api/adapter_types'; import { CMTagsAdapter } from './adapter_types'; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts index 4b2beabd35280..7b71d654d5e1f 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReturnTypeBulkCreate } from '../../../../../../legacy/plugins/beats_management/common/return_types'; +import { ReturnTypeBulkCreate } from '../../../../common/return_types'; import { RestAPIAdapter } from '../rest_api/adapter_types'; import { CMTokensAdapter } from './adapter_types'; diff --git a/x-pack/plugins/beats_management/public/lib/beats.ts b/x-pack/plugins/beats_management/public/lib/beats.ts index a6cfe0b88dcf8..0dc9a241f2f07 100644 --- a/x-pack/plugins/beats_management/public/lib/beats.ts +++ b/x-pack/plugins/beats_management/public/lib/beats.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReturnTypeBulkAction } from '../../../../legacy/plugins/beats_management/common/return_types'; -import { CMBeat } from '../../../../legacy/plugins/beats_management/common/domain_types'; +import { ReturnTypeBulkAction } from '../../common/return_types'; +import { CMBeat } from '../../common/domain_types'; import { BeatsTagAssignment, CMBeatsAdapter } from './adapters/beats/adapter_types'; import { ElasticsearchLib } from './elasticsearch'; diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts index d750417640f8c..68640469a7656 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -5,9 +5,9 @@ */ import { camelCase } from 'lodash'; -import { configBlockSchemas } from '../../../../../legacy/plugins/beats_management/common/config_schemas'; -import { translateConfigSchema } from '../../../../../legacy/plugins/beats_management/common/config_schemas_translations_map'; -import { INDEX_NAMES } from '../../../../../legacy/plugins/beats_management/common/constants/index_names'; +import { configBlockSchemas } from '../../../common/config_schemas'; +import { translateConfigSchema } from '../../../common/config_schemas_translations_map'; +import { INDEX_NAMES } from '../../../common/constants/index_names'; import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter'; import { RestConfigBlocksAdapter } from '../adapters/configuration_blocks/rest_config_blocks_adapter'; import { RestElasticsearchAdapter } from '../adapters/elasticsearch/rest'; @@ -20,7 +20,7 @@ import { ConfigBlocksLib } from '../configuration_blocks'; import { ElasticsearchLib } from '../elasticsearch'; import { TagsLib } from '../tags'; import { FrontendLibs } from '../types'; -import { PLUGIN } from '../../../../../legacy/plugins/beats_management/common/constants/plugin'; +import { PLUGIN } from '../../../common/constants/plugin'; import { FrameworkLib } from './../framework'; import { ManagementSetup } from '../../../../../../src/plugins/management/public'; import { SecurityPluginSetup } from '../../../../security/public'; diff --git a/x-pack/plugins/beats_management/public/lib/compose/scripts.ts b/x-pack/plugins/beats_management/public/lib/compose/scripts.ts index 093d618ba8d8b..83129384a77df 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/scripts.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/scripts.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { configBlockSchemas } from '../../../../../legacy/plugins/beats_management/common/config_schemas'; -import { translateConfigSchema } from '../../../../../legacy/plugins/beats_management/common/config_schemas_translations_map'; +import { configBlockSchemas } from '../../../common/config_schemas'; +import { translateConfigSchema } from '../../../common/config_schemas_translations_map'; import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter'; import { RestConfigBlocksAdapter } from '../adapters/configuration_blocks/rest_config_blocks_adapter'; import { MemoryElasticsearchAdapter } from '../adapters/elasticsearch/memory'; diff --git a/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts b/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts index b486ba82689e8..09c079ea129e6 100644 --- a/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts +++ b/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts @@ -7,10 +7,7 @@ import yaml from 'js-yaml'; import { set } from '@elastic/safer-lodash-set'; import { get, has, omit } from 'lodash'; -import { - ConfigBlockSchema, - ConfigurationBlock, -} from '../../../../legacy/plugins/beats_management/common/domain_types'; +import { ConfigBlockSchema, ConfigurationBlock } from '../../common/domain_types'; import { FrontendConfigBlocksAdapter } from './adapters/configuration_blocks/adapter_types'; export class ConfigBlocksLib { diff --git a/x-pack/plugins/beats_management/public/lib/framework.ts b/x-pack/plugins/beats_management/public/lib/framework.ts index 63a81e0895348..33fa975a99493 100644 --- a/x-pack/plugins/beats_management/public/lib/framework.ts +++ b/x-pack/plugins/beats_management/public/lib/framework.ts @@ -5,10 +5,7 @@ */ import { difference, get } from 'lodash'; -import { - LICENSES, - LicenseType, -} from '../../../../legacy/plugins/beats_management/common/constants/security'; +import { LICENSES, LicenseType } from '../../common/constants/security'; import { FrameworkAdapter } from './adapters/framework/adapter_types'; export class FrameworkLib { diff --git a/x-pack/plugins/beats_management/public/lib/tags.ts b/x-pack/plugins/beats_management/public/lib/tags.ts index 2d67edf7e347e..86562be3ff989 100644 --- a/x-pack/plugins/beats_management/public/lib/tags.ts +++ b/x-pack/plugins/beats_management/public/lib/tags.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import uuidv4 from 'uuid/v4'; -import { BeatTag, CMBeat } from '../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, CMBeat } from '../../common/domain_types'; import { CMTagsAdapter } from './adapters/tags/adapter_types'; import { ElasticsearchLib } from './elasticsearch'; diff --git a/x-pack/plugins/beats_management/public/pages/beat/details.tsx b/x-pack/plugins/beats_management/public/pages/beat/details.tsx index 4466a1ecba97d..cfe60aff67b1b 100644 --- a/x-pack/plugins/beats_management/public/pages/beat/details.tsx +++ b/x-pack/plugins/beats_management/public/pages/beat/details.tsx @@ -19,14 +19,10 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import React from 'react'; -import { configBlockSchemas } from '../../../../../legacy/plugins/beats_management/common/config_schemas'; -import { translateConfigSchema } from '../../../../../legacy/plugins/beats_management/common/config_schemas_translations_map'; -import { TABLE_CONFIG } from '../../../../../legacy/plugins/beats_management/common/constants'; -import { - BeatTag, - CMBeat, - ConfigurationBlock, -} from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { configBlockSchemas } from '../../../common/config_schemas'; +import { translateConfigSchema } from '../../../common/config_schemas_translations_map'; +import { TABLE_CONFIG } from '../../../common/constants'; +import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types'; import { Breadcrumb } from '../../components/navigation/breadcrumb'; import { ConnectedLink } from '../../components/navigation/connected_link'; import { TagBadge } from '../../components/tag'; diff --git a/x-pack/plugins/beats_management/public/pages/beat/index.tsx b/x-pack/plugins/beats_management/public/pages/beat/index.tsx index bea84377d2ab8..80590febc95be 100644 --- a/x-pack/plugins/beats_management/public/pages/beat/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/beat/index.tsx @@ -17,7 +17,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import moment from 'moment'; import React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; -import { CMBeat } from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { CMBeat } from '../../../common/domain_types'; import { PrimaryLayout } from '../../components/layouts/primary'; import { Breadcrumb } from '../../components/navigation/breadcrumb'; import { ChildRoutes } from '../../components/navigation/child_routes'; diff --git a/x-pack/plugins/beats_management/public/pages/beat/tags.tsx b/x-pack/plugins/beats_management/public/pages/beat/tags.tsx index 5a65473b25a24..672c0d89bb002 100644 --- a/x-pack/plugins/beats_management/public/pages/beat/tags.tsx +++ b/x-pack/plugins/beats_management/public/pages/beat/tags.tsx @@ -7,10 +7,7 @@ import { EuiGlobalToastList } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { - BeatTag, - CMBeat, -} from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, CMBeat } from '../../../common/domain_types'; import { Breadcrumb } from '../../components/navigation/breadcrumb'; import { BeatDetailTagsTable, Table } from '../../components/table'; import { FrontendLibs } from '../../lib/types'; diff --git a/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx index 7c26bb8de3f57..5e0bddbbe5411 100644 --- a/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx @@ -19,10 +19,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { flatten, sortBy } from 'lodash'; import moment from 'moment'; import React from 'react'; -import { - BeatTag, - CMBeat, -} from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, CMBeat } from '../../../common/domain_types'; import { EnrollBeat } from '../../components/enroll_beats'; import { Breadcrumb } from '../../components/navigation/breadcrumb'; import { BeatsTableType, Table } from '../../components/table'; diff --git a/x-pack/plugins/beats_management/public/pages/overview/index.tsx b/x-pack/plugins/beats_management/public/pages/overview/index.tsx index 4df1a7d065469..57b007753491c 100644 --- a/x-pack/plugins/beats_management/public/pages/overview/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/overview/index.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { Subscribe } from 'unstated'; -import { CMBeat } from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { CMBeat } from '../../../common/domain_types'; import { PrimaryLayout } from '../../components/layouts/primary'; import { ChildRoutes } from '../../components/navigation/child_routes'; import { BeatsContainer } from '../../containers/beats'; diff --git a/x-pack/plugins/beats_management/public/pages/tag/create.tsx b/x-pack/plugins/beats_management/public/pages/tag/create.tsx index 881bb433b1d9a..34785e5dce51d 100644 --- a/x-pack/plugins/beats_management/public/pages/tag/create.tsx +++ b/x-pack/plugins/beats_management/public/pages/tag/create.tsx @@ -12,11 +12,8 @@ import 'brace/mode/yaml'; import 'brace/theme/github'; import { isEqual } from 'lodash'; import React from 'react'; -import { UNIQUENESS_ENFORCING_TYPES } from '../../../../../legacy/plugins/beats_management/common/constants/configuration_blocks'; -import { - BeatTag, - ConfigurationBlock, -} from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants/configuration_blocks'; +import { BeatTag, ConfigurationBlock } from '../../../common/domain_types'; import { PrimaryLayout } from '../../components/layouts/primary'; import { TagEdit } from '../../components/tag'; import { AppPageProps } from '../../frontend_types'; diff --git a/x-pack/plugins/beats_management/public/pages/tag/edit.tsx b/x-pack/plugins/beats_management/public/pages/tag/edit.tsx index 10d7f7bbd7193..e3c2671061d99 100644 --- a/x-pack/plugins/beats_management/public/pages/tag/edit.tsx +++ b/x-pack/plugins/beats_management/public/pages/tag/edit.tsx @@ -10,12 +10,8 @@ import 'brace/mode/yaml'; import 'brace/theme/github'; import { flatten } from 'lodash'; import React from 'react'; -import { UNIQUENESS_ENFORCING_TYPES } from '../../../../../legacy/plugins/beats_management/common/constants'; -import { - BeatTag, - CMBeat, - ConfigurationBlock, -} from '../../../../../legacy/plugins/beats_management/common/domain_types'; +import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants'; +import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types'; import { PrimaryLayout } from '../../components/layouts/primary'; import { TagEdit } from '../../components/tag'; import { AppPageProps } from '../../frontend_types'; diff --git a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx index 71f0b8161131f..93489f5223ef8 100644 --- a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx +++ b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx @@ -6,7 +6,7 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; -import { CMBeat } from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { CMBeat } from '../../../../common/domain_types'; import { AppPageProps } from '../../../frontend_types'; interface PageState { diff --git a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx index 8ecdf8bc5c663..01976633d14ea 100644 --- a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx +++ b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx @@ -9,10 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { isEqual } from 'lodash'; import React, { Component } from 'react'; import uuidv4 from 'uuid/v4'; -import { - BeatTag, - ConfigurationBlock, -} from '../../../../../../legacy/plugins/beats_management/common/domain_types'; +import { BeatTag, ConfigurationBlock } from '../../../../common/domain_types'; import { TagEdit } from '../../../components/tag/tag_edit'; import { AppPageProps } from '../../../frontend_types'; interface PageState { diff --git a/x-pack/legacy/plugins/beats_management/readme.md b/x-pack/plugins/beats_management/readme.md similarity index 88% rename from x-pack/legacy/plugins/beats_management/readme.md rename to x-pack/plugins/beats_management/readme.md index 3414f09deed46..36db612f7affd 100644 --- a/x-pack/legacy/plugins/beats_management/readme.md +++ b/x-pack/plugins/beats_management/readme.md @@ -1,7 +1,7 @@ # Beats CM Notes: -Falure to have auth enabled in Kibana will make for a broken UI. UI based errors not yet in place +Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place ## Testing diff --git a/x-pack/legacy/plugins/beats_management/scripts/enroll.js b/x-pack/plugins/beats_management/scripts/enroll.js similarity index 100% rename from x-pack/legacy/plugins/beats_management/scripts/enroll.js rename to x-pack/plugins/beats_management/scripts/enroll.js diff --git a/x-pack/legacy/plugins/beats_management/scripts/fake_env.ts b/x-pack/plugins/beats_management/scripts/fake_env.ts similarity index 96% rename from x-pack/legacy/plugins/beats_management/scripts/fake_env.ts rename to x-pack/plugins/beats_management/scripts/fake_env.ts index 65254d24863cd..b9eff4615cd42 100644 --- a/x-pack/legacy/plugins/beats_management/scripts/fake_env.ts +++ b/x-pack/plugins/beats_management/scripts/fake_env.ts @@ -9,8 +9,7 @@ import request from 'request'; import uuidv4 from 'uuid/v4'; import { configBlockSchemas } from '../common/config_schemas'; import { BeatTag } from '../common/domain_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { compose } from '../../../../plugins/beats_management/public/lib/compose/scripts'; +import { compose } from '../public/lib/compose/scripts'; const args = process.argv.slice(2); const chance = new Chance(); diff --git a/x-pack/legacy/plugins/beats_management/server/index_templates/beats_template.json b/x-pack/plugins/beats_management/server/index_templates/beats_template.json similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/index_templates/beats_template.json rename to x-pack/plugins/beats_management/server/index_templates/beats_template.json diff --git a/x-pack/legacy/plugins/beats_management/server/index_templates/events_template.json b/x-pack/plugins/beats_management/server/index_templates/events_template.json similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/index_templates/events_template.json rename to x-pack/plugins/beats_management/server/index_templates/events_template.json diff --git a/x-pack/legacy/plugins/beats_management/server/index_templates/index.ts b/x-pack/plugins/beats_management/server/index_templates/index.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/index_templates/index.ts rename to x-pack/plugins/beats_management/server/index_templates/index.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/adapter_types.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/adapter_types.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts similarity index 57% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index 85a8618be5d18..2ef3d98450ae4 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -6,74 +6,19 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ -import { Lifecycle, ResponseToolkit } from 'hapi'; import * as t from 'io-ts'; -import { CoreSetup, CoreStart } from 'src/core/server'; -import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; -import { LicenseType } from '../../../../common/constants/security'; +import { Headers, KibanaRequest } from 'src/core/server'; export const internalAuthData = Symbol('internalAuthData'); export const internalUser: FrameworkInternalUser = { kind: 'internal', }; -export interface XpackInfo { - license: { - getType: () => LicenseType; - /** Is the license expired */ - isActive: () => boolean; - getExpiryDateInMillis: () => number; - }; - feature: (pluginId: string) => any; - isAvailable: () => boolean; -} - export interface BackendFrameworkAdapter { + getUser(request: KibanaRequest): FrameworkUser; internalUser: FrameworkInternalUser; info: null | FrameworkInfo; log(text: string): void; - on(event: 'xpack.status.green' | 'elasticsearch.status.green', cb: () => void): void; - getSetting(settingPath: string): any; - registerRoute( - route: FrameworkRouteOptions - ): void; -} - -export interface KibanaLegacyServer { - newPlatform: { - setup: { - core: CoreSetup; - plugins: { security: SecurityPluginSetup }; - }; - start: { - core: CoreStart; - }; - }; - plugins: { - xpack_main: { - status: { - on: (status: 'green' | 'yellow' | 'red', callback: () => void) => void; - }; - info: XpackInfo; - }; - kibana: { - status: { - plugin: { - version: string; - }; - }; - }; - elasticsearch: { - status: { - on: (status: 'green' | 'yellow' | 'red', callback: () => void) => void; - }; - getCluster: () => any; - }; - beats_management: {}; - }; - config: () => any; - route: (routeConfig: any) => void; - log: (message: string) => void; } export const RuntimeFrameworkInfo = t.interface( @@ -167,23 +112,3 @@ export interface FrameworkRequest< params: KibanaServerRequestGenaric['params']; query: KibanaServerRequestGenaric['query']; } - -export interface FrameworkRouteOptions< - RouteRequest extends FrameworkRequest = FrameworkRequest, - RouteResponse extends FrameworkResponse = any -> { - path: string; - method: string | string[]; - vhost?: string; - licenseRequired?: string[]; - requiredRoles?: string[]; - handler: FrameworkRouteHandler; - config?: {}; -} - -export type FrameworkRouteHandler< - RouteRequest extends KibanaServerRequest, - RouteResponse extends FrameworkResponse -> = (request: FrameworkRequest, h: ResponseToolkit) => Promise; - -export type FrameworkResponse = Lifecycle.ReturnValue; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..27387f269a9e4 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -0,0 +1,115 @@ +/* + * 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 { PathReporter } from 'io-ts/lib/PathReporter'; +import { isLeft } from 'fp-ts/lib/Either'; +import { KibanaRequest, Headers, Logger } from 'src/core/server'; +import { + BackendFrameworkAdapter, + FrameworkInfo, + FrameworkUser, + internalAuthData, + internalUser, + RuntimeFrameworkInfo, + RuntimeKibanaUser, +} from './adapter_types'; +import { BeatsManagementConfigType } from '../../../../common'; +import { ILicense, LicensingPluginStart } from '../../../../../licensing/server'; +import { SecurityPluginSetup } from '../../../../../security/server'; + +export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { + public readonly internalUser = internalUser; + public info: null | FrameworkInfo = null; + + constructor( + private readonly PLUGIN_ID: string, + private readonly kibanaVersion: string, + private readonly config: BeatsManagementConfigType, + private readonly logger: Logger, + private readonly licensing: LicensingPluginStart, + private readonly security?: SecurityPluginSetup + ) { + this.licensing.license$.subscribe((license) => this.licenseUpdateHandler(license)); + } + + public log(text: string) { + this.logger.info(text); + } + + getUser(request: KibanaRequest): FrameworkUser { + const user = this.security?.authc.getCurrentUser(request); + if (!user) { + return { + kind: 'unauthenticated', + }; + } + const assertKibanaUser = RuntimeKibanaUser.decode(user); + if (isLeft(assertKibanaUser)) { + throw new Error( + `Error parsing user info in ${this.PLUGIN_ID}, ${ + PathReporter.report(assertKibanaUser)[0] + }` + ); + } + + return { + kind: 'authenticated', + [internalAuthData]: request.headers, + ...user, + }; + } + + private licenseUpdateHandler = (license: ILicense) => { + let xpackInfoUnpacked: FrameworkInfo; + + // If, for some reason, we cannot get the license information + // from Elasticsearch, assume worst case and disable + if (!license.isAvailable) { + this.info = null; + return; + } + + const securityFeature = license.getFeature('security'); + const watcherFeature = license.getFeature('watcher'); + + try { + xpackInfoUnpacked = { + kibana: { + version: this.kibanaVersion, + }, + license: { + type: license.type!, + expired: !license.isActive, + expiry_date_in_millis: license.expiryDateInMillis ?? -1, + }, + security: { + enabled: securityFeature.isEnabled, + available: securityFeature.isAvailable, + }, + watcher: { + enabled: watcherFeature.isEnabled, + available: watcherFeature.isAvailable, + }, + }; + } catch (e) { + this.logger.error(`Error accessing required xPackInfo in ${this.PLUGIN_ID} Kibana adapter`); + throw e; + } + + const assertData = RuntimeFrameworkInfo.decode(xpackInfoUnpacked); + if (isLeft(assertData)) { + throw new Error( + `Error parsing xpack info in ${this.PLUGIN_ID}, ${PathReporter.report(assertData)[0]}` + ); + } + this.info = xpackInfoUnpacked; + + return { + security: xpackInfoUnpacked.security, + settings: { ...this.config }, + }; + }; +} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts b/x-pack/plugins/beats_management/server/lib/beat_events.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts rename to x-pack/plugins/beats_management/server/lib/beat_events.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/beats.ts b/x-pack/plugins/beats_management/server/lib/beats.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/beats.ts rename to x-pack/plugins/beats_management/server/lib/beats.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts similarity index 68% rename from x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts rename to x-pack/plugins/beats_management/server/lib/compose/kibana.ts index b6a645ded8164..bdae4962bd471 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts @@ -5,12 +5,14 @@ */ import { camelCase } from 'lodash'; +import type { ElasticsearchServiceStart, Logger } from 'src/core/server'; +import { SecurityPluginSetup } from '../../../../security/server'; +import { LicensingPluginStart } from '../../../../licensing/server'; import { PLUGIN } from '../../../common/constants'; -import { CONFIG_PREFIX } from '../../../common/constants/plugin'; +import { BeatsManagementConfigType } from '../../../common'; import { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; import { ElasticsearchConfigurationBlockAdapter } from '../adapters/configuration_blocks/elasticsearch_configuration_block_adapter'; import { KibanaDatabaseAdapter } from '../adapters/database/kibana_database_adapter'; -import { KibanaLegacyServer } from '../adapters/framework/adapter_types'; import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; import { ElasticsearchTokensAdapter } from '../adapters/tokens/elasticsearch_tokens_adapter'; @@ -22,11 +24,33 @@ import { CMTokensDomain } from '../tokens'; import { CMServerLibs } from '../types'; import { BackendFrameworkLib } from './../framework'; -export function compose(server: KibanaLegacyServer): CMServerLibs { - const framework = new BackendFrameworkLib( - new KibanaBackendFrameworkAdapter(camelCase(PLUGIN.ID), server, CONFIG_PREFIX) +interface ComposeOptions { + elasticsearch: ElasticsearchServiceStart; + licensing: LicensingPluginStart; + security?: SecurityPluginSetup; + config: BeatsManagementConfigType; + logger: Logger; + kibanaVersion: string; +} + +export function compose({ + elasticsearch, + config, + kibanaVersion, + logger, + licensing, + security, +}: ComposeOptions): CMServerLibs { + const backendAdapter = new KibanaBackendFrameworkAdapter( + camelCase(PLUGIN.ID), + kibanaVersion, + config, + logger, + licensing, + security ); - const database = new KibanaDatabaseAdapter(server.newPlatform.start.core.elasticsearch); + const framework = new BackendFrameworkLib(backendAdapter, config); + const database = new KibanaDatabaseAdapter(elasticsearch); const beatsAdapter = new ElasticsearchBeatsAdapter(database); const configAdapter = new ElasticsearchConfigurationBlockAdapter(database); diff --git a/x-pack/legacy/plugins/beats_management/server/lib/configuration_blocks.ts b/x-pack/plugins/beats_management/server/lib/configuration_blocks.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/configuration_blocks.ts rename to x-pack/plugins/beats_management/server/lib/configuration_blocks.ts diff --git a/x-pack/plugins/beats_management/server/lib/framework.ts b/x-pack/plugins/beats_management/server/lib/framework.ts new file mode 100644 index 0000000000000..abe41df1a279a --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/framework.ts @@ -0,0 +1,53 @@ +/* + * 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 { Headers, KibanaRequest } from 'kibana/server'; +import { BackendFrameworkAdapter, FrameworkUser } from './adapters/framework/adapter_types'; +import { BeatsManagementConfigType } from '../../common'; + +export class BackendFrameworkLib { + public log = this.adapter.log; + public internalUser = this.adapter.internalUser; + + constructor( + private readonly adapter: BackendFrameworkAdapter, + private readonly config: BeatsManagementConfigType + ) { + this.validateConfig(); + } + + public getConfig(): BeatsManagementConfigType { + return this.config; + } + + public getUser(request: KibanaRequest): FrameworkUser { + return this.adapter.getUser(request); + } + + /** + * Expired `null` happens when we have no xpack info + */ + public get license() { + return { + type: this.adapter.info ? this.adapter.info.license.type : 'unknown', + expired: this.adapter.info ? this.adapter.info.license.expired : null, + }; + } + + public get securityIsEnabled() { + return this.adapter.info ? this.adapter.info.security.enabled : false; + } + + private validateConfig() { + const encryptionKey = this.config.encryptionKey; + + if (!encryptionKey) { + this.adapter.log( + 'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token' + ); + } + } +} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/tags.ts b/x-pack/plugins/beats_management/server/lib/tags.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/server/lib/tags.ts rename to x-pack/plugins/beats_management/server/lib/tags.ts diff --git a/x-pack/legacy/plugins/beats_management/server/lib/tokens.ts b/x-pack/plugins/beats_management/server/lib/tokens.ts similarity index 92% rename from x-pack/legacy/plugins/beats_management/server/lib/tokens.ts rename to x-pack/plugins/beats_management/server/lib/tokens.ts index 759868810c0ce..366d3e8097980 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/tokens.ts +++ b/x-pack/plugins/beats_management/server/lib/tokens.ts @@ -58,7 +58,7 @@ export class CMTokensDomain { let expired = false; if (decode) { - const enrollmentTokenSecret = this.framework.getSetting('encryptionKey'); + const enrollmentTokenSecret = this.framework.getConfig().encryptionKey; try { verifyToken(recivedToken, enrollmentTokenSecret); @@ -98,7 +98,7 @@ export class CMTokensDomain { } public generateAccessToken() { - const enrollmentTokenSecret = this.framework.getSetting('encryptionKey'); + const enrollmentTokenSecret = this.framework.getConfig().encryptionKey; const tokenData = { created: moment().toJSON(), @@ -113,12 +113,12 @@ export class CMTokensDomain { numTokens: number = 1 ): Promise { const tokens = []; - const enrollmentTokensTtlInSeconds = this.framework.getSetting('enrollmentTokensTtlInSeconds'); + const enrollmentTokensTtlInSeconds = this.framework.getConfig().enrollmentTokensTtlInSeconds; const enrollmentTokenExpiration = moment() .add(enrollmentTokensTtlInSeconds, 'seconds') .toJSON(); - const enrollmentTokenSecret = this.framework.getSetting('encryptionKey'); + const enrollmentTokenSecret = this.framework.getConfig().encryptionKey; while (tokens.length < numTokens) { const tokenData = { diff --git a/x-pack/legacy/plugins/beats_management/server/lib/types.ts b/x-pack/plugins/beats_management/server/lib/types.ts similarity index 97% rename from x-pack/legacy/plugins/beats_management/server/lib/types.ts rename to x-pack/plugins/beats_management/server/lib/types.ts index 23894e3ffa6f7..d86aa8652fdbc 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/types.ts +++ b/x-pack/plugins/beats_management/server/lib/types.ts @@ -17,7 +17,7 @@ export type UserOrToken = FrameworkUser | string; export interface CMServerLibs { framework: BackendFrameworkLib; - database?: DatabaseAdapter; + database: DatabaseAdapter; beats: CMBeatsDomain; tags: CMTagsDomain; beatEvents: BeatEventsLib; diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index a82dbcb4a3a6e..92c2278148bc1 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { take } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -13,6 +14,11 @@ import { import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginStart } from '../../licensing/server'; import { BeatsManagementConfigType } from '../common'; +import { CMServerLibs } from './lib/types'; +import { registerRoutes } from './routes'; +import { compose } from './lib/compose/kibana'; +import { INDEX_NAMES } from '../common/constants'; +import { beatsIndexTemplate } from './index_templates'; interface SetupDeps { security?: SecurityPluginSetup; @@ -22,18 +28,49 @@ interface StartDeps { licensing: LicensingPluginStart; } +declare module 'src/core/server' { + interface RequestHandlerContext { + beatsManagement?: CMServerLibs; + } +} + export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { + private securitySetup?: SecurityPluginSetup; + private beatsLibs?: CMServerLibs; + constructor( private readonly initializerContext: PluginInitializerContext ) {} - public async setup(core: CoreSetup, plugins: SetupDeps) { - this.initializerContext.config.create(); + public async setup(core: CoreSetup, { security }: SetupDeps) { + this.securitySetup = security; + + const router = core.http.createRouter(); + registerRoutes(router); + + core.http.registerRouteHandlerContext('beatsManagement', (_, req) => { + return this.beatsLibs!; + }); return {}; } - public async start(core: CoreStart, { licensing }: StartDeps) { + public async start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { + const config = await this.initializerContext.config.create().pipe(take(1)).toPromise(); + const logger = this.initializerContext.logger.get(); + const kibanaVersion = this.initializerContext.env.packageInfo.version; + + this.beatsLibs = compose({ + elasticsearch, + licensing, + security: this.securitySetup, + config, + logger, + kibanaVersion, + }); + + await this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate); + return {}; } } diff --git a/x-pack/plugins/beats_management/server/routes/beats/configuration.ts b/x-pack/plugins/beats_management/server/routes/beats/configuration.ts new file mode 100644 index 0000000000000..1496e4bbfc99f --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/configuration.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { ConfigurationBlock } from '../../../common/domain_types'; +import { ReturnTypeList } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerGetBeatConfigurationRoute = (router: IRouter) => { + router.get( + { + path: '/api/beats/agent/{beatId}/configuration', + validate: { + params: schema.object({ + beatId: schema.string(), + }), + }, + options: { + authRequired: false, + }, + }, + wrapRouteWithSecurity({}, async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const accessToken = request.headers['kbn-beats-access-token']; + if (!accessToken) { + return response.badRequest({ + body: 'beats access token required', + }); + } + const beatId = request.params.beatId; + + let configurationBlocks: ConfigurationBlock[]; + const beat = await beatsManagement.beats.getById( + beatsManagement.framework.internalUser, + beatId + ); + if (beat === null) { + return response.notFound({ + body: { + message: `Beat "${beatId}" not found`, + }, + }); + } + + const isAccessTokenValid = beat.access_token === accessToken; + if (!isAccessTokenValid) { + return response.unauthorized({ + body: { + message: 'Invalid access token', + }, + }); + } + + await beatsManagement.beats.update(beatsManagement.framework.internalUser, beat.id, { + last_checkin: new Date(), + }); + + if (beat.tags) { + const result = await beatsManagement.configurationBlocks.getForTags( + beatsManagement.framework.internalUser, + beat.tags, + -1 + ); + + configurationBlocks = result.blocks; + } else { + configurationBlocks = []; + } + + return response.ok({ + body: { + list: configurationBlocks, + success: true, + } as ReturnTypeList, + }); + }) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/enroll.ts b/x-pack/plugins/beats_management/server/routes/beats/enroll.ts new file mode 100644 index 0000000000000..be8fff3b7c437 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/enroll.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ensureRawRequest } from '../../../../../../src/core/server/http/router'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { BeatEnrollmentStatus } from '../../lib/types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerBeatEnrollmentRoute = (router: IRouter) => { + // TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 + router.post( + { + path: '/api/beats/agent/{beatId}', + validate: { + params: schema.object({ + beatId: schema.string(), + }), + body: schema.object( + { + host_name: schema.string(), + name: schema.string(), + type: schema.string(), + version: schema.string(), + }, + { unknowns: 'ignore' } + ), + }, + options: { + authRequired: false, + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + + const { beatId } = request.params; + const enrollmentToken = request.headers['kbn-beats-enrollment-token'] as string; + if (!enrollmentToken) { + return response.badRequest({ + body: 'beats enrollment token required', + }); + } + + // TODO: fixme eventually, need to access `info.remoteAddress` from KibanaRequest. + const legacyRequest = ensureRawRequest(request); + + const { status, accessToken } = await beatsManagement.beats.enrollBeat( + enrollmentToken, + beatId, + legacyRequest.info.remoteAddress, + request.body + ); + + switch (status) { + case BeatEnrollmentStatus.ExpiredEnrollmentToken: + return response.badRequest({ + body: { + message: BeatEnrollmentStatus.ExpiredEnrollmentToken, + }, + }); + case BeatEnrollmentStatus.InvalidEnrollmentToken: + return response.badRequest({ + body: { + message: BeatEnrollmentStatus.InvalidEnrollmentToken, + }, + }); + case BeatEnrollmentStatus.Success: + default: + return response.ok({ + body: { + item: accessToken, + action: 'created', + success: true, + }, + }); + } + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/events.ts b/x-pack/plugins/beats_management/server/routes/beats/events.ts new file mode 100644 index 0000000000000..b87e6d684228a --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/events.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ReturnTypeBulkAction } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerBeatEventsRoute = (router: IRouter) => { + router.post( + { + path: '/api/beats/{beatId}/events', + validate: { + params: schema.object({ + beatId: schema.string(), + }), + body: schema.arrayOf(schema.any(), { defaultValue: [] }), + }, + options: { + authRequired: false, + }, + }, + wrapRouteWithSecurity({}, async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const accessToken = request.headers['kbn-beats-access-token']; + if (!accessToken) { + return response.badRequest({ + body: 'beats access token required', + }); + } + const beatId = request.params.beatId; + const events = request.body; + const internalUser = beatsManagement.framework.internalUser; + + const beat = await beatsManagement.beats.getById(internalUser, beatId); + if (beat === null) { + return response.badRequest({ + body: { + message: `Beat "${beatId}" not found`, + }, + }); + } + + const isAccessTokenValid = beat.access_token === accessToken; + if (!isAccessTokenValid) { + return response.unauthorized({ + body: { + message: `Invalid access token`, + }, + }); + } + + const results = await beatsManagement.beatEvents.log(internalUser, beat.id, events); + + return response.ok({ + body: { + results, + success: true, + } as ReturnTypeBulkAction, + }); + }) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/get.ts b/x-pack/plugins/beats_management/server/routes/beats/get.ts new file mode 100644 index 0000000000000..8762f325e7484 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/get.ts @@ -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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { CMBeat } from '../../../common/domain_types'; +import { ReturnTypeGet } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerGetBeatRoute = (router: IRouter) => { + router.get( + { + path: '/api/beats/agent/{beatId}/{token?}', + validate: { + params: schema.object({ + beatId: schema.string(), + token: schema.string({ defaultValue: '' }), + }), + }, + }, + wrapRouteWithSecurity( + { requiredRoles: ['beats_admin'] }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + const beatId = request.params.beatId; + + let beat: CMBeat | null; + if (beatId === 'unknown') { + beat = await beatsManagement.beats.getByEnrollmentToken(user, request.params.token); + if (beat === null) { + return response.ok({ body: { success: false } }); + } + } else { + beat = await beatsManagement.beats.getById(user, beatId); + if (beat === null) { + return response.notFound({ + body: { + message: 'Beat not found', + }, + }); + } + } + + delete beat.access_token; + + return response.ok({ + body: { + item: beat, + success: true, + } as ReturnTypeGet, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/index.ts b/x-pack/plugins/beats_management/server/routes/beats/index.ts new file mode 100644 index 0000000000000..0ebdc932142ae --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { registerGetBeatConfigurationRoute } from './configuration'; +export { registerBeatEnrollmentRoute } from './enroll'; +export { registerBeatEventsRoute } from './events'; +export { registerGetBeatRoute } from './get'; +export { registerListAgentsRoute } from './list'; +export { registerTagAssignmentsRoute } from './tag_assignment'; +export { registerTagRemovalsRoute } from './tag_removal'; +export { registerBeatUpdateRoute } from './update'; diff --git a/x-pack/plugins/beats_management/server/routes/beats/list.ts b/x-pack/plugins/beats_management/server/routes/beats/list.ts new file mode 100644 index 0000000000000..e4108238e3f2f --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/list.ts @@ -0,0 +1,69 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { CMBeat } from '../../../common/domain_types'; +import { ReturnTypeList } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerListAgentsRoute = (router: IRouter) => { + router.get( + { + path: '/api/beats/agents/{listByAndValue*}', + validate: { + params: schema.object({ + listByAndValue: schema.maybe(schema.string()), + }), + query: schema.object( + { + ESQuery: schema.maybe(schema.string()), + }, + { defaultValue: {} } + ), + }, + }, + wrapRouteWithSecurity( + { + requiredRoles: ['beats_admin'], + requiredLicense: REQUIRED_LICENSES, + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + + const listByAndValueParts = request.params.listByAndValue?.split('/') ?? []; + let listBy: string | null = null; + let listByValue: string | null = null; + if (listByAndValueParts.length === 2) { + listBy = listByAndValueParts[0]; + listByValue = listByAndValueParts[1]; + } + + let beats: CMBeat[]; + + switch (listBy) { + case 'tag': + beats = await beatsManagement.beats.getAllWithTag(user, listByValue || ''); + break; + + default: + beats = await beatsManagement.beats.getAll( + user, + request.query.ESQuery ? JSON.parse(request.query.ESQuery) : undefined + ); + + break; + } + + return response.ok({ + body: { list: beats, success: true, page: -1, total: -1 } as ReturnTypeList, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/tag_assignment.ts b/x-pack/plugins/beats_management/server/routes/beats/tag_assignment.ts new file mode 100644 index 0000000000000..0397f8ec4398e --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/tag_assignment.ts @@ -0,0 +1,68 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { ReturnTypeBulkAction } from '../../../common/return_types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { BeatsTagAssignment } from '../../../public/lib/adapters/beats/adapter_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerTagAssignmentsRoute = (router: IRouter) => { + // TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 + router.post( + { + path: '/api/beats/agents_tags/assignments', + validate: { + body: schema.object({ + assignments: schema.arrayOf( + schema.object({ + beatId: schema.string(), + tag: schema.string(), + }), + { defaultValue: [] } + ), + }), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + const { assignments }: { assignments: BeatsTagAssignment[] } = request.body; + + const result = await beatsManagement.beats.assignTagsToBeats(user, assignments); + + return response.ok({ + body: { + success: true, + results: result.assignments.map((assignment) => ({ + success: assignment.status && assignment.status >= 200 && assignment.status < 300, + error: + !assignment.status || assignment.status >= 300 + ? { + code: assignment.status || 400, + message: assignment.result, + } + : undefined, + result: + assignment.status && assignment.status >= 200 && assignment.status < 300 + ? { + message: assignment.result, + } + : undefined, + })), + } as ReturnTypeBulkAction, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/tag_removal.ts b/x-pack/plugins/beats_management/server/routes/beats/tag_removal.ts new file mode 100644 index 0000000000000..a04ed81fb183b --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/tag_removal.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { ReturnTypeBulkAction } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerTagRemovalsRoute = (router: IRouter) => { + // TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 + router.post( + { + path: '/api/beats/agents_tags/removals', + validate: { + body: schema.object({ + removals: schema.arrayOf( + schema.object({ + beatId: schema.string(), + tag: schema.string(), + }), + { defaultValue: [] } + ), + }), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + const { removals } = request.body; + + const result = await beatsManagement.beats.removeTagsFromBeats(user, removals); + + return response.ok({ + body: { + success: true, + results: result.removals.map((removal) => ({ + success: removal.status && removal.status >= 200 && removal.status < 300, + error: + !removal.status || removal.status >= 300 + ? { + code: removal.status || 400, + message: removal.result, + } + : undefined, + result: + removal.status && removal.status >= 200 && removal.status < 300 + ? { + message: removal.result, + } + : undefined, + })), + } as ReturnTypeBulkAction, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/update.ts b/x-pack/plugins/beats_management/server/routes/beats/update.ts new file mode 100644 index 0000000000000..21bd6555b28dd --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/beats/update.ts @@ -0,0 +1,104 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ensureRawRequest } from '../../../../../../src/core/server/http/router'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { CMBeat } from '../../../common/domain_types'; +import { ReturnTypeUpdate } from '../../../common/return_types'; +import { internalUser } from '../../lib/adapters/framework/adapter_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerBeatUpdateRoute = (router: IRouter) => { + // TODO: write to Kibana audit log file (include who did the verification as well) https://github.com/elastic/kibana/issues/26024 + router.put( + { + path: '/api/beats/agent/{beatId}', + validate: { + params: schema.object({ + beatId: schema.string(), + }), + body: schema.object( + { + active: schema.maybe(schema.boolean()), + ephemeral_id: schema.maybe(schema.string()), + host_name: schema.maybe(schema.string()), + local_configuration_yml: schema.maybe(schema.string()), + metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), + name: schema.maybe(schema.string()), + type: schema.maybe(schema.string()), + version: schema.maybe(schema.string()), + }, + { defaultValue: {} } + ), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const accessToken = request.headers['kbn-beats-access-token'] as string; + const { beatId } = request.params; + const user = beatsManagement.framework.getUser(request); + const userOrToken = accessToken || user; + + // TODO: fixme eventually, need to access `info.remoteAddress` from KibanaRequest. + const legacyRequest = ensureRawRequest(request); + const remoteAddress = legacyRequest.info.remoteAddress; + + if (user.kind === 'unauthenticated' && request.body.active !== undefined) { + return response.unauthorized({ + body: { + message: 'access-token is not a valid auth type to change beat status', + }, + }); + } + + const status = await beatsManagement.beats.update(userOrToken, beatId, { + ...request.body, + host_ip: remoteAddress, + }); + + switch (status) { + case 'beat-not-found': + return response.notFound({ + body: { + message: 'Beat not found', + }, + }); + case 'invalid-access-token': + return response.unauthorized({ + body: { + message: 'Invalid access token', + }, + }); + } + + const beat = await beatsManagement.beats.getById(internalUser, beatId); + if (!beat) { + return response.notFound({ + body: { + message: 'Beat not found', + }, + }); + } + + return response.ok({ + body: { + item: beat, + action: 'updated', + success: true, + } as ReturnTypeUpdate, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/configurations/delete.ts b/x-pack/plugins/beats_management/server/routes/configurations/delete.ts new file mode 100644 index 0000000000000..b60d3bd2d5a94 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/configurations/delete.ts @@ -0,0 +1,47 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { ReturnTypeBulkDelete } from '../../../common/return_types'; + +export const registerDeleteConfigurationBlocksRoute = (router: IRouter) => { + router.delete( + { + path: '/api/beats/configurations/{ids}', + validate: { + params: schema.object({ + ids: schema.string(), + }), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const ids = request.params.ids.split(',').filter((id) => id.length > 0); + const user = beatsManagement.framework.getUser(request); + + const results = await beatsManagement.configurationBlocks.delete(user, ids); + return response.ok({ + body: { + success: true, + results: results.map((result) => ({ + success: result.success, + action: 'deleted', + error: result.success ? undefined : { message: result.reason }, + })), + } as ReturnTypeBulkDelete, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/configurations/get.ts b/x-pack/plugins/beats_management/server/routes/configurations/get.ts new file mode 100644 index 0000000000000..6f422ca9ca8bd --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/configurations/get.ts @@ -0,0 +1,52 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { ConfigurationBlock } from '../../../common/domain_types'; +import { ReturnTypeList } from '../../../common/return_types'; + +export const registerGetConfigurationBlocksRoute = (router: IRouter) => { + router.get( + { + path: '/api/beats/configurations/{tagIds}/{page?}', + validate: { + params: schema.object({ + tagIds: schema.string(), + page: schema.maybe(schema.number()), + }), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const tagIds = request.params.tagIds.split(',').filter((id) => id.length > 0); + const user = beatsManagement.framework.getUser(request); + const result = await beatsManagement.configurationBlocks.getForTags( + user, + tagIds, + request.params.page, + 5 + ); + + return response.ok({ + body: { + page: result.page, + total: result.total, + list: result.blocks, + success: true, + } as ReturnTypeList, + }); + } + ) + ); +}; diff --git a/x-pack/legacy/plugins/beats_management/server/kibana.index.ts b/x-pack/plugins/beats_management/server/routes/configurations/index.ts similarity index 50% rename from x-pack/legacy/plugins/beats_management/server/kibana.index.ts rename to x-pack/plugins/beats_management/server/routes/configurations/index.ts index dd7bc443bc603..490d4a7cd7328 100644 --- a/x-pack/legacy/plugins/beats_management/server/kibana.index.ts +++ b/x-pack/plugins/beats_management/server/routes/configurations/index.ts @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { compose } from './lib/compose/kibana'; -import { initManagementServer } from './management_server'; - -export const initServerWithKibana = (hapiServer: any) => { - const libs = compose(hapiServer); - initManagementServer(libs); -}; +export { registerGetConfigurationBlocksRoute } from './get'; +export { registerDeleteConfigurationBlocksRoute } from './delete'; +export { registerUpsertConfigurationBlocksRoute } from './upsert'; diff --git a/x-pack/plugins/beats_management/server/routes/configurations/upsert.ts b/x-pack/plugins/beats_management/server/routes/configurations/upsert.ts new file mode 100644 index 0000000000000..e235b172e7d0b --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/configurations/upsert.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { isLeft } from 'fp-ts/lib/Either'; +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { REQUIRED_LICENSES } from '../../../common/constants'; +import { + ConfigurationBlock, + createConfigurationBlockInterface, +} from '../../../common/domain_types'; +import { ReturnTypeBulkUpsert } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerUpsertConfigurationBlocksRoute = (router: IRouter) => { + // TODO: write to Kibana audit log file + router.put( + { + path: '/api/beats/configurations', + validate: { + body: schema.arrayOf(schema.recordOf(schema.string(), schema.any()), { defaultValue: [] }), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + const input = request.body as ConfigurationBlock[]; + + const result = await Promise.all( + input.map(async (block: ConfigurationBlock) => { + const assertData = createConfigurationBlockInterface().decode(block); + if (isLeft(assertData)) { + return { + error: `Error parsing block info, ${PathReporter.report(assertData)[0]}`, + }; + } + + const { blockID, success, error } = await beatsManagement.configurationBlocks.save( + user, + block + ); + if (error) { + return { success, error }; + } + + return { success, blockID }; + }) + ); + + return response.ok({ + body: { + results: result.map((r) => ({ + success: r.success as boolean, + // TODO: we need to surface this data, not hard coded + action: 'created' as 'created' | 'updated', + })), + success: true, + } as ReturnTypeBulkUpsert, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/index.ts b/x-pack/plugins/beats_management/server/routes/index.ts new file mode 100644 index 0000000000000..423ecc85a5798 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/index.ts @@ -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 { IRouter } from 'src/core/server'; +import { + registerDeleteConfigurationBlocksRoute, + registerGetConfigurationBlocksRoute, + registerUpsertConfigurationBlocksRoute, +} from './configurations'; +import { registerCreateTokenRoute } from './tokens'; +import { + registerSetTagRoute, + registerListTagsRoute, + registerGetTagsWithIdsRoute, + registerDeleteTagsWithIdsRoute, + registerAssignableTagsRoute, +} from './tags'; +import { + registerBeatUpdateRoute, + registerTagRemovalsRoute, + registerTagAssignmentsRoute, + registerListAgentsRoute, + registerGetBeatRoute, + registerBeatEventsRoute, + registerBeatEnrollmentRoute, + registerGetBeatConfigurationRoute, +} from './beats'; + +export const registerRoutes = (router: IRouter) => { + // configurations + registerGetConfigurationBlocksRoute(router); + registerDeleteConfigurationBlocksRoute(router); + registerUpsertConfigurationBlocksRoute(router); + // beats + registerBeatUpdateRoute(router); + registerTagRemovalsRoute(router); + registerTagAssignmentsRoute(router); + registerListAgentsRoute(router); + registerGetBeatRoute(router); + registerBeatEventsRoute(router); + registerBeatEnrollmentRoute(router); + registerGetBeatConfigurationRoute(router); + // tags + registerSetTagRoute(router); + registerListTagsRoute(router); + registerGetTagsWithIdsRoute(router); + registerDeleteTagsWithIdsRoute(router); + registerAssignableTagsRoute(router); + // tokens + registerCreateTokenRoute(router); +}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/assignable.ts b/x-pack/plugins/beats_management/server/routes/tags/assignable.ts new file mode 100644 index 0000000000000..60d4748bf1fa6 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/tags/assignable.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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { flatten } from 'lodash'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { BeatTag } from '../../../common/domain_types'; +import { ReturnTypeBulkGet } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerAssignableTagsRoute = (router: IRouter) => { + router.get( + { + path: '/api/beats/tags/assignable/{beatIds}', + validate: { + params: schema.object({ + beatIds: schema.string(), + }), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + const beatIds = request.params.beatIds.split(',').filter((id) => id.length > 0); + + const beats = await beatsManagement.beats.getByIds(user, beatIds); + const tags = await beatsManagement.tags.getNonConflictingTags( + user, + flatten(beats.map((beat) => beat.tags)) + ); + + return response.ok({ + body: { + items: tags, + success: true, + } as ReturnTypeBulkGet, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/delete.ts b/x-pack/plugins/beats_management/server/routes/tags/delete.ts new file mode 100644 index 0000000000000..78d0c80d42060 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/tags/delete.ts @@ -0,0 +1,47 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { ReturnTypeBulkDelete } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerDeleteTagsWithIdsRoute = (router: IRouter) => { + router.delete( + { + path: '/api/beats/tags/{tagIds}', + validate: { + params: schema.object({ + tagIds: schema.string(), + }), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + const tagIds = request.params.tagIds.split(',').filter((id) => id.length > 0); + + const success = await beatsManagement.tags.delete(user, tagIds); + + return response.ok({ + body: { + results: tagIds.map(() => ({ + success, + action: 'deleted', + })), + success, + } as ReturnTypeBulkDelete, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/get.ts b/x-pack/plugins/beats_management/server/routes/tags/get.ts new file mode 100644 index 0000000000000..48da829aa09e5 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/tags/get.ts @@ -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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { BeatTag } from '../../../common/domain_types'; +import { ReturnTypeBulkGet } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerGetTagsWithIdsRoute = (router: IRouter) => { + router.get( + { + path: '/api/beats/tags/{tagIds}', + validate: { + params: schema.object({ + tagIds: schema.string(), + }), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + const tagIds = request.params.tagIds.split(',').filter((id) => id.length > 0); + + const tags = await beatsManagement.tags.getWithIds(user, tagIds); + + return response.ok({ + body: { + items: tags, + success: true, + } as ReturnTypeBulkGet, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/index.ts b/x-pack/plugins/beats_management/server/routes/tags/index.ts new file mode 100644 index 0000000000000..2f0590026ca7e --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/tags/index.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. + */ + +export { registerAssignableTagsRoute } from './assignable'; +export { registerDeleteTagsWithIdsRoute } from './delete'; +export { registerGetTagsWithIdsRoute } from './get'; +export { registerListTagsRoute } from './list'; +export { registerSetTagRoute } from './set'; diff --git a/x-pack/plugins/beats_management/server/routes/tags/list.ts b/x-pack/plugins/beats_management/server/routes/tags/list.ts new file mode 100644 index 0000000000000..ce913cda337c5 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/tags/list.ts @@ -0,0 +1,52 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { BeatTag } from '../../../common/domain_types'; +import { ReturnTypeList } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerListTagsRoute = (router: IRouter) => { + router.get( + { + path: '/api/beats/tags', + validate: { + query: schema.object( + { + ESQuery: schema.maybe(schema.string()), + }, + { defaultValue: {} } + ), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + + const tags = await beatsManagement.tags.getAll( + user, + request.query && request.query.ESQuery ? JSON.parse(request.query.ESQuery) : undefined + ); + + return response.ok({ + body: { + list: tags, + success: true, + page: -1, + total: -1, + } as ReturnTypeList, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/set.ts b/x-pack/plugins/beats_management/server/routes/tags/set.ts new file mode 100644 index 0000000000000..ef9e181514a55 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/tags/set.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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { REQUIRED_LICENSES } from '../../../common/constants'; +import { BeatTag } from '../../../common/domain_types'; +import { ReturnTypeUpsert } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +export const registerSetTagRoute = (router: IRouter) => { + // TODO: write to Kibana audit log file + router.put( + { + path: '/api/beats/tag/{tagId}', + validate: { + params: schema.object({ + tagId: schema.string(), + }), + body: schema.object( + { + color: schema.maybe(schema.string()), + name: schema.maybe(schema.string()), + }, + { defaultValue: {} } + ), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + + const config = { + id: request.params.tagId, + name: request.params.tagId, + color: '#DD0A73', + hasConfigurationBlocksTypes: [], + ...request.body, + }; + const id = await beatsManagement.tags.upsertTag(user, config); + const tag = await beatsManagement.tags.getWithIds(user, [id]); + + // TODO the action needs to be surfaced + return response.ok({ + body: { + success: true, + item: tag[0], + action: 'created', + } as ReturnTypeUpsert, + }); + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/tokens/create.ts b/x-pack/plugins/beats_management/server/routes/tokens/create.ts new file mode 100644 index 0000000000000..2fd7d4614c570 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/tokens/create.ts @@ -0,0 +1,61 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { REQUIRED_LICENSES } from '../../../common/constants/security'; +import { ReturnTypeBulkCreate } from '../../../common/return_types'; +import { wrapRouteWithSecurity } from '../wrap_route_with_security'; + +const DEFAULT_NUM_TOKENS = 1; + +export const registerCreateTokenRoute = (router: IRouter) => { + // TODO: write to Kibana audit log file + router.post( + { + path: '/api/beats/enrollment_tokens', + validate: { + body: schema.nullable( + schema.object({ + num_tokens: schema.number({ defaultValue: DEFAULT_NUM_TOKENS, min: 1 }), + }) + ), + }, + }, + wrapRouteWithSecurity( + { + requiredLicense: REQUIRED_LICENSES, + requiredRoles: ['beats_admin'], + }, + async (context, request, response) => { + const beatsManagement = context.beatsManagement!; + const user = beatsManagement.framework.getUser(request); + + const numTokens = request.body?.num_tokens ?? DEFAULT_NUM_TOKENS; + try { + const tokens = await beatsManagement.tokens.createEnrollmentTokens(user, numTokens); + return response.ok({ + body: { + results: tokens.map((token) => ({ + item: token, + success: true, + action: 'created', + })), + success: true, + } as ReturnTypeBulkCreate, + }); + } catch (err) { + beatsManagement.framework.log(err.message); + return response.internalError({ + body: { + message: 'An error occurred, please check your Kibana logs', + }, + }); + } + } + ) + ); +}; diff --git a/x-pack/plugins/beats_management/server/routes/tokens/index.ts b/x-pack/plugins/beats_management/server/routes/tokens/index.ts new file mode 100644 index 0000000000000..3e34fff0a6c6b --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/tokens/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 { registerCreateTokenRoute } from './create'; diff --git a/x-pack/plugins/beats_management/server/routes/wrap_route_with_security.ts b/x-pack/plugins/beats_management/server/routes/wrap_route_with_security.ts new file mode 100644 index 0000000000000..ad4f8080127b2 --- /dev/null +++ b/x-pack/plugins/beats_management/server/routes/wrap_route_with_security.ts @@ -0,0 +1,69 @@ +/* + * 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 { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteMethod, +} from 'src/core/server'; +import { difference } from 'lodash'; + +export function wrapRouteWithSecurity( + { + requiredLicense = [], + requiredRoles = [], + }: { requiredLicense?: string[]; requiredRoles?: string[] }, + handler: RequestHandler +): RequestHandler { + return async ( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) => { + const beatsManagement = context.beatsManagement!; + const license = beatsManagement.framework.license; + const user = beatsManagement.framework.getUser(request); + + if ( + requiredLicense.length > 0 && + (license.expired || !requiredLicense.includes(license.type)) + ) { + return response.forbidden({ + body: { + message: `Your ${license.type} license does not support this API or is expired. Please upgrade your license.`, + }, + }); + } + + if (requiredRoles.length > 0) { + if (user.kind !== 'authenticated') { + return response.forbidden({ + body: { + message: `Request must be authenticated`, + }, + }); + } + + if ( + user.kind === 'authenticated' && + !user.roles.includes('superuser') && + difference(requiredRoles, user.roles).length !== 0 + ) { + return response.forbidden({ + body: { + message: `Request must be authenticated by a user with one of the following user roles: ${requiredRoles.join( + ',' + )}`, + }, + }); + } + } + + return handler(context, request, response); + }; +} diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index ca19bddb60297..7d4143f9bcfa7 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -6,7 +6,7 @@ import { getAppResultsMock } from './application.test.mocks'; -import { of, EMPTY } from 'rxjs'; +import { EMPTY, of } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { ApplicationStart, AppNavLinkStatus, AppStatus, PublicAppInfo } from 'src/core/public'; import { @@ -100,6 +100,20 @@ describe('applicationResultProvider', () => { expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]); }); + it('ignores apps with non-visible navlink', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1', navLinkStatus: AppNavLinkStatus.visible }), + createApp({ id: 'disabled', title: 'disabled', navLinkStatus: AppNavLinkStatus.disabled }), + createApp({ id: 'hidden', title: 'hidden', navLinkStatus: AppNavLinkStatus.hidden }), + ]) + ); + const provider = createApplicationResultProvider(Promise.resolve(application)); + await provider.find('term', defaultOption).toPromise(); + + expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]); + }); + it('ignores chromeless apps', async () => { application.applications$ = of( createAppMap([ diff --git a/x-pack/plugins/global_search_providers/public/providers/application.ts b/x-pack/plugins/global_search_providers/public/providers/application.ts index e40fcef17f73c..45264a3b2c521 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.ts @@ -17,7 +17,8 @@ export const createApplicationResultProvider = ( mergeMap((application) => application.applications$), map((apps) => [...apps.values()].filter( - (app) => app.status === 0 && (app.legacy === true || app.chromeless !== true) + // only include non-chromeless enabled apps with visible navLinks + (app) => app.status === 0 && app.navLinkStatus === 1 && app.chromeless !== true ) ), shareReplay(1) diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts index 84e05c67c5f66..11e3a40ddee17 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts @@ -112,7 +112,7 @@ describe('savedObjectsResultProvider', () => { expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ page: 1, perPage: defaultOption.maxResults, - search: 'term', + search: 'term*', preference: 'pref', searchFields: ['title', 'description'], type: ['typeA', 'typeB'], diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts index b423b19ebc672..8a3d3d732531f 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts @@ -25,7 +25,7 @@ export const createSavedObjectsResultProvider = (): GlobalSearchResultProvider = const responsePromise = client.find({ page: 1, perPage: maxResults, - search: term, + search: term ? `${term}*` : undefined, preference, searchFields, type: searchableTypes.map((type) => type.name), diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx index ecaa40b398d08..ce5f2a60f5165 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx @@ -111,14 +111,17 @@ export const CreateField = React.memo(function CreateFieldComponent({ {/* Field subType (if any) */} - {({ type }) => ( - - )} + {({ type }) => { + const [fieldType] = type; + return ( + + ); + }} ); @@ -188,7 +191,10 @@ export const CreateField = React.memo(function CreateFieldComponent({ {({ type, subType }) => { - const ParametersForm = getParametersFormForType(type, subType); + const ParametersForm = getParametersFormForType( + type?.[0].value, + subType?.[0].value + ); if (!ParametersForm) { return null; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index e6950ccfe253e..a9bbf008e5129 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -98,15 +98,15 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF {({ type, subType }) => { const linkDocumentation = - documentationService.getTypeDocLink(subType) || - documentationService.getTypeDocLink(type); + documentationService.getTypeDocLink(subType?.[0].value) || + documentationService.getTypeDocLink(type?.[0].value); if (!linkDocumentation) { return null; } - const typeDefinition = TYPE_DEFINITION[type as MainType]; - const subTypeDefinition = TYPE_DEFINITION[subType as SubType]; + const typeDefinition = TYPE_DEFINITION[type[0].value as MainType]; + const subTypeDefinition = TYPE_DEFINITION[subType?.[0].value as SubType]; return ( @@ -148,7 +148,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF {({ type, subType }) => { - const ParametersForm = getParametersFormForType(type, subType); + const ParametersForm = getParametersFormForType(type?.[0].value, subType?.[0].value); if (!ParametersForm) { return null; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx index 5b969fa7ed827..b4b5bce21f768 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -36,15 +36,18 @@ export const EditFieldHeaderForm = React.memo( {/* Field subType (if any) */} - {({ type }) => ( - - )} + {({ type }) => { + const [fieldType] = type; + return ( + + ); + }} @@ -52,7 +55,7 @@ export const EditFieldHeaderForm = React.memo( {({ type, subType }) => { - const typeDefinition = TYPE_DEFINITION[type as MainType]; + const typeDefinition = TYPE_DEFINITION[type[0].value as MainType]; const hasSubType = typeDefinition.subTypes !== undefined; if (hasSubType) { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index 18a8270117ea4..2a8368c666859 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -335,6 +335,11 @@ export const reducer = (state: State, action: Action): State => { return { ...state, fields: updatedFields, + documentFields: { + ...state.documentFields, + // If we removed the last field, show the "Create field" form + status: updatedFields.rootLevelFields.length === 0 ? 'creatingField' : 'idle', + }, // If we have a search in progress, we reexecute the search to update our result array search: Boolean(state.search.term) ? { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx index 228094c0dfac5..7fb92e89c9f68 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { XJsonLang } from '@kbn/monaco'; import React, { FunctionComponent, useCallback } from 'react'; import { FieldHook, Monaco } from '../../../../../../shared_imports'; @@ -33,9 +32,6 @@ export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => value: xJson, languageId: XJsonLang.ID, options: { minimap: { enabled: false } }, - editorDidMount: (m: any) => { - XJsonLang.registerGrammarChecker(m); - }, onChange, ...editorProps, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx index 502045084b24d..b5f7df9117f35 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx @@ -81,7 +81,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { FieldsComponent: DateIndexName, docLinkPath: '/date-index-name-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.dateIndexName', { - defaultMessage: 'Date Index Name', + defaultMessage: 'Date index name', }), }, dissect: { @@ -95,7 +95,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { FieldsComponent: DotExpander, docLinkPath: '/dot-expand-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.dotExpander', { - defaultMessage: 'Dot Expander', + defaultMessage: 'Dot expander', }), }, drop: { @@ -144,7 +144,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { FieldsComponent: undefined, // TODO: Implement docLinkPath: '/htmlstrip-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.htmlStrip', { - defaultMessage: 'HTML Strip', + defaultMessage: 'HTML strip', }), }, inference: { @@ -214,7 +214,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { FieldsComponent: undefined, // TODO: Implement docLinkPath: '/ingest-node-set-security-user-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.setSecurityUser', { - defaultMessage: 'Set Security User', + defaultMessage: 'Set security user', }), }, split: { @@ -249,14 +249,14 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { FieldsComponent: undefined, // TODO: Implement docLinkPath: '/urldecode-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.urldecode', { - defaultMessage: 'URL Decode', + defaultMessage: 'URL decode', }), }, user_agent: { FieldsComponent: undefined, // TODO: Implement docLinkPath: '/user-agent-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.userAgent', { - defaultMessage: 'User Agent', + defaultMessage: 'User agent', }), }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 5f041a8d8562f..446f5b44c2e50 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -89,11 +89,12 @@ function LayerPanels( return ( - {layerIds.map((layerId) => ( + {layerIds.map((layerId, index) => ( { onRemoveLayer: jest.fn(), dispatch: jest.fn(), core: coreMock.createStart(), + dataTestSubj: 'lns_layerPanel-0', }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index a384e339e8fbd..38224bf962a3f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -36,6 +36,7 @@ const initialPopoverState = { export function LayerPanel( props: Exclude & { layerId: string; + dataTestSubj: string; isOnlyLayer: boolean; updateVisualization: StateSetter; updateDatasource: (datasourceId: string, newState: unknown) => void; @@ -50,7 +51,7 @@ export function LayerPanel( const dragDropContext = useContext(DragContext); const [popoverState, setPopoverState] = useState(initialPopoverState); - const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer } = props; + const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, dataTestSubj } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; useEffect(() => { @@ -96,7 +97,7 @@ export function LayerPanel( return ( - + { return ( - - {preview.expression ? ( - - ) : ( - - - - )} - {showTitleAsLabel && ( - {preview.title} - )} - +
+ + {preview.expression ? ( + + ) : ( + + + + )} + {showTitleAsLabel && ( + {preview.title} + )} + +
); }; @@ -204,8 +206,8 @@ export function SuggestionPanel({ : undefined; return { suggestions: newSuggestions, currentStateExpression: newStateExpression }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - frame, currentDatasourceStates, currentVisualizationState, currentVisualizationId, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 895d217555ef4..197160a1be4d9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -338,7 +338,16 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) {generalizationEval.error !== null && ( - {generalizationEval.error} + {isTrainingFilter === true && + generalizationDocsCount === 0 && + generalizationEval.error.includes('No documents found') + ? i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.evaluateNoTestingDocsError', + { + defaultMessage: 'No testing documents found', + } + ) + : generalizationEval.error} )} @@ -421,7 +430,16 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) {trainingEval.error !== null && ( - {trainingEval.error} + {isTrainingFilter === false && + trainingDocsCount === 0 && + trainingEval.error.includes('No documents found') + ? i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.evaluateNoTrainingDocsError', + { + defaultMessage: 'No training documents found', + } + ) + : trainingEval.error} )} diff --git a/x-pack/plugins/observability/public/pages/home/index.tsx b/x-pack/plugins/observability/public/pages/home/index.tsx index 349533346f2ad..4310cd1aa4485 100644 --- a/x-pack/plugins/observability/public/pages/home/index.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.tsx @@ -3,7 +3,7 @@ * 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 React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { fetchHasData } from '../../data_handler'; import { useFetcher } from '../../hooks/use_fetcher'; @@ -15,12 +15,14 @@ export function HomePage() { const values = Object.values(data); const hasSomeData = values.length ? values.some((hasData) => hasData) : null; - if (hasSomeData === true) { - history.push({ pathname: '/overview' }); - } - if (hasSomeData === false) { - history.push({ pathname: '/landing' }); - } + useEffect(() => { + if (hasSomeData === true) { + history.push({ pathname: '/overview' }); + } + if (hasSomeData === false) { + history.push({ pathname: '/landing' }); + } + }, [hasSomeData, history]); return <>; } diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js index 305322e766e3c..18e0b250a91b5 100644 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -97,8 +97,9 @@ export default function ({ getService }) { .expect(400); expect(apiResponse).to.eql({ - success: false, - error: { code: 400, message: 'Invalid enrollment token' }, + statusCode: 400, + error: 'Bad Request', + message: 'Invalid enrollment token', }); }); @@ -128,8 +129,9 @@ export default function ({ getService }) { .expect(400); expect(apiResponse).to.eql({ - success: false, - error: { code: 400, message: 'Expired enrollment token' }, + statusCode: 400, + error: 'Bad Request', + message: 'Expired enrollment token', }); }); diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js index dff9c2a0ec072..dc4afe88353e1 100644 --- a/x-pack/test/api_integration/apis/beats/update_beat.js +++ b/x-pack/test/api_integration/apis/beats/update_beat.js @@ -92,7 +92,7 @@ export default function ({ getService }) { .send(beat) .expect(401); - expect(body.error.message).to.be('Invalid access token'); + expect(body.message).to.be('Invalid access token'); const beatInEs = await es.get({ index: ES_INDEX_NAME, @@ -115,7 +115,7 @@ export default function ({ getService }) { .send(beat) .expect(404); - expect(body.error.message).to.be('Beat not found'); + expect(body.message).to.be('Beat not found'); }); }); } diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 77b9aa1e25edd..8c4321d77acf4 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -11,6 +11,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens']); const find = getService('find'); const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); describe('lens smokescreen tests', () => { it('should allow editing saved visualizations', async () => { @@ -109,6 +110,54 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getLayerCount()).to.eql(2); }); + it('should switch from a multi-layer stacked bar to donut chart using suggestions', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.dest', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.createLayer(); + + await PageObjects.lens.configureDimension( + { + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }, + 1 + ); + + await PageObjects.lens.configureDimension( + { + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }, + 1 + ); + await PageObjects.lens.save('twolayerchart'); + await testSubjects.click('lnsSuggestion-asDonut > lnsSuggestion'); + + expect(await PageObjects.lens.getLayerCount()).to.eql(1); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sliceByDimensionPanel')).to.eql( + 'Top values of geo.dest' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql( + 'Average of bytes' + ); + }); + it('should allow transition from line chart to donut chart and to bar chart', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index beb43c557a0e8..a62bfdcde0572 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -12,8 +12,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - // failing test due to backend issue, see #75095 - describe.skip('classification creation', function () { + describe('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 4284ad20b951f..e8f0a69b397cd 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // failing test due to backend issue, see #75095 - describe.skip('jobs cloning supported by UI form', function () { + describe('jobs cloning supported by UI form', function () { const testDataList: Array<{ suiteTitle: string; archive: string; diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 3cbfad6b33c5f..a67a348323347 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -12,8 +12,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - // failing test due to backend issue, see #75095 - describe.skip('regression creation', function () { + describe('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index bed0e3a159e23..acba3fa472b1a 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -89,10 +89,14 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param opts.dimension - the selector of the dimension being changed * @param opts.operation - the desired operation ID for the dimension * @param opts.field - the desired field for the dimension + * @param layerIndex - the index of the layer */ - async configureDimension(opts: { dimension: string; operation: string; field: string }) { + async configureDimension( + opts: { dimension: string; operation: string; field: string }, + layerIndex = 0 + ) { await retry.try(async () => { - await testSubjects.click(opts.dimension); + await testSubjects.click(`lns-layerPanel-${layerIndex} > ${opts.dimension}`); await testSubjects.exists(`lns-indexPatternDimension-${opts.operation}`); }); diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts index 726115958d027..4b5b372c92641 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts @@ -40,7 +40,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(results.length).to.be(1); expect(results[0].type).to.be('index-pattern'); expect(results[0].title).to.be('logstash-*'); - expect(results[0].score).to.be.greaterThan(1); + expect(results[0].score).to.be.greaterThan(0.9); }); it('can search for visualizations', async () => { @@ -70,5 +70,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(results.map((r) => r.title)).to.contain('dashboard with map'); expect(results.map((r) => r.title)).to.contain('Amazing Dashboard'); }); + + it('can search by prefix', async () => { + const results = await findResultsWithAPI('Amaz'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('dashboard'); + expect(results[0].title).to.be('Amazing Dashboard'); + }); }); }