From a20ac0d710f35e2db15cce6e1cc8df9536573557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 21 Jan 2020 14:27:51 +0530 Subject: [PATCH 01/19] [Mappings editor] Add missing max_shingle_size parameter to search_as_you_type (#55161) --- .../document_fields/field_parameters/index.ts | 2 + .../max_shingle_size_parameter.tsx | 45 +++++++++++++++++++ .../fields/field_types/search_as_you_type.tsx | 8 +++- .../constants/parameters_definition.tsx | 11 +++++ .../lib/mappings_validator.test.ts | 2 + .../app/components/mappings_editor/types.ts | 3 +- 6 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts index 9622466ad795cf..b248776c884f12 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -51,3 +51,5 @@ export * from './fielddata_parameter'; export * from './split_queries_on_whitespace_parameter'; export * from './locale_parameter'; + +export * from './max_shingle_size_parameter'; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx new file mode 100644 index 00000000000000..bc1917b2da966b --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { getFieldConfig } from '../../../lib'; +import { EditFieldFormRow } from '../fields/edit_field'; +import { UseField, Field } from '../../../shared_imports'; + +interface Props { + defaultToggleValue: boolean; +} + +export const MaxShingleSizeParameter = ({ defaultToggleValue }: Props) => ( + + + +); diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx index 83541ec982ee68..dafbebd24b3fa7 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx @@ -14,6 +14,7 @@ import { NormsParameter, SimilarityParameter, TermVectorParameter, + MaxShingleSizeParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; @@ -24,7 +25,8 @@ interface Props { const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'similarity': - case 'term_vector': { + case 'term_vector': + case 'max_shingle_size': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } case 'analyzers': { @@ -47,6 +49,10 @@ export const SearchAsYouType = React.memo(({ field }: Props) => { + + { enable_position_increments: [], depth_limit: true, dims: false, + max_shingle_size: 'string_not_allowed', }, // All the parameters in "goodField" have the correct format // and should still be there after the validation ran. @@ -307,6 +308,7 @@ describe('Properties validator', () => { enable_position_increments: true, depth_limit: 20, dims: 'abc', + max_shingle_size: 2, }, }; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts index 0fce3422344bc2..b7bf4e6b112d37 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts @@ -119,7 +119,8 @@ export type ParameterName = | 'points_only' | 'path' | 'dims' - | 'depth_limit'; + | 'depth_limit' + | 'max_shingle_size'; export interface Parameter { fieldConfig: FieldConfig; From 4ca2fbdb114a2e43db740eb8192857aa0981ca83 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 21 Jan 2020 11:03:55 +0200 Subject: [PATCH 02/19] [BUG] Data fetching twice on discover timefilter change (#55279) * Fix bug #54887 - Filters are not only fetch once on timefilter change - Make sure that discover doesn't fetch data when a disabled filter is changed - Support compareFilters on an array of filters. - Added tests to compare filters - Exctracted sortFilters and added tests to it. * code review + FilterCompareOptions * Remove sort by Co-authored-by: Elastic Machine --- .../filter_manager/filter_state_manager.ts | 19 ++- .../discover/np_ready/angular/discover.js | 2 +- .../query/filter_manager/filter_manager.ts | 18 +-- .../lib/compare_filters.test.ts | 131 +++++++++++++++++- .../filter_manager/lib/compare_filters.ts | 78 ++++++++--- .../query/filter_manager/lib/dedup_filters.ts | 4 +- .../query/filter_manager/lib/only_disabled.ts | 5 +- .../filter_manager/lib/sort_filters.test.ts | 91 ++++++++++++ .../query/filter_manager/lib/sort_filters.ts | 42 ++++++ 9 files changed, 351 insertions(+), 39 deletions(-) create mode 100644 src/plugins/data/public/query/filter_manager/lib/sort_filters.test.ts create mode 100644 src/plugins/data/public/query/filter_manager/lib/sort_filters.ts diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts index 61821b7ad45e94..633b7e630700d2 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts @@ -21,6 +21,13 @@ import _ from 'lodash'; import { State } from 'ui/state_management/state'; import { FilterManager, esFilters } from '../../../../../../plugins/data/public'; +import { + compareFilters, + COMPARE_ALL_OPTIONS, + // this whole file will soon be deprecated by new state management. + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/data/public/query/filter_manager/lib/compare_filters'; + type GetAppStateFunc = () => State | undefined | null; /** @@ -63,8 +70,16 @@ export class FilterStateManager { const globalFilters = this.globalState.filters || []; const appFilters = (appState && appState.filters) || []; - const globalFilterChanged = !_.isEqual(this.filterManager.getGlobalFilters(), globalFilters); - const appFilterChanged = !_.isEqual(this.filterManager.getAppFilters(), appFilters); + const globalFilterChanged = !compareFilters( + this.filterManager.getGlobalFilters(), + globalFilters, + COMPARE_ALL_OPTIONS + ); + const appFilterChanged = !compareFilters( + this.filterManager.getAppFilters(), + appFilters, + COMPARE_ALL_OPTIONS + ); const filterStateChanged = globalFilterChanged || appFilterChanged; if (!filterStateChanged) return; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 2927565e61dce9..2bf554860f7434 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -637,7 +637,7 @@ function discoverController( // fetch data when filters fire fetch event subscriptions.add( - subscribeWithScope($scope, filterManager.getUpdates$(), { + subscribeWithScope($scope, filterManager.getFetches$(), { next: $scope.fetch, }) ); diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index c25e5df2b168a2..f7d0dddd5bf039 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -22,7 +22,8 @@ import { Subject } from 'rxjs'; import { IUiSettingsClient } from 'src/core/public'; -import { compareFilters } from './lib/compare_filters'; +import { compareFilters, COMPARE_ALL_OPTIONS } from './lib/compare_filters'; +import { sortFilters } from './lib/sort_filters'; import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; import { uniqFilters } from './lib/uniq_filters'; import { onlyDisabledFiltersChanged } from './lib/only_disabled'; @@ -76,20 +77,9 @@ export class FilterManager { } private handleStateUpdate(newFilters: esFilters.Filter[]) { - // global filters should always be first - - newFilters.sort(({ $state: a }: esFilters.Filter, { $state: b }: esFilters.Filter): number => { - if (a!.store === b!.store) { - return 0; - } else { - return a!.store === esFilters.FilterStateStore.GLOBAL_STATE && - b!.store !== esFilters.FilterStateStore.GLOBAL_STATE - ? -1 - : 1; - } - }); + newFilters.sort(sortFilters); - const filtersUpdated = !_.isEqual(this.filters, newFilters); + const filtersUpdated = !compareFilters(this.filters, newFilters, COMPARE_ALL_OPTIONS); const updatedOnlyDisabledFilters = onlyDisabledFiltersChanged(newFilters, this.filters); this.filters = newFilters; diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts index 34fd662c4ba460..9cc5938750c4ed 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { compareFilters } from './compare_filters'; +import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; import { esFilters } from '../../../../common'; describe('filter manager utilities', () => { @@ -83,5 +83,134 @@ describe('filter manager utilities', () => { expect(compareFilters(f1, f2)).toBeTruthy(); }); + + test('should compare filters array to non array', () => { + const f1 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + + const f2 = esFilters.buildQueryFilter( + { _type: { match: { query: 'mochi', type: 'phrase' } } }, + 'index', + '' + ); + + expect(compareFilters([f1, f2], f1)).toBeFalsy(); + }); + + test('should compare filters array to partial array', () => { + const f1 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + + const f2 = esFilters.buildQueryFilter( + { _type: { match: { query: 'mochi', type: 'phrase' } } }, + 'index', + '' + ); + + expect(compareFilters([f1, f2], [f1])).toBeFalsy(); + }); + + test('should compare filters array to exact array', () => { + const f1 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + + const f2 = esFilters.buildQueryFilter( + { _type: { match: { query: 'mochi', type: 'phrase' } } }, + 'index', + '' + ); + + expect(compareFilters([f1, f2], [f1, f2])).toBeTruthy(); + }); + + test('should compare array of duplicates, ignoring meta attributes', () => { + const f1 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index1', + '' + ); + const f2 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index2', + '' + ); + + expect(compareFilters([f1], [f2])).toBeTruthy(); + }); + + test('should compare array of duplicates, ignoring $state attributes', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + expect(compareFilters([f1], [f2])).toBeTruthy(); + }); + + test('should compare duplicates with COMPARE_ALL_OPTIONS should check store', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeFalsy(); + }); + + test('should compare duplicates with COMPARE_ALL_OPTIONS should not check key and value ', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + f2.meta.key = 'wassup'; + f2.meta.value = 'dog'; + + expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeTruthy(); + }); }); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts index 9b171ab0aacb2a..218b9d492b61f6 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts @@ -17,32 +17,64 @@ * under the License. */ -import { defaults, isEqual, omit } from 'lodash'; +import { defaults, isEqual, omit, map } from 'lodash'; import { esFilters } from '../../../../common'; +export interface FilterCompareOptions { + disabled?: boolean; + negate?: boolean; + state?: boolean; +} + +/** + * Include disabled, negate and store when comparing filters + */ +export const COMPARE_ALL_OPTIONS: FilterCompareOptions = { + disabled: true, + negate: true, + state: true, +}; + +const mapFilter = ( + filter: esFilters.Filter, + comparators: FilterCompareOptions, + excludedAttributes: string[] +) => { + const cleaned: esFilters.FilterMeta = omit(filter, excludedAttributes); + + if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); + if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); + + return cleaned; +}; + +const mapFilterArray = ( + filters: esFilters.Filter[], + comparators: FilterCompareOptions, + excludedAttributes: string[] +) => { + return map(filters, (filter: esFilters.Filter) => + mapFilter(filter, comparators, excludedAttributes) + ); +}; + /** - * Compare two filters to see if they match + * Compare two filters or filter arrays to see if they match. + * For filter arrays, the assumption is they are sorted. * - * @param {object} first The first filter to compare - * @param {object} second The second filter to compare - * @param {object} comparatorOptions Parameters to use for comparison + * @param {esFilters.Filter | esFilters.Filter[]} first The first filter or filter array to compare + * @param {esFilters.Filter | esFilters.Filter[]} second The second filter or filter array to compare + * @param {FilterCompareOptions} comparatorOptions Parameters to use for comparison * * @returns {bool} Filters are the same */ export const compareFilters = ( - first: esFilters.Filter, - second: esFilters.Filter, - comparatorOptions: any = {} + first: esFilters.Filter | esFilters.Filter[], + second: esFilters.Filter | esFilters.Filter[], + comparatorOptions: FilterCompareOptions = {} ) => { - let comparators: any = {}; - const mapFilter = (filter: esFilters.Filter) => { - const cleaned: esFilters.FilterMeta = omit(filter, excludedAttributes); - - if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); - if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); + let comparators: FilterCompareOptions = {}; - return cleaned; - }; const excludedAttributes: string[] = ['$$hashKey', 'meta']; comparators = defaults(comparatorOptions || {}, { @@ -53,5 +85,17 @@ export const compareFilters = ( if (!comparators.state) excludedAttributes.push('$state'); - return isEqual(mapFilter(first), mapFilter(second)); + if (Array.isArray(first) && Array.isArray(second)) { + return isEqual( + mapFilterArray(first, comparators, excludedAttributes), + mapFilterArray(second, comparators, excludedAttributes) + ); + } else if (!Array.isArray(first) && !Array.isArray(second)) { + return isEqual( + mapFilter(first, comparators, excludedAttributes), + mapFilter(second, comparators, excludedAttributes) + ); + } else { + return false; + } }; diff --git a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts b/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts index 6dae14f480b4f1..897a645e87b5a7 100644 --- a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts @@ -18,7 +18,7 @@ */ import { filter, find } from 'lodash'; -import { compareFilters } from './compare_filters'; +import { compareFilters, FilterCompareOptions } from './compare_filters'; import { esFilters } from '../../../../common'; /** @@ -33,7 +33,7 @@ import { esFilters } from '../../../../common'; export const dedupFilters = ( existingFilters: esFilters.Filter[], filters: esFilters.Filter[], - comparatorOptions: any = {} + comparatorOptions: FilterCompareOptions = {} ) => { if (!Array.isArray(filters)) { filters = [filters]; diff --git a/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts b/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts index c040d2f2960c75..3f35c94a3f858f 100644 --- a/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts +++ b/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts @@ -17,8 +17,9 @@ * under the License. */ -import { filter, isEqual } from 'lodash'; +import { filter } from 'lodash'; import { esFilters } from '../../../../common'; +import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; const isEnabled = (f: esFilters.Filter) => f && f.meta && !f.meta.disabled; @@ -35,5 +36,5 @@ export const onlyDisabledFiltersChanged = ( const newEnabledFilters = filter(newFilters || [], isEnabled); const oldEnabledFilters = filter(oldFilters || [], isEnabled); - return isEqual(oldEnabledFilters, newEnabledFilters); + return compareFilters(oldEnabledFilters, newEnabledFilters, COMPARE_ALL_OPTIONS); }; diff --git a/src/plugins/data/public/query/filter_manager/lib/sort_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/sort_filters.test.ts new file mode 100644 index 00000000000000..949c57e43ce743 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/sort_filters.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { sortFilters } from './sort_filters'; +import { esFilters } from '../../../../common'; + +describe('sortFilters', () => { + describe('sortFilters()', () => { + test('Not sort two application level filters', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + const filters = [f1, f2].sort(sortFilters); + expect(filters[0]).toBe(f1); + }); + + test('Not sort two global level filters', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + const filters = [f1, f2].sort(sortFilters); + expect(filters[0]).toBe(f1); + }); + + test('Move global level filter to the beginning of the array', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + const filters = [f1, f2].sort(sortFilters); + expect(filters[0]).toBe(f2); + }); + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/sort_filters.ts b/src/plugins/data/public/query/filter_manager/lib/sort_filters.ts new file mode 100644 index 00000000000000..657c80fe0ccb23 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/sort_filters.ts @@ -0,0 +1,42 @@ +/* + * 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 { esFilters } from '../../../../common'; + +/** + * Sort filters according to their store - global filters go first + * + * @param {object} first The first filter to compare + * @param {object} second The second filter to compare + * + * @returns {number} Sorting order of filters + */ +export const sortFilters = ( + { $state: a }: esFilters.Filter, + { $state: b }: esFilters.Filter +): number => { + if (a!.store === b!.store) { + return 0; + } else { + return a!.store === esFilters.FilterStateStore.GLOBAL_STATE && + b!.store !== esFilters.FilterStateStore.GLOBAL_STATE + ? -1 + : 1; + } +}; From 4971a2c772317ed8092ca8f35e539fe71b828b3b Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 21 Jan 2020 12:58:40 +0100 Subject: [PATCH 03/19] Expose fatalErrors API from the Start contract (#55300) * Expose FatalErrors from the Start contract. This API is intended to be used for runtime as well. * update docs * update data plugin snapshot to fix tests * address comments Co-authored-by: Elastic Machine --- ...ana-plugin-public.chromenavlinks.update.md | 60 +++++++++---------- ...ana-plugin-public.corestart.fatalerrors.md | 13 ++++ .../public/kibana-plugin-public.corestart.md | 1 + .../kibana-plugin-public.fatalerrorsstart.md | 13 ++++ .../core/public/kibana-plugin-public.md | 1 + src/core/public/core_system.ts | 2 + .../fatal_errors/fatal_errors_service.mock.ts | 4 ++ .../fatal_errors/fatal_errors_service.tsx | 23 ++++++- src/core/public/fatal_errors/index.ts | 2 +- src/core/public/index.ts | 5 +- src/core/public/legacy/legacy_service.test.ts | 2 + src/core/public/mocks.ts | 1 + src/core/public/plugins/plugin_context.ts | 1 + .../public/plugins/plugins_service.test.ts | 1 + src/core/public/public.api.md | 5 ++ .../query_string_input.test.tsx.snap | 24 ++++++++ 16 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.corestart.fatalerrors.md create mode 100644 docs/development/core/public/kibana-plugin-public.fatalerrorsstart.md diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlinks.update.md b/docs/development/core/public/kibana-plugin-public.chromenavlinks.update.md index d1cd2d3b049501..155d149f334a17 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlinks.update.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlinks.update.md @@ -1,30 +1,30 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) > [update](./kibana-plugin-public.chromenavlinks.update.md) - -## ChromeNavLinks.update() method - -> Warning: This API is now obsolete. -> -> Uses the [AppBase.updater$](./kibana-plugin-public.appbase.updater_.md) property when registering your application with [ApplicationSetup.register()](./kibana-plugin-public.applicationsetup.register.md) instead. -> - -Update the navlink for the given id with the updated attributes. Returns the updated navlink or `undefined` if it does not exist. - -Signature: - -```typescript -update(id: string, values: ChromeNavLinkUpdateableFields): ChromeNavLink | undefined; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| id | string | | -| values | ChromeNavLinkUpdateableFields | | - -Returns: - -`ChromeNavLink | undefined` - + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) > [update](./kibana-plugin-public.chromenavlinks.update.md) + +## ChromeNavLinks.update() method + +> Warning: This API is now obsolete. +> +> Uses the [AppBase.updater$](./kibana-plugin-public.appbase.updater_.md) property when registering your application with [ApplicationSetup.register()](./kibana-plugin-public.applicationsetup.register.md) instead. +> + +Update the navlink for the given id with the updated attributes. Returns the updated navlink or `undefined` if it does not exist. + +Signature: + +```typescript +update(id: string, values: ChromeNavLinkUpdateableFields): ChromeNavLink | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| id | string | | +| values | ChromeNavLinkUpdateableFields | | + +Returns: + +`ChromeNavLink | undefined` + diff --git a/docs/development/core/public/kibana-plugin-public.corestart.fatalerrors.md b/docs/development/core/public/kibana-plugin-public.corestart.fatalerrors.md new file mode 100644 index 00000000000000..540b17b5a6f0b3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.corestart.fatalerrors.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [fatalErrors](./kibana-plugin-public.corestart.fatalerrors.md) + +## CoreStart.fatalErrors property + +[FatalErrorsStart](./kibana-plugin-public.fatalerrorsstart.md) + +Signature: + +```typescript +fatalErrors: FatalErrorsStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index e561ee313f1000..83af82d590c364 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -19,6 +19,7 @@ export interface CoreStart | [application](./kibana-plugin-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | [chrome](./kibana-plugin-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-public.chromestart.md) | | [docLinks](./kibana-plugin-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | +| [fatalErrors](./kibana-plugin-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-public.fatalerrorsstart.md) | | [http](./kibana-plugin-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-public.httpstart.md) | | [i18n](./kibana-plugin-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-public.i18nstart.md) | | [injectedMetadata](./kibana-plugin-public.corestart.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Use the legacy platform API instead. | diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorsstart.md b/docs/development/core/public/kibana-plugin-public.fatalerrorsstart.md new file mode 100644 index 00000000000000..a8ece7dcb7e02d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorsstart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorsStart](./kibana-plugin-public.fatalerrorsstart.md) + +## FatalErrorsStart type + +FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. + +Signature: + +```typescript +export declare type FatalErrorsStart = FatalErrorsSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 27ca9f2d9fd577..27037d46926c18 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -129,6 +129,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | | | [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | +| [FatalErrorsStart](./kibana-plugin-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 5b31c740518e4a..97bd49416f7052 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -214,6 +214,7 @@ export class CoreSystem { const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup! }); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); + const fatalErrors = await this.fatalErrors.start(); await this.integrations.start({ uiSettings }); const coreUiTargetDomElement = document.createElement('div'); @@ -271,6 +272,7 @@ export class CoreSystem { notifications, overlays, uiSettings, + fatalErrors, }; const plugins = await this.plugins.start(core); diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index dd7702a7ee7dd9..6d9876f787fa97 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -26,18 +26,22 @@ const createSetupContractMock = () => { return setupContract; }; +const createStartContractMock = createSetupContractMock; type FatalErrorsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), + start: jest.fn(), }; mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.start.mockReturnValue(createStartContractMock()); return mocked; }; export const fatalErrorsServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/fatal_errors/fatal_errors_service.tsx b/src/core/public/fatal_errors/fatal_errors_service.tsx index 5c6a7bb322ae1f..309f07859ef264 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.tsx +++ b/src/core/public/fatal_errors/fatal_errors_service.tsx @@ -54,9 +54,18 @@ export interface FatalErrorsSetup { get$: () => Rx.Observable; } +/** + * FatalErrors stop the Kibana Public Core and displays a fatal error screen + * with details about the Kibana build and the error. + * + * @public + */ +export type FatalErrorsStart = FatalErrorsSetup; + /** @interal */ export class FatalErrorsService { private readonly errorInfo$ = new Rx.ReplaySubject(); + private fatalErrors?: FatalErrorsSetup; /** * @@ -82,7 +91,7 @@ export class FatalErrorsService { }, }); - const fatalErrorsSetup: FatalErrorsSetup = { + this.fatalErrors = { add: (error, source?) => { const errorInfo = getErrorInfo(error, source); @@ -101,9 +110,17 @@ export class FatalErrorsService { }, }; - this.setupGlobalErrorHandlers(fatalErrorsSetup); + this.setupGlobalErrorHandlers(this.fatalErrors!); - return fatalErrorsSetup; + return this.fatalErrors!; + } + + public start() { + const { fatalErrors } = this; + if (!fatalErrors) { + throw new Error('FatalErrorsService#setup() must be invoked before start.'); + } + return fatalErrors; } private renderError(injectedMetadata: InjectedMetadataSetup, i18n: I18nStart) { diff --git a/src/core/public/fatal_errors/index.ts b/src/core/public/fatal_errors/index.ts index e37a36152cf91b..c8ea1c0bccd227 100644 --- a/src/core/public/fatal_errors/index.ts +++ b/src/core/public/fatal_errors/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { FatalErrorsSetup, FatalErrorsService } from './fatal_errors_service'; +export { FatalErrorsSetup, FatalErrorsStart, FatalErrorsService } from './fatal_errors_service'; export { FatalErrorInfo } from './get_error_info'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 5b17eccc37f8b8..5e732dd05e6168 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -55,7 +55,7 @@ import { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, } from './chrome'; -import { FatalErrorsSetup, FatalErrorInfo } from './fatal_errors'; +import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors'; import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; import { InjectedMetadataSetup, InjectedMetadataStart, LegacyNavLink } from './injected_metadata'; @@ -232,6 +232,8 @@ export interface CoreStart { overlays: OverlayStart; /** {@link IUiSettingsClient} */ uiSettings: IUiSettingsClient; + /** {@link FatalErrorsStart} */ + fatalErrors: FatalErrorsStart; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values @@ -302,6 +304,7 @@ export { DocLinksStart, FatalErrorInfo, FatalErrorsSetup, + FatalErrorsStart, HttpSetup, HttpStart, I18nStart, diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 9dd24f9e4a7a3c..d08c8b52e39c9b 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -98,6 +98,7 @@ const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); const uiSettingsStart = uiSettingsServiceMock.createStartContract(); const savedObjectsStart = savedObjectsMock.createStartContract(); +const fatalErrorsStart = fatalErrorsServiceMock.createStartContract(); const mockStorage = { getItem: jest.fn() } as any; const defaultStartDeps = { @@ -112,6 +113,7 @@ const defaultStartDeps = { overlays: overlayStart, uiSettings: uiSettingsStart, savedObjects: savedObjectsStart, + fatalErrors: fatalErrorsStart, }, lastSubUrlStorage: mockStorage, targetDomElement: document.createElement('div'), diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 43c8aa6f1d6b96..ce90d49065ad4e 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -74,6 +74,7 @@ function createCoreStartMock({ basePath = '' } = {}) { injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar, }, + fatalErrors: fatalErrorsServiceMock.createStartContract(), }; return mock; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index f146c2452868b4..48100cba4f26e0 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -151,5 +151,6 @@ export function createPluginStartContext< injectedMetadata: { getInjectedVar: deps.injectedMetadata.getInjectedVar, }, + fatalErrors: deps.fatalErrors, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index cafc7e5887e385..dbbcda8d60e128 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -111,6 +111,7 @@ describe('PluginsService', () => { overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsMock.createStartContract(), + fatalErrors: fatalErrorsServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index abd39e864bd302..f7b260c68ee96b 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -377,6 +377,8 @@ export interface CoreStart { // (undocumented) docLinks: DocLinksStart; // (undocumented) + fatalErrors: FatalErrorsStart; + // (undocumented) http: HttpStart; // (undocumented) i18n: I18nStart; @@ -532,6 +534,9 @@ export interface FatalErrorsSetup { get$: () => Rx.Observable; } +// @public +export type FatalErrorsStart = FatalErrorsSetup; + // @public export type HandlerContextType> = T extends HandlerFunction ? U : never; diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index bc08c87304fcab..d8db53d4c6020b 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -304,6 +304,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -930,6 +934,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -1538,6 +1546,10 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -2161,6 +2173,10 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -2769,6 +2785,10 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -3392,6 +3412,10 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { From 27c8a4bc25badb3ee3c1feaa7e1e68b10bab1ccd Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 21 Jan 2020 13:04:49 +0100 Subject: [PATCH 04/19] [State Management] remove AppState from Dashboard app (#54105) Removes AppState from dashboard app and replaces it with state containers and state syncing utilities. --- .../filter_manager/filter_state_manager.ts | 14 +- .../public/dashboard/__tests__/index.ts | 1 - .../kibana/public/dashboard/legacy_imports.ts | 6 - .../public/dashboard/np_ready/application.ts | 7 - .../dashboard/np_ready/dashboard_app.tsx | 16 +- .../np_ready/dashboard_app_controller.tsx | 110 ++++++--- .../np_ready/dashboard_state.test.ts | 13 +- .../np_ready/dashboard_state_manager.ts | 233 ++++++++++++------ .../public/dashboard/np_ready/legacy_app.js | 12 +- .../np_ready/lib/migrate_app_state.test.ts | 35 +-- .../np_ready/lib/migrate_app_state.ts | 15 +- .../dashboard/np_ready/lib/save_dashboard.ts | 11 +- .../np_ready/lib/update_saved_dashboard.ts | 4 +- .../kibana/public/dashboard/np_ready/types.ts | 43 ++-- src/legacy/ui/public/chrome/api/controls.ts | 2 + .../ui/public/chrome/directives/kbn_chrome.js | 10 + .../ui/public/state_management/state.js | 2 +- .../kibana_utils/public/history/index.ts | 20 ++ .../public/history/remove_query_param.test.ts | 75 ++++++ .../public/history/remove_query_param.ts} | 43 ++-- src/plugins/kibana_utils/public/index.ts | 2 + .../url/kbn_url_storage.test.ts | 54 +++- .../state_management/url/kbn_url_storage.ts | 41 ++- .../utils/diff_object.test.ts | 0 .../state_management/utils/diff_object.ts | 0 .../public/state_sync/state_sync.test.ts | 36 +++ .../public/state_sync/state_sync.ts | 14 +- .../create_kbn_url_state_storage.test.ts | 17 +- .../create_kbn_url_state_storage.ts | 17 +- .../apps/dashboard/dashboard_clone.js | 2 +- 30 files changed, 573 insertions(+), 282 deletions(-) create mode 100644 src/plugins/kibana_utils/public/history/index.ts create mode 100644 src/plugins/kibana_utils/public/history/remove_query_param.test.ts rename src/{legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts => plugins/kibana_utils/public/history/remove_query_param.ts} (53%) rename src/{legacy/ui => plugins/kibana_utils}/public/state_management/utils/diff_object.test.ts (100%) rename src/{legacy/ui => plugins/kibana_utils}/public/state_management/utils/diff_object.ts (100%) diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts index 633b7e630700d2..ad5c91d2e19de1 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { Subscription } from 'rxjs'; import { State } from 'ui/state_management/state'; import { FilterManager, esFilters } from '../../../../../../plugins/data/public'; @@ -28,7 +29,7 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../plugins/data/public/query/filter_manager/lib/compare_filters'; -type GetAppStateFunc = () => State | undefined | null; +type GetAppStateFunc = () => { filters?: esFilters.Filter[]; save?: () => void } | undefined | null; /** * FilterStateManager is responsible for watching for filter changes @@ -36,10 +37,12 @@ type GetAppStateFunc = () => State | undefined | null; * back to the URL. **/ export class FilterStateManager { + private filterManagerUpdatesSubscription: Subscription; + filterManager: FilterManager; globalState: State; getAppState: GetAppStateFunc; - interval: NodeJS.Timeout | undefined; + interval: number | undefined; constructor(globalState: State, getAppState: GetAppStateFunc, filterManager: FilterManager) { this.getAppState = getAppState; @@ -48,7 +51,7 @@ export class FilterStateManager { this.watchFilterState(); - this.filterManager.getUpdates$().subscribe(() => { + this.filterManagerUpdatesSubscription = this.filterManager.getUpdates$().subscribe(() => { this.updateAppState(); }); } @@ -57,12 +60,13 @@ export class FilterStateManager { if (this.interval) { clearInterval(this.interval); } + this.filterManagerUpdatesSubscription.unsubscribe(); } private watchFilterState() { // This is a temporary solution to remove rootscope. // Moving forward, state should provide observable subscriptions. - this.interval = setInterval(() => { + this.interval = window.setInterval(() => { const appState = this.getAppState(); const stateUndefined = !appState || !this.globalState; if (stateUndefined) return; @@ -95,7 +99,7 @@ export class FilterStateManager { private saveState() { const appState = this.getAppState(); - if (appState) appState.save(); + if (appState && appState.save) appState.save(); this.globalState.save(); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts index ab8dfe81163e48..2b992f95695f38 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts @@ -17,6 +17,5 @@ * under the License. */ -export { getAppStateMock } from './get_app_state_mock'; export { getSavedDashboardMock } from './get_saved_dashboard_mock'; export { getEmbeddableFactoryMock } from './get_embeddable_factories_mock'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index b44d1993db23a4..244a58e8a65e5f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -28,8 +28,6 @@ import chrome from 'ui/chrome'; export const legacyChrome = chrome; export { State } from 'ui/state_management/state'; -export { AppState } from 'ui/state_management/app_state'; -export { AppStateClass } from 'ui/state_management/app_state'; export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { IPrivate } from 'ui/private'; @@ -45,8 +43,6 @@ export { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; // @ts-ignore -export { AppStateProvider } from 'ui/state_management/app_state'; -// @ts-ignore export { PrivateProvider } from 'ui/private/private'; // @ts-ignore export { EventsProvider } from 'ui/events'; @@ -60,9 +56,7 @@ export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; // @ts-ignore export { confirmModalFactory } from 'ui/modals/confirm_modal'; export { configureAppAngularModule } from 'ui/legacy_compat'; -export { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory'; export { ensureDefaultIndexPattern } from 'ui/legacy_compat'; -export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { IInjector } from 'ui/chrome'; export { SavedObjectLoader } from 'ui/saved_objects'; export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/embeddable'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 7f7bf7cf47bdaf..429a7f7279996f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -31,7 +31,6 @@ import { Storage } from '../../../../../../plugins/kibana_utils/public'; import { GlobalStateProvider, StateManagementConfigProvider, - AppStateProvider, PrivateProvider, EventsProvider, PersistedState, @@ -155,12 +154,6 @@ function createLocalStateModule() { 'app/dashboard/Promise', 'app/dashboard/PersistedState', ]) - .factory('AppState', function(Private: any) { - return Private(AppStateProvider); - }) - .service('getAppState', function(Private: any) { - return Private(AppStateProvider).getAppState; - }) .service('globalState', function(Private: any) { return Private(GlobalStateProvider); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index e9fdc335ba5729..f56990ae82e560 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -20,12 +20,7 @@ import moment from 'moment'; import { Subscription } from 'rxjs'; -import { - AppStateClass as TAppStateClass, - AppState as TAppState, - IInjector, - KbnUrl, -} from '../legacy_imports'; +import { IInjector } from '../legacy_imports'; import { ViewMode } from '../../../../embeddable_api/public/np_ready/public'; import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; @@ -43,7 +38,7 @@ import { RenderDeps } from './application'; export interface DashboardAppScope extends ng.IScope { dash: SavedObjectDashboard; - appState: TAppState; + appState: DashboardAppState; screenTitle: string; model: { query: Query; @@ -60,7 +55,6 @@ export interface DashboardAppScope extends ng.IScope { refreshInterval: any; panels: SavedDashboardPanel[]; indexPatterns: IIndexPattern[]; - $evalAsync: any; dashboardViewMode: ViewMode; expandedPanel?: string; getShouldShowEditHelp: () => boolean; @@ -91,8 +85,6 @@ export interface DashboardAppScope extends ng.IScope { export function initDashboardAppDirective(app: any, deps: RenderDeps) { app.directive('dashboardApp', function($injector: IInjector) { - const AppState = $injector.get>('AppState'); - const kbnUrl = $injector.get('kbnUrl'); const confirmModal = $injector.get('confirmModal'); const config = deps.uiSettings; @@ -105,17 +97,13 @@ export function initDashboardAppDirective(app: any, deps: RenderDeps) { $routeParams: { id?: string; }, - getAppState: any, globalState: any ) => new DashboardAppController({ $route, $scope, $routeParams, - getAppState, globalState, - kbnUrl, - AppStateClass: AppState, config, confirmModal, indexPatterns: deps.npDataStart.indexPatterns, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 2706b588a2ec4f..7da32ac97126f4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -17,57 +17,55 @@ * under the License. */ -import _ from 'lodash'; +import _, { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; import angular from 'angular'; -import { uniq } from 'lodash'; import { Subscription } from 'rxjs'; +import { createHashHistory } from 'history'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; import { - subscribeWithScope, ConfirmationButtonTypes, - showSaveModal, - SaveResult, migrateLegacyQuery, - State, - AppStateClass as TAppStateClass, - KbnUrl, SavedObjectSaveOpts, - unhashUrl, + SaveResult, + showSaveModal, + State, + subscribeWithScope, } from '../legacy_imports'; import { FilterStateManager } from '../../../../data/public'; import { + esFilters, IndexPattern, + IndexPatternsContract, Query, SavedQuery, - IndexPatternsContract, } from '../../../../../../plugins/data/public'; import { - DashboardContainer, DASHBOARD_CONTAINER_TYPE, + DashboardContainer, DashboardContainerFactory, DashboardContainerInput, DashboardPanelState, } from '../../../../dashboard_embeddable_container/public/np_ready/public'; import { - isErrorEmbeddable, + EmbeddableFactoryNotFoundError, ErrorEmbeddable, - ViewMode, + isErrorEmbeddable, openAddPanelFlyout, - EmbeddableFactoryNotFoundError, + ViewMode, } from '../../../../embeddable_api/public/np_ready/public'; -import { DashboardAppState, NavAction, ConfirmModalFn, SavedDashboardPanel } from './types'; +import { ConfirmModalFn, NavAction, SavedDashboardPanel } from './types'; import { showOptionsPopover } from './top_nav/show_options_popover'; import { DashboardSaveModal } from './top_nav/save_modal'; import { showCloneModal } from './top_nav/show_clone_modal'; import { saveDashboard } from './lib'; import { DashboardStateManager } from './dashboard_state_manager'; -import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; +import { createDashboardEditUrl, DashboardConstants } from './dashboard_constants'; import { getTopNavConfig } from './top_nav/get_top_nav_config'; import { TopNavIds } from './top_nav/top_nav_ids'; import { getDashboardTitle } from './dashboard_strings'; @@ -78,17 +76,15 @@ import { SavedObjectFinderProps, SavedObjectFinderUi, } from '../../../../../../plugins/kibana_react/public'; +import { removeQueryParam, unhashUrl } from '../../../../../../plugins/kibana_utils/public'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; $route: any; $routeParams: any; - getAppState: any; globalState: State; indexPatterns: IndexPatternsContract; dashboardConfig: any; - kbnUrl: KbnUrl; - AppStateClass: TAppStateClass; config: any; confirmModal: ConfirmModalFn; } @@ -103,12 +99,9 @@ export class DashboardAppController { $scope, $route, $routeParams, - getAppState, globalState, dashboardConfig, localStorage, - kbnUrl, - AppStateClass, indexPatterns, config, confirmModal, @@ -124,7 +117,6 @@ export class DashboardAppController { }, core: { notifications, overlays, chrome, injectedMetadata, uiSettings, savedObjects, http }, }: DashboardAppControllerDependencies) { - new FilterStateManager(globalState, getAppState, filterManager); const queryFilter = filterManager; let lastReloadRequestTime = 0; @@ -134,14 +126,30 @@ export class DashboardAppController { chrome.docTitle.change(dash.title); } + const history = createHashHistory(); const dashboardStateManager = new DashboardStateManager({ savedDashboard: dash, - AppStateClass, + useHashedUrl: config.get('state:storeInSessionStorage'), hideWriteControls: dashboardConfig.getHideWriteControls(), kibanaVersion: injectedMetadata.getKibanaVersion(), + history, }); - $scope.appState = dashboardStateManager.getAppState(); + const filterStateManager = new FilterStateManager( + globalState, + () => { + // Temporary AppState replacement + return { + set filters(_filters: esFilters.Filter[]) { + dashboardStateManager.setFilters(_filters); + }, + get filters() { + return dashboardStateManager.appState.filters; + }, + }; + }, + filterManager + ); // The hash check is so we only update the time filter on dashboard open, not during // normal cross app navigation. @@ -316,8 +324,8 @@ export class DashboardAppController { dirty = true; } + dashboardStateManager.handleDashboardContainerChanges(container); $scope.$evalAsync(() => { - dashboardStateManager.handleDashboardContainerChanges(container); if (dirty) { updateState(); } @@ -337,8 +345,8 @@ export class DashboardAppController { const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]; const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID]; container.addSavedObjectEmbeddable(type, id); - kbnUrl.removeParam(DashboardConstants.ADD_EMBEDDABLE_TYPE); - kbnUrl.removeParam(DashboardConstants.ADD_EMBEDDABLE_ID); + removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE); + removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID); } } @@ -409,7 +417,9 @@ export class DashboardAppController { key ]; if (!_.isEqual(containerValue, appStateValue)) { - (differences as { [key: string]: unknown })[key] = appStateValue; + // cloneDeep hack is needed, as there are multiple place, where container's input mutated, + // but values from appStateValue are deeply frozen, as they can't be mutated directly + (differences as { [key: string]: unknown })[key] = _.cloneDeep(appStateValue); } }); @@ -560,11 +570,6 @@ export class DashboardAppController { ); function updateViewMode(newMode: ViewMode) { - $scope.topNavMenu = getTopNavConfig( - newMode, - navActions, - dashboardConfig.getHideWriteControls() - ); // eslint-disable-line no-use-before-define dashboardStateManager.switchViewMode(newMode); } @@ -580,17 +585,28 @@ export class DashboardAppController { function revertChangesAndExitEditMode() { dashboardStateManager.resetState(); - kbnUrl.change( - dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL - ); // This is only necessary for new dashboards, which will default to Edit mode. updateViewMode(ViewMode.VIEW); + // Angular's $location skips this update because of history updates from syncState which happen simultaneously + // when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it, + // the update is considered outdated and angular skips it + // so have to use implementation of dashboardStateManager.changeDashboardUrl, which workarounds those issues + dashboardStateManager.changeDashboardUrl( + dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL + ); + // We need to do a hard reset of the timepicker. appState will not reload like // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on // reload will cause it not to sync. if (dashboardStateManager.getIsTimeSavedWithDashboard()) { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); + // have to use $evalAsync here until '_g' is migrated from $location to state sync utility ('history') + // When state sync utility changes url, angular's $location is missing it's own updates which happen during the same digest cycle + // temporary solution is to delay $location updates to next digest cycle + // unfortunately, these causes 2 browser history entries, but this is temporary and will be fixed after migrating '_g' to state_sync utilities + $scope.$evalAsync(() => { + dashboardStateManager.syncTimefilterWithDashboard(timefilter); + }); } } @@ -642,7 +658,11 @@ export class DashboardAppController { }); if (dash.id !== $routeParams.id) { - kbnUrl.change(createDashboardEditUrl(dash.id)); + // Angular's $location skips this update because of history updates from syncState which happen simultaneously + // when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it, + // the update is considered outdated and angular skips it + // so have to use implementation of dashboardStateManager.changeDashboardUrl, which workarounds those issues + dashboardStateManager.changeDashboardUrl(createDashboardEditUrl(dash.id)); } else { chrome.docTitle.change(dash.lastSavedTitle); updateViewMode(ViewMode.VIEW); @@ -844,6 +864,15 @@ export class DashboardAppController { }); }); + dashboardStateManager.registerChangeListener(() => { + // view mode could have changed, so trigger top nav update + $scope.topNavMenu = getTopNavConfig( + dashboardStateManager.getViewMode(), + navActions, + dashboardConfig.getHideWriteControls() + ); + }); + $scope.$on('$destroy', () => { updateSubscription.unsubscribe(); visibleSubscription.unsubscribe(); @@ -859,6 +888,9 @@ export class DashboardAppController { if (dashboardContainer) { dashboardContainer.destroy(); } + if (filterStateManager) { + filterStateManager.destroy(); + } }); } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts index d9a93b2ceedc31..8806684aab13cf 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts @@ -18,16 +18,12 @@ */ import './np_core.test.mocks'; +import { createBrowserHistory } from 'history'; import { DashboardStateManager } from './dashboard_state_manager'; -import { getAppStateMock, getSavedDashboardMock } from '../__tests__'; -import { AppStateClass } from '../legacy_imports'; -import { DashboardAppState } from './types'; -import { TimeRange, TimefilterContract, InputTimeRange } from 'src/plugins/data/public'; +import { getSavedDashboardMock } from '../__tests__'; +import { InputTimeRange, TimefilterContract, TimeRange } from 'src/plugins/data/public'; import { ViewMode } from 'src/plugins/embeddable/public'; -jest.mock('ui/state_management/state', () => ({ - State: {}, -})); jest.mock('ui/agg_types', () => ({ aggTypes: { metrics: [], @@ -52,9 +48,10 @@ describe('DashboardState', function() { function initDashboardState() { dashboardState = new DashboardStateManager({ savedDashboard, - AppStateClass: getAppStateMock() as AppStateClass, + useHashedUrl: false, hideWriteControls: false, kibanaVersion: '7.0.0', + history: createBrowserHistory(), }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts index 6df18757da6f51..451e7c8ff96db0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts @@ -19,20 +19,16 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; - +import { History } from 'history'; +import { Subscription } from 'rxjs'; import { Moment } from 'moment'; import { DashboardContainer } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public'; import { ViewMode } from '../../../../../../plugins/embeddable/public'; +import { migrateLegacyQuery } from '../legacy_imports'; import { - stateMonitorFactory, - StateMonitor, - AppStateClass as TAppStateClass, - migrateLegacyQuery, -} from '../legacy_imports'; -import { - Query, esFilters, + Query, TimefilterContract as Timefilter, } from '../../../../../../plugins/data/public'; @@ -41,7 +37,20 @@ import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_o import { FilterUtils } from './lib/filter_utils'; import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; -import { SavedDashboardPanel, DashboardAppState, DashboardAppStateDefaults } from './types'; +import { + DashboardAppState, + DashboardAppStateDefaults, + DashboardAppStateTransitions, + SavedDashboardPanel, +} from './types'; +import { + createKbnUrlStateStorage, + createStateContainer, + IKbnUrlStateStorage, + ISyncStateRef, + ReduxLikeStateContainer, + syncState, +} from '../../../../../../plugins/kibana_utils/public'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the @@ -51,7 +60,6 @@ import { SavedDashboardPanel, DashboardAppState, DashboardAppStateDefaults } fro */ export class DashboardStateManager { public savedDashboard: SavedObjectDashboard; - public appState: DashboardAppState; public lastSavedDashboardFilters: { timeTo?: string | Moment; timeFrom?: string | Moment; @@ -63,38 +71,78 @@ export class DashboardStateManager { private kibanaVersion: string; public isDirty: boolean; private changeListeners: Array<(status: { dirty: boolean }) => void>; - private stateMonitor: StateMonitor; + + public get appState(): DashboardAppState { + return this.stateContainer.get(); + } + + private readonly stateContainer: ReduxLikeStateContainer< + DashboardAppState, + DashboardAppStateTransitions + >; + private readonly stateContainerChangeSub: Subscription; + private readonly STATE_STORAGE_KEY = '_a'; + private readonly kbnUrlStateStorage: IKbnUrlStateStorage; + private readonly stateSyncRef: ISyncStateRef; + private readonly history: History; /** * * @param savedDashboard - * @param AppState The AppState class to use when instantiating a new AppState instance. * @param hideWriteControls true if write controls should be hidden. + * @param kibanaVersion current kibanaVersion + * @param */ constructor({ savedDashboard, - AppStateClass, hideWriteControls, kibanaVersion, + useHashedUrl, + history, }: { savedDashboard: SavedObjectDashboard; - AppStateClass: TAppStateClass; hideWriteControls: boolean; kibanaVersion: string; + useHashedUrl: boolean; + history: History; }) { + this.history = history; this.kibanaVersion = kibanaVersion; this.savedDashboard = savedDashboard; this.hideWriteControls = hideWriteControls; - this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls); + // get state defaults from saved dashboard, make sure it is migrated + this.stateDefaults = migrateAppState( + getAppStateDefaults(this.savedDashboard, this.hideWriteControls), + kibanaVersion + ); + + this.kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: useHashedUrl, history }); - this.appState = new AppStateClass(this.stateDefaults); + // setup initial state by merging defaults with state from url + // also run migration, as state in url could be of older version + const initialState = migrateAppState( + { + ...this.stateDefaults, + ...this.kbnUrlStateStorage.get(this.STATE_STORAGE_KEY), + }, + kibanaVersion + ); - // 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 Dashboard, but also any old state in the - // url. - migrateAppState(this.appState, kibanaVersion); + // setup state container using initial state both from defaults and from url + this.stateContainer = createStateContainer( + initialState, + { + set: state => (prop, value) => ({ ...state, [prop]: value }), + setOption: state => (option, value) => ({ + ...state, + options: { + ...state.options, + [option]: value, + }, + }), + } + ); this.isDirty = false; @@ -104,29 +152,35 @@ export class DashboardStateManager { // in the 'lose changes' warning message. this.lastSavedDashboardFilters = this.getFilterState(); - /** - * Creates a state monitor and saves it to this.stateMonitor. Used to track unsaved changes made to appState. - */ - this.stateMonitor = stateMonitorFactory.create( - this.appState, - this.stateDefaults - ); - - this.stateMonitor.ignoreProps('viewMode'); - // Filters need to be compared manually because they sometimes have a $$hashkey stored on the object. - this.stateMonitor.ignoreProps('filters'); - // Query needs to be compared manually because saved legacy queries get migrated in app state automatically - this.stateMonitor.ignoreProps('query'); + this.changeListeners = []; - this.stateMonitor.onChange((status: { dirty: boolean }) => { - this.isDirty = status.dirty; + this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { + this.isDirty = this.checkIsDirty(); + this.changeListeners.forEach(listener => listener({ dirty: this.isDirty })); }); - this.changeListeners = []; - - this.stateMonitor.onChange((status: { dirty: boolean }) => { - this.changeListeners.forEach(listener => listener(status)); + // make sure url ('_a') matches initial state + this.kbnUrlStateStorage.set(this.STATE_STORAGE_KEY, initialState, { replace: true }); + + // setup state syncing utils. state container will be synched with url into `this.STATE_STORAGE_KEY` query param + this.stateSyncRef = syncState({ + storageKey: this.STATE_STORAGE_KEY, + stateContainer: { + ...this.stateContainer, + set: (state: DashboardAppState | null) => { + // sync state required state container to be able to handle null + // overriding set() so it could handle null coming from url + this.stateContainer.set({ + ...this.stateDefaults, + ...state, + }); + }, + }, + stateStorage: this.kbnUrlStateStorage, }); + + // actually start syncing state with container + this.stateSyncRef.start(); } public registerChangeListener(callback: (status: { dirty: boolean }) => void) { @@ -172,7 +226,7 @@ export class DashboardStateManager { }); if (dirty) { - this.appState.panels = Object.values(convertedPanelStateMap); + this.stateContainer.transitions.set('panels', Object.values(convertedPanelStateMap)); } if (input.isFullScreenMode !== this.getFullScreenMode()) { @@ -184,7 +238,6 @@ export class DashboardStateManager { } this.changeListeners.forEach(listener => listener({ dirty })); - this.saveState(); } public getFullScreenMode() { @@ -192,8 +245,11 @@ export class DashboardStateManager { } public setFullScreenMode(fullScreenMode: boolean) { - this.appState.fullScreenMode = fullScreenMode; - this.saveState(); + this.stateContainer.transitions.set('fullScreenMode', fullScreenMode); + } + + public setFilters(filters: esFilters.Filter[]) { + this.stateContainer.transitions.set('filters', filters); } /** @@ -210,7 +266,10 @@ export class DashboardStateManager { // The right way to fix this might be to ensure the defaults object stored on state is a deep // clone, but given how much code uses the state object, I determined that to be too risky of a change for // now. TODO: revisit this! - this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls); + this.stateDefaults = migrateAppState( + getAppStateDefaults(this.savedDashboard, this.hideWriteControls), + this.kibanaVersion + ); // The original query won't be restored by the above because the query on this.savedDashboard is applied // in place in order for it to affect the visualizations. this.stateDefaults.query = this.lastSavedDashboardFilters.query; @@ -218,9 +277,7 @@ export class DashboardStateManager { this.stateDefaults.filters = [...this.getLastSavedFilterBars()]; this.isDirty = false; - this.appState.setDefaults(this.stateDefaults); - this.appState.reset(); - this.stateMonitor.setInitialState(this.appState.toJSON()); + this.stateContainer.set(this.stateDefaults); } /** @@ -252,31 +309,28 @@ export class DashboardStateManager { } public setDescription(description: string) { - this.appState.description = description; - this.saveState(); + this.stateContainer.transitions.set('description', description); } public setTitle(title: string) { - this.appState.title = title; this.savedDashboard.title = title; - this.saveState(); + this.stateContainer.transitions.set('title', title); } public getAppState() { - return this.appState; + return this.stateContainer.get(); } public getQuery(): Query { - return migrateLegacyQuery(this.appState.query); + return migrateLegacyQuery(this.stateContainer.get().query); } public getSavedQueryId() { - return this.appState.savedQuery; + return this.stateContainer.get().savedQuery; } public setSavedQueryId(id?: string) { - this.appState.savedQuery = id; - this.saveState(); + this.stateContainer.transitions.set('savedQuery', id); } public getUseMargins() { @@ -287,8 +341,7 @@ export class DashboardStateManager { } public setUseMargins(useMargins: boolean) { - this.appState.options.useMargins = useMargins; - this.saveState(); + this.stateContainer.transitions.setOption('useMargins', useMargins); } public getHidePanelTitles() { @@ -296,8 +349,7 @@ export class DashboardStateManager { } public setHidePanelTitles(hidePanelTitles: boolean) { - this.appState.options.hidePanelTitles = hidePanelTitles; - this.saveState(); + this.stateContainer.transitions.setOption('hidePanelTitles', hidePanelTitles); } public getTimeRestore() { @@ -305,8 +357,7 @@ export class DashboardStateManager { } public setTimeRestore(timeRestore: boolean) { - this.appState.timeRestore = timeRestore; - this.saveState(); + this.stateContainer.transitions.set('timeRestore', timeRestore); } public getIsTimeSavedWithDashboard() { @@ -397,7 +448,6 @@ export class DashboardStateManager { (panel: SavedDashboardPanel) => panel.panelIndex === panelIndex ); Object.assign(foundPanel, panelAttributes); - this.saveState(); return foundPanel; } @@ -456,15 +506,37 @@ export class DashboardStateManager { } /** - * Saves the current application state to the URL. + * Synchronously writes current state to url + * returned boolean indicates whether the update happened and if history was updated */ - public saveState() { - this.appState.save(); + private saveState({ replace }: { replace: boolean }): boolean { + // schedules setting current state to url + this.kbnUrlStateStorage.set( + this.STATE_STORAGE_KEY, + this.stateContainer.get() + ); + // immediately forces scheduled updates and changes location + return this.kbnUrlStateStorage.flush({ replace }); + } + + // TODO: find nicer solution for this + // this function helps to make just 1 browser history update, when we imperatively changing the dashboard url + // It could be that there is pending *dashboardStateManager* updates, which aren't flushed yet to the url. + // So to prevent 2 browser updates: + // 1. Force flush any pending state updates (syncing state to query) + // 2. If url was updated, then apply path change with replace + public changeDashboardUrl(pathname: string) { + // synchronously persist current state to url with push() + const updated = this.saveState({ replace: false }); + // change pathname + this.history[updated ? 'replace' : 'push']({ + ...this.history.location, + pathname, + }); } public setQuery(query: Query) { - this.appState.query = query; - this.saveState(); + this.stateContainer.transitions.set('query', query); } /** @@ -472,24 +544,33 @@ export class DashboardStateManager { * @param filter An array of filter bar filters. */ public applyFilters(query: Query, filters: esFilters.Filter[]) { - this.appState.query = query; this.savedDashboard.searchSource.setField('query', query); this.savedDashboard.searchSource.setField('filter', filters); - this.saveState(); + this.stateContainer.transitions.set('query', query); } public switchViewMode(newMode: ViewMode) { - this.appState.viewMode = newMode; - this.saveState(); + this.stateContainer.transitions.set('viewMode', newMode); } /** * Destroys and cleans up this object when it's no longer used. */ public destroy() { - if (this.stateMonitor) { - this.stateMonitor.destroy(); - } + this.stateContainerChangeSub.unsubscribe(); this.savedDashboard.destroy(); + if (this.stateSyncRef) { + this.stateSyncRef.stop(); + } + } + + private checkIsDirty() { + // Filters need to be compared manually because they sometimes have a $$hashkey stored on the object. + // Query needs to be compared manually because saved legacy queries get migrated in app state automatically + const propsToIgnore: Array = ['viewMode', 'filters', 'query']; + + const initial = _.omit(this.stateDefaults, propsToIgnore); + const current = _.omit(this.stateContainer.get(), propsToIgnore); + return !_.isEqual(initial, current); } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 540bfcf5aa6847..7dc408ea4b8013 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -35,6 +35,7 @@ import { import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; import { syncOnMount } from './global_state_sync'; +import { createHashHistory } from 'history'; export function initDashboardApp(app, deps) { initDashboardAppDirective(app, deps); @@ -190,7 +191,7 @@ export function initDashboardApp(app, deps) { template: dashboardTemplate, controller: createNewDashboardCtrl, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, AppState) { + dash: function($rootScope, $route, redirectWhenMissing, kbnUrl) { const id = $route.current.params.id; return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl) @@ -216,8 +217,13 @@ export function initDashboardApp(app, deps) { // Preserve BWC of v5.3.0 links for new, unsaved dashboards. // See https://github.com/elastic/kibana/issues/10951 for more context. if (error instanceof SavedObjectNotFound && id === 'create') { - // Note "new AppState" is necessary so the state in the url is preserved through the redirect. - kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState()); + // Note preserve querystring part is necessary so the state is preserved through the redirect. + const history = createHashHistory(); + history.replace({ + ...history.location, // preserve query, + pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL, + }); + deps.toastNotifications.addWarning( i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', { defaultMessage: diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.test.ts index 4aa2461bb65930..73336ec951894e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.test.ts @@ -23,7 +23,6 @@ import { SavedDashboardPanel } from '../types'; import { migrateAppState } from './migrate_app_state'; test('migrate app state from 6.0', async () => { - const mockSave = jest.fn(); const appState = { uiState: { 'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } }, @@ -39,11 +38,8 @@ test('migrate app state from 6.0', async () => { type: 'visualization', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, }; - migrateAppState(appState, '8.0'); + migrateAppState(appState as any, '8.0'); expect(appState.uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -54,12 +50,10 @@ test('migrate app state from 6.0', async () => { expect(newPanel.gridData.y).toBe(0); expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)'); - expect(mockSave).toBeCalledTimes(1); }); test('migrate sort from 6.1', async () => { const TARGET_VERSION = '8.0'; - const mockSave = jest.fn(); const appState = { uiState: { 'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } }, @@ -76,12 +70,9 @@ test('migrate sort from 6.1', async () => { sort: 'sort', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, useMargins: false, }; - migrateAppState(appState, TARGET_VERSION); + migrateAppState(appState as any, TARGET_VERSION); expect(appState.uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -91,11 +82,9 @@ test('migrate sort from 6.1', async () => { expect((newPanel.embeddableConfig as any).sort).toBe('sort'); expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)'); - expect(mockSave).toBeCalledTimes(1); }); test('migrates 6.0 even when uiState does not exist', async () => { - const mockSave = jest.fn(); const appState = { panels: [ { @@ -109,11 +98,8 @@ test('migrates 6.0 even when uiState does not exist', async () => { sort: 'sort', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, }; - migrateAppState(appState, '8.0'); + migrateAppState(appState as any, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -122,11 +108,9 @@ test('migrates 6.0 even when uiState does not exist', async () => { expect((newPanel as any).sort).toBeUndefined(); expect((newPanel.embeddableConfig as any).sort).toBe('sort'); - expect(mockSave).toBeCalledTimes(1); }); test('6.2 migration adjusts w & h without margins', async () => { - const mockSave = jest.fn(); const appState = { panels: [ { @@ -143,12 +127,9 @@ test('6.2 migration adjusts w & h without margins', async () => { version: '6.2.0', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, useMargins: false, }; - migrateAppState(appState, '8.0'); + migrateAppState(appState as any, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -159,11 +140,9 @@ test('6.2 migration adjusts w & h without margins', async () => { expect((newPanel as any).sort).toBeUndefined(); expect((newPanel.embeddableConfig as any).sort).toBe('sort'); - expect(mockSave).toBeCalledTimes(1); }); test('6.2 migration adjusts w & h with margins', async () => { - const mockSave = jest.fn(); const appState = { panels: [ { @@ -180,12 +159,9 @@ test('6.2 migration adjusts w & h with margins', async () => { version: '6.2.0', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, useMargins: true, }; - migrateAppState(appState, '8.0'); + migrateAppState(appState as any, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -196,5 +172,4 @@ test('6.2 migration adjusts w & h with margins', async () => { expect((newPanel as any).sort).toBeUndefined(); expect((newPanel.embeddableConfig as any).sort).toBe('sort'); - expect(mockSave).toBeCalledTimes(1); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.ts index 4083900c7ede7e..0cd958ced0eb1c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.ts @@ -28,6 +28,7 @@ import { SavedDashboardPanel630, SavedDashboardPanel640To720, SavedDashboardPanel620, + SavedDashboardPanel, } from '../types'; import { migratePanelsTo730 } from '../../migrations/migrate_to_730_panels'; @@ -37,9 +38,9 @@ import { migratePanelsTo730 } from '../../migrations/migrate_to_730_panels'; * Once we hit a major version, we can remove support for older style URLs and get rid of this logic. */ export function migrateAppState( - appState: { [key: string]: unknown } | DashboardAppState, + appState: { [key: string]: unknown } & DashboardAppState, kibanaVersion: string -) { +): DashboardAppState { if (!appState.panels) { throw new Error( i18n.translate('kbn.dashboard.panel.invalidData', { @@ -76,11 +77,11 @@ export function migrateAppState( | SavedDashboardPanel640To720 >, kibanaVersion, - appState.useMargins, - appState.uiState - ); + appState.useMargins as boolean, + appState.uiState as Record> + ) as SavedDashboardPanel[]; delete appState.uiState; - - appState.save(); } + + return appState; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts index 691c87122564f6..d80208ce27ffe4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts @@ -36,16 +36,19 @@ export function saveDashboard( dashboardStateManager: DashboardStateManager, saveOptions: SavedObjectSaveOpts ): Promise { - dashboardStateManager.saveState(); - const savedDashboard = dashboardStateManager.savedDashboard; const appState = dashboardStateManager.appState; updateSavedDashboard(savedDashboard, appState, timeFilter, toJson); return savedDashboard.save(saveOptions).then((id: string) => { - dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); - dashboardStateManager.resetState(); + if (id) { + // reset state only when save() was successful + // e.g. save() could be interrupted if title is duplicated and not confirmed + dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); + dashboardStateManager.resetState(); + } + return id; }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/update_saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/update_saved_dashboard.ts index 2072b5d4f6eb06..ec8073c0f72f74 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/update_saved_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/update_saved_dashboard.ts @@ -19,13 +19,13 @@ import _ from 'lodash'; import { RefreshInterval, TimefilterContract } from 'src/plugins/data/public'; -import { AppState } from '../../legacy_imports'; import { FilterUtils } from './filter_utils'; import { SavedObjectDashboard } from '../../saved_dashboard/saved_dashboard'; +import { DashboardAppState } from '../types'; export function updateSavedDashboard( savedDashboard: SavedObjectDashboard, - appState: AppState, + appState: DashboardAppState, timeFilter: TimefilterContract, toJson: (object: T) => string ) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts index e3eb25a208856f..3151fbf821b9fe 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts @@ -18,7 +18,6 @@ */ import { ViewMode } from 'src/plugins/embeddable/public'; -import { AppState } from '../legacy_imports'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -93,11 +92,7 @@ export type SavedDashboardPanelTo60 = Pick< readonly type: string; }; -export type DashboardAppStateDefaults = DashboardAppStateParameters & { - description?: string; -}; - -export interface DashboardAppStateParameters { +export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; title: string; @@ -113,9 +108,24 @@ export interface DashboardAppStateParameters { savedQuery?: string; } -// This could probably be improved if we flesh out AppState more... though AppState will be going away -// so maybe not worth too much time atm. -export type DashboardAppState = DashboardAppStateParameters & AppState; +export type DashboardAppStateDefaults = DashboardAppState & { + description?: string; +}; + +export interface DashboardAppStateTransitions { + set: ( + state: DashboardAppState + ) => ( + prop: T, + value: DashboardAppState[T] + ) => DashboardAppState; + setOption: ( + state: DashboardAppState + ) => ( + prop: T, + value: DashboardAppState['options'][T] + ) => DashboardAppState; +} export interface SavedDashboardPanelMap { [key: string]: SavedDashboardPanel; @@ -139,18 +149,3 @@ export type ConfirmModalFn = ( title: string; } ) => void; - -export type AddFilterFn = ( - { - field, - value, - operator, - index, - }: { - field: string; - value: string; - operator: string; - index: string; - }, - appState: AppState -) => void; diff --git a/src/legacy/ui/public/chrome/api/controls.ts b/src/legacy/ui/public/chrome/api/controls.ts index 6dde26be20df26..a95d53ec2eab69 100644 --- a/src/legacy/ui/public/chrome/api/controls.ts +++ b/src/legacy/ui/public/chrome/api/controls.ts @@ -42,4 +42,6 @@ export function initChromeControlsApi(chrome: { [key: string]: any }) { * might be incorrect in the moments just before the UI is updated. */ chrome.getVisible = () => visible$.getValue(); + + chrome.visible$ = visible$.asObservable(); } diff --git a/src/legacy/ui/public/chrome/directives/kbn_chrome.js b/src/legacy/ui/public/chrome/directives/kbn_chrome.js index 72d26a37a60a16..4c5d7981e962ac 100644 --- a/src/legacy/ui/public/chrome/directives/kbn_chrome.js +++ b/src/legacy/ui/public/chrome/directives/kbn_chrome.js @@ -30,6 +30,7 @@ import { chromeHeaderNavControlsRegistry, NavControlSide, } from '../../registry/chrome_header_nav_controls'; +import { subscribeWithScope } from '../../utils/subscribe_with_scope'; export function kbnChromeProvider(chrome, internals) { uiModules.get('kibana').directive('kbnChrome', () => { @@ -83,6 +84,15 @@ export function kbnChromeProvider(chrome, internals) { ); } + const chromeVisibility = subscribeWithScope($scope, chrome.visible$, { + next: () => { + // just makes sure change detection is triggered when chrome visibility changes + }, + }); + $scope.$on('$destroy', () => { + chromeVisibility.unsubscribe(); + }); + return chrome; }, }; diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index 289d4b8006cba9..c2274eae59f507 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -29,7 +29,6 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import angular from 'angular'; import rison from 'rison-node'; -import { applyDiff } from './utils/diff_object'; import { EventsProvider } from '../events'; import { fatalError, toastNotifications } from '../notify'; import './config_provider'; @@ -38,6 +37,7 @@ import { hashedItemStore, isStateHash, createStateHash, + applyDiff, } from '../../../../plugins/kibana_utils/public'; export function StateProvider( diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts new file mode 100644 index 00000000000000..b4b5658c1c886e --- /dev/null +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { removeQueryParam } from './remove_query_param'; diff --git a/src/plugins/kibana_utils/public/history/remove_query_param.test.ts b/src/plugins/kibana_utils/public/history/remove_query_param.test.ts new file mode 100644 index 00000000000000..0b2547ae94668a --- /dev/null +++ b/src/plugins/kibana_utils/public/history/remove_query_param.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { removeQueryParam } from './remove_query_param'; +import { createMemoryHistory, Location } from 'history'; + +describe('removeQueryParam', () => { + it('should remove query param from url', () => { + const startLocation: Location = { + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: "?_a=(description:'')&_b=3", + state: null, + hash: '', + }; + + const history = createMemoryHistory(); + history.push(startLocation); + removeQueryParam(history, '_a'); + + expect(history.location).toEqual( + expect.objectContaining({ + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: '?_b=3', + state: null, + hash: '', + }) + ); + }); + + it('should not fail if nothing to remove', () => { + const startLocation: Location = { + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: "?_a=(description:'')&_b=3", + state: null, + hash: '', + }; + + const history = createMemoryHistory(); + history.push(startLocation); + removeQueryParam(history, '_c'); + + expect(history.location).toEqual(expect.objectContaining(startLocation)); + }); + + it('should not fail if no search', () => { + const startLocation: Location = { + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: '', + state: null, + hash: '', + }; + + const history = createMemoryHistory(); + history.push(startLocation); + removeQueryParam(history, '_c'); + + expect(history.location).toEqual(expect.objectContaining(startLocation)); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts b/src/plugins/kibana_utils/public/history/remove_query_param.ts similarity index 53% rename from src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts rename to src/plugins/kibana_utils/public/history/remove_query_param.ts index d9dea35a8a1c03..fbf985998b4cd1 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts +++ b/src/plugins/kibana_utils/public/history/remove_query_param.ts @@ -17,32 +17,23 @@ * under the License. */ -import { AppStateClass } from '../legacy_imports'; +import { History, Location } from 'history'; +import { parse } from 'querystring'; +import { stringifyQueryString } from '../state_management/url/stringify_query_string'; // TODO: extract it to ../url -/** - * A poor excuse for a mock just to get some basic tests to run in jest without requiring the injector. - * This could be improved if we extract the appState and state classes externally of their angular providers. - * @return {AppStateMock} - */ -export function getAppStateMock(): AppStateClass { - class AppStateMock { - constructor(defaults: any) { - Object.assign(this, defaults); - } - - on() {} - off() {} - toJSON() { - return ''; - } - save() {} - translateHashToRison(stateHashOrRison: string | string[]) { - return stateHashOrRison; - } - getQueryParamName() { - return ''; - } +export function removeQueryParam(history: History, param: string, replace: boolean = true) { + const oldLocation = history.location; + const search = (oldLocation.search || '').replace(/^\?/, ''); + const query = parse(search); + delete query[param]; + const newSearch = stringifyQueryString(query); + const newLocation: Location = { + ...oldLocation, + search: newSearch, + }; + if (replace) { + history.replace(newLocation); + } else { + history.push(newLocation); } - - return AppStateMock; } diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index fa58a61e51232b..00c1c95028b4dc 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -58,3 +58,5 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; +export { removeQueryParam } from './history'; +export { applyDiff } from './state_management/utils/diff_object'; diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index f1c527d3d53097..6e4c505c62ebc8 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -85,6 +85,7 @@ describe('kbn_url_storage', () => { beforeEach(() => { history = createMemoryHistory(); urlControls = createKbnUrlControls(history); + urlControls.update('/', true); }); const getCurrentUrl = () => createPath(history.location); @@ -143,17 +144,6 @@ describe('kbn_url_storage', () => { expect(cb).toHaveBeenCalledTimes(3); }); - it('should flush async url updates', async () => { - const pr1 = urlControls.updateAsync(() => '/1', false); - const pr2 = urlControls.updateAsync(() => '/2', false); - const pr3 = urlControls.updateAsync(() => '/3', false); - expect(getCurrentUrl()).toBe('/'); - urlControls.flush(); - expect(getCurrentUrl()).toBe('/3'); - await Promise.all([pr1, pr2, pr3]); - expect(getCurrentUrl()).toBe('/3'); - }); - it('flush should take priority over regular replace behaviour', async () => { const pr1 = urlControls.updateAsync(() => '/1', true); const pr2 = urlControls.updateAsync(() => '/2', false); @@ -174,6 +164,48 @@ describe('kbn_url_storage', () => { await Promise.all([pr1, pr2, pr3]); expect(getCurrentUrl()).toBe('/'); }); + + it('should retrieve pending url ', async () => { + const pr1 = urlControls.updateAsync(() => '/1', true); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', true); + expect(urlControls.getPendingUrl()).toEqual('/3'); + expect(getCurrentUrl()).toBe('/'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + + expect(urlControls.getPendingUrl()).toBeUndefined(); + }); + }); + + describe('urlControls - browser history integration', () => { + let history: History; + let urlControls: IKbnUrlControls; + beforeEach(() => { + history = createBrowserHistory(); + urlControls = createKbnUrlControls(history); + urlControls.update('/', true); + }); + + const getCurrentUrl = () => window.location.href; + + it('should flush async url updates', async () => { + const pr1 = urlControls.updateAsync(() => '/1', false); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', false); + expect(getCurrentUrl()).toBe('http://localhost/'); + expect(urlControls.flush()).toBe('http://localhost/3'); + expect(getCurrentUrl()).toBe('http://localhost/3'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('http://localhost/3'); + }); + + it('flush() should return undefined, if no url updates happened', () => { + expect(urlControls.flush()).toBeUndefined(); + urlControls.updateAsync(() => 'http://localhost/1', false); + urlControls.updateAsync(() => 'http://localhost/', false); + expect(urlControls.flush()).toBeUndefined(); + }); }); describe('getRelativeToHistoryPath', () => { diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 03c136ea3d0928..1dd204e7172132 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -107,25 +107,34 @@ export interface IKbnUrlControls { listen: (cb: () => void) => () => void; /** - * Updates url synchronously + * Updates url synchronously, if needed + * skips the update and returns undefined in case when trying to update to current url + * otherwise returns new url + * * @param url - url to update to * @param replace - use replace instead of push */ - update: (url: string, replace: boolean) => string; + update: (url: string, replace: boolean) => string | undefined; /** * Schedules url update to next microtask, * Useful to batch sync changes to url to cause only one browser history update * @param updater - fn which receives current url and should return next url to update to * @param replace - use replace instead of push + * */ - updateAsync: (updater: UrlUpdaterFnType, replace?: boolean) => Promise; + updateAsync: (updater: UrlUpdaterFnType, replace?: boolean) => Promise; /** - * Synchronously flushes scheduled url updates + * If there is a pending url update - returns url that is scheduled for update + */ + getPendingUrl: () => string | undefined; + + /** + * Synchronously flushes scheduled url updates. Returns new flushed url, if there was an update. Otherwise - undefined. * @param replace - if replace passed in, then uses it instead of push. Otherwise push or replace is picked depending on updateQueue */ - flush: (replace?: boolean) => string; + flush: (replace?: boolean) => string | undefined; /** * Cancels any pending url updates @@ -143,9 +152,9 @@ export const createKbnUrlControls = ( // if any call in a queue asked to push, then we should push let shouldReplace = true; - function updateUrl(newUrl: string, replace = false): string { + function updateUrl(newUrl: string, replace = false): string | undefined { const currentUrl = getCurrentUrl(); - if (newUrl === currentUrl) return currentUrl; // skip update + if (newUrl === currentUrl) return undefined; // skip update const historyPath = getRelativeToHistoryPath(newUrl, history); @@ -166,15 +175,22 @@ export const createKbnUrlControls = ( // runs scheduled url updates function flush(replace = shouldReplace) { - if (updateQueue.length === 0) return getCurrentUrl(); - const resultUrl = updateQueue.reduce((url, nextUpdate) => nextUpdate(url), getCurrentUrl()); + const nextUrl = getPendingUrl(); - cleanUp(); + if (!nextUrl) return; - const newUrl = updateUrl(resultUrl, replace); + cleanUp(); + const newUrl = updateUrl(nextUrl, replace); return newUrl; } + function getPendingUrl() { + if (updateQueue.length === 0) return undefined; + const resultUrl = updateQueue.reduce((url, nextUpdate) => nextUpdate(url), getCurrentUrl()); + + return resultUrl; + } + return { listen: (cb: () => void) => history.listen(() => { @@ -199,6 +215,9 @@ export const createKbnUrlControls = ( cancel: () => { cleanUp(); }, + getPendingUrl: () => { + return getPendingUrl(); + }, }; }; diff --git a/src/legacy/ui/public/state_management/utils/diff_object.test.ts b/src/plugins/kibana_utils/public/state_management/utils/diff_object.test.ts similarity index 100% rename from src/legacy/ui/public/state_management/utils/diff_object.test.ts rename to src/plugins/kibana_utils/public/state_management/utils/diff_object.test.ts diff --git a/src/legacy/ui/public/state_management/utils/diff_object.ts b/src/plugins/kibana_utils/public/state_management/utils/diff_object.ts similarity index 100% rename from src/legacy/ui/public/state_management/utils/diff_object.ts rename to src/plugins/kibana_utils/public/state_management/utils/diff_object.ts diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index 08ad1551420d2e..17f41483a0a21e 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -291,6 +291,42 @@ describe('state_sync', () => { stop(); }); + + it("should preserve reference to unchanged state slices if them didn't change", async () => { + const otherUnchangedSlice = { a: 'test' }; + const oldState = { + todos: container.get().todos, + otherUnchangedSlice, + }; + container.set(oldState as any); + + const { stop, start } = syncStates([ + { + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: urlSyncStrategy, + }, + ]); + await urlSyncStrategy.set('_s', container.get()); + expect(getCurrentUrl()).toMatchInlineSnapshot( + `"/#?_s=(otherUnchangedSlice:(a:test),todos:!((completed:!f,id:0,text:'Learning%20state%20containers')))"` + ); + start(); + + history.replace( + "/#?_s=(otherUnchangedSlice:(a:test),todos:!((completed:!t,id:0,text:'Learning%20state%20containers')))" + ); + + const newState = container.get(); + expect(newState.todos).toEqual([ + { id: 0, text: 'Learning state containers', completed: true }, + ]); + + // reference to unchanged slice is preserved + expect((newState as any).otherUnchangedSlice).toBe(otherUnchangedSlice); + + stop(); + }); }); }); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts index 9c1116e5da5318..28d133829e07c4 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -24,6 +24,7 @@ import { IStateSyncConfig } from './types'; import { IStateStorage } from './state_sync_state_storage'; import { distinctUntilChangedWithInitialValue } from '../../common'; import { BaseState } from '../state_containers'; +import { applyDiff } from '../state_management/utils/diff_object'; /** * Utility for syncing application state wrapped in state container @@ -100,7 +101,18 @@ export function syncState< const updateState = () => { const newState = stateStorage.get(storageKey); const oldState = stateContainer.get(); - if (!defaultComparator(newState, oldState)) { + if (newState) { + // apply only real differences to new state + const mergedState = { ...oldState } as State; + // merges into 'mergedState' all differences from newState, + // but leaves references if they are deeply the same + const diff = applyDiff(mergedState, newState); + + if (diff.keys.length > 0) { + stateContainer.set(mergedState); + } + } else if (oldState !== newState) { + // empty new state case stateContainer.set(newState); } }; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts index 826122176e061e..cc3f1df7c1e00a 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -46,9 +46,11 @@ describe('KbnUrlStateStorage', () => { const key = '_s'; urlStateStorage.set(key, state); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); - urlStateStorage.flush(); + expect(urlStateStorage.flush()).toBe(true); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_s=(ok:1,test:test)"`); expect(urlStateStorage.get(key)).toEqual(state); + + expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update }); it('should cancel url updates', async () => { @@ -62,6 +64,19 @@ describe('KbnUrlStateStorage', () => { expect(urlStateStorage.get(key)).toEqual(null); }); + it('should cancel url updates if synchronously returned to the same state', async () => { + const state1 = { test: 'test', ok: 1 }; + const state2 = { test: 'test', ok: 2 }; + const key = '_s'; + const pr1 = urlStateStorage.set(key, state1); + await pr1; + const historyLength = history.length; + const pr2 = urlStateStorage.set(key, state2); + const pr3 = urlStateStorage.set(key, state1); + await Promise.all([pr2, pr3]); + expect(history.length).toBe(historyLength); + }); + it('should notify about url changes', async () => { expect(urlStateStorage.change$).toBeDefined(); const key = '_s'; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index 245006349ad55b..082eaa5095ab94 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -28,7 +28,11 @@ import { } from '../../state_management/url'; export interface IKbnUrlStateStorage extends IStateStorage { - set: (key: string, state: State, opts?: { replace: boolean }) => Promise; + set: ( + key: string, + state: State, + opts?: { replace: boolean } + ) => Promise; get: (key: string) => State | null; change$: (key: string) => Observable; @@ -36,7 +40,8 @@ export interface IKbnUrlStateStorage extends IStateStorage { cancel: () => void; // synchronously runs any pending url updates - flush: (opts?: { replace?: boolean }) => void; + // returned boolean indicates if change occurred + flush: (opts?: { replace?: boolean }) => boolean; } /** @@ -60,7 +65,11 @@ export const createKbnUrlStateStorage = ( replace ); }, - get: key => getStateFromKbnUrl(key), + get: key => { + // if there is a pending url update, then state will be extracted from that pending url, + // otherwise current url will be used to retrieve state from + return getStateFromKbnUrl(key, url.getPendingUrl()); + }, change$: (key: string) => new Observable(observer => { const unlisten = url.listen(() => { @@ -75,7 +84,7 @@ export const createKbnUrlStateStorage = ( share() ), flush: ({ replace = false }: { replace?: boolean } = {}) => { - url.flush(replace); + return !!url.flush(replace); }, cancel() { url.cancel(); diff --git a/test/functional/apps/dashboard/dashboard_clone.js b/test/functional/apps/dashboard/dashboard_clone.js index f5485c1db206e7..8b7f6ba6a34dd3 100644 --- a/test/functional/apps/dashboard/dashboard_clone.js +++ b/test/functional/apps/dashboard/dashboard_clone.js @@ -37,7 +37,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.dashboard.addVisualizations( PageObjects.dashboard.getTestVisualizationNames() ); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName); + await PageObjects.dashboard.saveDashboard(dashboardName); await PageObjects.dashboard.clickClone(); await PageObjects.dashboard.confirmClone(); From 9d3d3cdc67e692c90d3bc959984114f007156615 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 21 Jan 2020 13:40:39 +0100 Subject: [PATCH 05/19] Migrate Management views to Kibana Platform plugin (#53880) --- .../saved_objects_client.test.ts | 19 - .../saved_objects/saved_objects_client.ts | 6 +- src/plugins/management/public/index.ts | 7 +- .../legacy/plugins/security/common/model.ts | 28 - x-pack/legacy/plugins/security/index.d.ts | 2 +- x-pack/legacy/plugins/security/index.js | 3 - .../security/public/documentation_links.js | 16 - .../plugins/security/public/images/logout.svg | 3 - .../plugins/security/public/images/person.svg | 3 - .../legacy/plugins/security/public/index.scss | 3 + .../security/public/lib/__tests__/util.js | 49 -- .../legacy/plugins/security/public/lib/api.ts | 56 -- .../security/public/lib/api_keys_api.ts | 47 -- .../plugins/security/public/lib/role_utils.ts | 58 -- .../plugins/security/public/lib/roles_api.ts | 25 - .../public/lib/transform_role_for_save.ts | 41 - .../plugins/security/public/lib/util.js | 19 - .../security/public/objects/lib/get_fields.ts | 15 - .../security/public/objects/lib/roles.ts | 21 - .../security/public/register_feature.js | 29 - .../public/services/shield_indices.js | 18 - .../security/public/services/shield_role.js | 30 - .../plugins/security/public/views/_index.scss | 3 - .../public/views/account/account.html | 1 - .../security/public/views/account/account.js | 27 +- .../security/public/views/login/_index.scss | 3 +- .../public/views/login/components/_index.scss | 1 + .../basic_login_form.test.tsx | 2 +- .../basic_login_form/basic_login_form.tsx | 2 +- .../login/components/login_page/_index.scss | 1 + .../login_page/_login_page.scss} | 5 - .../components/login_page/login_page.test.tsx | 2 +- .../components/login_page/login_page.tsx | 2 +- .../security/public/views/login/login.html | 1 - .../security/public/views/login/login.tsx | 8 +- .../views/login}/login_state.ts | 0 .../login/parse_next.test.ts} | 47 +- .../public/{lib => views/login}/parse_next.ts | 0 .../public/views/management/_index.scss | 4 - .../management/api_keys_grid/api_keys.html | 3 - .../management/api_keys_grid/api_keys.js | 35 - .../public/views/management/breadcrumbs.ts | 115 --- .../_change_password_form.scss | 17 - .../change_password_form/_index.scss | 1 - .../change_password_form.html | 141 ---- .../change_password_form.js | 48 -- .../views/management/edit_role/_index.scss | 1 - .../edit_role/components/_index.scss | 9 - .../components/edit_role_page.test.tsx | 716 ------------------ .../edit_role/components/edit_role_page.tsx | 409 ---------- .../es/elasticsearch_privileges.test.tsx | 96 --- .../views/management/edit_role/edit_role.html | 3 - .../views/management/edit_role/index.js | 176 ----- .../views/management/edit_user/_index.scss | 1 - .../views/management/edit_user/edit_user.html | 3 - .../views/management/edit_user/edit_user.js | 58 -- .../public/views/management/management.js | 134 ---- .../password_form/password_form.html | 53 -- .../management/password_form/password_form.js | 24 - .../edit_role_mapping/_index.scss | 1 - .../edit_role_mapping_page.test.tsx | 341 --------- .../edit_role_mapping/edit_role_mapping.html | 3 - .../role_mappings/edit_role_mapping/index.tsx | 45 -- .../role_mappings_grid_page.test.tsx | 182 ----- .../role_mappings_grid/index.tsx | 40 - .../role_mappings_grid/role_mappings.html | 3 - .../views/management/roles_grid/roles.html | 3 - .../views/management/roles_grid/roles.js | 35 - .../views/management/users_grid/users.html | 3 - .../views/management/users_grid/users.js | 47 -- .../overwritten_session.tsx | 3 +- x-pack/plugins/security/common/model/index.ts | 11 +- .../security/common/model/role.test.ts} | 9 +- x-pack/plugins/security/common/model/role.ts | 51 ++ x-pack/plugins/security/kibana.json | 3 +- x-pack/plugins/security/public/_index.scss | 2 + .../account_management_page.test.tsx | 37 +- .../account_management_page.tsx | 18 +- .../change_password/change_password.tsx | 19 +- .../change_password/index.ts | 0 .../public/account_management}/index.ts | 0 .../personal_info/index.ts | 0 .../personal_info/personal_info.tsx | 11 +- .../security/public/management/_index.scss | 3 + .../api_keys/api_keys_api_client.mock.ts | 13 + .../api_keys/api_keys_api_client.test.ts | 86 +++ .../api_keys/api_keys_api_client.ts | 42 + .../api_keys_grid_page.test.tsx.snap | 20 +- .../api_keys_grid_page.test.tsx | 124 +-- .../api_keys_grid}/api_keys_grid_page.tsx | 50 +- .../empty_prompt/empty_prompt.tsx | 7 +- .../api_keys_grid}/empty_prompt/index.ts | 0 .../api_keys/api_keys_grid}/index.ts | 2 +- .../invalidate_provider/index.ts | 0 .../invalidate_provider.tsx | 21 +- .../api_keys_grid}/not_enabled/index.ts | 0 .../not_enabled/not_enabled.tsx | 10 +- .../api_keys_grid}/permission_denied/index.ts | 0 .../permission_denied/permission_denied.tsx | 0 .../api_keys/api_keys_management_app.test.tsx | 54 ++ .../api_keys/api_keys_management_app.tsx | 58 ++ .../api_keys}/documentation_links.ts | 16 +- .../public/management/api_keys/index.mock.ts | 7 + .../public/management/api_keys/index.ts | 7 + .../security/public/management/index.ts | 8 + .../management/management_service.test.ts | 226 ++++++ .../public/management/management_service.ts | 95 +++ .../public}/management/management_urls.ts | 9 +- .../management/role_mappings/_index.scss | 1 + .../delete_provider/delete_provider.test.tsx | 152 ++-- .../delete_provider/delete_provider.tsx | 21 +- .../components/delete_provider/index.ts | 0 .../role_mappings/components/index.ts | 0 .../components/no_compatible_realms/index.ts | 0 .../no_compatible_realms.tsx | 10 +- .../components/permission_denied/index.ts | 0 .../permission_denied/permission_denied.tsx | 0 .../components/section_loading/index.ts | 0 .../section_loading/section_loading.test.tsx | 0 .../section_loading/section_loading.tsx | 0 .../role_mappings}/documentation_links.ts | 12 +- .../edit_role_mapping/_index.scss | 1 + .../edit_role_mapping_page.test.tsx | 399 ++++++++++ .../edit_role_mapping_page.tsx | 44 +- .../role_mappings/edit_role_mapping}/index.ts | 0 .../mapping_info_panel/index.ts | 0 .../mapping_info_panel.test.tsx | 36 +- .../mapping_info_panel/mapping_info_panel.tsx | 13 +- .../add_role_template_button.test.tsx | 0 .../add_role_template_button.tsx | 0 .../role_selector/index.tsx | 0 .../role_selector/role_selector.test.tsx | 25 +- .../role_selector/role_selector.tsx | 7 +- .../role_template_editor.test.tsx | 0 .../role_selector/role_template_editor.tsx | 4 +- .../role_template_type_select.tsx | 4 +- .../rule_editor_panel/_index.scss | 1 + .../_rule_editor_group.scss} | 0 .../add_rule_button.test.tsx | 2 +- .../rule_editor_panel/add_rule_button.tsx | 2 +- .../field_rule_editor.test.tsx | 2 +- .../rule_editor_panel/field_rule_editor.tsx | 2 +- .../rule_editor_panel/index.tsx | 0 .../json_rule_editor.test.tsx | 10 +- .../rule_editor_panel/json_rule_editor.tsx | 7 +- .../rule_editor_panel.test.tsx | 9 +- .../rule_editor_panel/rule_editor_panel.tsx | 14 +- .../rule_group_editor.test.tsx | 2 +- .../rule_editor_panel/rule_group_editor.tsx | 4 +- .../rule_editor_panel/rule_group_title.tsx | 9 +- .../visual_rule_editor.test.tsx | 2 +- .../rule_editor_panel/visual_rule_editor.tsx | 6 +- .../services/is_rule_group.ts | 0 .../services/role_mapping_constants.ts | 0 .../services/role_mapping_validation.test.ts | 2 +- .../services/role_mapping_validation.ts | 2 +- .../services/role_template_type.test.ts | 2 +- .../services/role_template_type.ts | 2 +- .../management/role_mappings/index.mock.ts | 7 + .../public/management/role_mappings/index.ts | 7 + .../__snapshots__/rule_builder.test.ts.snap | 0 .../role_mappings/model/all_rule.test.ts | 0 .../role_mappings/model/all_rule.ts | 0 .../role_mappings/model/any_rule.test.ts | 0 .../role_mappings/model/any_rule.ts | 0 .../model/except_all_rule.test.ts | 0 .../role_mappings/model/except_all_rule.ts | 0 .../model/except_any_rule.test.ts | 0 .../role_mappings/model/except_any_rule.ts | 0 .../role_mappings/model/field_rule.test.ts | 0 .../role_mappings/model/field_rule.ts | 0 .../management/role_mappings/model/index.ts | 0 .../management/role_mappings/model/rule.ts | 0 .../role_mappings/model/rule_builder.test.ts | 2 +- .../role_mappings/model/rule_builder.ts | 2 +- .../role_mappings/model/rule_builder_error.ts | 0 .../role_mappings/model/rule_group.ts | 2 +- .../role_mappings_api_client.mock.ts | 15 + .../role_mappings_api_client.ts} | 8 +- .../create_role_mapping_button.tsx | 2 +- .../create_role_mapping_button/index.ts | 0 .../empty_prompt/empty_prompt.tsx | 0 .../role_mappings_grid}/empty_prompt/index.ts | 0 .../role_mappings_grid}/index.ts | 0 .../role_mappings_grid_page.test.tsx | 219 ++++++ .../role_mappings_grid_page.tsx | 29 +- .../role_mappings_management_app.test.tsx | 127 ++++ .../role_mappings_management_app.tsx | 104 +++ .../public/management/roles/_index.scss | 1 + .../management/roles/documentation_links.ts | 27 + .../__snapshots__/validate_role.test.ts.snap | 0 .../management/roles/edit_role/_index.scss | 3 + .../collapsible_panel.test.tsx.snap | 0 .../collapsible_panel/_collapsible_panel.scss | 0 .../edit_role/collapsible_panel/_index.scss | 1 + .../collapsible_panel.test.tsx | 0 .../collapsible_panel/collapsible_panel.tsx | 1 - .../edit_role}/collapsible_panel/index.ts | 0 .../edit_role}/delete_role_button.test.tsx | 6 +- .../roles/edit_role}/delete_role_button.tsx | 8 +- .../roles/edit_role/edit_role_page.test.tsx | 552 ++++++++++++++ .../roles/edit_role/edit_role_page.tsx | 594 +++++++++++++++ .../management/roles/edit_role}/index.ts | 0 .../roles/edit_role}/privilege_utils.test.ts | 0 .../roles/edit_role}/privilege_utils.ts | 2 +- .../roles/edit_role/privileges/_index.scss | 2 + .../privileges/_privilege_feature_icon.scss | 4 + .../cluster_privileges.test.tsx.snap | 0 .../elasticsearch_privileges.test.tsx.snap | 24 +- .../index_privilege_form.test.tsx.snap | 0 .../index_privileges.test.tsx.snap | 0 .../privileges/es/cluster_privileges.test.tsx | 2 +- .../privileges/es/cluster_privileges.tsx | 3 +- .../es/elasticsearch_privileges.test.tsx | 67 ++ .../es/elasticsearch_privileges.tsx | 34 +- .../es/index_privilege_form.test.tsx | 2 +- .../privileges/es/index_privilege_form.tsx | 7 +- .../privileges/es/index_privileges.test.tsx | 27 +- .../privileges/es/index_privileges.tsx | 32 +- .../roles/edit_role}/privileges/index.ts | 0 .../kibana_privileges_region.test.tsx.snap | 0 .../edit_role/privileges/kibana/_index.scss | 2 + .../edit_role/privileges/kibana}/constants.ts | 0 .../__snapshots__/feature_table.test.tsx.snap | 0 .../_change_all_privileges.scss} | 0 .../kibana/feature_table/_index.scss | 1 + .../feature_table/change_all_privileges.tsx | 0 .../feature_table/feature_table.test.tsx | 7 +- .../kibana/feature_table/feature_table.tsx | 32 +- .../privileges/kibana/feature_table/index.ts | 0 .../__fixtures__/build_role.ts | 2 +- .../__fixtures__/common_allowed_privileges.ts | 0 .../default_privilege_definition.ts | 2 +- .../__fixtures__/index.ts | 0 .../kibana_privilege_calculator/index.ts | 0 ...bana_allowed_privileges_calculator.test.ts | 2 +- .../kibana_allowed_privileges_calculator.ts | 11 +- .../kibana_base_privilege_calculator.test.ts | 6 +- .../kibana_base_privilege_calculator.ts | 9 +- ...ibana_feature_privilege_calculator.test.ts | 6 +- .../kibana_feature_privilege_calculator.ts | 13 +- .../kibana_privilege_calculator.test.ts | 4 +- .../kibana_privilege_calculator.ts | 5 +- .../kibana_privilege_calculator_types.ts | 0 .../kibana_privileges_calculator_factory.ts | 12 +- .../kibana/kibana_privileges_region.test.tsx | 4 +- .../kibana/kibana_privileges_region.tsx | 17 +- .../simple_privilege_section.test.tsx.snap | 0 .../kibana/simple_privilege_section/index.ts | 0 .../privilege_selector.tsx | 2 +- .../simple_privilege_section.test.tsx | 8 +- .../simple_privilege_section.tsx | 21 +- .../unsupported_space_privileges_warning.tsx | 0 .../__fixtures__/index.ts | 0 .../__fixtures__/raw_kibana_privileges.ts | 2 +- .../privilege_display.test.tsx.snap | 0 .../privilege_space_form.test.tsx.snap | 102 --- ...pace_aware_privilege_section.test.tsx.snap | 0 .../space_aware_privilege_section/_index.scss | 0 .../_privilege_matrix.scss | 2 +- .../space_aware_privilege_section/index.ts | 0 .../privilege_display.test.tsx | 2 +- .../privilege_display.tsx | 7 +- .../privilege_matrix.test.tsx | 10 +- .../privilege_matrix.tsx | 17 +- .../privilege_space_form.test.tsx | 4 +- .../privilege_space_form.tsx | 14 +- .../privilege_space_table.test.tsx | 4 +- .../privilege_space_table.tsx | 15 +- .../space_aware_privilege_section.test.tsx | 6 +- .../space_aware_privilege_section.tsx | 15 +- .../space_selector.tsx | 4 +- .../kibana/transform_error_section/index.ts | 0 .../transform_error_section.tsx | 0 .../edit_role}/reserved_role_badge.test.tsx | 2 +- .../roles/edit_role}/reserved_role_badge.tsx | 3 +- .../edit_role/spaces_popover_list/_index.scss | 1 + .../_spaces_popover_list.scss | 0 .../edit_role}/spaces_popover_list/index.ts | 0 .../spaces_popover_list.tsx | 7 +- .../roles/edit_role}/validate_role.test.ts | 2 +- .../roles/edit_role}/validate_role.ts | 2 +- .../public/management/roles/index.mock.ts | 9 + .../security/public/management/roles/index.ts | 8 + .../roles/indices_api_client.mock.ts | 11 + .../management/roles/indices_api_client.ts | 18 + .../roles/privileges_api_client.mock.ts | 12 + .../management/roles/privileges_api_client.ts | 22 + .../management/roles/roles_api_client.mock.ts | 14 + .../roles/roles_api_client.test.ts} | 73 +- .../management/roles/roles_api_client.ts | 62 ++ .../roles_grid_page.test.tsx.snap | 0 .../confirm_delete/confirm_delete.tsx | 57 +- .../roles/roles_grid}/confirm_delete/index.ts | 0 .../management/roles/roles_grid}/index.ts | 0 .../roles_grid}/permission_denied/index.ts | 0 .../permission_denied/permission_denied.tsx | 0 .../roles_grid}/roles_grid_page.test.tsx | 89 ++- .../roles/roles_grid}/roles_grid_page.tsx | 89 +-- .../roles/roles_management_app.test.tsx | 160 ++++ .../management/roles/roles_management_app.tsx | 116 +++ .../public/management/users/_index.scss | 1 + .../change_password_form.test.tsx | 24 +- .../change_password_form.tsx | 16 +- .../components}/change_password_form/index.ts | 0 .../confirm_delete_users.test.tsx} | 53 +- .../confirm_delete_users.tsx} | 81 +- .../components/confirm_delete_users}/index.ts | 2 +- .../management/users/components/index.ts | 8 + .../users/edit_user/_edit_user_page.scss} | 0 .../management/users/edit_user/_index.scss | 1 + .../users/edit_user}/edit_user_page.test.tsx | 55 +- .../users/edit_user}/edit_user_page.tsx | 175 ++--- .../management/users/edit_user}/index.ts | 0 .../users/edit_user}/validate_user.test.ts | 2 +- .../users/edit_user}/validate_user.ts | 2 +- .../public/management/users/index.mock.ts} | 2 +- .../public/management/users}/index.ts | 5 +- .../management/users/user_api_client.mock.ts | 15 + .../management/users/user_api_client.ts | 45 ++ .../management/users/users_grid}/index.ts | 2 +- .../users_grid/users_grid_page.test.tsx} | 34 +- .../users/users_grid/users_grid_page.tsx} | 52 +- .../users/users_management_app.test.tsx | 131 ++++ .../management/users/users_management_app.tsx | 94 +++ x-pack/plugins/security/public/plugin.ts | 74 -- x-pack/plugins/security/public/plugin.tsx | 147 ++++ .../server/routes/role_mapping/get.ts | 2 +- x-pack/plugins/spaces/common/index.ts | 2 +- .../translations/translations/ja-JP.json | 15 - .../translations/translations/zh-CN.json | 15 - .../functional/apps/security/management.js | 2 +- .../functional/apps/security/role_mappings.ts | 2 +- 333 files changed, 4797 insertions(+), 4496 deletions(-) delete mode 100644 x-pack/legacy/plugins/security/common/model.ts delete mode 100644 x-pack/legacy/plugins/security/public/documentation_links.js delete mode 100644 x-pack/legacy/plugins/security/public/images/logout.svg delete mode 100644 x-pack/legacy/plugins/security/public/images/person.svg delete mode 100644 x-pack/legacy/plugins/security/public/lib/__tests__/util.js delete mode 100644 x-pack/legacy/plugins/security/public/lib/api.ts delete mode 100644 x-pack/legacy/plugins/security/public/lib/api_keys_api.ts delete mode 100644 x-pack/legacy/plugins/security/public/lib/role_utils.ts delete mode 100644 x-pack/legacy/plugins/security/public/lib/roles_api.ts delete mode 100644 x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts delete mode 100644 x-pack/legacy/plugins/security/public/lib/util.js delete mode 100644 x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts delete mode 100644 x-pack/legacy/plugins/security/public/objects/lib/roles.ts delete mode 100644 x-pack/legacy/plugins/security/public/register_feature.js delete mode 100644 x-pack/legacy/plugins/security/public/services/shield_indices.js delete mode 100644 x-pack/legacy/plugins/security/public/services/shield_role.js delete mode 100644 x-pack/legacy/plugins/security/public/views/account/account.html create mode 100644 x-pack/legacy/plugins/security/public/views/login/components/_index.scss create mode 100644 x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss rename x-pack/legacy/plugins/security/public/views/login/{_login.scss => components/login_page/_login_page.scss} (88%) delete mode 100644 x-pack/legacy/plugins/security/public/views/login/login.html rename x-pack/legacy/plugins/security/{common => public/views/login}/login_state.ts (100%) rename x-pack/legacy/plugins/security/public/{lib/__tests__/parse_next.js => views/login/parse_next.test.ts} (80%) rename x-pack/legacy/plugins/security/public/{lib => views/login}/parse_next.ts (100%) delete mode 100644 x-pack/legacy/plugins/security/public/views/management/_index.scss delete mode 100644 x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js delete mode 100644 x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss delete mode 100644 x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss delete mode 100644 x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_role/index.js delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js delete mode 100644 x-pack/legacy/plugins/security/public/views/management/management.js delete mode 100644 x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js delete mode 100644 x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss delete mode 100644 x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx delete mode 100644 x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx delete mode 100644 x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx delete mode 100644 x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx delete mode 100644 x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js delete mode 100644 x-pack/legacy/plugins/security/public/views/management/users_grid/users.html delete mode 100644 x-pack/legacy/plugins/security/public/views/management/users_grid/users.js rename x-pack/{legacy/plugins/security/public/lib/role_utils.test.ts => plugins/security/common/model/role.test.ts} (96%) create mode 100644 x-pack/plugins/security/public/_index.scss rename x-pack/{legacy/plugins/security/public/views/account/components => plugins/security/public/account_management}/account_management_page.test.tsx (72%) rename x-pack/{legacy/plugins/security/public/views/account/components => plugins/security/public/account_management}/account_management_page.tsx (62%) rename x-pack/{legacy/plugins/security/public/views/account/components => plugins/security/public/account_management}/change_password/change_password.tsx (81%) rename x-pack/{legacy/plugins/security/public/views/account/components => plugins/security/public/account_management}/change_password/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/account/components => plugins/security/public/account_management}/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/account/components => plugins/security/public/account_management}/personal_info/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/account/components => plugins/security/public/account_management}/personal_info/personal_info.tsx (89%) create mode 100644 x-pack/plugins/security/public/management/_index.scss create mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts create mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts create mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/__snapshots__/api_keys_grid_page.test.tsx.snap (91%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/api_keys_grid_page.test.tsx (65%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/api_keys_grid_page.tsx (90%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/empty_prompt/empty_prompt.tsx (89%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/empty_prompt/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/index.ts (81%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/invalidate_provider/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/invalidate_provider/invalidate_provider.tsx (91%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/not_enabled/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/not_enabled/not_enabled.tsx (78%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/permission_denied/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/components => plugins/security/public/management/api_keys/api_keys_grid}/permission_denied/permission_denied.tsx (100%) create mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx create mode 100644 x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx rename x-pack/{legacy/plugins/security/public/views/management/api_keys_grid/services => plugins/security/public/management/api_keys}/documentation_links.ts (51%) create mode 100644 x-pack/plugins/security/public/management/api_keys/index.mock.ts create mode 100644 x-pack/plugins/security/public/management/api_keys/index.ts create mode 100644 x-pack/plugins/security/public/management/index.ts create mode 100644 x-pack/plugins/security/public/management/management_service.test.ts create mode 100644 x-pack/plugins/security/public/management/management_service.ts rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/management_urls.ts (74%) create mode 100644 x-pack/plugins/security/public/management/role_mappings/_index.scss rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/delete_provider/delete_provider.test.tsx (64%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/delete_provider/delete_provider.tsx (93%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/delete_provider/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/no_compatible_realms/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx (78%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/permission_denied/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/permission_denied/permission_denied.tsx (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/section_loading/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/section_loading/section_loading.test.tsx (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/components/section_loading/section_loading.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/services => plugins/security/public/management/role_mappings}/documentation_links.ts (70%) create mode 100644 x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/_index.scss create mode 100644 x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/edit_role_mapping_page.tsx (87%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/mapping_info_panel/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/mapping_info_panel/mapping_info_panel.test.tsx (82%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/mapping_info_panel/mapping_info_panel.tsx (95%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/role_selector/add_role_template_button.test.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/role_selector/add_role_template_button.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/role_selector/index.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/role_selector/role_selector.test.tsx (85%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/role_selector/role_selector.tsx (94%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/role_selector/role_template_editor.test.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/role_selector/role_template_editor.tsx (98%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/role_selector/role_template_type_select.tsx (92%) create mode 100644 x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss => plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss} (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/add_rule_button.test.tsx (97%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/add_rule_button.tsx (97%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/field_rule_editor.test.tsx (99%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/field_rule_editor.tsx (99%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/index.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/json_rule_editor.test.tsx (89%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/json_rule_editor.tsx (95%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/rule_editor_panel.test.tsx (88%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/rule_editor_panel.tsx (94%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/rule_group_editor.test.tsx (99%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/rule_group_editor.tsx (97%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/rule_group_title.tsx (97%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/visual_rule_editor.test.tsx (99%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components => plugins/security/public/management/role_mappings/edit_role_mapping}/rule_editor_panel/visual_rule_editor.tsx (95%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/edit_role_mapping/services/is_rule_group.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts (98%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts (97%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts (96%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/edit_role_mapping/services/role_template_type.ts (96%) create mode 100644 x-pack/plugins/security/public/management/role_mappings/index.mock.ts create mode 100644 x-pack/plugins/security/public/management/role_mappings/index.ts rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/all_rule.test.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/all_rule.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/any_rule.test.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/any_rule.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/except_all_rule.test.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/except_all_rule.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/except_any_rule.test.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/except_any_rule.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/field_rule.test.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/field_rule.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/rule.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/rule_builder.test.ts (99%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/rule_builder.ts (99%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/rule_builder_error.ts (100%) rename x-pack/{legacy/plugins/security/public/views => plugins/security/public}/management/role_mappings/model/rule_group.ts (94%) create mode 100644 x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts rename x-pack/{legacy/plugins/security/public/lib/role_mappings_api.ts => plugins/security/public/management/role_mappings/role_mappings_api_client.ts} (89%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components => plugins/security/public/management/role_mappings/role_mappings_grid}/create_role_mapping_button/create_role_mapping_button.tsx (90%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components => plugins/security/public/management/role_mappings/role_mappings_grid}/create_role_mapping_button/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components => plugins/security/public/management/role_mappings/role_mappings_grid}/empty_prompt/empty_prompt.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components => plugins/security/public/management/role_mappings/role_mappings_grid}/empty_prompt/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components => plugins/security/public/management/role_mappings/role_mappings_grid}/index.ts (100%) create mode 100644 x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx rename x-pack/{legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components => plugins/security/public/management/role_mappings/role_mappings_grid}/role_mappings_grid_page.tsx (93%) create mode 100644 x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx create mode 100644 x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx create mode 100644 x-pack/plugins/security/public/management/roles/_index.scss create mode 100644 x-pack/plugins/security/public/management/roles/documentation_links.ts rename x-pack/{legacy/plugins/security/public/views/management/edit_role/lib => plugins/security/public/management/roles/edit_role}/__snapshots__/validate_role.test.ts.snap (100%) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/_index.scss rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/collapsible_panel/_collapsible_panel.scss (100%) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/collapsible_panel/collapsible_panel.test.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/collapsible_panel/collapsible_panel.tsx (99%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/collapsible_panel/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/delete_role_button.test.tsx (94%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/delete_role_button.tsx (95%) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/index.ts (100%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role}/privilege_utils.test.ts (100%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role}/privilege_utils.ts (93%) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap (87%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/__snapshots__/index_privileges.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/cluster_privileges.test.tsx (96%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/cluster_privileges.tsx (93%) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/elasticsearch_privileges.tsx (89%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/index_privilege_form.test.tsx (99%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/index_privilege_form.tsx (98%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/index_privileges.test.tsx (74%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/es/index_privileges.tsx (84%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap (100%) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss rename x-pack/{legacy/plugins/security/public/views/management/edit_role/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/constants.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss => plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss} (100%) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/feature_table/change_all_privileges.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/feature_table/feature_table.test.tsx (94%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/feature_table/feature_table.tsx (91%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/feature_table/index.ts (100%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/__fixtures__/build_role.ts (90%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts (100%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts (94%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/__fixtures__/index.ts (100%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/index.ts (100%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts (99%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts (94%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_base_privilege_calculator.test.ts (98%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_base_privilege_calculator.ts (91%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts (99%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts (94%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_privilege_calculator.test.ts (99%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_privilege_calculator.ts (96%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_privilege_calculator_types.ts (100%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/roles/edit_role/privileges/kibana}/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts (90%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/kibana_privileges_region.test.tsx (95%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/kibana_privileges_region.tsx (79%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/simple_privilege_section/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/simple_privilege_section/privilege_selector.tsx (95%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx (95%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx (94%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts (94%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap (82%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/_index.scss (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss (87%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx (96%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/privilege_display.tsx (96%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx (89%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx (93%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx (95%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx (96%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx (99%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx (94%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx (95%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx (93%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/space_aware_privilege_section/space_selector.tsx (93%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/transform_error_section/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/privileges/kibana/transform_error_section/transform_error_section.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/reserved_role_badge.test.tsx (96%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/reserved_role_badge.tsx (89%) create mode 100644 x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/spaces_popover_list/_spaces_popover_list.scss (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/spaces_popover_list/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/components => plugins/security/public/management/roles/edit_role}/spaces_popover_list/spaces_popover_list.tsx (95%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/lib => plugins/security/public/management/roles/edit_role}/validate_role.test.ts (99%) rename x-pack/{legacy/plugins/security/public/views/management/edit_role/lib => plugins/security/public/management/roles/edit_role}/validate_role.ts (98%) create mode 100644 x-pack/plugins/security/public/management/roles/index.mock.ts create mode 100644 x-pack/plugins/security/public/management/roles/index.ts create mode 100644 x-pack/plugins/security/public/management/roles/indices_api_client.mock.ts create mode 100644 x-pack/plugins/security/public/management/roles/indices_api_client.ts create mode 100644 x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts create mode 100644 x-pack/plugins/security/public/management/roles/privileges_api_client.ts create mode 100644 x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts rename x-pack/{legacy/plugins/security/public/lib/transform_role_for_save.test.ts => plugins/security/public/management/roles/roles_api_client.test.ts} (82%) create mode 100644 x-pack/plugins/security/public/management/roles/roles_api_client.ts rename x-pack/{legacy/plugins/security/public/views/management/roles_grid/components => plugins/security/public/management/roles/roles_grid}/__snapshots__/roles_grid_page.test.tsx.snap (100%) rename x-pack/{legacy/plugins/security/public/views/management/roles_grid/components => plugins/security/public/management/roles/roles_grid}/confirm_delete/confirm_delete.tsx (72%) rename x-pack/{legacy/plugins/security/public/views/management/roles_grid/components => plugins/security/public/management/roles/roles_grid}/confirm_delete/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/roles_grid/components => plugins/security/public/management/roles/roles_grid}/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/roles_grid/components => plugins/security/public/management/roles/roles_grid}/permission_denied/index.ts (100%) rename x-pack/{legacy/plugins/security/public/views/management/roles_grid/components => plugins/security/public/management/roles/roles_grid}/permission_denied/permission_denied.tsx (100%) rename x-pack/{legacy/plugins/security/public/views/management/roles_grid/components => plugins/security/public/management/roles/roles_grid}/roles_grid_page.test.tsx (63%) rename x-pack/{legacy/plugins/security/public/views/management/roles_grid/components => plugins/security/public/management/roles/roles_grid}/roles_grid_page.tsx (78%) create mode 100644 x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx create mode 100644 x-pack/plugins/security/public/management/roles/roles_management_app.tsx create mode 100644 x-pack/plugins/security/public/management/users/_index.scss rename x-pack/{legacy/plugins/security/public/components/management => plugins/security/public/management/users/components}/change_password_form/change_password_form.test.tsx (82%) rename x-pack/{legacy/plugins/security/public/components/management => plugins/security/public/management/users/components}/change_password_form/change_password_form.tsx (96%) rename x-pack/{legacy/plugins/security/public/components/management => plugins/security/public/management/users/components}/change_password_form/index.ts (100%) rename x-pack/{legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx => plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx} (58%) rename x-pack/{legacy/plugins/security/public/components/management/users/confirm_delete.tsx => plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx} (50%) rename x-pack/{legacy/plugins/security/public/components/management/users => plugins/security/public/management/users/components/confirm_delete_users}/index.ts (79%) create mode 100644 x-pack/plugins/security/public/management/users/components/index.ts rename x-pack/{legacy/plugins/security/public/views/management/edit_user/_users.scss => plugins/security/public/management/users/edit_user/_edit_user_page.scss} (100%) create mode 100644 x-pack/plugins/security/public/management/users/edit_user/_index.scss rename x-pack/{legacy/plugins/security/public/views/management/edit_user/components => plugins/security/public/management/users/edit_user}/edit_user_page.test.tsx (70%) rename x-pack/{legacy/plugins/security/public/views/management/edit_user/components => plugins/security/public/management/users/edit_user}/edit_user_page.tsx (77%) rename x-pack/{legacy/plugins/security/public/views/management/edit_user/components => plugins/security/public/management/users/edit_user}/index.ts (100%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/users/edit_user}/validate_user.test.ts (98%) rename x-pack/{legacy/plugins/security/public/lib => plugins/security/public/management/users/edit_user}/validate_user.ts (98%) rename x-pack/{legacy/plugins/security/public/views/management/index.js => plugins/security/public/management/users/index.mock.ts} (80%) rename x-pack/{legacy/plugins/security/public/objects => plugins/security/public/management/users}/index.ts (68%) create mode 100644 x-pack/plugins/security/public/management/users/user_api_client.mock.ts create mode 100644 x-pack/plugins/security/public/management/users/user_api_client.ts rename x-pack/{legacy/plugins/security/public/views/management/users_grid/components => plugins/security/public/management/users/users_grid}/index.ts (82%) rename x-pack/{legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx => plugins/security/public/management/users/users_grid/users_grid_page.test.tsx} (63%) rename x-pack/{legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx => plugins/security/public/management/users/users_grid/users_grid_page.tsx} (85%) create mode 100644 x-pack/plugins/security/public/management/users/users_management_app.test.tsx create mode 100644 x-pack/plugins/security/public/management/users/users_management_app.tsx delete mode 100644 x-pack/plugins/security/public/plugin.ts create mode 100644 x-pack/plugins/security/public/plugin.tsx diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index e633e00965c6ab..0c34a16c68e99d 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -448,23 +448,4 @@ describe('SavedObjectsClient', () => { `); }); }); - - it('maintains backwards compatibility by transforming http.fetch errors to be compatible with kfetch errors', () => { - const err = { - response: { ok: false, redirected: false, status: 409, statusText: 'Conflict' }, - body: 'response body', - }; - http.fetch.mockRejectedValue(err); - return expect(savedObjectsClient.get(doc.type, doc.id)).rejects.toMatchInlineSnapshot(` - Object { - "body": "response body", - "res": Object { - "ok": false, - "redirected": false, - "status": 409, - "statusText": "Conflict", - }, - } - `); - }); }); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index dab98ee66cdb10..ccb23793a85349 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -465,11 +465,7 @@ export class SavedObjectsClient { * uses `{response: {status: number}}`. */ private savedObjectsFetch(path: string, { method, query, body }: HttpFetchOptions) { - return this.http.fetch(path, { method, query, body }).catch(err => { - const kfetchError = Object.assign(err, { res: err.response }); - delete kfetchError.response; - return Promise.reject(kfetchError); - }); + return this.http.fetch(path, { method, query, body }); } } diff --git a/src/plugins/management/public/index.ts b/src/plugins/management/public/index.ts index faec466dbd6714..4ece75bbf36da3 100644 --- a/src/plugins/management/public/index.ts +++ b/src/plugins/management/public/index.ts @@ -24,7 +24,12 @@ export function plugin(initializerContext: PluginInitializerContext) { return new ManagementPlugin(); } -export { ManagementSetup, ManagementStart, RegisterManagementApp } from './types'; +export { + ManagementSetup, + ManagementStart, + RegisterManagementApp, + RegisterManagementAppArgs, +} from './types'; export { ManagementApp } from './management_app'; export { ManagementSection } from './management_section'; export { ManagementSidebarNav } from './components'; // for use in legacy management apps diff --git a/x-pack/legacy/plugins/security/common/model.ts b/x-pack/legacy/plugins/security/common/model.ts deleted file mode 100644 index 733e89f774db8c..00000000000000 --- a/x-pack/legacy/plugins/security/common/model.ts +++ /dev/null @@ -1,28 +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. - */ - -export { - ApiKey, - ApiKeyToInvalidate, - AuthenticatedUser, - BuiltinESPrivileges, - EditUser, - FeaturesPrivileges, - InlineRoleTemplate, - InvalidRoleTemplate, - KibanaPrivileges, - RawKibanaFeaturePrivileges, - RawKibanaPrivileges, - Role, - RoleIndexPrivilege, - RoleKibanaPrivilege, - RoleMapping, - RoleTemplate, - StoredRoleTemplate, - User, - canUserChangePassword, - getUserDisplayName, -} from '../../../../plugins/security/common/model'; diff --git a/x-pack/legacy/plugins/security/index.d.ts b/x-pack/legacy/plugins/security/index.d.ts index 18284c8be689a1..d453415f73376c 100644 --- a/x-pack/legacy/plugins/security/index.d.ts +++ b/x-pack/legacy/plugins/security/index.d.ts @@ -5,7 +5,7 @@ */ import { Legacy } from 'kibana'; -import { AuthenticatedUser } from './common/model'; +import { AuthenticatedUser } from '../../../plugins/security/public'; /** * Public interface of the security plugin. diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index bc403b803b8df0..4988c30a1398b0 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -40,8 +40,6 @@ export const security = kibana => }, uiExports: { - chromeNavControls: [], - managementSections: ['plugins/security/views/management'], styleSheetPaths: resolve(__dirname, 'public/index.scss'), apps: [ { @@ -76,7 +74,6 @@ export const security = kibana => 'plugins/security/hacks/on_unauthorized_response', 'plugins/security/hacks/register_account_management_app', ], - home: ['plugins/security/register_feature'], injectDefaultVars: server => { const securityPlugin = server.newPlatform.setup.plugins.security; if (!securityPlugin) { diff --git a/x-pack/legacy/plugins/security/public/documentation_links.js b/x-pack/legacy/plugins/security/public/documentation_links.js deleted file mode 100644 index 8050289b95e9df..00000000000000 --- a/x-pack/legacy/plugins/security/public/documentation_links.js +++ /dev/null @@ -1,16 +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 { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; - -const ES_REF_URL = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; - -export const documentationLinks = { - dashboardViewMode: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-view-modes.html`, - esClusterPrivileges: `${ES_REF_URL}/security-privileges.html#privileges-list-cluster`, - esIndicesPrivileges: `${ES_REF_URL}/security-privileges.html#privileges-list-indices`, - esRunAsPrivileges: `${ES_REF_URL}/security-privileges.html#_run_as_privilege`, -}; diff --git a/x-pack/legacy/plugins/security/public/images/logout.svg b/x-pack/legacy/plugins/security/public/images/logout.svg deleted file mode 100644 index d6533c07199040..00000000000000 --- a/x-pack/legacy/plugins/security/public/images/logout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/images/person.svg b/x-pack/legacy/plugins/security/public/images/person.svg deleted file mode 100644 index 988ddac8859d71..00000000000000 --- a/x-pack/legacy/plugins/security/public/images/person.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/index.scss b/x-pack/legacy/plugins/security/public/index.scss index 2d7696bed39890..187ad5231534d2 100644 --- a/x-pack/legacy/plugins/security/public/index.scss +++ b/x-pack/legacy/plugins/security/public/index.scss @@ -15,3 +15,6 @@ $secFormWidth: 460px; // Public views @import './views/index'; +// Styles of Kibana Platform plugin +@import '../../../../plugins/security/public/index'; + diff --git a/x-pack/legacy/plugins/security/public/lib/__tests__/util.js b/x-pack/legacy/plugins/security/public/lib/__tests__/util.js deleted file mode 100644 index 3f7d8aea53a85f..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/__tests__/util.js +++ /dev/null @@ -1,49 +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 expect from '@kbn/expect'; -import { toggle, toggleSort } from '../../../public/lib/util'; - -describe('util', () => { - describe('toggle', () => { - it('should add an item to a collection if not already included', () => { - const collection = [1, 2, 3, 4, 5]; - toggle(collection, 6); - expect(collection.indexOf(6)).to.be.above(0); - }); - - it('should remove an item from a collection if already included', () => { - const collection = [1, 2, 3, 4, 5]; - toggle(collection, 3); - expect(collection.indexOf(3)).to.be.below(0); - }); - }); - - describe('toggleSort', () => { - it('should toggle reverse if called with the same orderBy', () => { - const sort = { orderBy: 'foo', reverse: false }; - - toggleSort(sort, 'foo'); - expect(sort.reverse).to.be.true; - - toggleSort(sort, 'foo'); - expect(sort.reverse).to.be.false; - }); - - it('should change orderBy and set reverse to false when called with a different orderBy', () => { - const sort = { orderBy: 'foo', reverse: false }; - - toggleSort(sort, 'bar'); - expect(sort.orderBy).to.equal('bar'); - expect(sort.reverse).to.be.false; - - sort.reverse = true; - toggleSort(sort, 'foo'); - expect(sort.orderBy).to.equal('foo'); - expect(sort.reverse).to.be.false; - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/lib/api.ts b/x-pack/legacy/plugins/security/public/lib/api.ts deleted file mode 100644 index c5c6994bf4be36..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/api.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 { kfetch } from 'ui/kfetch'; -import { Role, User, EditUser } from '../../common/model'; - -const usersUrl = '/internal/security/users'; -const rolesUrl = '/api/security/role'; - -export class UserAPIClient { - public async getUsers(): Promise { - return await kfetch({ pathname: usersUrl }); - } - - public async getUser(username: string): Promise { - const url = `${usersUrl}/${encodeURIComponent(username)}`; - return await kfetch({ pathname: url }); - } - - public async deleteUser(username: string) { - const url = `${usersUrl}/${encodeURIComponent(username)}`; - await kfetch({ pathname: url, method: 'DELETE' }, {}); - } - - public async saveUser(user: EditUser) { - const url = `${usersUrl}/${encodeURIComponent(user.username)}`; - - await kfetch({ pathname: url, body: JSON.stringify(user), method: 'POST' }); - } - - public async getRoles(): Promise { - return await kfetch({ pathname: rolesUrl }); - } - - public async getRole(name: string): Promise { - const url = `${rolesUrl}/${encodeURIComponent(name)}`; - return await kfetch({ pathname: url }); - } - - public async changePassword(username: string, password: string, currentPassword: string) { - const data: Record = { - newPassword: password, - }; - if (currentPassword) { - data.password = currentPassword; - } - await kfetch({ - pathname: `${usersUrl}/${encodeURIComponent(username)}/password`, - method: 'POST', - body: JSON.stringify(data), - }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts deleted file mode 100644 index fbc0460c5908a7..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/api_keys_api.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 { kfetch } from 'ui/kfetch'; -import { ApiKey, ApiKeyToInvalidate } from '../../common/model'; - -interface CheckPrivilegesResponse { - areApiKeysEnabled: boolean; - isAdmin: boolean; -} - -interface InvalidateApiKeysResponse { - itemsInvalidated: ApiKeyToInvalidate[]; - errors: any[]; -} - -interface GetApiKeysResponse { - apiKeys: ApiKey[]; -} - -const apiKeysUrl = `/internal/security/api_key`; - -export class ApiKeysApi { - public static async checkPrivileges(): Promise { - return kfetch({ pathname: `${apiKeysUrl}/privileges` }); - } - - public static async getApiKeys(isAdmin: boolean = false): Promise { - const query = { - isAdmin, - }; - - return kfetch({ pathname: apiKeysUrl, query }); - } - - public static async invalidateApiKeys( - apiKeys: ApiKeyToInvalidate[], - isAdmin: boolean = false - ): Promise { - const pathname = `${apiKeysUrl}/invalidate`; - const body = JSON.stringify({ apiKeys, isAdmin }); - return kfetch({ pathname, method: 'POST', body }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/role_utils.ts b/x-pack/legacy/plugins/security/public/lib/role_utils.ts deleted file mode 100644 index c33b7385306fb8..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/role_utils.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 { cloneDeep, get } from 'lodash'; -import { Role } from '../../common/model'; - -/** - * Returns whether given role is enabled or not - * - * @param role Object Role JSON, as returned by roles API - * @return Boolean true if role is enabled; false otherwise - */ -export function isRoleEnabled(role: Partial) { - return get(role, 'transient_metadata.enabled', true); -} - -/** - * Returns whether given role is reserved or not. - * - * @param {role} the Role as returned by roles API - */ -export function isReservedRole(role: Partial) { - return get(role, 'metadata._reserved', false); -} - -/** - * Returns whether given role is editable through the UI or not. - * - * @param role the Role as returned by roles API - */ -export function isReadOnlyRole(role: Partial): boolean { - return isReservedRole(role) || !!(role._transform_error && role._transform_error.length > 0); -} - -/** - * Returns a deep copy of the role. - * - * @param role the Role to copy. - */ -export function copyRole(role: Role) { - return cloneDeep(role); -} - -/** - * Creates a deep copy of the role suitable for cloning. - * - * @param role the Role to clone. - */ -export function prepareRoleClone(role: Role): Role { - const clone = copyRole(role); - - clone.name = ''; - - return clone; -} diff --git a/x-pack/legacy/plugins/security/public/lib/roles_api.ts b/x-pack/legacy/plugins/security/public/lib/roles_api.ts deleted file mode 100644 index 20c1491ccaac69..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/roles_api.ts +++ /dev/null @@ -1,25 +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 { kfetch } from 'ui/kfetch'; -import { Role } from '../../common/model'; - -export class RolesApi { - public static async getRoles(): Promise { - return kfetch({ pathname: '/api/security/role' }); - } - - public static async getRole(roleName: string): Promise { - return kfetch({ pathname: `/api/security/role/${encodeURIComponent(roleName)}` }); - } - - public static async deleteRole(roleName: string) { - return kfetch({ - pathname: `/api/security/role/${encodeURIComponent(roleName)}`, - method: 'DELETE', - }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts b/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts deleted file mode 100644 index 861ba530050a13..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts +++ /dev/null @@ -1,41 +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 { Role, RoleIndexPrivilege } from '../../common/model'; -import { isGlobalPrivilegeDefinition } from './privilege_utils'; - -export function transformRoleForSave(role: Role, spacesEnabled: boolean) { - // Remove any placeholder index privileges - role.elasticsearch.indices = role.elasticsearch.indices.filter( - indexPrivilege => !isPlaceholderPrivilege(indexPrivilege) - ); - - // Remove any placeholder query entries - role.elasticsearch.indices.forEach(index => index.query || delete index.query); - - // If spaces are disabled, then do not persist any space privileges - if (!spacesEnabled) { - role.kibana = role.kibana.filter(isGlobalPrivilegeDefinition); - } - - role.kibana.forEach(kibanaPrivilege => { - // If a base privilege is defined, then do not persist feature privileges - if (kibanaPrivilege.base.length > 0) { - kibanaPrivilege.feature = {}; - } - }); - - delete role.name; - delete role.transient_metadata; - delete role._unrecognized_applications; - delete role._transform_error; - - return role; -} - -function isPlaceholderPrivilege(indexPrivilege: RoleIndexPrivilege) { - return indexPrivilege.names.length === 0; -} diff --git a/x-pack/legacy/plugins/security/public/lib/util.js b/x-pack/legacy/plugins/security/public/lib/util.js deleted file mode 100644 index bdf44aa3f10bb8..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/util.js +++ /dev/null @@ -1,19 +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. - */ - -export function toggle(collection, item) { - const i = collection.indexOf(item); - if (i >= 0) collection.splice(i, 1); - else collection.push(item); -} - -export function toggleSort(sort, orderBy) { - if (sort.orderBy === orderBy) sort.reverse = !sort.reverse; - else { - sort.orderBy = orderBy; - sort.reverse = false; - } -} diff --git a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts b/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts deleted file mode 100644 index 91d98782dab423..00000000000000 --- a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts +++ /dev/null @@ -1,15 +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 { IHttpResponse } from 'angular'; -import chrome from 'ui/chrome'; - -const apiBase = chrome.addBasePath(`/internal/security/fields`); - -export async function getFields($http: any, query: string): Promise { - return await $http - .get(`${apiBase}/${query}`) - .then((response: IHttpResponse) => response.data || []); -} diff --git a/x-pack/legacy/plugins/security/public/objects/lib/roles.ts b/x-pack/legacy/plugins/security/public/objects/lib/roles.ts deleted file mode 100644 index e33cbe4c6c031c..00000000000000 --- a/x-pack/legacy/plugins/security/public/objects/lib/roles.ts +++ /dev/null @@ -1,21 +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 chrome from 'ui/chrome'; -import { Role } from '../../../common/model'; -import { copyRole } from '../../lib/role_utils'; -import { transformRoleForSave } from '../../lib/transform_role_for_save'; - -const apiBase = chrome.addBasePath(`/api/security/role`); - -export async function saveRole($http: any, role: Role, spacesEnabled: boolean) { - const data = transformRoleForSave(copyRole(role), spacesEnabled); - - return await $http.put(`${apiBase}/${role.name}`, data); -} - -export async function deleteRole($http: any, name: string) { - return await $http.delete(`${apiBase}/${name}`); -} diff --git a/x-pack/legacy/plugins/security/public/register_feature.js b/x-pack/legacy/plugins/security/public/register_feature.js deleted file mode 100644 index c0bd42690b6fdb..00000000000000 --- a/x-pack/legacy/plugins/security/public/register_feature.js +++ /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 { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; - -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'security', - title: i18n.translate('xpack.security.registerFeature.securitySettingsTitle', { - defaultMessage: 'Security Settings', - }), - description: i18n.translate('xpack.security.registerFeature.securitySettingsDescription', { - defaultMessage: - 'Protect your data and easily manage who has access to what with users and roles.', - }), - icon: 'securityApp', - path: '/app/kibana#/management/security', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/services/shield_indices.js b/x-pack/legacy/plugins/security/public/services/shield_indices.js deleted file mode 100644 index 791fa6cb596488..00000000000000 --- a/x-pack/legacy/plugins/security/public/services/shield_indices.js +++ /dev/null @@ -1,18 +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 { uiModules } from 'ui/modules'; - -const module = uiModules.get('security', []); -module.service('shieldIndices', ($http, chrome) => { - return { - getFields: query => { - return $http - .get(chrome.addBasePath(`/internal/security/fields/${query}`)) - .then(response => response.data); - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/services/shield_role.js b/x-pack/legacy/plugins/security/public/services/shield_role.js deleted file mode 100644 index 261d3449a7a2d1..00000000000000 --- a/x-pack/legacy/plugins/security/public/services/shield_role.js +++ /dev/null @@ -1,30 +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 'angular-resource'; -import { omit } from 'lodash'; -import angular from 'angular'; -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('security', ['ngResource']); -module.service('ShieldRole', ($resource, chrome) => { - return $resource( - chrome.addBasePath('/api/security/role/:name'), - { - name: '@name', - }, - { - save: { - method: 'PUT', - transformRequest(data) { - return angular.toJson( - omit(data, 'name', 'transient_metadata', '_unrecognized_applications') - ); - }, - }, - } - ); -}); diff --git a/x-pack/legacy/plugins/security/public/views/_index.scss b/x-pack/legacy/plugins/security/public/views/_index.scss index b85a7e19973906..6c2a091adf536e 100644 --- a/x-pack/legacy/plugins/security/public/views/_index.scss +++ b/x-pack/legacy/plugins/security/public/views/_index.scss @@ -1,5 +1,2 @@ // Login styles @import './login/index'; - -// Management styles -@import './management/index'; diff --git a/x-pack/legacy/plugins/security/public/views/account/account.html b/x-pack/legacy/plugins/security/public/views/account/account.html deleted file mode 100644 index 0935c415b18295..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/account/account.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/x-pack/legacy/plugins/security/public/views/account/account.js b/x-pack/legacy/plugins/security/public/views/account/account.js index 70a7b8dce727ec..13abc44e08f965 100644 --- a/x-pack/legacy/plugins/security/public/views/account/account.js +++ b/x-pack/legacy/plugins/security/public/views/account/account.js @@ -4,17 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import routes from 'ui/routes'; -import template from './account.html'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { AccountManagementPage } from './components'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import routes from 'ui/routes'; routes.when('/account', { - template, + template: '
', k7Breadcrumbs: () => [ { text: i18n.translate('xpack.security.account.breadcrumb', { @@ -24,19 +21,15 @@ routes.when('/account', { ], controllerAs: 'accountController', controller($scope) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('userProfileReactRoot'); - if (elem) { - unmountComponentAtNode(elem); - } - }); $scope.$$postDigest(() => { + const domNode = document.getElementById('userProfileReactRoot'); + render( - - - , - document.getElementById('userProfileReactRoot') + , + domNode ); + + $scope.$on('$destroy', () => unmountComponentAtNode(domNode)); }); }, }); diff --git a/x-pack/legacy/plugins/security/public/views/login/_index.scss b/x-pack/legacy/plugins/security/public/views/login/_index.scss index 9f133940f79777..9083c8dc3b7751 100644 --- a/x-pack/legacy/plugins/security/public/views/login/_index.scss +++ b/x-pack/legacy/plugins/security/public/views/login/_index.scss @@ -5,5 +5,4 @@ // loginChart__legend--small // loginChart__legend-isLoading -@import 'login'; - +@import './components/index'; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/_index.scss b/x-pack/legacy/plugins/security/public/views/login/components/_index.scss new file mode 100644 index 00000000000000..a6f9598b9cc043 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/login/components/_index.scss @@ -0,0 +1 @@ +@import './login_page/index'; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx index 93451453a523ab..3a970d582bdc8d 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiCallOut } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; import { BasicLoginForm } from './basic_login_form'; const createMockHttp = ({ simulateError = false } = {}) => { diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index e6d3b5b7536b6a..c263381fbdb564 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -9,7 +9,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; import ReactMarkdown from 'react-markdown'; import { EuiText } from '@elastic/eui'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; interface Props { http: any; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss new file mode 100644 index 00000000000000..4dd2c0cabfb5e8 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss @@ -0,0 +1 @@ +@import './login_page'; diff --git a/x-pack/legacy/plugins/security/public/views/login/_login.scss b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss similarity index 88% rename from x-pack/legacy/plugins/security/public/views/login/_login.scss rename to x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss index 607e9e6ec5e3f3..cdfad55ee064af 100644 --- a/x-pack/legacy/plugins/security/public/views/login/_login.scss +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss @@ -1,4 +1,3 @@ - .loginWelcome { @include kibanaFullScreenGraphics; } @@ -16,10 +15,6 @@ margin-bottom: $euiSizeXL; } -.loginWelcome__footerAction { - margin-right: $euiSizeS; -} - .loginWelcome__content { position: relative; margin: auto; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx index c16db007bdbdcf..a0318d50a45e58 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { LoginLayout, LoginState } from '../../../../../common/login_state'; +import { LoginLayout, LoginState } from '../../login_state'; import { LoginPage } from './login_page'; const createMockHttp = ({ simulateError = false } = {}) => { diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx index e7e56947ca58f8..8035789a30e9df 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx @@ -19,7 +19,7 @@ import { EuiTitle, } from '@elastic/eui'; import classNames from 'classnames'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; import { BasicLoginForm } from '../basic_login_form'; import { DisabledLoginForm } from '../disabled_login_form'; diff --git a/x-pack/legacy/plugins/security/public/views/login/login.html b/x-pack/legacy/plugins/security/public/views/login/login.html deleted file mode 100644 index 2695fabdd63671..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/login/login.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/login/login.tsx b/x-pack/legacy/plugins/security/public/views/login/login.tsx index d9daf2d1f4d0de..0b89ac553c9a88 100644 --- a/x-pack/legacy/plugins/security/public/views/login/login.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/login.tsx @@ -6,16 +6,14 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { parseNext } from 'plugins/security/lib/parse_next'; import { LoginPage } from 'plugins/security/views/login/components'; -// @ts-ignore -import template from 'plugins/security/views/login/login.html'; import React from 'react'; import { render } from 'react-dom'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; import { parse } from 'url'; -import { LoginState } from '../../../common/login_state'; +import { parseNext } from './parse_next'; +import { LoginState } from './login_state'; const messageMap = { SESSION_EXPIRED: i18n.translate('xpack.security.login.sessionExpiredDescription', { defaultMessage: 'Your session has timed out. Please log in again.', @@ -31,7 +29,7 @@ interface AnyObject { (chrome as AnyObject) .setVisible(false) - .setRootTemplate(template) + .setRootTemplate('
') .setRootController( 'login', ( diff --git a/x-pack/legacy/plugins/security/common/login_state.ts b/x-pack/legacy/plugins/security/public/views/login/login_state.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/login_state.ts rename to x-pack/legacy/plugins/security/public/views/login/login_state.ts diff --git a/x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js b/x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts similarity index 80% rename from x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js rename to x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts index 7516433c77f83d..b5e6c7dca41d8f 100644 --- a/x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js +++ b/x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { parseNext } from '../parse_next'; +import { parseNext } from './parse_next'; describe('parseNext', () => { it('should return a function', () => { - expect(parseNext).to.be.a('function'); + expect(parseNext).toBeInstanceOf(Function); }); describe('with basePath defined', () => { @@ -17,14 +16,14 @@ describe('parseNext', () => { it('should return basePath with a trailing slash when next is not specified', () => { const basePath = '/iqf'; const href = `${basePath}/login`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); it('should properly handle next without hash', () => { const basePath = '/iqf'; const next = `${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(next); + expect(parseNext(href, basePath)).toEqual(next); }); it('should properly handle next with hash', () => { @@ -32,7 +31,7 @@ describe('parseNext', () => { const next = `${basePath}/app/kibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${next}#${hash}`); + expect(parseNext(href, basePath)).toEqual(`${next}#${hash}`); }); it('should properly decode special characters', () => { @@ -40,7 +39,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(basePath)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(decodeURIComponent(`${next}#${hash}`)); + expect(parseNext(href, basePath)).toEqual(decodeURIComponent(`${next}#${hash}`)); }); // to help prevent open redirect to a different url @@ -48,7 +47,7 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = `https://example.com${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different url by abusing encodings @@ -58,7 +57,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different port @@ -66,7 +65,7 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = `http://localhost:5601${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different port by abusing encodings @@ -76,7 +75,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different base path @@ -84,18 +83,18 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = '/notbasepath/app/kibana'; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // disallow network-path references it('should return / if next is url without protocol', () => { const nextWithTwoSlashes = '//example.com'; const hrefWithTwoSlashes = `/login?next=${nextWithTwoSlashes}`; - expect(parseNext(hrefWithTwoSlashes)).to.equal('/'); + expect(parseNext(hrefWithTwoSlashes)).toEqual('/'); const nextWithThreeSlashes = '///example.com'; const hrefWithThreeSlashes = `/login?next=${nextWithThreeSlashes}`; - expect(parseNext(hrefWithThreeSlashes)).to.equal('/'); + expect(parseNext(hrefWithThreeSlashes)).toEqual('/'); }); }); @@ -103,34 +102,34 @@ describe('parseNext', () => { // trailing slash is important since it must match the cookie path exactly it('should return / with a trailing slash when next is not specified', () => { const href = '/login'; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); it('should properly handle next without hash', () => { const next = '/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal(next); + expect(parseNext(href)).toEqual(next); }); it('should properly handle next with hash', () => { const next = '/app/kibana'; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal(`${next}#${hash}`); + expect(parseNext(href)).toEqual(`${next}#${hash}`); }); it('should properly decode special characters', () => { const next = '%2Fapp%2Fkibana'; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal(decodeURIComponent(`${next}#${hash}`)); + expect(parseNext(href)).toEqual(decodeURIComponent(`${next}#${hash}`)); }); // to help prevent open redirect to a different url it('should return / if next includes a protocol/hostname', () => { const next = 'https://example.com/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different url by abusing encodings @@ -139,14 +138,14 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different port it('should return / if next includes a port', () => { const next = 'http://localhost:5601/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different port by abusing encodings @@ -155,18 +154,18 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // disallow network-path references it('should return / if next is url without protocol', () => { const nextWithTwoSlashes = '//example.com'; const hrefWithTwoSlashes = `/login?next=${nextWithTwoSlashes}`; - expect(parseNext(hrefWithTwoSlashes)).to.equal('/'); + expect(parseNext(hrefWithTwoSlashes)).toEqual('/'); const nextWithThreeSlashes = '///example.com'; const hrefWithThreeSlashes = `/login?next=${nextWithThreeSlashes}`; - expect(parseNext(hrefWithThreeSlashes)).to.equal('/'); + expect(parseNext(hrefWithThreeSlashes)).toEqual('/'); }); }); }); diff --git a/x-pack/legacy/plugins/security/public/lib/parse_next.ts b/x-pack/legacy/plugins/security/public/views/login/parse_next.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/lib/parse_next.ts rename to x-pack/legacy/plugins/security/public/views/login/parse_next.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/_index.scss b/x-pack/legacy/plugins/security/public/views/management/_index.scss deleted file mode 100644 index 78b53845071e4a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './change_password_form/index'; -@import './edit_role/index'; -@import './edit_user/index'; -@import './role_mappings/edit_role_mapping/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html deleted file mode 100644 index e46c6f72b5d20e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js deleted file mode 100644 index e7143b10208148..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js +++ /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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from './api_keys.html'; -import { API_KEYS_PATH } from '../management_urls'; -import { getApiKeysBreadcrumbs } from '../breadcrumbs'; -import { I18nContext } from 'ui/i18n'; -import { ApiKeysGridPage } from './components'; - -routes.when(API_KEYS_PATH, { - template, - k7Breadcrumbs: getApiKeysBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('apiKeysGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts b/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts deleted file mode 100644 index 4ab7e45e848498..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts +++ /dev/null @@ -1,115 +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 { i18n } from '@kbn/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management/breadcrumbs'; - -export function getUsersBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.users.breadcrumb', { - defaultMessage: 'Users', - }), - href: '#/management/security/users', - }, - ]; -} - -export function getEditUserBreadcrumbs($route: Record) { - const { username } = $route.current.params; - return [ - ...getUsersBreadcrumbs(), - { - text: username, - href: `#/management/security/users/edit/${username}`, - }, - ]; -} - -export function getCreateUserBreadcrumbs() { - return [ - ...getUsersBreadcrumbs(), - { - text: i18n.translate('xpack.security.users.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]; -} - -export function getRolesBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.roles.breadcrumb', { - defaultMessage: 'Roles', - }), - href: '#/management/security/roles', - }, - ]; -} - -export function getEditRoleBreadcrumbs($route: Record) { - const { name } = $route.current.params; - return [ - ...getRolesBreadcrumbs(), - { - text: name, - href: `#/management/security/roles/edit/${name}`, - }, - ]; -} - -export function getCreateRoleBreadcrumbs() { - return [ - ...getUsersBreadcrumbs(), - { - text: i18n.translate('xpack.security.roles.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]; -} - -export function getApiKeysBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.apiKeys.breadcrumb', { - defaultMessage: 'API Keys', - }), - href: '#/management/security/api_keys', - }, - ]; -} - -export function getRoleMappingBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.roleMapping.breadcrumb', { - defaultMessage: 'Role Mappings', - }), - href: '#/management/security/role_mappings', - }, - ]; -} - -export function getEditRoleMappingBreadcrumbs($route: Record) { - const { name } = $route.current.params; - return [ - ...getRoleMappingBreadcrumbs(), - { - text: - name || - i18n.translate('xpack.security.roleMappings.createBreadcrumb', { - defaultMessage: 'Create', - }), - href: `#/management/security/role_mappings/edit/${name}`, - }, - ]; -} diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss b/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss deleted file mode 100644 index 98331c2070a31e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss +++ /dev/null @@ -1,17 +0,0 @@ -.secChangePasswordForm__panel { - max-width: $secFormWidth; -} - -.secChangePasswordForm__subLabel { - margin-bottom: $euiSizeS; -} - -.secChangePasswordForm__footer { - display: flex; - justify-content: flex-start; - align-items: center; - - .kuiButton + .kuiButton { - margin-left: $euiSizeS; - } -} diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss b/x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss deleted file mode 100644 index a6058b5ddebbf0..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './change_password_form'; diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html b/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html deleted file mode 100644 index 92fb95861a6f8e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html +++ /dev/null @@ -1,141 +0,0 @@ - - -
- - - - - - -
- -
- - - - -
-
- - -
- - - - -
-
- - -
- - - - -
- - -
- - -
-
- - - -
-
-
diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js b/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js deleted file mode 100644 index d9aa59f6df1427..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiModules } from 'ui/modules'; -import template from './change_password_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnChangePasswordForm', function() { - return { - template, - scope: { - requireCurrentPassword: '=', - showKibanaWarning: '=', - onChangePassword: '&', - }, - restrict: 'E', - replace: true, - controllerAs: 'changePasswordController', - controller: function($scope) { - this.currentPassword = null; - this.newPassword = null; - this.newPasswordConfirmation = null; - this.isFormVisible = false; - this.isIncorrectPassword = false; - - this.showForm = () => { - this.isFormVisible = true; - }; - - this.hideForm = () => { - $scope.changePasswordForm.$setPristine(); - $scope.changePasswordForm.$setUntouched(); - this.currentPassword = null; - this.newPassword = null; - this.newPasswordConfirmation = null; - this.isFormVisible = false; - this.isIncorrectPassword = false; - }; - - this.onIncorrectPassword = () => { - this.isIncorrectPassword = true; - }; - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss deleted file mode 100644 index 192091fb04e3c9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss deleted file mode 100644 index 32b3832e7a9fa9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import './collapsible_panel/collapsible_panel'; -@import './privileges/kibana/space_aware_privilege_section/index'; -@import './privileges/kibana/feature_table/index'; -@import './spaces_popover_list/spaces_popover_list'; - -.secPrivilegeFeatureIcon { - flex-shrink: 0; - margin-right: $euiSizeS; -} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx deleted file mode 100644 index 67c32c8393171f..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx +++ /dev/null @@ -1,716 +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 { ReactWrapper } from 'enzyme'; -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UICapabilities } from 'ui/capabilities'; -import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/public'; -// These modules should be moved into a common directory -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Actions } from '../../../../../../../../plugins/security/server/authorization/actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { privilegesFactory } from '../../../../../../../../plugins/security/server/authorization/privileges'; -import { RawKibanaPrivileges, Role } from '../../../../../common/model'; -import { EditRolePage } from './edit_role_page'; -import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; -import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; -import { TransformErrorSection } from './privileges/kibana/transform_error_section'; - -const buildFeatures = () => { - return [ - { - id: 'feature1', - name: 'Feature 1', - icon: 'addDataApp', - app: ['feature1App'], - privileges: { - all: { - app: ['feature1App'], - ui: ['feature1-ui'], - savedObject: { - all: [], - read: [], - }, - }, - }, - }, - { - id: 'feature2', - name: 'Feature 2', - icon: 'addDataApp', - app: ['feature2App'], - privileges: { - all: { - app: ['feature2App'], - ui: ['feature2-ui'], - savedObject: { - all: ['feature2'], - read: ['config'], - }, - }, - }, - }, - ] as Feature[]; -}; - -const buildRawKibanaPrivileges = () => { - return privilegesFactory(new Actions('unit_test_version'), { - getFeatures: () => buildFeatures(), - }).get(); -}; - -const buildBuiltinESPrivileges = () => { - return { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }; -}; - -const buildUICapabilities = (canManageSpaces = true) => { - return { - catalogue: {}, - management: {}, - navLinks: {}, - spaces: { - manage: canManageSpaces, - }, - } as UICapabilities; -}; - -const buildSpaces = () => { - return [ - { - id: 'default', - name: 'Default', - disabledFeatures: [], - _reserved: true, - }, - { - id: 'space_1', - name: 'Space 1', - disabledFeatures: [], - }, - { - id: 'space_2', - name: 'Space 2', - disabledFeatures: ['feature2'], - }, - ] as Space[]; -}; - -const expectReadOnlyFormButtons = (wrapper: ReactWrapper) => { - expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(1); - expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(0); -}; - -const expectSaveFormButtons = (wrapper: ReactWrapper) => { - expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(0); - expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(1); -}; - -describe('', () => { - describe('with spaces enabled', () => { - it('can render a reserved role', () => { - const role: Role = { - name: 'superuser', - metadata: { - _reserved: true, - }, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', () => { - // @ts-ignore - const role: Role = { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', () => { - const role: Role = { - metadata: { - _reserved: false, - }, - name: '', - elasticsearch: { - cluster: ['all', 'manage'], - indices: [ - { - names: ['foo*'], - privileges: ['all'], - field_security: { - except: ['f'], - grant: ['b*'], - }, - }, - ], - run_as: ['elastic'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('renders an auth error when not authorized to manage spaces', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(1); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [], - _transform_error: ['kibana'], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); - - describe('with spaces disabled', () => { - it('can render a reserved role', () => { - const role: Role = { - name: 'superuser', - metadata: { - _reserved: true, - }, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', () => { - // @ts-ignore - const role: Role = { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', () => { - const role: Role = { - metadata: { - _reserved: false, - }, - name: '', - elasticsearch: { - cluster: ['all', 'manage'], - indices: [ - { - names: ['foo*'], - privileges: ['all'], - field_security: { - except: ['f'], - grant: ['b*'], - }, - }, - ], - run_as: ['elastic'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('does not care if user cannot manage spaces', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(0); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [], - _transform_error: ['kibana'], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx deleted file mode 100644 index 2ba012afa689dc..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ /dev/null @@ -1,409 +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 { - EuiButton, - EuiButtonEmpty, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { toastNotifications } from 'ui/notify'; -import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { - KibanaPrivileges, - RawKibanaPrivileges, - Role, - BuiltinESPrivileges, -} from '../../../../../common/model'; -import { - isReadOnlyRole, - isReservedRole, - copyRole, - prepareRoleClone, -} from '../../../../lib/role_utils'; -import { deleteRole, saveRole } from '../../../../objects'; -import { ROLES_PATH } from '../../management_urls'; -import { RoleValidationResult, RoleValidator } from '../lib/validate_role'; -import { DeleteRoleButton } from './delete_role_button'; -import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; -import { ReservedRoleBadge } from './reserved_role_badge'; - -interface Props { - action: 'edit' | 'clone'; - role: Role; - runAsUsers: string[]; - indexPatterns: string[]; - httpClient: any; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; - kibanaPrivileges: RawKibanaPrivileges; - builtinESPrivileges: BuiltinESPrivileges; - spaces?: Space[]; - spacesEnabled: boolean; - intl: InjectedIntl; - uiCapabilities: UICapabilities; - features: Feature[]; -} - -interface State { - role: Role; - formError: RoleValidationResult | null; -} - -class EditRolePageUI extends Component { - private validator: RoleValidator; - - constructor(props: Props) { - super(props); - - this.validator = new RoleValidator({ shouldValidate: false }); - - let role: Role; - if (props.action === 'clone') { - role = prepareRoleClone(props.role); - } else { - role = copyRole(props.role); - } - - this.state = { - role, - formError: null, - }; - } - - public componentDidMount() { - if (this.props.action === 'clone' && isReservedRole(this.props.role)) { - this.backToRoleList(); - } - } - - public render() { - const description = this.props.spacesEnabled ? ( - - ) : ( - - ); - - return ( -
- - {this.getFormTitle()} - - - - {description} - - {isReservedRole(this.state.role) && ( - - - -

- -

-
-
- )} - - - - {this.getRoleName()} - - {this.getElasticsearchPrivileges()} - - {this.getKibanaPrivileges()} - - - - {this.getFormButtons()} -
-
- ); - } - - private getFormTitle = () => { - let titleText; - const props: HTMLProps = { - tabIndex: 0, - }; - if (isReservedRole(this.state.role)) { - titleText = ( - - ); - props['aria-describedby'] = 'reservedRoleDescription'; - } else if (this.editingExistingRole()) { - titleText = ( - - ); - } else { - titleText = ( - - ); - } - - return ( - -

- {titleText} -

-
- ); - }; - - private getActionButton = () => { - if (this.editingExistingRole() && !isReadOnlyRole(this.state.role)) { - return ( - - - - ); - } - - return null; - }; - - private getRoleName = () => { - return ( - - - } - helpText={ - !isReservedRole(this.state.role) && this.editingExistingRole() ? ( - - ) : ( - undefined - ) - } - {...this.validator.validateRoleName(this.state.role)} - > - - - - ); - }; - - private onNameChange = (e: ChangeEvent) => { - const rawValue = e.target.value; - const name = rawValue.replace(/\s/g, '_'); - - this.setState({ - role: { - ...this.state.role, - name, - }, - }); - }; - - private getElasticsearchPrivileges() { - return ( -
- - -
- ); - } - - private onRoleChange = (role: Role) => { - this.setState({ - role, - }); - }; - - private getKibanaPrivileges = () => { - return ( -
- - -
- ); - }; - - private getFormButtons = () => { - if (isReadOnlyRole(this.state.role)) { - return this.getReturnToRoleListButton(); - } - - return ( - - {this.getSaveButton()} - {this.getCancelButton()} - - {this.getActionButton()} - - ); - }; - - private getReturnToRoleListButton = () => { - return ( - - - - ); - }; - - private getSaveButton = () => { - const saveText = this.editingExistingRole() ? ( - - ) : ( - - ); - - return ( - - {saveText} - - ); - }; - - private getCancelButton = () => { - return ( - - - - ); - }; - - private editingExistingRole = () => { - return !!this.props.role.name && this.props.action === 'edit'; - }; - - private saveRole = () => { - this.validator.enableValidation(); - - const result = this.validator.validateForSave(this.state.role); - if (result.isInvalid) { - this.setState({ - formError: result, - }); - } else { - this.setState({ - formError: null, - }); - - const { httpClient, intl, spacesEnabled } = this.props; - - saveRole(httpClient, this.state.role, spacesEnabled) - .then(() => { - toastNotifications.addSuccess( - intl.formatMessage({ - id: 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', - defaultMessage: 'Saved role', - }) - ); - this.backToRoleList(); - }) - .catch((error: any) => { - toastNotifications.addDanger(get(error, 'data.message')); - }); - } - }; - - private handleDeleteRole = () => { - const { httpClient, role, intl } = this.props; - - deleteRole(httpClient, role.name) - .then(() => { - toastNotifications.addSuccess( - intl.formatMessage({ - id: 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted role', - }) - ); - this.backToRoleList(); - }) - .catch((error: any) => { - toastNotifications.addDanger(get(error, 'data.message')); - }); - }; - - private backToRoleList = () => { - window.location.hash = ROLES_PATH; - }; -} - -export const EditRolePage = injectI18n(EditRolePageUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx deleted file mode 100644 index 5ba3d1daf61acb..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx +++ /dev/null @@ -1,96 +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 React from 'react'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { RoleValidator } from '../../../lib/validate_role'; -import { ClusterPrivileges } from './cluster_privileges'; -import { ElasticsearchPrivileges } from './elasticsearch_privileges'; -import { IndexPrivileges } from './index_privileges'; - -test('it renders without crashing', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); - -test('it renders ClusterPrivileges', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = mountWithIntl(); - expect(wrapper.find(ClusterPrivileges)).toHaveLength(1); -}); - -test('it renders IndexPrivileges', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = mountWithIntl(); - expect(wrapper.find(IndexPrivileges)).toHaveLength(1); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html b/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html deleted file mode 100644 index ca4073dcad6f5c..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js deleted file mode 100644 index 27c9beb4ba8284..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js +++ /dev/null @@ -1,176 +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 _ from 'lodash'; -import routes from 'ui/routes'; -import { capabilities } from 'ui/capabilities'; -import { kfetch } from 'ui/kfetch'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; -import template from 'plugins/security/views/management/edit_role/edit_role.html'; -import 'plugins/security/services/shield_role'; -import 'plugins/security/services/shield_indices'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { UserAPIClient } from '../../../lib/api'; -import { ROLES_PATH, CLONE_ROLES_PATH, EDIT_ROLES_PATH } from '../management_urls'; -import { getEditRoleBreadcrumbs, getCreateRoleBreadcrumbs } from '../breadcrumbs'; - -import { EditRolePage } from './components'; - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { i18n } from '@kbn/i18n'; - -const routeDefinition = action => ({ - template, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke( - action === 'edit' && $route.current.params.name - ? getEditRoleBreadcrumbs - : getCreateRoleBreadcrumbs - ), - resolve: { - role($route, ShieldRole, Promise, kbnUrl) { - const name = $route.current.params.name; - - let role; - - if (name != null) { - role = ShieldRole.get({ name }).$promise.catch(response => { - if (response.status === 404) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.security.management.roles.roleNotFound', { - defaultMessage: 'No "{roleName}" role found.', - values: { roleName: name }, - }), - }); - kbnUrl.redirect(ROLES_PATH); - } else { - return fatalError(response); - } - }); - } else { - role = Promise.resolve( - new ShieldRole({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _unrecognized_applications: [], - }) - ); - } - - return role.then(res => res.toJSON()); - }, - users() { - return new UserAPIClient().getUsers().then(users => _.map(users, 'username')); - }, - indexPatterns() { - return npStart.plugins.data.indexPatterns.getTitles(); - }, - spaces(spacesEnabled) { - if (spacesEnabled) { - return kfetch({ method: 'get', pathname: '/api/spaces/space' }); - } - return []; - }, - kibanaPrivileges() { - return kfetch({ - method: 'get', - pathname: '/api/security/privileges', - query: { includeActions: true }, - }); - }, - builtinESPrivileges() { - return kfetch({ method: 'get', pathname: '/internal/security/esPrivileges/builtin' }); - }, - features() { - return kfetch({ method: 'get', pathname: '/api/features' }).catch(e => { - // TODO: This check can be removed once all of these `resolve` entries are moved out of Angular and into the React app. - const unauthorizedForFeatures = _.get(e, 'body.statusCode') === 404; - if (unauthorizedForFeatures) { - return []; - } - throw e; - }); - }, - }, - controllerAs: 'editRole', - controller($injector, $scope, $http, enableSpaceAwarePrivileges) { - const $route = $injector.get('$route'); - const role = $route.current.locals.role; - - const allowDocumentLevelSecurity = xpackInfo.get( - 'features.security.allowRoleDocumentLevelSecurity' - ); - const allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity'); - if (role.elasticsearch.indices.length === 0) { - const emptyOption = { - names: [], - privileges: [], - }; - - if (allowFieldLevelSecurity) { - emptyOption.field_security = { - grant: ['*'], - except: [], - }; - } - - if (allowDocumentLevelSecurity) { - emptyOption.query = ''; - } - - role.elasticsearch.indices.push(emptyOption); - } - - const { - users, - indexPatterns, - spaces, - kibanaPrivileges, - builtinESPrivileges, - features, - } = $route.current.locals; - - $scope.$$postDigest(async () => { - const domNode = document.getElementById('editRoleReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); - -routes.when(`${CLONE_ROLES_PATH}/:name`, routeDefinition('clone')); -routes.when(`${EDIT_ROLES_PATH}/:name?`, routeDefinition('edit')); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss deleted file mode 100644 index c5da74aa3f785c..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './users'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html b/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html deleted file mode 100644 index 4fa2768480874a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js b/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js deleted file mode 100644 index ab218022c6ee64..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js +++ /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 routes from 'ui/routes'; -import template from 'plugins/security/views/management/edit_user/edit_user.html'; -import 'angular-resource'; -import 'ui/angular_ui_select'; -import 'plugins/security/services/shield_role'; -import { EDIT_USERS_PATH } from '../management_urls'; -import { EditUserPage } from './components'; -import { UserAPIClient } from '../../../lib/api'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { getEditUserBreadcrumbs, getCreateUserBreadcrumbs } from '../breadcrumbs'; - -const renderReact = (elem, changeUrl, username) => { - render( - - - , - elem - ); -}; - -routes.when(`${EDIT_USERS_PATH}/:username?`, { - template, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke( - $route.current.params.username ? getEditUserBreadcrumbs : getCreateUserBreadcrumbs - ), - controllerAs: 'editUser', - controller($scope, $route, kbnUrl) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('editUserReactRoot'); - if (elem) { - unmountComponentAtNode(elem); - } - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('editUserReactRoot'); - const username = $route.current.params.username; - const changeUrl = url => { - kbnUrl.change(url); - $scope.$apply(); - }; - renderReact(elem, changeUrl, username); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/management.js b/x-pack/legacy/plugins/security/public/views/management/management.js deleted file mode 100644 index f0369f232aeba8..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/management.js +++ /dev/null @@ -1,134 +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 'plugins/security/views/management/change_password_form/change_password_form'; -import 'plugins/security/views/management/password_form/password_form'; -import 'plugins/security/views/management/users_grid/users'; -import 'plugins/security/views/management/roles_grid/roles'; -import 'plugins/security/views/management/api_keys_grid/api_keys'; -import 'plugins/security/views/management/edit_user/edit_user'; -import 'plugins/security/views/management/edit_role/index'; -import 'plugins/security/views/management/role_mappings/role_mappings_grid'; -import 'plugins/security/views/management/role_mappings/edit_role_mapping'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { ROLES_PATH, USERS_PATH, API_KEYS_PATH, ROLE_MAPPINGS_PATH } from './management_urls'; - -import { management } from 'ui/management'; -import { npSetup } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; - -routes - .defaults(/^\/management\/security(\/|$)/, { - resolve: { - showLinks(kbnUrl, Promise) { - if (!xpackInfo.get('features.security.showLinks')) { - toastNotifications.addDanger({ - title: xpackInfo.get('features.security.linksMessage'), - }); - kbnUrl.redirect('/management'); - return Promise.halt(); - } - }, - }, - }) - .defaults(/\/management/, { - resolve: { - securityManagementSection: function() { - const showSecurityLinks = xpackInfo.get('features.security.showLinks'); - const showRoleMappingsManagementLink = xpackInfo.get( - 'features.security.showRoleMappingsManagement' - ); - - function deregisterSecurity() { - management.deregister('security'); - } - - function deregisterRoleMappingsManagement() { - if (management.hasItem('security')) { - const security = management.getSection('security'); - if (security.hasItem('roleMappings')) { - security.deregister('roleMappings'); - } - } - } - - function ensureSecurityRegistered() { - const registerSecurity = () => - management.register('security', { - display: i18n.translate('xpack.security.management.securityTitle', { - defaultMessage: 'Security', - }), - order: 100, - icon: 'securityApp', - }); - const getSecurity = () => management.getSection('security'); - - const security = management.hasItem('security') ? getSecurity() : registerSecurity(); - - if (!security.hasItem('users')) { - security.register('users', { - name: 'securityUsersLink', - order: 10, - display: i18n.translate('xpack.security.management.usersTitle', { - defaultMessage: 'Users', - }), - url: `#${USERS_PATH}`, - }); - } - - if (!security.hasItem('roles')) { - security.register('roles', { - name: 'securityRolesLink', - order: 20, - display: i18n.translate('xpack.security.management.rolesTitle', { - defaultMessage: 'Roles', - }), - url: `#${ROLES_PATH}`, - }); - } - - if (!security.hasItem('apiKeys')) { - security.register('apiKeys', { - name: 'securityApiKeysLink', - order: 30, - display: i18n.translate('xpack.security.management.apiKeysTitle', { - defaultMessage: 'API Keys', - }), - url: `#${API_KEYS_PATH}`, - }); - } - - if (showRoleMappingsManagementLink && !security.hasItem('roleMappings')) { - security.register('roleMappings', { - name: 'securityRoleMappingLink', - order: 30, - display: i18n.translate('xpack.security.management.roleMappingsTitle', { - defaultMessage: 'Role Mappings', - }), - url: `#${ROLE_MAPPINGS_PATH}`, - }); - } - } - - if (!showSecurityLinks) { - deregisterSecurity(); - } else { - if (!showRoleMappingsManagementLink) { - deregisterRoleMappingsManagement(); - } - - // getCurrentUser will reject if there is no authenticated user, so we prevent them from - // seeing the security management screens. - return npSetup.plugins.security.authc - .getCurrentUser() - .then(ensureSecurityRegistered) - .catch(deregisterSecurity); - } - }, - }, - }); diff --git a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html b/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html deleted file mode 100644 index 72956992100f5e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html +++ /dev/null @@ -1,53 +0,0 @@ - - -
- - - - -
-
- - -
- - - - -
-
-
diff --git a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js b/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js deleted file mode 100644 index edcccdb5e6e697..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js +++ /dev/null @@ -1,24 +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 { uiModules } from 'ui/modules'; -import template from './password_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnPasswordForm', function() { - return { - template, - scope: { - password: '=', - }, - restrict: 'E', - replace: true, - controllerAs: 'passwordController', - controller: function() { - this.confirmation = null; - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss deleted file mode 100644 index 80e08ebcf12267..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/rule_editor_panel/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx deleted file mode 100644 index 375a8d9f374a86..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx +++ /dev/null @@ -1,341 +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 React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from 'test_utils/find_test_subject'; - -// brace/ace uses the Worker class, which is not currently provided by JSDOM. -// This is not required for the tests to pass, but it rather suppresses lengthy -// warnings in the console which adds unnecessary noise to the test output. -import 'test_utils/stub_web_worker'; - -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { EditRoleMappingPage } from '.'; -import { NoCompatibleRealms, SectionLoading, PermissionDenied } from '../../components'; -import { VisualRuleEditor } from './rule_editor_panel/visual_rule_editor'; -import { JSONRuleEditor } from './rule_editor_panel/json_rule_editor'; -import { EuiComboBox } from '@elastic/eui'; - -jest.mock('../../../../../lib/roles_api', () => { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); - -describe('EditRoleMappingPage', () => { - it('allows a role mapping to be created', async () => { - const roleMappingsAPI = ({ - saveRoleMapping: jest.fn().mockResolvedValue(null), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - - await nextTick(); - wrapper.update(); - - findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { - target: { value: 'my-role-mapping' }, - }); - - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); - - findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); - - findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); - - expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ - name: 'my-role-mapping', - enabled: true, - roles: ['foo_role'], - role_templates: [], - rules: { - all: [{ field: { username: '*' } }], - }, - metadata: {}, - }); - }); - - it('allows a role mapping to be updated', async () => { - const roleMappingsAPI = ({ - saveRoleMapping: jest.fn().mockResolvedValue(null), - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { id: 'foo' }, - }, - ], - enabled: true, - rules: { - any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], - }, - metadata: { - foo: 'bar', - bar: 'baz', - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); - - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); - - findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); - wrapper.find('button[id="addRuleOption"]').simulate('click'); - - findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); - - expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ - name: 'foo', - enabled: true, - roles: ['foo_role'], - role_templates: [], - rules: { - any: [ - { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, - { field: { username: '*' } }, - ], - }, - metadata: { - foo: 'bar', - bar: 'baz', - }, - }); - }); - - it('renders a permission denied message when unauthorized to manage role mappings', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: false, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(PermissionDenied)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(PermissionDenied)).toHaveLength(1); - }); - - it('renders a warning when there are no compatible realms enabled', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); - }); - - it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { id: 'foo' }, - }, - ], - enabled: true, - rules: { - field: { username: '*' }, - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); - }); - - it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { source: 'foo' }, - }, - ], - enabled: true, - rules: { - field: { username: '*' }, - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: false, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - }); - - it('renders the visual editor by default for simple rule sets', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - roles: ['superuser'], - enabled: true, - rules: { - all: [ - { - field: { - username: '*', - }, - }, - { - field: { - dn: null, - }, - }, - { - field: { - realm: ['ldap', 'pki', null, 12], - }, - }, - ], - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); - expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); - }); - - it('renders the JSON editor by default for complex rule sets', async () => { - const createRule = (depth: number): Record => { - if (depth > 0) { - const rule = { - all: [ - { - field: { - username: '*', - }, - }, - ], - } as Record; - - const subRule = createRule(depth - 1); - if (subRule) { - rule.all.push(subRule); - } - - return rule; - } - return null as any; - }; - - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - roles: ['superuser'], - enabled: true, - rules: createRule(10), - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); - expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html deleted file mode 100644 index ca8ab9c35c49ba..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx deleted file mode 100644 index b064a4dc50a228..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx +++ /dev/null @@ -1,45 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; -// @ts-ignore -import template from './edit_role_mapping.html'; -import { CREATE_ROLE_MAPPING_PATH } from '../../management_urls'; -import { getEditRoleMappingBreadcrumbs } from '../../breadcrumbs'; -import { EditRoleMappingPage } from './components'; - -routes.when(`${CREATE_ROLE_MAPPING_PATH}/:name?`, { - template, - k7Breadcrumbs: getEditRoleMappingBreadcrumbs, - controller($scope, $route) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('editRoleMappingReactRoot'); - - const { name } = $route.current.params; - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - if (domNode) { - unmountComponentAtNode(domNode); - } - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx deleted file mode 100644 index 259cdc71e25a22..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx +++ /dev/null @@ -1,182 +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 React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { RoleMappingsGridPage } from '.'; -import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../../components'; -import { EmptyPrompt } from './empty_prompt'; -import { findTestSubject } from 'test_utils/find_test_subject'; -import { EuiLink } from '@elastic/eui'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { act } from '@testing-library/react'; - -describe('RoleMappingsGridPage', () => { - it('renders an empty prompt when no role mappings exist', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(EmptyPrompt)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(EmptyPrompt)).toHaveLength(1); - }); - - it('renders a permission denied message when unauthorized to manage role mappings', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: false, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(PermissionDenied)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(PermissionDenied)).toHaveLength(1); - }); - - it('renders a warning when there are no compatible realms enabled', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - roles: [], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); - }); - - it('renders links to mapped roles', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - roles: ['superuser'], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); - expect(links).toHaveLength(1); - expect(links.at(0).props()).toMatchObject({ - href: '#/management/security/roles/edit/superuser', - }); - }); - - it('describes the number of mapped role templates', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - role_templates: [{}, {}], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - const templates = findTestSubject(wrapper, 'roleMappingRoles'); - expect(templates).toHaveLength(1); - expect(templates.text()).toEqual(`2 role templates defined`); - }); - - it('allows role mappings to be deleted, refreshing the grid after', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some-realm', - enabled: true, - roles: ['superuser'], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'some-realm', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); - expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); - - findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); - expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); - - await act(async () => { - findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); - await nextTick(); - wrapper.update(); - }); - - expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); - // Expect an additional API call to refresh the grid - expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx deleted file mode 100644 index 9e925d0fa6dc0a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx +++ /dev/null @@ -1,40 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; -// @ts-ignore -import template from './role_mappings.html'; -import { ROLE_MAPPINGS_PATH } from '../../management_urls'; -import { getRoleMappingBreadcrumbs } from '../../breadcrumbs'; -import { RoleMappingsGridPage } from './components'; - -routes.when(ROLE_MAPPINGS_PATH, { - template, - k7Breadcrumbs: getRoleMappingBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('roleMappingsGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - if (domNode) { - unmountComponentAtNode(domNode); - } - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html deleted file mode 100644 index cff3b821d132c0..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html b/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html deleted file mode 100644 index 0552b655afafd9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js b/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js deleted file mode 100644 index e9c42824711b31..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js +++ /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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/roles_grid/roles.html'; -import { ROLES_PATH } from '../management_urls'; -import { getRolesBreadcrumbs } from '../breadcrumbs'; -import { I18nContext } from 'ui/i18n'; -import { RolesGridPage } from './components'; - -routes.when(ROLES_PATH, { - template, - k7Breadcrumbs: getRolesBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('rolesGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html b/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html deleted file mode 100644 index 3dce7326d001ae..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js b/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js deleted file mode 100644 index 8d4e0526251d76..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js +++ /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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/users_grid/users.html'; -import { SECURITY_PATH, USERS_PATH } from '../management_urls'; -import { UsersListPage } from './components'; -import { UserAPIClient } from '../../../lib/api'; -import { I18nContext } from 'ui/i18n'; -import { getUsersBreadcrumbs } from '../breadcrumbs'; - -routes.when(SECURITY_PATH, { - redirectTo: USERS_PATH, -}); - -const renderReact = (elem, changeUrl) => { - render( - - - , - elem - ); -}; - -routes.when(USERS_PATH, { - template, - k7Breadcrumbs: getUsersBreadcrumbs, - controller($scope, $http, kbnUrl) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('usersReactRoot'); - if (elem) unmountComponentAtNode(elem); - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('usersReactRoot'); - const changeUrl = url => { - kbnUrl.change(url); - $scope.$apply(); - }; - renderReact(elem, $http, changeUrl); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx b/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx index fb39c517e1c2ca..4c79c499cc0e6a 100644 --- a/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx +++ b/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx @@ -11,8 +11,7 @@ import { render } from 'react-dom'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; import { npSetup } from 'ui/new_platform'; -import { SecurityPluginSetup } from '../../../../../../plugins/security/public'; -import { AuthenticatedUser } from '../../../common/model'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../../../../plugins/security/public'; import { AuthenticationStatePage } from '../../components/authentication_state_page'; chrome diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index f3c65ed7e3cf17..121791d113bd5d 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -10,7 +10,16 @@ export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; export { BuiltinESPrivileges } from './builtin_es_privileges'; export { FeaturesPrivileges } from './features_privileges'; export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; -export { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; +export { + Role, + RoleIndexPrivilege, + RoleKibanaPrivilege, + copyRole, + isReadOnlyRole, + isReservedRole, + isRoleEnabled, + prepareRoleClone, +} from './role'; export { KibanaPrivileges } from './kibana_privileges'; export { InlineRoleTemplate, diff --git a/x-pack/legacy/plugins/security/public/lib/role_utils.test.ts b/x-pack/plugins/security/common/model/role.test.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/lib/role_utils.test.ts rename to x-pack/plugins/security/common/model/role.test.ts index 9d94017c3f0fe4..d4a910a1785ebd 100644 --- a/x-pack/legacy/plugins/security/public/lib/role_utils.test.ts +++ b/x-pack/plugins/security/common/model/role.test.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Role } from '../../common/model'; -import { - copyRole, - isReadOnlyRole, - isReservedRole, - isRoleEnabled, - prepareRoleClone, -} from './role_utils'; +import { Role, isReadOnlyRole, isReservedRole, isRoleEnabled, copyRole, prepareRoleClone } from '.'; describe('role', () => { describe('isRoleEnabled', () => { diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 89f68aaa55b5cb..1edcf147262ede 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { cloneDeep } from 'lodash'; import { FeaturesPrivileges } from './features_privileges'; export interface RoleIndexPrivilege { @@ -40,3 +41,53 @@ export interface Role { _transform_error?: string[]; _unrecognized_applications?: string[]; } + +/** + * Returns whether given role is enabled or not + * + * @param role Object Role JSON, as returned by roles API + * @return Boolean true if role is enabled; false otherwise + */ +export function isRoleEnabled(role: Partial) { + return role.transient_metadata?.enabled ?? true; +} + +/** + * Returns whether given role is reserved or not. + * + * @param role Role as returned by roles API + */ +export function isReservedRole(role: Partial) { + return (role.metadata?._reserved as boolean) ?? false; +} + +/** + * Returns whether given role is editable through the UI or not. + * + * @param role the Role as returned by roles API + */ +export function isReadOnlyRole(role: Partial): boolean { + return isReservedRole(role) || (role._transform_error?.length ?? 0) > 0; +} + +/** + * Returns a deep copy of the role. + * + * @param role the Role to copy. + */ +export function copyRole(role: Role) { + return cloneDeep(role); +} + +/** + * Creates a deep copy of the role suitable for cloning. + * + * @param role the Role to clone. + */ +export function prepareRoleClone(role: Role): Role { + const clone = copyRole(role); + + clone.name = ''; + + return clone; +} diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 32f860b1423d30..7d1940e393becf 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -3,7 +3,8 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "security"], - "requiredPlugins": ["features", "licensing"], + "requiredPlugins": ["data", "features", "licensing"], + "optionalPlugins": ["home", "management"], "server": true, "ui": true } diff --git a/x-pack/plugins/security/public/_index.scss b/x-pack/plugins/security/public/_index.scss new file mode 100644 index 00000000000000..9fa81bad7c3f40 --- /dev/null +++ b/x-pack/plugins/security/public/_index.scss @@ -0,0 +1,2 @@ +// Management styles +@import './management/index'; diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx similarity index 72% rename from x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx rename to x-pack/plugins/security/public/account_management/account_management_page.test.tsx index 366842e58e9e4a..b7cf8e6dd14181 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx @@ -6,11 +6,12 @@ import React from 'react'; import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { securityMock } from '../../../../../../../plugins/security/public/mocks'; +import { AuthenticatedUser } from '../../common/model'; import { AccountManagementPage } from './account_management_page'; -import { AuthenticatedUser } from '../../../../common/model'; -jest.mock('ui/kfetch'); +import { coreMock } from 'src/core/public/mocks'; +import { securityMock } from '../mocks'; +import { userAPIClientMock } from '../management/users/index.mock'; interface Options { withFullName?: boolean; @@ -45,7 +46,11 @@ describe('', () => { it(`displays users full name, username, and email address`, async () => { const user = createUser(); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -63,7 +68,11 @@ describe('', () => { it(`displays username when full_name is not provided`, async () => { const user = createUser({ withFullName: false }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -77,7 +86,11 @@ describe('', () => { it(`displays a placeholder when no email address is provided`, async () => { const user = createUser({ withEmail: false }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -91,7 +104,11 @@ describe('', () => { it(`displays change password form for users in the native realm`, async () => { const user = createUser(); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -106,7 +123,11 @@ describe('', () => { it(`does not display change password form for users in the saml realm`, async () => { const user = createUser({ realm: 'saml' }); const wrapper = mountWithIntl( - + ); await act(async () => { diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx b/x-pack/plugins/security/public/account_management/account_management_page.tsx similarity index 62% rename from x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx rename to x-pack/plugins/security/public/account_management/account_management_page.tsx index 6abee73e0b3535..3f764adc7949f2 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.tsx @@ -5,20 +5,24 @@ */ import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; -import { SecurityPluginSetup } from '../../../../../../../plugins/security/public'; -import { getUserDisplayName, AuthenticatedUser } from '../../../../common/model'; +import { NotificationsStart } from 'src/core/public'; +import { getUserDisplayName, AuthenticatedUser } from '../../common/model'; +import { AuthenticationServiceSetup } from '../authentication'; import { ChangePassword } from './change_password'; +import { UserAPIClient } from '../management'; import { PersonalInfo } from './personal_info'; interface Props { - securitySetup: SecurityPluginSetup; + authc: AuthenticationServiceSetup; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } -export const AccountManagementPage = (props: Props) => { +export const AccountManagementPage = ({ apiClient, authc, notifications }: Props) => { const [currentUser, setCurrentUser] = useState(null); useEffect(() => { - props.securitySetup.authc.getCurrentUser().then(setCurrentUser); - }, [props]); + authc.getCurrentUser().then(setCurrentUser); + }, [authc]); if (!currentUser) { return null; @@ -36,7 +40,7 @@ export const AccountManagementPage = (props: Props) => { - + diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx similarity index 81% rename from x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx rename to x-pack/plugins/security/public/account_management/change_password/change_password.tsx index 63abb4539470d2..f5ac5f3b21d2e6 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx +++ b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx @@ -3,18 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiDescribedFormGroup, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { UserAPIClient } from '../../../../lib/api'; -import { AuthenticatedUser, canUserChangePassword } from '../../../../../common/model'; -import { ChangePasswordForm } from '../../../../components/management/change_password_form'; +import { EuiDescribedFormGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsSetup } from 'src/core/public'; +import { AuthenticatedUser, canUserChangePassword } from '../../../common/model'; +import { UserAPIClient } from '../../management/users'; +import { ChangePasswordForm } from '../../management/users/components/change_password_form'; interface Props { user: AuthenticatedUser; + apiClient: PublicMethodsOf; + notifications: NotificationsSetup; } export class ChangePassword extends Component { @@ -48,7 +48,8 @@ export class ChangePassword extends Component { ); diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/index.ts b/x-pack/plugins/security/public/account_management/change_password/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/change_password/index.ts rename to x-pack/plugins/security/public/account_management/change_password/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/index.ts b/x-pack/plugins/security/public/account_management/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/index.ts rename to x-pack/plugins/security/public/account_management/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/index.ts b/x-pack/plugins/security/public/account_management/personal_info/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/personal_info/index.ts rename to x-pack/plugins/security/public/account_management/personal_info/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx rename to x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx index 7121bf7ab28ee7..9cbbc242e8400e 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx +++ b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx @@ -3,15 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiDescribedFormGroup, - EuiFormRow, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { AuthenticatedUser } from '../../../../../common/model'; +import { EuiDescribedFormGroup, EuiFormRow, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AuthenticatedUser } from '../../../common/model'; interface Props { user: AuthenticatedUser; diff --git a/x-pack/plugins/security/public/management/_index.scss b/x-pack/plugins/security/public/management/_index.scss new file mode 100644 index 00000000000000..5d419b53230799 --- /dev/null +++ b/x-pack/plugins/security/public/management/_index.scss @@ -0,0 +1,3 @@ +@import './roles/index'; +@import './users/index'; +@import './role_mappings/index'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts new file mode 100644 index 00000000000000..2a45d497029f41 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const apiKeysAPIClientMock = { + create: () => ({ + checkPrivileges: jest.fn(), + getApiKeys: jest.fn(), + invalidateApiKeys: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts new file mode 100644 index 00000000000000..7d51a80459a6e9 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { APIKeysAPIClient } from './api_keys_api_client'; + +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +describe('APIKeysAPIClient', () => { + it('checkPrivileges() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.get.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + + await expect(apiClient.checkPrivileges()).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key/privileges'); + }); + + it('getApiKeys() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.get.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + + await expect(apiClient.getApiKeys()).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: false }, + }); + httpMock.get.mockClear(); + + await expect(apiClient.getApiKeys(false)).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: false }, + }); + httpMock.get.mockClear(); + + await expect(apiClient.getApiKeys(true)).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: true }, + }); + }); + + it('invalidateApiKeys() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.post.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + const mockAPIKeys = [ + { id: 'one', name: 'name-one' }, + { id: 'two', name: 'name-two' }, + ]; + + await expect(apiClient.invalidateApiKeys(mockAPIKeys)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: false }), + }); + httpMock.post.mockClear(); + + await expect(apiClient.invalidateApiKeys(mockAPIKeys, false)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: false }), + }); + httpMock.post.mockClear(); + + await expect(apiClient.invalidateApiKeys(mockAPIKeys, true)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: true }), + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts new file mode 100644 index 00000000000000..372b1e56a73c47 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; + +interface CheckPrivilegesResponse { + areApiKeysEnabled: boolean; + isAdmin: boolean; +} + +interface InvalidateApiKeysResponse { + itemsInvalidated: ApiKeyToInvalidate[]; + errors: any[]; +} + +interface GetApiKeysResponse { + apiKeys: ApiKey[]; +} + +const apiKeysUrl = '/internal/security/api_key'; + +export class APIKeysAPIClient { + constructor(private readonly http: HttpStart) {} + + public async checkPrivileges() { + return await this.http.get(`${apiKeysUrl}/privileges`); + } + + public async getApiKeys(isAdmin = false) { + return await this.http.get(apiKeysUrl, { query: { isAdmin } }); + } + + public async invalidateApiKeys(apiKeys: ApiKeyToInvalidate[], isAdmin = false) { + return await this.http.post(`${apiKeysUrl}/invalidate`, { + body: JSON.stringify({ apiKeys, isAdmin }), + }); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap similarity index 91% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index c2537235c99f61..42fd4417e238b1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -1,7 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ApiKeysGridPage renders a callout when API keys are not enabled 1`] = ` - +exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` + Contact your system administrator and refer to the `; -exports[`ApiKeysGridPage renders permission denied if user does not have required permissions 1`] = ` +exports[`APIKeysGridPage renders permission denied if user does not have required permissions 1`] = ` ({ body: { statusCode: 403 } }); -const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); - -jest.mock('../../../../lib/api_keys_api', () => { - return { - ApiKeysApi: { - async checkPrivileges() { - if (mockSimulate403) { - throw mock403(); - } - - return { - isAdmin: mockIsAdmin, - areApiKeysEnabled: mockAreApiKeysEnabled, - }; - }, - async getApiKeys() { - if (mockSimulate500) { - throw mock500(); - } - - return { - apiKeys: [ - { - creation: 1571322182082, - expiration: 1571408582082, - id: '0QQZ2m0BO2XZwgJFuWTT', - invalidated: false, - name: 'my-api-key', - realm: 'reserved', - username: 'elastic', - }, - ], - }; - }, - }, - }; -}); - import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ApiKeysGridPage } from './api_keys_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; import { NotEnabled } from './not_enabled'; import { PermissionDenied } from './permission_denied'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import { DocumentationLinksService } from '../documentation_links'; +import { APIKeysGridPage } from './api_keys_grid_page'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { apiKeysAPIClientMock } from '../index.mock'; + +const mock403 = () => ({ body: { statusCode: 403 } }); +const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); const waitForRender = async ( wrapper: ReactWrapper, @@ -77,23 +41,51 @@ const waitForRender = async ( }); }; -describe('ApiKeysGridPage', () => { +describe('APIKeysGridPage', () => { + let apiClientMock: jest.Mocked>; beforeEach(() => { - mockSimulate403 = false; - mockSimulate500 = false; - mockAreApiKeysEnabled = true; - mockIsAdmin = true; + apiClientMock = apiKeysAPIClientMock.create(); + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: true, + areApiKeysEnabled: true, + }); + apiClientMock.getApiKeys.mockResolvedValue({ + apiKeys: [ + { + creation: 1571322182082, + expiration: 1571408582082, + id: '0QQZ2m0BO2XZwgJFuWTT', + invalidated: false, + name: 'my-api-key', + realm: 'reserved', + username: 'elastic', + }, + ], + }); }); + const getViewProperties = () => { + const { docLinks, notifications } = coreMock.createStart(); + return { + docLinks: new DocumentationLinksService(docLinks), + notifications, + apiKeysAPIClient: apiClientMock, + }; + }; + it('renders a loading state when fetching API keys', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="apiKeysSectionLoading"]')).toHaveLength(1); }); it('renders a callout when API keys are not enabled', async () => { - mockAreApiKeysEnabled = false; - const wrapper = mountWithIntl(); + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: true, + areApiKeysEnabled: false, + }); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(NotEnabled).length > 0; @@ -103,8 +95,9 @@ describe('ApiKeysGridPage', () => { }); it('renders permission denied if user does not have required permissions', async () => { - mockSimulate403 = true; - const wrapper = mountWithIntl(); + apiClientMock.checkPrivileges.mockRejectedValue(mock403()); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(PermissionDenied).length > 0; @@ -114,8 +107,9 @@ describe('ApiKeysGridPage', () => { }); it('renders error callout if error fetching API keys', async () => { - mockSimulate500 = true; - const wrapper = mountWithIntl(); + apiClientMock.getApiKeys.mockRejectedValue(mock500()); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(EuiCallOut).length > 0; @@ -125,7 +119,10 @@ describe('ApiKeysGridPage', () => { }); describe('Admin view', () => { - const wrapper = mountWithIntl(); + let wrapper: ReactWrapper; + beforeEach(() => { + wrapper = mountWithIntl(); + }); it('renders a callout indicating the user is an administrator', async () => { const calloutEl = 'EuiCallOut[data-test-subj="apiKeyAdminDescriptionCallOut"]'; @@ -151,8 +148,15 @@ describe('ApiKeysGridPage', () => { }); describe('Non-admin view', () => { - mockIsAdmin = false; - const wrapper = mountWithIntl(); + let wrapper: ReactWrapper; + beforeEach(() => { + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: false, + areApiKeysEnabled: true, + }); + + wrapper = mountWithIntl(); + }); it('does NOT render a callout indicating the user is an administrator', async () => { const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx similarity index 90% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 92633a4b0ef57c..779a2302cfadf9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -27,16 +27,23 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; import _ from 'lodash'; -import { toastNotifications } from 'ui/notify'; +import { NotificationsStart } from 'src/core/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SectionLoading } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; -import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model'; -import { ApiKeysApi } from '../../../../lib/api_keys_api'; +import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; +import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import { DocumentationLinksService } from '../documentation_links'; import { PermissionDenied } from './permission_denied'; import { EmptyPrompt } from './empty_prompt'; import { NotEnabled } from './not_enabled'; import { InvalidateProvider } from './invalidate_provider'; +interface Props { + notifications: NotificationsStart; + docLinks: DocumentationLinksService; + apiKeysAPIClient: PublicMethodsOf; +} + interface State { isLoadingApp: boolean; isLoadingTable: boolean; @@ -50,7 +57,7 @@ interface State { const DATE_FORMAT = 'MMMM Do YYYY HH:mm:ss'; -export class ApiKeysGridPage extends Component { +export class APIKeysGridPage extends Component { constructor(props: any) { super(props); this.state = { @@ -124,7 +131,7 @@ export class ApiKeysGridPage extends Component { if (!areApiKeysEnabled) { return ( - + ); } @@ -132,7 +139,7 @@ export class ApiKeysGridPage extends Component { if (!isLoadingTable && apiKeys && apiKeys.length === 0) { return ( - + ); } @@ -210,7 +217,11 @@ export class ApiKeysGridPage extends Component { const search: EuiInMemoryTableProps['search'] = { toolsLeft: selectedItems.length ? ( - + {invalidateApiKeyPrompt => { return ( { return ( - + {invalidateApiKeyPrompt => { return ( { private async checkPrivileges() { try { - const { isAdmin, areApiKeysEnabled } = await ApiKeysApi.checkPrivileges(); + const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges(); this.setState({ isAdmin, areApiKeysEnabled }); if (areApiKeysEnabled) { @@ -494,14 +509,11 @@ export class ApiKeysGridPage extends Component { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true, isLoadingApp: false }); } else { - toastNotifications.addDanger( - this.props.i18n.translate( - 'xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', - { - defaultMessage: 'Error checking privileges: {message}', - values: { message: _.get(e, 'body.message', '') }, - } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { + defaultMessage: 'Error checking privileges: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) ); } } @@ -520,7 +532,7 @@ export class ApiKeysGridPage extends Component { private loadApiKeys = async () => { try { const { isAdmin } = this.state; - const { apiKeys } = await ApiKeysApi.getApiKeys(isAdmin); + const { apiKeys } = await this.props.apiKeysAPIClient.getApiKeys(isAdmin); this.setState({ apiKeys }); } catch (e) { this.setState({ error: e }); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx index 957ca7010a1a0a..7d762a1ceb04e0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx @@ -7,13 +7,14 @@ import React, { Fragment } from 'react'; import { EuiEmptyPrompt, EuiButton, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; interface Props { isAdmin: boolean; + docLinks: DocumentationLinksService; } -export const EmptyPrompt: React.FunctionComponent = ({ isAdmin }) => ( +export const EmptyPrompt: React.FunctionComponent = ({ isAdmin, docLinks }) => ( = ({ isAdmin }) => ( defaultMessage="You can create an {link} from Console." values={{ link: ( - + React.ReactElement; + notifications: NotificationsStart; + apiKeysAPIClient: PublicMethodsOf; } export type InvalidateApiKeys = ( @@ -23,7 +25,12 @@ export type InvalidateApiKeys = ( type OnSuccessCallback = (apiKeysInvalidated: ApiKeyToInvalidate[]) => void; -export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, children }) => { +export const InvalidateProvider: React.FunctionComponent = ({ + isAdmin, + children, + notifications, + apiKeysAPIClient, +}) => { const [apiKeys, setApiKeys] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const onSuccessCallback = useRef(null); @@ -48,7 +55,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch let errors; try { - result = await ApiKeysApi.invalidateApiKeys(apiKeys, isAdmin); + result = await apiKeysAPIClient.invalidateApiKeys(apiKeys, isAdmin); } catch (e) { error = e; } @@ -77,7 +84,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch values: { name: itemsInvalidated[0].name }, } ); - toastNotifications.addSuccess(successMessage); + notifications.toasts.addSuccess(successMessage); if (onSuccessCallback.current) { onSuccessCallback.current([...itemsInvalidated]); } @@ -106,7 +113,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch values: { name: (errors && errors[0].name) || apiKeys[0].name }, } ); - toastNotifications.addDanger(errorMessage); + notifications.toasts.addDanger(errorMessage); } }; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx index c419e15450c1e1..08fe5425577573 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx @@ -7,9 +7,13 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; -export const NotEnabled: React.FunctionComponent = () => ( +interface Props { + docLinks: DocumentationLinksService; +} + +export const NotEnabled: React.FunctionComponent = ({ docLinks }) => ( ( defaultMessage="Contact your system administrator and refer to the {link} to enable API keys." values={{ link: ( - + ({ + APIKeysGridPage: (props: any) => `Page: ${JSON.stringify(props)}`, +})); + +import { apiKeysManagementApp } from './api_keys_management_app'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +describe('apiKeysManagementApp', () => { + it('create() returns proper management app descriptor', () => { + const { getStartServices } = coreMock.createSetup(); + + expect(apiKeysManagementApp.create({ getStartServices: getStartServices as any })) + .toMatchInlineSnapshot(` + Object { + "id": "api_keys", + "mount": [Function], + "order": 30, + "title": "API Keys", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const { getStartServices } = coreMock.createSetup(); + const container = document.createElement('div'); + + const setBreadcrumbs = jest.fn(); + const unmount = await apiKeysManagementApp + .create({ getStartServices: getStartServices as any }) + .mount({ + basePath: '/some-base-path', + element: container, + setBreadcrumbs, + }); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '#/some-base-path', text: 'API Keys' }]); + expect(container).toMatchInlineSnapshot(` +
+ Page: {"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"apiKeysAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); +}); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx new file mode 100644 index 00000000000000..35de732b84ce9b --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { PluginStartDependencies } from '../../plugin'; +import { APIKeysGridPage } from './api_keys_grid'; +import { APIKeysAPIClient } from './api_keys_api_client'; +import { DocumentationLinksService } from './documentation_links'; + +interface CreateParams { + getStartServices: CoreSetup['getStartServices']; +} + +export const apiKeysManagementApp = Object.freeze({ + id: 'api_keys', + create({ getStartServices }: CreateParams) { + return { + id: this.id, + order: 30, + title: i18n.translate('xpack.security.management.apiKeysTitle', { + defaultMessage: 'API Keys', + }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); + setBreadcrumbs([ + { + text: i18n.translate('xpack.security.apiKeys.breadcrumb', { + defaultMessage: 'API Keys', + }), + href: `#${basePath}`, + }, + ]); + + render( + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts similarity index 51% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts rename to x-pack/plugins/security/public/management/api_keys/documentation_links.ts index 1f03763eb542a3..4165c2a2372c96 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts +++ b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { DocLinksStart } from 'src/core/public'; -class DocumentationLinksService { - private esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; +export class DocumentationLinksService { + private readonly esDocBasePath: string; - public getApiKeyServiceSettingsDocUrl(): string { + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/`; + } + + public getApiKeyServiceSettingsDocUrl() { return `${this.esDocBasePath}security-settings.html#api-key-service-settings`; } - public getCreateApiKeyDocUrl(): string { + public getCreateApiKeyDocUrl() { return `${this.esDocBasePath}security-api-create-api-key.html`; } } - -export const documentationLinks = new DocumentationLinksService(); diff --git a/x-pack/plugins/security/public/management/api_keys/index.mock.ts b/x-pack/plugins/security/public/management/api_keys/index.mock.ts new file mode 100644 index 00000000000000..3c11cd6bb9c654 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/index.mock.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 { apiKeysAPIClientMock } from './api_keys_api_client.mock'; diff --git a/x-pack/plugins/security/public/management/api_keys/index.ts b/x-pack/plugins/security/public/management/api_keys/index.ts new file mode 100644 index 00000000000000..e15da7d5eb4092 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/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 { apiKeysManagementApp } from './api_keys_management_app'; diff --git a/x-pack/plugins/security/public/management/index.ts b/x-pack/plugins/security/public/management/index.ts new file mode 100644 index 00000000000000..e1a13d66e68836 --- /dev/null +++ b/x-pack/plugins/security/public/management/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ManagementService } from './management_service'; +export { UserAPIClient } from './users/user_api_client'; diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts new file mode 100644 index 00000000000000..53c12ad7ab12c4 --- /dev/null +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -0,0 +1,226 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { ManagementApp } from '../../../../../src/plugins/management/public'; +import { SecurityLicenseFeatures } from '../../common/licensing/license_features'; +import { ManagementService } from './management_service'; +import { usersManagementApp } from './users'; + +import { coreMock } from '../../../../../src/core/public/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; +import { securityMock } from '../mocks'; +import { rolesManagementApp } from './roles'; +import { apiKeysManagementApp } from './api_keys'; +import { roleMappingsManagementApp } from './role_mappings'; + +describe('ManagementService', () => { + describe('setup()', () => { + it('properly registers security section and its applications', () => { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + const { authc } = securityMock.createSetup(); + const license = licenseMock.create(); + + const mockSection = { registerApp: jest.fn() }; + const managementSetup = { + sections: { + getSection: jest.fn(), + getAllSections: jest.fn(), + register: jest.fn().mockReturnValue(mockSection), + }, + }; + + const service = new ManagementService(); + service.setup({ + getStartServices: getStartServices as any, + license, + fatalErrors, + authc, + management: managementSetup, + }); + + expect(managementSetup.sections.register).toHaveBeenCalledTimes(1); + expect(managementSetup.sections.register).toHaveBeenCalledWith({ + id: 'security', + title: 'Security', + order: 100, + euiIconType: 'securityApp', + }); + + expect(mockSection.registerApp).toHaveBeenCalledTimes(4); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'users', + mount: expect.any(Function), + order: 10, + title: 'Users', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'roles', + mount: expect.any(Function), + order: 20, + title: 'Roles', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'api_keys', + mount: expect.any(Function), + order: 30, + title: 'API Keys', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'role_mappings', + mount: expect.any(Function), + order: 40, + title: 'Role Mappings', + }); + }); + }); + + describe('start()', () => { + function startService(initialFeatures: Partial) { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + + const licenseSubject = new BehaviorSubject( + (initialFeatures as unknown) as SecurityLicenseFeatures + ); + const license = licenseMock.create(); + license.features$ = licenseSubject; + + const service = new ManagementService(); + service.setup({ + getStartServices: getStartServices as any, + license, + fatalErrors, + authc: securityMock.createSetup().authc, + management: { + sections: { + getSection: jest.fn(), + getAllSections: jest.fn(), + register: jest.fn().mockReturnValue({ registerApp: jest.fn() }), + }, + }, + }); + + const getMockedApp = () => { + // All apps are enabled by default. + let enabled = true; + return ({ + get enabled() { + return enabled; + }, + enable: jest.fn().mockImplementation(() => { + enabled = true; + }), + disable: jest.fn().mockImplementation(() => { + enabled = false; + }), + } as unknown) as jest.Mocked; + }; + const mockApps = new Map>([ + [usersManagementApp.id, getMockedApp()], + [rolesManagementApp.id, getMockedApp()], + [apiKeysManagementApp.id, getMockedApp()], + [roleMappingsManagementApp.id, getMockedApp()], + ] as Array<[string, jest.Mocked]>); + + service.start({ + management: { + sections: { + getSection: jest + .fn() + .mockReturnValue({ getApp: jest.fn().mockImplementation(id => mockApps.get(id)) }), + getAllSections: jest.fn(), + navigateToApp: jest.fn(), + }, + legacy: undefined, + }, + }); + + return { + mockApps, + updateFeatures(features: Partial) { + licenseSubject.next((features as unknown) as SecurityLicenseFeatures); + }, + }; + } + + it('does not do anything if `showLinks` is `true` at `start`', () => { + const { mockApps } = startService({ showLinks: true, showRoleMappingsManagement: true }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enable).not.toHaveBeenCalled(); + expect(mockApp.disable).not.toHaveBeenCalled(); + expect(mockApp.enabled).toBe(true); + } + }); + + it('disables all apps if `showLinks` is `false` at `start`', () => { + const { mockApps } = startService({ showLinks: false, showRoleMappingsManagement: true }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + + it('disables only Role Mappings app if `showLinks` is `true`, but `showRoleMappingsManagement` is `false` at `start`', () => { + const { mockApps } = startService({ showLinks: true, showRoleMappingsManagement: false }); + for (const [appId, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(appId !== roleMappingsManagementApp.id); + } + }); + + it('apps are disabled if `showLinks` changes after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: false, showRoleMappingsManagement: false }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + + it('role mappings app is disabled if `showRoleMappingsManagement` changes after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: true, showRoleMappingsManagement: false }); + + for (const [appId, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(appId !== roleMappingsManagementApp.id); + } + }); + + it('apps are re-enabled if `showLinks` eventually transitions to `true` after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: false, showRoleMappingsManagement: false }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + + updateFeatures({ showLinks: true, showRoleMappingsManagement: true }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/management_service.ts b/x-pack/plugins/security/public/management/management_service.ts new file mode 100644 index 00000000000000..5ad3681590fbf6 --- /dev/null +++ b/x-pack/plugins/security/public/management/management_service.ts @@ -0,0 +1,95 @@ +/* + * 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 { Subscription } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, FatalErrorsSetup } from 'src/core/public'; +import { + ManagementApp, + ManagementSetup, + ManagementStart, +} from '../../../../../src/plugins/management/public'; +import { SecurityLicense } from '../../common/licensing'; +import { AuthenticationServiceSetup } from '../authentication'; +import { PluginStartDependencies } from '../plugin'; +import { apiKeysManagementApp } from './api_keys'; +import { roleMappingsManagementApp } from './role_mappings'; +import { rolesManagementApp } from './roles'; +import { usersManagementApp } from './users'; + +interface SetupParams { + management: ManagementSetup; + license: SecurityLicense; + authc: AuthenticationServiceSetup; + fatalErrors: FatalErrorsSetup; + getStartServices: CoreSetup['getStartServices']; +} + +interface StartParams { + management: ManagementStart; +} + +export class ManagementService { + private license!: SecurityLicense; + private licenseFeaturesSubscription?: Subscription; + + setup({ getStartServices, management, authc, license, fatalErrors }: SetupParams) { + this.license = license; + + const securitySection = management.sections.register({ + id: 'security', + title: i18n.translate('xpack.security.management.securityTitle', { + defaultMessage: 'Security', + }), + order: 100, + euiIconType: 'securityApp', + }); + + securitySection.registerApp(usersManagementApp.create({ authc, getStartServices })); + securitySection.registerApp( + rolesManagementApp.create({ fatalErrors, license, getStartServices }) + ); + securitySection.registerApp(apiKeysManagementApp.create({ getStartServices })); + securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); + } + + start({ management }: StartParams) { + this.licenseFeaturesSubscription = this.license.features$.subscribe(async features => { + const securitySection = management.sections.getSection('security')!; + + const securityManagementAppsStatuses: Array<[ManagementApp, boolean]> = [ + [securitySection.getApp(usersManagementApp.id)!, features.showLinks], + [securitySection.getApp(rolesManagementApp.id)!, features.showLinks], + [securitySection.getApp(apiKeysManagementApp.id)!, features.showLinks], + [ + securitySection.getApp(roleMappingsManagementApp.id)!, + features.showLinks && features.showRoleMappingsManagement, + ], + ]; + + // Iterate over all registered apps and update their enable status depending on the available + // license features. + for (const [app, enableStatus] of securityManagementAppsStatuses) { + if (app.enabled === enableStatus) { + continue; + } + + if (enableStatus) { + app.enable(); + } else { + app.disable(); + } + } + }); + } + + stop() { + if (this.licenseFeaturesSubscription) { + this.licenseFeaturesSubscription.unsubscribe(); + this.licenseFeaturesSubscription = undefined; + } + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts b/x-pack/plugins/security/public/management/management_urls.ts similarity index 74% rename from x-pack/legacy/plugins/security/public/views/management/management_urls.ts rename to x-pack/plugins/security/public/management/management_urls.ts index 881740c0b2895b..0d4e3fc920bdba 100644 --- a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts +++ b/x-pack/plugins/security/public/management/management_urls.ts @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export const MANAGEMENT_PATH = '/management'; -export const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; +const MANAGEMENT_PATH = '/management'; +const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; export const ROLES_PATH = `${SECURITY_PATH}/roles`; export const EDIT_ROLES_PATH = `${ROLES_PATH}/edit`; export const CLONE_ROLES_PATH = `${ROLES_PATH}/clone`; export const USERS_PATH = `${SECURITY_PATH}/users`; export const EDIT_USERS_PATH = `${USERS_PATH}/edit`; -export const API_KEYS_PATH = `${SECURITY_PATH}/api_keys`; export const ROLE_MAPPINGS_PATH = `${SECURITY_PATH}/role_mappings`; -export const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; +const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; export const getEditRoleHref = (roleName: string) => - `#${EDIT_ROLES_PATH}/${encodeURIComponent(roleName)}`; + `#${ROLES_PATH}/edit/${encodeURIComponent(roleName)}`; export const getCreateRoleMappingHref = () => `#${CREATE_ROLE_MAPPING_PATH}`; diff --git a/x-pack/plugins/security/public/management/role_mappings/_index.scss b/x-pack/plugins/security/public/management/role_mappings/_index.scss new file mode 100644 index 00000000000000..bae6effcd2ec56 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/_index.scss @@ -0,0 +1 @@ +@import './edit_role_mapping/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx similarity index 64% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx index b826d68053e276..69142b1ad610e6 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx @@ -5,24 +5,15 @@ */ import React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { DeleteProvider } from '.'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { RoleMapping } from '../../../../../../common/model'; import { EuiConfirmModal } from '@elastic/eui'; -import { findTestSubject } from 'test_utils/find_test_subject'; import { act } from '@testing-library/react'; -import { toastNotifications } from 'ui/notify'; - -jest.mock('ui/notify', () => { - return { - toastNotifications: { - addError: jest.fn(), - addSuccess: jest.fn(), - addDanger: jest.fn(), - }, - }; -}); +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { RoleMapping } from '../../../../../common/model'; +import { DeleteProvider } from '.'; + +import { roleMappingsAPIClientMock } from '../../index.mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('DeleteProvider', () => { beforeEach(() => { @@ -30,17 +21,14 @@ describe('DeleteProvider', () => { }); it('allows a single role mapping to be deleted', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([{ name: 'delete-me', success: true }]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -79,11 +67,10 @@ describe('DeleteProvider', () => { expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -94,21 +81,23 @@ describe('DeleteProvider', () => { }); it('allows multiple role mappings to be deleted', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'delete-me', + success: true, + }, + { + name: 'delete-me-too', + success: true, + }, + ]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - { - name: 'delete-me-too', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -152,11 +141,11 @@ describe('DeleteProvider', () => { 'delete-me', 'delete-me-too', ]); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -167,22 +156,24 @@ describe('DeleteProvider', () => { }); it('handles mixed success/failure conditions', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'delete-me', + success: true, + }, + { + name: 'i-wont-work', + success: false, + error: new Error('something went wrong. sad.'), + }, + ]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - { - name: 'i-wont-work', - success: false, - error: new Error('something went wrong. sad.'), - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -223,10 +214,9 @@ describe('DeleteProvider', () => { 'i-wont-work', ]); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -235,8 +225,8 @@ describe('DeleteProvider', () => { ] `); - expect(notifications.addDanger).toHaveBeenCalledTimes(1); - expect(notifications.addDanger.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addDanger.mock.calls[0]).toMatchInlineSnapshot(` Array [ "Error deleting role mapping 'i-wont-work'", ] @@ -244,12 +234,13 @@ describe('DeleteProvider', () => { }); it('handles errors calling the API', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockRejectedValue(new Error('AHHHHH')); + + const notifications = coreMock.createStart().notifications; const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockImplementation(() => { - throw new Error('AHHHHH'); - }), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -284,12 +275,11 @@ describe('DeleteProvider', () => { expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(0); - expect(notifications.addError).toHaveBeenCalledTimes(1); - expect(notifications.addError.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addError.mock.calls[0]).toMatchInlineSnapshot(` Array [ [Error: AHHHHH], Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx index 2072cedeab4628..860fe22cb8032c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -6,13 +6,14 @@ import React, { Fragment, useRef, useState, ReactElement } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../../common/model'; +import { RoleMappingsAPIClient } from '../../role_mappings_api_client'; interface Props { - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + notifications: NotificationsStart; children: (deleteMappings: DeleteRoleMappings) => ReactElement; } @@ -23,7 +24,11 @@ export type DeleteRoleMappings = ( type OnSuccessCallback = (deletedRoleMappings: string[]) => void; -export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI, children }) => { +export const DeleteProvider: React.FunctionComponent = ({ + roleMappingsAPI, + children, + notifications, +}) => { const [roleMappings, setRoleMappings] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteInProgress, setIsDeleteInProgress] = useState(false); @@ -55,7 +60,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI try { result = await roleMappingsAPI.deleteRoleMappings(roleMappings.map(rm => rm.name)); } catch (e) { - toastNotifications.addError(e, { + notifications.toasts.addError(e, { title: i18n.translate( 'xpack.security.management.roleMappings.deleteRoleMapping.unknownError', { @@ -92,7 +97,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI values: { name: successfulDeletes[0].name }, } ); - toastNotifications.addSuccess({ + notifications.toasts.addSuccess({ title: successMessage, 'data-test-subj': 'deletedRoleMappingSuccessToast', }); @@ -121,7 +126,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI values: { name: erroredDeletes[0].name }, } ); - toastNotifications.addDanger(errorMessage); + notifications.toasts.addDanger(errorMessage); } }; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx index 969832b3ecbae1..5e14b0c179bfd7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx @@ -7,9 +7,13 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; -export const NoCompatibleRealms: React.FunctionComponent = () => ( +interface Props { + docLinks: DocumentationLinksService; +} + +export const NoCompatibleRealms: React.FunctionComponent = ({ docLinks }: Props) => ( ( defaultMessage="Role mappings will not be applied to any users. Contact your system administrator and refer to the {link} for more information." values={{ link: ( - + { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + + it('allows a role mapping to be created', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.saveRoleMapping.mockResolvedValue(null); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { + target: { value: 'my-role-mapping' }, + }); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'my-role-mapping', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + all: [{ field: { username: '*' } }], + }, + metadata: {}, + }); + }); + + it('allows a role mapping to be updated', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.saveRoleMapping.mockResolvedValue(null); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + wrapper.find('button[id="addRuleOption"]').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'foo', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + any: [ + { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, + { field: { username: '*' } }, + ], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { source: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: false, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + }); + + it('renders the visual editor by default for simple rule sets', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: { + all: [ + { + field: { + username: '*', + }, + }, + { + field: { + dn: null, + }, + }, + { + field: { + realm: ['ldap', 'pki', null, 12], + }, + }, + ], + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); + }); + + it('renders the JSON editor by default for complex rule sets', async () => { + const createRule = (depth: number): Record => { + if (depth > 0) { + const rule = { + all: [ + { + field: { + username: '*', + }, + }, + ], + } as Record; + + const subRule = createRule(depth - 1); + if (subRule) { + rule.all.push(subRule); + } + + return rule; + } + return null as any; + }; + + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: createRule(10), + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx index b8a75a4ad9fdf7..142b53cbb50f29 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx @@ -19,20 +19,21 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../common/model'; import { RuleEditorPanel } from './rule_editor_panel'; import { NoCompatibleRealms, PermissionDenied, DeleteProvider, SectionLoading, -} from '../../components'; -import { ROLE_MAPPINGS_PATH } from '../../../management_urls'; -import { validateRoleMappingForSave } from '../services/role_mapping_validation'; +} from '../components'; +import { RolesAPIClient } from '../../roles'; +import { ROLE_MAPPINGS_PATH } from '../../management_urls'; +import { validateRoleMappingForSave } from './services/role_mapping_validation'; import { MappingInfoPanel } from './mapping_info_panel'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../documentation_links'; +import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface State { loadState: 'loading' | 'permissionDenied' | 'ready' | 'saveInProgress'; @@ -50,7 +51,10 @@ interface State { interface Props { name?: string; - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + notifications: NotificationsStart; + docLinks: DocumentationLinksService; } export class EditRoleMappingPage extends Component { @@ -74,6 +78,12 @@ export class EditRoleMappingPage extends Component { this.loadAppData(); } + public async componentDidUpdate(prevProps: Props) { + if (prevProps.name !== this.props.name) { + await this.loadAppData(); + } + } + public render() { const { loadState } = this.state; @@ -101,6 +111,8 @@ export class EditRoleMappingPage extends Component { validateForm={this.state.validateForm} canUseInlineScripts={this.state.canUseInlineScripts} canUseStoredScripts={this.state.canUseStoredScripts} + rolesAPIClient={this.props.rolesAPIClient} + docLinks={this.props.docLinks} /> { }, }) } + docLinks={this.props.docLinks} /> {this.getFormButtons()} @@ -149,7 +162,7 @@ export class EditRoleMappingPage extends Component { values={{ learnMoreLink: ( @@ -166,7 +179,7 @@ export class EditRoleMappingPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} @@ -201,7 +214,10 @@ export class EditRoleMappingPage extends Component { {this.editingExistingRoleMapping() && ( - + {deleteRoleMappingsPrompt => { return ( { this.props.roleMappingsAPI .saveRoleMapping(this.state.roleMapping) .then(() => { - toastNotifications.addSuccess({ + this.props.notifications.toasts.addSuccess({ title: i18n.translate('xpack.security.management.editRoleMapping.saveSuccess', { defaultMessage: `Saved role mapping '{roleMappingName}'`, values: { @@ -264,7 +280,7 @@ export class EditRoleMappingPage extends Component { this.backToRoleMappingsList(); }) .catch(e => { - toastNotifications.addError(e, { + this.props.notifications.toasts.addError(e, { title: i18n.translate('xpack.security.management.editRoleMapping.saveError', { defaultMessage: `Error saving role mapping`, }), @@ -312,7 +328,7 @@ export class EditRoleMappingPage extends Component { roleMapping, }); } catch (e) { - toastNotifications.addDanger({ + this.props.notifications.toasts.addDanger({ title: i18n.translate( 'xpack.security.management.editRoleMapping.table.fetchingRoleMappingsErrorMessage', { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx index d821b33ace6a7b..9b62ca27ca569c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx @@ -6,21 +6,27 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { MappingInfoPanel } from '.'; -import { RoleMapping } from '../../../../../../../common/model'; import { findTestSubject } from 'test_utils/find_test_subject'; +import { Role, RoleMapping } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; +import { DocumentationLinksService } from '../../documentation_links'; import { RoleSelector } from '../role_selector'; import { RoleTemplateEditor } from '../role_selector/role_template_editor'; +import { MappingInfoPanel } from '.'; -jest.mock('../../../../../../lib/roles_api', () => { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); +import { rolesAPIClientMock } from '../../../roles/roles_api_client.mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('MappingInfoPanel', () => { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + it('renders when creating a role mapping, default to the "roles" view', () => { const props = { roleMapping: { @@ -32,6 +38,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'create', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); @@ -77,6 +85,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); @@ -101,6 +111,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: true, canUseStoredScripts: false, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -140,6 +152,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: true, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -179,6 +193,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: false, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -202,6 +218,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx index a02b4fc1709f02..02af6bfbafa7e0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx @@ -18,14 +18,15 @@ import { EuiSwitch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { RoleMapping } from '../../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; import { validateRoleMappingName, validateRoleMappingRoles, validateRoleMappingRoleTemplates, -} from '../../services/role_mapping_validation'; +} from '../services/role_mapping_validation'; import { RoleSelector } from '../role_selector'; -import { documentationLinks } from '../../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; interface Props { roleMapping: RoleMapping; @@ -34,6 +35,8 @@ interface Props { validateForm: boolean; canUseInlineScripts: boolean; canUseStoredScripts: boolean; + rolesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; } interface State { @@ -163,6 +166,7 @@ export class MappingInfoPanel extends Component { > { defaultMessage="Create templates that describe the roles to assign to your users." />{' '} @@ -230,6 +234,7 @@ export class MappingInfoPanel extends Component { > { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); +import { RolesAPIClient } from '../../../roles'; +import { rolesAPIClientMock } from '../../../roles/roles_api_client.mock'; describe('RoleSelector', () => { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + it('allows roles to be selected, removing any previously selected role templates', () => { const props = { roleMapping: { @@ -36,6 +39,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'roles', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -57,6 +61,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -87,6 +92,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -122,6 +128,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx index 6b92d6b4907f16..992c2741ae93ee 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx @@ -7,12 +7,13 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; -import { RoleMapping, Role } from '../../../../../../../common/model'; -import { RolesApi } from '../../../../../../lib/roles_api'; +import { RoleMapping, Role } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; import { AddRoleTemplateButton } from './add_role_template_button'; import { RoleTemplateEditor } from './role_template_editor'; interface Props { + rolesAPIClient: PublicMethodsOf; roleMapping: RoleMapping; canUseInlineScripts: boolean; canUseStoredScripts: boolean; @@ -32,7 +33,7 @@ export class RoleSelector extends React.Component { } public async componentDidMount() { - const roles = await RolesApi.getRoles(); + const roles = await this.props.rolesAPIClient.getRoles(); this.setState({ roles }); } diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.test.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx index 4b8d34d2719960..d79651d7b9cd6d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx @@ -18,12 +18,12 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RoleTemplate } from '../../../../../../../common/model'; +import { RoleTemplate } from '../../../../../common/model'; import { isInlineRoleTemplate, isStoredRoleTemplate, isInvalidRoleTemplate, -} from '../../services/role_template_type'; +} from '../services/role_template_type'; import { RoleTemplateTypeSelect } from './role_template_type_select'; interface Props { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx similarity index 92% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx index 4a06af0fb436ba..aa65c5c9bcae74 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox } from '@elastic/eui'; -import { RoleTemplate } from '../../../../../../../common/model'; -import { isInlineRoleTemplate, isStoredRoleTemplate } from '../../services/role_template_type'; +import { RoleTemplate } from '../../../../../common/model'; +import { isInlineRoleTemplate, isStoredRoleTemplate } from '../services/role_template_type'; const templateTypeOptions = [ { diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss new file mode 100644 index 00000000000000..c3b2764e647130 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss @@ -0,0 +1 @@ +@import './rule_editor_group'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx index 917b822acef3f2..d1411bd9bf2b97 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { AddRuleButton } from './add_rule_button'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from 'test_utils/find_test_subject'; -import { FieldRule, AllRule } from '../../../model'; +import { FieldRule, AllRule } from '../../model'; describe('AddRuleButton', () => { it('allows a field rule to be created', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx index 100c0dd3eeaee4..9696fa337a74fc 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Rule, FieldRule, AllRule } from '../../../model'; +import { Rule, FieldRule, AllRule } from '../../model'; interface Props { onClick: (newRule: Rule) => void; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx index 8d5d5c99ee99d7..5374f4625336de 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { FieldRuleEditor } from './field_rule_editor'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { FieldRule } from '../../../model'; +import { FieldRule } from '../../model'; import { findTestSubject } from 'test_utils/find_test_subject'; import { ReactWrapper } from 'enzyme'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx index 52cf70dbd12bd2..2506c18dcaf3a0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx @@ -18,7 +18,7 @@ import { EuiIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldRule, FieldRuleValue } from '../../../model'; +import { FieldRule, FieldRuleValue } from '../../model'; interface Props { rule: FieldRule; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/index.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/index.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx index 8a9b37ab0f4065..263c456e901cd9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx @@ -17,7 +17,10 @@ import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { JSONRuleEditor } from './json_rule_editor'; import { EuiCodeEditor } from '@elastic/eui'; -import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; +import { DocumentationLinksService } from '../../documentation_links'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('JSONRuleEditor', () => { it('renders an empty rule set', () => { @@ -25,6 +28,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -46,6 +50,7 @@ describe('JSONRuleEditor', () => { ]), onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -79,6 +84,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -100,6 +106,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -130,6 +137,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx index 371fb59f7a5d13..e7a9149513d208 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx @@ -11,13 +11,14 @@ import 'brace/theme/github'; import { EuiCodeEditor, EuiFormRow, EuiButton, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../../model'; -import { documentationLinks } from '../../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; +import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../model'; interface Props { rules: Rule | null; onChange: (updatedRules: Rule | null) => void; onValidityChange: (isValid: boolean) => void; + docLinks: DocumentationLinksService; } export const JSONRuleEditor = (props: Props) => { @@ -107,7 +108,7 @@ export const JSONRuleEditor = (props: Props) => { values={{ roleMappingAPI: ( diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx similarity index 88% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx index 809264183d30ca..b9c650cc1f77a4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx @@ -15,8 +15,11 @@ import { findTestSubject } from 'test_utils/find_test_subject'; // This is not required for the tests to pass, but it rather suppresses lengthy // warnings in the console which adds unnecessary noise to the test output. import 'test_utils/stub_web_worker'; -import { AllRule, FieldRule } from '../../../model'; +import { AllRule, FieldRule } from '../../model'; import { EuiErrorBoundary } from '@elastic/eui'; +import { DocumentationLinksService } from '../../documentation_links'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('RuleEditorPanel', () => { it('renders the visual editor when no rules are defined', () => { @@ -25,6 +28,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); @@ -45,6 +49,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); @@ -68,6 +73,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -103,6 +109,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx index 4aab49b2b2efcf..6e6641caa1f399 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx @@ -22,19 +22,20 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; import { VisualRuleEditor } from './visual_rule_editor'; import { JSONRuleEditor } from './json_rule_editor'; -import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; -import { Rule, generateRulesFromRaw } from '../../../model'; -import { validateRoleMappingRules } from '../../services/role_mapping_validation'; -import { documentationLinks } from '../../../services/documentation_links'; +import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; +import { Rule, generateRulesFromRaw } from '../../model'; +import { DocumentationLinksService } from '../../documentation_links'; +import { validateRoleMappingRules } from '../services/role_mapping_validation'; interface Props { rawRules: RoleMapping['rules']; onChange: (rawRules: RoleMapping['rules']) => void; onValidityChange: (isValid: boolean) => void; validateForm: boolean; + docLinks: DocumentationLinksService; } interface State { @@ -91,7 +92,7 @@ export class RuleEditorPanel extends Component { values={{ learnMoreLink: ( @@ -214,6 +215,7 @@ export class RuleEditorPanel extends Component { rules={this.state.rules} onChange={this.onRuleChange} onValidityChange={this.onValidityChange} + docLinks={this.props.docLinks} /> ); default: diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx index 3e0e0e386e98c2..5946aac4306b1f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { RuleGroupEditor } from './rule_group_editor'; import { shallowWithIntl, mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../../model'; +import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../model'; import { FieldRuleEditor } from './field_rule_editor'; import { AddRuleButton } from './add_rule_button'; import { EuiContextMenuItem } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx index 6fb33db179e8a6..c17a853a654676 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx @@ -16,8 +16,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AddRuleButton } from './add_rule_button'; import { RuleGroupTitle } from './rule_group_title'; import { FieldRuleEditor } from './field_rule_editor'; -import { RuleGroup, Rule, FieldRule } from '../../../model'; -import { isRuleGroup } from '../../services/is_rule_group'; +import { RuleGroup, Rule, FieldRule } from '../../model'; +import { isRuleGroup } from '../services/is_rule_group'; interface Props { rule: RuleGroup; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx index e46893afd4d86e..6bef9c09eeef39 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx @@ -15,14 +15,7 @@ import { EuiConfirmModal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - RuleGroup, - AllRule, - AnyRule, - ExceptAllRule, - ExceptAnyRule, - FieldRule, -} from '../../../model'; +import { RuleGroup, AllRule, AnyRule, ExceptAllRule, ExceptAnyRule, FieldRule } from '../../model'; interface Props { rule: RuleGroup; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx index 7c63613ee1cc9e..b40f0063acb08a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { VisualRuleEditor } from './visual_rule_editor'; import { findTestSubject } from 'test_utils/find_test_subject'; -import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; import { RuleGroupEditor } from './rule_group_editor'; import { FieldRuleEditor } from './field_rule_editor'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx index 214c583189fb80..2e3db275788ee4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx @@ -9,9 +9,9 @@ import { EuiEmptyPrompt, EuiCallOut, EuiSpacer, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FieldRuleEditor } from './field_rule_editor'; import { RuleGroupEditor } from './rule_group_editor'; -import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; -import { Rule, FieldRule, RuleGroup, AllRule } from '../../../model'; -import { isRuleGroup } from '../../services/is_rule_group'; +import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; +import { Rule, FieldRule, RuleGroup, AllRule } from '../../model'; +import { isRuleGroup } from '../services/is_rule_group'; interface Props { rules: Rule | null; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/is_rule_group.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/is_rule_group.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts index 9614c4338b631f..0c3f988ae6b105 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts @@ -10,7 +10,7 @@ import { validateRoleMappingRules, validateRoleMappingForSave, } from './role_mapping_validation'; -import { RoleMapping } from '../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; describe('validateRoleMappingName', () => { it('requires a value', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts index 5916d6fd9e1891..7695f1da14881a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; import { generateRulesFromRaw } from '../../model'; interface ValidationResult { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts index 8e1f47a4157ae9..c093bb1b3fbbc9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts @@ -9,7 +9,7 @@ import { isInlineRoleTemplate, isInvalidRoleTemplate, } from './role_template_type'; -import { RoleTemplate } from '../../../../../../common/model'; +import { RoleTemplate } from '../../../../../common/model'; describe('#isStoredRoleTemplate', () => { it('returns true for stored templates, false otherwise', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts index 90d8d1a09e5877..5e646535a6c4d5 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts @@ -9,7 +9,7 @@ import { StoredRoleTemplate, InlineRoleTemplate, InvalidRoleTemplate, -} from '../../../../../../common/model'; +} from '../../../../../common/model'; export function isStoredRoleTemplate( roleMappingTemplate: RoleTemplate diff --git a/x-pack/plugins/security/public/management/role_mappings/index.mock.ts b/x-pack/plugins/security/public/management/role_mappings/index.mock.ts new file mode 100644 index 00000000000000..826477a1a5b153 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/index.mock.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 { roleMappingsAPIClientMock } from './role_mappings_api_client.mock'; diff --git a/x-pack/plugins/security/public/management/role_mappings/index.ts b/x-pack/plugins/security/public/management/role_mappings/index.ts new file mode 100644 index 00000000000000..f670ea61810386 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/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 { roleMappingsManagementApp } from './role_mappings_management_app'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap b/x-pack/plugins/security/public/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap rename to x-pack/plugins/security/public/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/all_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/all_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/all_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/all_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/any_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/any_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/any_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/any_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/field_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/field_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/field_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/field_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts b/x-pack/plugins/security/public/management/role_mappings/model/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts rename to x-pack/plugins/security/public/management/role_mappings/model/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts index ebd48f6d15d99e..ad486a8b314c4f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts @@ -5,7 +5,7 @@ */ import { generateRulesFromRaw, FieldRule } from '.'; -import { RoleMapping } from '../../../../../common/model'; +import { RoleMapping } from '../../../../common/model'; import { RuleBuilderError } from './rule_builder_error'; describe('generateRulesFromRaw', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts index fe344b2ae38dd0..a384e61e521aba 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../common/model'; +import { RoleMapping } from '../../../../common/model'; import { FieldRule, FieldRuleValue } from './field_rule'; import { AllRule } from './all_rule'; import { AnyRule } from './any_rule'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder_error.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder_error.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts index 3e1e7fad9b36f5..5077c79a543c46 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts @@ -7,7 +7,7 @@ import { Rule } from './rule'; /** - * Represents a catagory of Role Mapping rules which are capable of containing other rules. + * Represents a category of Role Mapping rules which are capable of containing other rules. */ export abstract class RuleGroup extends Rule { /** diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts new file mode 100644 index 00000000000000..07d583d1e983f6 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const roleMappingsAPIClientMock = { + create: () => ({ + checkRoleMappingFeatures: jest.fn(), + getRoleMappings: jest.fn(), + getRoleMapping: jest.fn(), + saveRoleMapping: jest.fn(), + deleteRoleMappings: jest.fn(), + }), +}; diff --git a/x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts similarity index 89% rename from x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts index b8bcba91388b53..0a88ed1da9ac3a 100644 --- a/x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/public'; -import { RoleMapping } from '../../common/model'; +import { HttpStart } from 'src/core/public'; +import { RoleMapping } from '../../../common/model'; interface CheckRoleMappingFeaturesResponse { canManageRoleMappings: boolean; @@ -20,8 +20,8 @@ type DeleteRoleMappingsResponse = Array<{ error?: Error; }>; -export class RoleMappingsAPI { - constructor(private readonly http: CoreSetup['http']) {} +export class RoleMappingsAPIClient { + constructor(private readonly http: HttpStart) {} public async checkRoleMappingFeatures(): Promise { return this.http.get(`/internal/security/_check_role_mapping_features`); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx similarity index 90% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx index 2342eeb97d03e3..6fe4bcc7a0bbb0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getCreateRoleMappingHref } from '../../../../management_urls'; +import { getCreateRoleMappingHref } from '../../../management_urls'; export const CreateRoleMappingButton = () => { return ( diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/empty_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/empty_prompt.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/index.ts diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx new file mode 100644 index 00000000000000..de0722b4cd85e2 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -0,0 +1,219 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { RoleMappingsGridPage } from '.'; +import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../components'; +import { EmptyPrompt } from './empty_prompt'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { EuiLink } from '@elastic/eui'; +import { act } from '@testing-library/react'; +import { DocumentationLinksService } from '../documentation_links'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; + +describe('RoleMappingsGridPage', () => { + it('renders an empty prompt when no role mappings exist', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(EmptyPrompt)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(EmptyPrompt)).toHaveLength(1); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: [], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders links to mapped roles', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); + expect(links).toHaveLength(1); + expect(links.at(0).props()).toMatchObject({ + href: '#/management/security/roles/edit/superuser', + }); + }); + + it('describes the number of mapped role templates', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + role_templates: [{}, {}], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const templates = findTestSubject(wrapper, 'roleMappingRoles'); + expect(templates).toHaveLength(1); + expect(templates.text()).toEqual(`2 role templates defined`); + }); + + it('allows role mappings to be deleted, refreshing the grid after', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + success: true, + }, + ]); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); + expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); + + findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); + expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); + // Expect an additional API call to refresh the grid + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index 7b23f2288d1ba6..feb918cb6b301f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -25,24 +25,27 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../common/model'; import { EmptyPrompt } from './empty_prompt'; import { NoCompatibleRealms, DeleteProvider, PermissionDenied, SectionLoading, -} from '../../components'; -import { documentationLinks } from '../../services/documentation_links'; +} from '../components'; import { getCreateRoleMappingHref, getEditRoleMappingHref, getEditRoleHref, -} from '../../../management_urls'; +} from '../../management_urls'; +import { DocumentationLinksService } from '../documentation_links'; +import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface Props { - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + notifications: NotificationsStart; + docLinks: DocumentationLinksService; } interface State { @@ -140,7 +143,7 @@ export class RoleMappingsGridPage extends Component { values={{ learnMoreLink: ( @@ -168,7 +171,7 @@ export class RoleMappingsGridPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} @@ -214,7 +217,10 @@ export class RoleMappingsGridPage extends Component { const search = { toolsLeft: selectedItems.length ? ( - + {deleteRoleMappingsPrompt => { return ( { return ( - + {deleteRoleMappingPrompt => { return ( ({ + RoleMappingsGridPage: (props: any) => `Role Mappings Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_role_mapping', () => ({ + EditRoleMappingPage: (props: any) => `Role Mapping Edit Page: ${JSON.stringify(props)}`, +})); + +import { roleMappingsManagementApp } from './role_mappings_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +async function mountApp(basePath: string) { + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await roleMappingsManagementApp + .create({ getStartServices: coreMock.createSetup().getStartServices as any }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('roleMappingsManagementApp', () => { + it('create() returns proper management app descriptor', () => { + expect( + roleMappingsManagementApp.create({ + getStartServices: coreMock.createSetup().getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "role_mappings", + "mount": [Function], + "order": 40, + "title": "Role Mappings", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/role_mappings'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Role Mappings' }]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mappings Page: {"notifications":{"toasts":{}},"roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create role mapping` page', async () => { + const basePath = '/some-base-path/role_mappings'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mapping Edit Page: {"roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit role mapping` page', async () => { + const basePath = '/some-base-path/role_mappings'; + const roleMappingName = 'someRoleMappingName'; + window.location.hash = `${basePath}/edit/${roleMappingName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { href: `#/some-base-path/role_mappings/edit/${roleMappingName}`, text: roleMappingName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mapping Edit Page: {"name":"someRoleMappingName","roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes role mapping name in `edit role mapping` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/role_mappings'; + const roleMappingName = 'some 安全性 role mapping'; + window.location.hash = `${basePath}/edit/${roleMappingName}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { + href: + '#/some-base-path/role_mappings/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role%20mapping', + text: roleMappingName, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx new file mode 100644 index 00000000000000..af1572cedbadef --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -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 React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { PluginStartDependencies } from '../../plugin'; +import { RolesAPIClient } from '../roles'; +import { RoleMappingsAPIClient } from './role_mappings_api_client'; +import { DocumentationLinksService } from './documentation_links'; +import { RoleMappingsGridPage } from './role_mappings_grid'; +import { EditRoleMappingPage } from './edit_role_mapping'; + +interface CreateParams { + getStartServices: CoreSetup['getStartServices']; +} + +export const roleMappingsManagementApp = Object.freeze({ + id: 'role_mappings', + create({ getStartServices }: CreateParams) { + return { + id: this.id, + order: 40, + title: i18n.translate('xpack.security.management.roleMappingsTitle', { + defaultMessage: 'Role Mappings', + }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); + const roleMappingsBreadcrumbs = [ + { + text: i18n.translate('xpack.security.roleMapping.breadcrumb', { + defaultMessage: 'Role Mappings', + }), + href: `#${basePath}`, + }, + ]; + + const roleMappingsAPIClient = new RoleMappingsAPIClient(http); + const dockLinksService = new DocumentationLinksService(docLinks); + const RoleMappingsGridPageWithBreadcrumbs = () => { + setBreadcrumbs(roleMappingsBreadcrumbs); + return ( + + ); + }; + + const EditRoleMappingsPageWithBreadcrumbs = () => { + const { name } = useParams<{ name?: string }>(); + + setBreadcrumbs([ + ...roleMappingsBreadcrumbs, + name + ? { text: name, href: `#${basePath}/edit/${encodeURIComponent(name)}` } + : { + text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/management/roles/_index.scss b/x-pack/plugins/security/public/management/roles/_index.scss new file mode 100644 index 00000000000000..5256c79f01f10f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/_index.scss @@ -0,0 +1 @@ +@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/management/roles/documentation_links.ts b/x-pack/plugins/security/public/management/roles/documentation_links.ts new file mode 100644 index 00000000000000..cf46973d3541ca --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/documentation_links.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DocLinksStart } from 'src/core/public'; + +export class DocumentationLinksService { + private readonly esDocBasePath: string; + + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/`; + } + + public getESClusterPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#privileges-list-cluster`; + } + + public getESRunAsPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#_run_as_privilege`; + } + + public getESIndicesPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#privileges-list-indices`; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap b/x-pack/plugins/security/public/management/roles/edit_role/__snapshots__/validate_role.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap rename to x-pack/plugins/security/public/management/roles/edit_role/__snapshots__/validate_role.test.ts.snap diff --git a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss new file mode 100644 index 00000000000000..0153b1734ceba0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss @@ -0,0 +1,3 @@ +@import './collapsible_panel/index'; +@import './spaces_popover_list/index'; +@import './privileges/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/_collapsible_panel.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/_collapsible_panel.scss rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss new file mode 100644 index 00000000000000..c0f4f8ab9a8701 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss @@ -0,0 +1 @@ +@import './collapsible_panel'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.test.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx index 416dd7f6c4e5c7..01af7cb4509f64 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx @@ -50,7 +50,6 @@ export class CollapsiblePanel extends Component { public getTitle = () => { return ( - // @ts-ignore diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx index cc16866c883555..f4af935be66487 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore - EuiConfirmModal, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DeleteRoleButton } from './delete_role_button'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx index 1ae84d3fb72242..c6a10396f235c4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore - EuiConfirmModal, - // @ts-ignore - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx new file mode 100644 index 00000000000000..2b3d7c811f6de3 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -0,0 +1,552 @@ +/* + * 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 { ReactWrapper } from 'enzyme'; +import React from 'react'; +import { act } from '@testing-library/react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { Capabilities } from 'src/core/public'; +import { Space } from '../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../features/public'; +// These modules should be moved into a common directory +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Actions } from '../../../../server/authorization/actions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { privilegesFactory } from '../../../../server/authorization/privileges'; +import { Role } from '../../../../common/model'; +import { DocumentationLinksService } from '../documentation_links'; +import { EditRolePage } from './edit_role_page'; +import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; +import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; + +import { TransformErrorSection } from './privileges/kibana/transform_error_section'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { licenseMock } from '../../../../common/licensing/index.mock'; +import { userAPIClientMock } from '../../users/index.mock'; +import { rolesAPIClientMock, indicesAPIClientMock, privilegesAPIClientMock } from '../index.mock'; + +const buildFeatures = () => { + return [ + { + id: 'feature1', + name: 'Feature 1', + icon: 'addDataApp', + app: ['feature1App'], + privileges: { + all: { + app: ['feature1App'], + ui: ['feature1-ui'], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, + { + id: 'feature2', + name: 'Feature 2', + icon: 'addDataApp', + app: ['feature2App'], + privileges: { + all: { + app: ['feature2App'], + ui: ['feature2-ui'], + savedObject: { + all: ['feature2'], + read: ['config'], + }, + }, + }, + }, + ] as Feature[]; +}; + +const buildRawKibanaPrivileges = () => { + return privilegesFactory(new Actions('unit_test_version'), { + getFeatures: () => buildFeatures(), + }).get(); +}; + +const buildBuiltinESPrivileges = () => { + return { + cluster: ['all', 'manage', 'monitor'], + index: ['all', 'read', 'write', 'index'], + }; +}; + +const buildUICapabilities = (canManageSpaces = true) => { + return { + catalogue: {}, + management: {}, + navLinks: {}, + spaces: { + manage: canManageSpaces, + }, + } as Capabilities; +}; + +const buildSpaces = () => { + return [ + { + id: 'default', + name: 'Default', + disabledFeatures: [], + _reserved: true, + }, + { + id: 'space_1', + name: 'Space 1', + disabledFeatures: [], + }, + { + id: 'space_2', + name: 'Space 2', + disabledFeatures: ['feature2'], + }, + ] as Space[]; +}; + +const expectReadOnlyFormButtons = (wrapper: ReactWrapper) => { + expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(1); + expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(0); +}; + +const expectSaveFormButtons = (wrapper: ReactWrapper) => { + expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(0); + expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(1); +}; + +function getProps({ + action, + role, + canManageSpaces = true, + spacesEnabled = true, +}: { + action: 'edit' | 'clone'; + role?: Role; + canManageSpaces?: boolean; + spacesEnabled?: boolean; +}) { + const rolesAPIClient = rolesAPIClientMock.create(); + rolesAPIClient.getRole.mockResolvedValue(role); + + const indexPatterns = dataPluginMock.createStartContract().indexPatterns; + indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*']); + + const indicesAPIClient = indicesAPIClientMock.create(); + + const userAPIClient = userAPIClientMock.create(); + userAPIClient.getUsers.mockResolvedValue([]); + + const privilegesAPIClient = privilegesAPIClientMock.create(); + privilegesAPIClient.getAll.mockResolvedValue(buildRawKibanaPrivileges()); + privilegesAPIClient.getBuiltIn.mockResolvedValue(buildBuiltinESPrivileges()); + + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleDocumentLevelSecurity: true, + allowRoleFieldLevelSecurity: true, + } as any); + + const { fatalErrors } = coreMock.createSetup(); + const { http, docLinks, notifications } = coreMock.createStart(); + http.get.mockImplementation(async path => { + if (path === '/api/features') { + return buildFeatures(); + } + + if (path === '/api/spaces/space') { + return buildSpaces(); + } + }); + + return { + action, + roleName: role?.name, + license, + http, + indexPatterns, + indicesAPIClient, + privilegesAPIClient, + rolesAPIClient, + userAPIClient, + notifications, + docLinks: new DocumentationLinksService(docLinks), + fatalErrors, + spacesEnabled, + uiCapabilities: buildUICapabilities(canManageSpaces), + }; +} + +describe('', () => { + describe('with spaces enabled', () => { + it('can render a reserved role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + + it('can render a user defined role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when creating a new role', async () => { + const wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when cloning an existing role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('renders an auth error when not authorized to manage spaces', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + + expect( + wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') + ).toHaveLength(1); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('renders a partial read-only view when there is a transform error', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(TransformErrorSection)).toHaveLength(1); + expectReadOnlyFormButtons(wrapper); + }); + }); + + describe('with spaces disabled', () => { + it('can render a reserved role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + + it('can render a user defined role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when creating a new role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('can render when cloning an existing role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('does not care if user cannot manage spaces', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + + expect( + wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') + ).toHaveLength(0); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('renders a partial read-only view when there is a transform error', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(TransformErrorSection)).toHaveLength(1); + expectReadOnlyFormButtons(wrapper); + }); + }); + + it('can render if features are not available', async () => { + const { http } = coreMock.createStart(); + http.get.mockImplementation(async path => { + if (path === '/api/features') { + const error = { response: { status: 404 } }; + throw error; + } + + if (path === '/api/spaces/space') { + return buildSpaces(); + } + }); + + const wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render if index patterns are not available', async () => { + const indexPatterns = dataPluginMock.createStartContract().indexPatterns; + indexPatterns.getTitles = jest.fn().mockRejectedValue({ response: { status: 403 } }); + + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx new file mode 100644 index 00000000000000..33e69a68ca8960 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -0,0 +1,594 @@ +/* + * 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 _, { get } from 'lodash'; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { + ChangeEvent, + Fragment, + FunctionComponent, + HTMLProps, + useEffect, + useRef, + useState, +} from 'react'; +import { + Capabilities, + FatalErrorsSetup, + HttpStart, + IHttpFetchError, + NotificationsStart, +} from 'src/core/public'; +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; +import { Space } from '../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../features/public'; +import { + KibanaPrivileges, + RawKibanaPrivileges, + Role, + BuiltinESPrivileges, + isReadOnlyRole as checkIfRoleReadOnly, + isReservedRole as checkIfRoleReserved, + copyRole, + prepareRoleClone, + RoleIndexPrivilege, +} from '../../../../common/model'; +import { ROLES_PATH } from '../../management_urls'; +import { RoleValidationResult, RoleValidator } from './validate_role'; +import { DeleteRoleButton } from './delete_role_button'; +import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; +import { ReservedRoleBadge } from './reserved_role_badge'; +import { SecurityLicense } from '../../../../common/licensing'; +import { UserAPIClient } from '../../users'; +import { DocumentationLinksService } from '../documentation_links'; +import { IndicesAPIClient } from '../indices_api_client'; +import { RolesAPIClient } from '../roles_api_client'; +import { PrivilegesAPIClient } from '../privileges_api_client'; + +interface Props { + action: 'edit' | 'clone'; + roleName?: string; + indexPatterns: IndexPatternsContract; + userAPIClient: PublicMethodsOf; + indicesAPIClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + privilegesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; + http: HttpStart; + license: SecurityLicense; + spacesEnabled: boolean; + uiCapabilities: Capabilities; + notifications: NotificationsStart; + fatalErrors: FatalErrorsSetup; +} + +function useRunAsUsers( + userAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup +) { + const [userNames, setUserNames] = useState(null); + useEffect(() => { + userAPIClient.getUsers().then( + users => setUserNames(users.map(user => user.username)), + err => fatalErrors.add(err) + ); + }, [fatalErrors, userAPIClient]); + + return userNames; +} + +function useIndexPatternsTitles( + indexPatterns: IndexPatternsContract, + fatalErrors: FatalErrorsSetup, + notifications: NotificationsStart +) { + const [indexPatternsTitles, setIndexPatternsTitles] = useState(null); + useEffect(() => { + indexPatterns + .getTitles() + .catch((err: IHttpFetchError) => { + // If user doesn't have access to the index patterns they still should be able to create new + // or edit existing role. + if (err.response?.status === 403) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.roles.noIndexPatternsPermission', { + defaultMessage: 'You need permission to access the list of available index patterns.', + }), + }); + return []; + } + + fatalErrors.add(err); + throw err; + }) + .then(setIndexPatternsTitles); + }, [fatalErrors, indexPatterns, notifications]); + + return indexPatternsTitles; +} + +function usePrivileges( + privilegesAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup +) { + const [privileges, setPrivileges] = useState<[RawKibanaPrivileges, BuiltinESPrivileges] | null>( + null + ); + useEffect(() => { + Promise.all([ + privilegesAPIClient.getAll({ includeActions: true }), + privilegesAPIClient.getBuiltIn(), + ]).then( + ([kibanaPrivileges, builtInESPrivileges]) => + setPrivileges([kibanaPrivileges, builtInESPrivileges]), + err => fatalErrors.add(err) + ); + }, [privilegesAPIClient, fatalErrors]); + + return privileges; +} + +function useRole( + rolesAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup, + notifications: NotificationsStart, + license: SecurityLicense, + action: string, + roleName?: string +) { + const [role, setRole] = useState(null); + useEffect(() => { + const rolePromise = roleName + ? rolesAPIClient.getRole(roleName) + : Promise.resolve({ + name: '', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + _unrecognized_applications: [], + } as Role); + + rolePromise + .then(fetchedRole => { + if (action === 'clone' && checkIfRoleReserved(fetchedRole)) { + backToRoleList(); + return; + } + + if (fetchedRole.elasticsearch.indices.length === 0) { + const emptyOption: RoleIndexPrivilege = { + names: [], + privileges: [], + }; + + const { + allowRoleDocumentLevelSecurity, + allowRoleFieldLevelSecurity, + } = license.getFeatures(); + + if (allowRoleFieldLevelSecurity) { + emptyOption.field_security = { + grant: ['*'], + except: [], + }; + } + + if (allowRoleDocumentLevelSecurity) { + emptyOption.query = ''; + } + + fetchedRole.elasticsearch.indices.push(emptyOption); + } + + setRole(action === 'clone' ? prepareRoleClone(fetchedRole) : copyRole(fetchedRole)); + }) + .catch((err: IHttpFetchError) => { + if (err.response?.status === 404) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.roles.roleNotFound', { + defaultMessage: 'No "{roleName}" role found.', + values: { roleName }, + }), + }); + backToRoleList(); + } else { + fatalErrors.add(err); + } + }); + }, [roleName, action, fatalErrors, rolesAPIClient, notifications, license]); + + return [role, setRole] as [Role | null, typeof setRole]; +} + +function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup, spacesEnabled: boolean) { + const [spaces, setSpaces] = useState(null); + useEffect(() => { + (spacesEnabled ? http.get('/api/spaces/space') : Promise.resolve([])).then( + fetchedSpaces => setSpaces(fetchedSpaces), + err => fatalErrors.add(err) + ); + }, [http, fatalErrors, spacesEnabled]); + + return spaces; +} + +function useFeatures(http: HttpStart, fatalErrors: FatalErrorsSetup) { + const [features, setFeatures] = useState(null); + useEffect(() => { + http + .get('/api/features') + .catch((err: IHttpFetchError) => { + // Currently, the `/api/features` endpoint effectively requires the "Global All" kibana privilege (e.g., what + // the `kibana_user` grants), because it returns information about all registered features (#35841). It's + // possible that a user with `manage_security` will attempt to visit the role management page without the + // correct Kibana privileges. If that's the case, then they receive a partial view of the role, and the UI does + // not allow them to make changes to that role's kibana privileges. When this user visits the edit role page, + // this API endpoint will throw a 404, which causes view to fail completely. So we instead attempt to detect the + // 404 here, and respond in a way that still allows the UI to render itself. + const unauthorizedForFeatures = err.response?.status === 404; + if (unauthorizedForFeatures) { + return []; + } + + fatalErrors.add(err); + throw err; + }) + .then(setFeatures); + }, [http, fatalErrors]); + + return features; +} + +function backToRoleList() { + window.location.hash = ROLES_PATH; +} + +export const EditRolePage: FunctionComponent = ({ + userAPIClient, + indexPatterns, + rolesAPIClient, + indicesAPIClient, + privilegesAPIClient, + http, + roleName, + action, + fatalErrors, + spacesEnabled, + license, + docLinks, + uiCapabilities, + notifications, +}) => { + // We should keep the same mutable instance of Validator for every re-render since we'll + // eventually enable validation after the first time user tries to save a role. + const { current: validator } = useRef(new RoleValidator({ shouldValidate: false })); + + const [formError, setFormError] = useState(null); + const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors); + const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications); + const privileges = usePrivileges(privilegesAPIClient, fatalErrors); + const spaces = useSpaces(http, fatalErrors, spacesEnabled); + const features = useFeatures(http, fatalErrors); + const [role, setRole] = useRole( + rolesAPIClient, + fatalErrors, + notifications, + license, + action, + roleName + ); + + if (!role || !runAsUsers || !indexPatternsTitles || !privileges || !spaces || !features) { + return null; + } + + const isEditingExistingRole = !!roleName && action === 'edit'; + const isReadOnlyRole = checkIfRoleReadOnly(role); + const isReservedRole = checkIfRoleReserved(role); + + const [kibanaPrivileges, builtInESPrivileges] = privileges; + + const getFormTitle = () => { + let titleText; + const props: HTMLProps = { + tabIndex: 0, + }; + if (isReservedRole) { + titleText = ( + + ); + props['aria-describedby'] = 'reservedRoleDescription'; + } else if (isEditingExistingRole) { + titleText = ( + + ); + } else { + titleText = ( + + ); + } + + return ( + +

+ {titleText} +

+
+ ); + }; + + const getActionButton = () => { + if (isEditingExistingRole && !isReadOnlyRole) { + return ( + + + + ); + } + + return null; + }; + + const getRoleName = () => { + return ( + + + } + helpText={ + !isReservedRole && isEditingExistingRole ? ( + + ) : ( + undefined + ) + } + {...validator.validateRoleName(role)} + > + + + + ); + }; + + const onNameChange = (e: ChangeEvent) => + setRole({ + ...role, + name: e.target.value.replace(/\s/g, '_'), + }); + + const getElasticsearchPrivileges = () => { + return ( +
+ + +
+ ); + }; + + const onRoleChange = (newRole: Role) => setRole(newRole); + + const getKibanaPrivileges = () => { + return ( +
+ + +
+ ); + }; + + const getFormButtons = () => { + if (isReadOnlyRole) { + return getReturnToRoleListButton(); + } + + return ( + + {getSaveButton()} + {getCancelButton()} + + {getActionButton()} + + ); + }; + + const getReturnToRoleListButton = () => { + return ( + + + + ); + }; + + const getSaveButton = () => { + const saveText = isEditingExistingRole ? ( + + ) : ( + + ); + + return ( + + {saveText} + + ); + }; + + const getCancelButton = () => { + return ( + + + + ); + }; + + const saveRole = async () => { + validator.enableValidation(); + + const result = validator.validateForSave(role); + if (result.isInvalid) { + setFormError(result); + } else { + setFormError(null); + + try { + await rolesAPIClient.saveRole({ role, spacesEnabled }); + } catch (error) { + notifications.toasts.addDanger(get(error, 'data.message')); + return; + } + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', + { defaultMessage: 'Saved role' } + ) + ); + + backToRoleList(); + } + }; + + const handleDeleteRole = async () => { + try { + await rolesAPIClient.deleteRole(role.name); + } catch (error) { + notifications.toasts.addDanger(get(error, 'data.message')); + return; + } + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted role' } + ) + ); + + backToRoleList(); + }; + + const description = spacesEnabled ? ( + + ) : ( + + ); + + return ( +
+ + {getFormTitle()} + + + + {description} + + {isReservedRole && ( + + + +

+ +

+
+
+ )} + + + + {getRoleName()} + + {getElasticsearchPrivileges()} + + {getKibanaPrivileges()} + + + + {getFormButtons()} +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/index.ts diff --git a/x-pack/legacy/plugins/security/public/lib/privilege_utils.test.ts b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/lib/privilege_utils.test.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.test.ts diff --git a/x-pack/legacy/plugins/security/public/lib/privilege_utils.ts b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts similarity index 93% rename from x-pack/legacy/plugins/security/public/lib/privilege_utils.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts index 74bde71dc421ab..3fd8536951967a 100644 --- a/x-pack/legacy/plugins/security/public/lib/privilege_utils.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RoleKibanaPrivilege } from '../../common/model'; +import { RoleKibanaPrivilege } from '../../../../common/model'; /** * Determines if the passed privilege spec defines global privileges. diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss new file mode 100644 index 00000000000000..a1a9d038065e61 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss @@ -0,0 +1,2 @@ +@import './privilege_feature_icon'; +@import './kibana/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss new file mode 100644 index 00000000000000..a7f24c96a28216 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss @@ -0,0 +1,4 @@ +.secPrivilegeFeatureIcon { + flex-shrink: 0; + margin-right: $euiSizeS; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index 795131337c31fd..323629de7578d7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -15,7 +15,7 @@ exports[`it renders without crashing 1`] = ` /> { + expect(shallowWithIntl()).toMatchSnapshot(); +}); + +test('it renders ClusterPrivileges', () => { + expect( + mountWithIntl().find(ClusterPrivileges) + ).toHaveLength(1); +}); + +test('it renders IndexPrivileges', () => { + expect( + mountWithIntl().find(IndexPrivileges) + ).toHaveLength(1); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx index c0e6db3fef21c8..96249ccf3ff87e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx @@ -7,7 +7,6 @@ import { EuiButton, EuiComboBox, - // @ts-ignore EuiDescribedFormGroup, EuiFormRow, EuiHorizontalRule, @@ -19,26 +18,26 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Role, BuiltinESPrivileges } from '../../../../../../../common/model'; -// @ts-ignore -import { documentationLinks } from '../../../../../../documentation_links'; -import { RoleValidator } from '../../../lib/validate_role'; +import { Role, BuiltinESPrivileges } from '../../../../../../common/model'; +import { SecurityLicense } from '../../../../../../common/licensing'; +import { IndicesAPIClient } from '../../../indices_api_client'; +import { RoleValidator } from '../../validate_role'; import { CollapsiblePanel } from '../../collapsible_panel'; import { ClusterPrivileges } from './cluster_privileges'; - import { IndexPrivileges } from './index_privileges'; +import { DocumentationLinksService } from '../../../documentation_links'; interface Props { role: Role; editable: boolean; - httpClient: any; + indicesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; + license: SecurityLicense; onChange: (role: Role) => void; runAsUsers: string[]; validator: RoleValidator; builtinESPrivileges: BuiltinESPrivileges; indexPatterns: string[]; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; } export class ElasticsearchPrivileges extends Component { @@ -53,23 +52,22 @@ export class ElasticsearchPrivileges extends Component { public getForm = () => { const { role, - httpClient, + indicesAPIClient, + docLinks, validator, onChange, editable, indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, + license, builtinESPrivileges, } = this.props; const indexProps = { role, - httpClient, + indicesAPIClient, validator, indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, + license, onChange, availableIndexPrivileges: builtinESPrivileges.index, }; @@ -91,7 +89,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription" defaultMessage="Manage the actions this role can perform against your cluster. " /> - {this.learnMore(documentationLinks.esClusterPrivileges)} + {this.learnMore(docLinks.getESClusterPrivilegesDocUrl())}

} > @@ -121,7 +119,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription" defaultMessage="Allow requests to be submitted on the behalf of other users. " /> - {this.learnMore(documentationLinks.esRunAsPrivileges)} + {this.learnMore(docLinks.getESRunAsPrivilegesDocUrl())}

} > @@ -165,7 +163,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToClusterDataDescription" defaultMessage="Control access to the data in your cluster. " /> - {this.learnMore(documentationLinks.esIndicesPrivileges)} + {this.learnMore(docLinks.getESIndicesPrivilegesDocUrl())}

diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx index 6d386fd78a11b1..5e2da513143653 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, EuiTextArea } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { RoleValidator } from '../../../lib/validate_role'; +import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; test('it renders without crashing', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx index bafc56dc167ea5..15e0367c2b6dc1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ChangeEvent, Component, Fragment } from 'react'; -import { RoleIndexPrivilege } from '../../../../../../../common/model'; -import { RoleValidator } from '../../../lib/validate_role'; +import { RoleIndexPrivilege } from '../../../../../../common/model'; +import { RoleValidator } from '../../validate_role'; const fromOption = (option: any) => option.label; const toOption = (value: string) => ({ label: value }); @@ -164,7 +164,6 @@ export class IndexPrivilegeForm extends Component { {!isReadOnlyRole && ( { - // @ts-ignore missing "compressed" prop definition { } return ( - // @ts-ignore {!this.props.isReadOnlyRole && ( { - // @ts-ignore missing "compressed" proptype new Promise(setImmediate); test('it renders without crashing', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + const props = { role: { name: '', @@ -25,14 +34,13 @@ test('it renders without crashing', async () => { run_as: [], }, }, - httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], editable: true, - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient: indicesAPIClientMock.create(), + license, }; const wrapper = shallowWithIntl(); await flushPromises(); @@ -40,6 +48,12 @@ test('it renders without crashing', async () => { }); test('it renders a IndexPrivilegeForm for each privilege on the role', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + const props = { role: { name: '', @@ -59,14 +73,13 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () run_as: [], }, }, - httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], editable: true, - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient: indicesAPIClientMock.create(), + license, }; const wrapper = mountWithIntl(); await flushPromises(); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx similarity index 84% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index f09084ad2bb382..2c745067fede2d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -5,19 +5,23 @@ */ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { Role, RoleIndexPrivilege } from '../../../../../../../common/model'; -import { isReadOnlyRole, isRoleEnabled } from '../../../../../../lib/role_utils'; -import { getFields } from '../../../../../../objects'; -import { RoleValidator } from '../../../lib/validate_role'; +import { + Role, + RoleIndexPrivilege, + isReadOnlyRole, + isRoleEnabled, +} from '../../../../../../common/model'; +import { SecurityLicense } from '../../../../../../common/licensing'; +import { IndicesAPIClient } from '../../../indices_api_client'; +import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; interface Props { role: Role; indexPatterns: string[]; availableIndexPrivileges: string[]; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; - httpClient: any; + indicesAPIClient: PublicMethodsOf; + license: SecurityLicense; onChange: (role: Role) => void; validator: RoleValidator; } @@ -43,20 +47,16 @@ export class IndexPrivileges extends Component { public render() { const { indices = [] } = this.props.role.elasticsearch; - const { - indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, - availableIndexPrivileges, - } = this.props; + const { indexPatterns, license, availableIndexPrivileges } = this.props; + const { allowRoleDocumentLevelSecurity, allowRoleFieldLevelSecurity } = license.getFeatures(); const props = { indexPatterns, // If editing an existing role while that has been disabled, always show the FLS/DLS fields because currently // a role is only marked as disabled if it has FLS/DLS setup (usually before the user changed to a license that // doesn't permit FLS/DLS). - allowDocumentLevelSecurity: allowDocumentLevelSecurity || !isRoleEnabled(this.props.role), - allowFieldLevelSecurity: allowFieldLevelSecurity || !isRoleEnabled(this.props.role), + allowDocumentLevelSecurity: allowRoleDocumentLevelSecurity || !isRoleEnabled(this.props.role), + allowFieldLevelSecurity: allowRoleFieldLevelSecurity || !isRoleEnabled(this.props.role), isReadOnlyRole: isReadOnlyRole(this.props.role), }; @@ -171,7 +171,7 @@ export class IndexPrivileges extends Component { try { return { - [pattern]: await getFields(this.props.httpClient, pattern), + [pattern]: await this.props.indicesAPIClient.getFields(pattern), }; } catch (e) { return { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss new file mode 100644 index 00000000000000..19547c0e1953e0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss @@ -0,0 +1,2 @@ +@import './feature_table/index'; +@import './space_aware_privilege_section/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/lib/constants.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/constants.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/lib/constants.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/constants.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss new file mode 100644 index 00000000000000..6a96553742819f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss @@ -0,0 +1 @@ +@import './change_all_privileges'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx index 9648bf1d111bf4..dea42e16f99d42 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore import { EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { FeatureTable } from './feature_table'; const defaultPrivilegeDefinition = new KibanaPrivileges({ @@ -113,7 +112,6 @@ describe('FeatureTable', () => { onChange={jest.fn()} onChangeAll={jest.fn()} spacesIndex={0} - intl={null as any} /> ); @@ -141,7 +139,6 @@ describe('FeatureTable', () => { onChange={jest.fn()} onChangeAll={jest.fn()} spacesIndex={-1} - intl={null as any} /> ); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx similarity index 91% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index a05dc687fce4ac..8283efe23260af 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -4,28 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import React, { Component } from 'react'; import { - // @ts-ignore EuiButtonGroup, EuiIcon, EuiIconTip, - // @ts-ignore EuiInMemoryTable, EuiText, IconType, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import _ from 'lodash'; -import React, { Component } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Feature } from '../../../../../../../../features/public'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model'; import { AllowedPrivilege, CalculatedPrivilege, PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { NO_PRIVILEGE_VALUE } from '../constants'; import { PrivilegeDisplay } from '../space_aware_privilege_section/privilege_display'; import { ChangeAllPrivilegesControl } from './change_all_privileges'; @@ -36,7 +35,6 @@ interface Props { allowedPrivileges: AllowedPrivilege; rankedFeaturePrivileges: FeaturesPrivileges; kibanaPrivileges: KibanaPrivileges; - intl: InjectedIntl; spacesIndex: number; onChange: (featureId: string, privileges: string[]) => void; onChangeAll: (privileges: string[]) => void; @@ -100,9 +98,7 @@ export class FeatureTable extends Component { const availablePrivileges = Object.values(rankedFeaturePrivileges)[0]; return ( - // @ts-ignore missing responsive from typedef { private getColumns = (availablePrivileges: string[]) => [ { field: 'feature', - name: this.props.intl.formatMessage({ - id: 'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle', - defaultMessage: 'Feature', - }), + name: i18n.translate( + 'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle', + { defaultMessage: 'Feature' } + ), render: (feature: TableFeature) => { let tooltipElement = null; if (feature.privilegesTooltip) { @@ -239,9 +235,7 @@ export class FeatureTable extends Component { }); return ( - // @ts-ignore missing name from typedef void; validator: RoleValidator; - intl: InjectedIntl; } export class KibanaPrivilegesRegion extends Component { @@ -81,7 +79,6 @@ export class KibanaPrivilegesRegion extends Component { privilegeCalculatorFactory={privilegeCalculatorFactory} onChange={onChange} editable={editable} - intl={this.props.intl} /> ); } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx index 135419cc9a10d2..bda0227372c094 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx @@ -6,7 +6,7 @@ import { EuiSelect } from '@elastic/eui'; import React, { ChangeEvent, Component } from 'react'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { NO_PRIVILEGE_VALUE } from '../constants'; interface Props { ['data-test-subj']: string; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx index f97fa93294ff5b..db1e3cfd616212 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore + import { EuiButtonGroup, EuiButtonGroupProps, EuiComboBox, EuiSuperSelect } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { SimplePrivilegeSection } from './simple_privilege_section'; import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx index 7768dc769a32f7..2221fc6bab2797 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx @@ -6,21 +6,24 @@ import { EuiComboBox, - // @ts-ignore EuiDescribedFormGroup, EuiFormRow, EuiSuperSelect, EuiText, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { Feature } from '../../../../../../../../features/public'; +import { + KibanaPrivileges, + Role, + RoleKibanaPrivilege, + copyRole, +} from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; import { FeatureTable } from '../feature_table'; import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; @@ -31,7 +34,6 @@ interface Props { features: Feature[]; onChange: (role: Role) => void; editable: boolean; - intl: InjectedIntl; } interface State { @@ -230,7 +232,6 @@ export class SimplePrivilegeSection extends Component { allowedPrivileges={allowedPrivileges} rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges} features={this.props.features} - intl={this.props.intl} onChange={this.onFeaturePrivilegeChange} onChangeAll={this.onChangeAllFeaturePrivileges} spacesIndex={this.props.role.kibana.findIndex(k => isGlobalPrivilegeDefinition(k))} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts index d412ba63403e19..428836c9f181b9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RawKibanaPrivileges } from '../../../../../../../../../common/model'; +import { RawKibanaPrivileges } from '../../../../../../../../common/model'; export const rawKibanaPrivileges: RawKibanaPrivileges = { global: { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index c20a391cdb20c5..e9f2f946e98859 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -351,108 +351,6 @@ exports[` renders without crashing 1`] = ` } disabled={true} features={Array []} - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } kibanaPrivileges={ KibanaPrivileges { "rawKibanaPrivileges": Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss index 5f9fbced5ee6ac..8f47727fdf8d62 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss @@ -9,6 +9,6 @@ .secPrivilegeMatrix__row--isBasePrivilege, .secPrivilegeMatrix__cell--isGlobalPrivilege, -.secPrivilegeTable__row--isGlobalSpace, { +.secPrivilegeTable__row--isGlobalSpace { background-color: $euiColorLightestShade; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx index 62e22050132fd7..c6268e19abfd1f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx @@ -7,7 +7,7 @@ import { EuiIconTip, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { PRIVILEGE_SOURCE } from '../../../../../../../lib/kibana_privilege_calculator'; +import { PRIVILEGE_SOURCE } from '../kibana_privilege_calculator'; import { PrivilegeDisplay } from './privilege_display'; describe('PrivilegeDisplay', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index 6af7672f6fef86..55ac99da4c8c16 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx @@ -7,11 +7,8 @@ import { EuiIcon, EuiIconTip, EuiText, IconType, PropsOf, EuiToolTip } from '@el import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ReactNode, FC } from 'react'; -import { - PRIVILEGE_SOURCE, - PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { PRIVILEGE_SOURCE, PrivilegeExplanation } from '../kibana_privilege_calculator'; +import { NO_PRIVILEGE_VALUE } from '../constants'; interface Props extends PropsOf { privilege: string | string[] | undefined; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx index ee121caa13a2af..16aad4826ae44c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore + import { EuiButtonEmpty, EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../..//lib/kibana_privilege_calculator'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { PrivilegeMatrix } from './privilege_matrix'; describe('PrivilegeMatrix', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx index 962487312c83df..b3449e32c6c917 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx @@ -8,7 +8,6 @@ import { EuiButtonEmpty, EuiIcon, EuiIconTip, - // @ts-ignore EuiInMemoryTable, EuiModal, EuiModalBody, @@ -16,18 +15,16 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiOverlayMask, - // @ts-ignore - EuiToolTip, IconType, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { SpaceAvatar } from '../../../../../../../../../spaces/public/space_avatar'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { FeaturesPrivileges, Role } from '../../../../../../../../common/model'; -import { CalculatedPrivilege } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; +import { SpaceAvatar } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { FeaturesPrivileges, Role } from '../../../../../../../common/model'; +import { CalculatedPrivilege } from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; @@ -258,11 +255,9 @@ export class PrivilegeMatrix extends Component { ]; return ( - // @ts-ignore missing rowProps from typedef { return { className: item.feature.isBase ? 'secPrivilegeMatrix__row--isBasePrivilege' : '', diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx index 2b7d87f663d729..675f02a81f9e1d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx @@ -9,8 +9,8 @@ import { merge } from 'lodash'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; -import { KibanaPrivileges } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { KibanaPrivileges } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { rawKibanaPrivileges } from './__fixtures__'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index 5abb87d23bb6e6..6d1f5117c52e98 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -24,17 +24,16 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role, copyRole } from '../../../../../../../common/model'; import { AllowedPrivilege, KibanaPrivilegeCalculatorFactory, PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { hasAssignedFeaturePrivileges } from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../kibana_privilege_calculator'; +import { hasAssignedFeaturePrivileges } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE } from '../constants'; import { FeatureTable } from '../feature_table'; import { SpaceSelector } from './space_selector'; @@ -285,7 +284,6 @@ export class PrivilegeSpaceForm extends Component { calculatedPrivileges={calculatedPrivileges} allowedPrivileges={allowedPrivileges} rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges} - intl={this.props.intl} onChange={this.onFeaturePrivilegesChange} onChangeAll={this.onChangeAllFeaturePrivileges} kibanaPrivileges={this.props.kibanaPrivileges} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index 37ee43c5473b0d..f0a391c98c9100 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -10,8 +10,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { PrivilegeSpaceTable } from './privilege_space_table'; import { PrivilegeDisplay } from './privilege_display'; -import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { rawKibanaPrivileges } from './__fixtures__'; interface TableRow { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 65a3df9fb47a1a..1c27ec84f50dcd 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -12,22 +12,21 @@ import { EuiBasicTableColumn, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import _ from 'lodash'; import React, { Component } from 'react'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; +import { getSpaceColor } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; import { FeaturesPrivileges, Role, RoleKibanaPrivilege, -} from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; + copyRole, +} from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { isGlobalPrivilegeDefinition, hasAssignedFeaturePrivileges, -} from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx index 2756b1c4472744..e06d2a4f7dc337 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx @@ -6,9 +6,9 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { KibanaPrivileges } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { RoleValidator } from '../../../../lib/validate_role'; +import { KibanaPrivileges } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index d324cf99c8418a..21cadfafe1790b 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -14,13 +14,12 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isReservedRole } from '../../../../../../../lib/role_utils'; -import { RoleValidator } from '../../../../lib/validate_role'; +import { Capabilities } from 'src/core/public'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role, isReservedRole } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; @@ -34,7 +33,7 @@ interface Props { editable: boolean; validator: RoleValidator; intl: InjectedIntl; - uiCapabilities: UICapabilities; + uiCapabilities: Capabilities; features: Feature[]; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx index 0eb9cf0b0ee9d7..cfeb5b9f37d8ce 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx @@ -7,8 +7,8 @@ import { EuiComboBox, EuiComboBoxOptionProps, EuiHealth, EuiHighlight } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; +import { getSpaceColor } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; const spaceToOption = (space?: Space, currentSelection?: 'global' | 'spaces') => { if (!space) { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/transform_error_section.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/transform_error_section.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx index 9b483d92cde41b..d29b442420a90c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx @@ -7,7 +7,7 @@ import { EuiIcon } from '@elastic/eui'; import { shallow } from 'enzyme'; import React from 'react'; -import { Role } from '../../../../../common/model'; +import { Role } from '../../../../common/model'; import { ReservedRoleBadge } from './reserved_role_badge'; const reservedRole: Role = { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx index 3d817d1e07d212..501ca7589dafde 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Role } from '../../../../../common/model'; -import { isReservedRole } from '../../../../lib/role_utils'; +import { Role, isReservedRole } from '../../../../common/model'; interface Props { role: Role; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss new file mode 100644 index 00000000000000..b40a32cb8df96b --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss @@ -0,0 +1 @@ +@import './spaces_popover_list'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index a99e389044eaad..bb7a6db97f7c88 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -14,9 +14,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { SpaceAvatar } from '../../../../../../../spaces/public/space_avatar'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../../../../plugins/spaces/common/constants'; -import { Space } from '../../../../../../../../../plugins/spaces/common/model/space'; +import { SpaceAvatar } from '../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../spaces/common'; +import { Space } from '../../../../../../spaces/common/model/space'; interface Props { spaces: Space[]; @@ -146,7 +146,6 @@ export class SpacesPopoverList extends Component { return (
{ - // @ts-ignore onSearch isn't defined on the type ({ + getFields: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/roles/indices_api_client.ts b/x-pack/plugins/security/public/management/roles/indices_api_client.ts new file mode 100644 index 00000000000000..65d9a40a776eb0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/indices_api_client.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; + +export class IndicesAPIClient { + constructor(private readonly http: HttpStart) {} + + async getFields(query: string) { + return ( + (await this.http.get(`/internal/security/fields/${encodeURIComponent(query)}`)) || + [] + ); + } +} diff --git a/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts new file mode 100644 index 00000000000000..2564914a1d3d83 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const privilegesAPIClientMock = { + create: () => ({ + getAll: jest.fn(), + getBuiltIn: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/roles/privileges_api_client.ts b/x-pack/plugins/security/public/management/roles/privileges_api_client.ts new file mode 100644 index 00000000000000..45bd2fd8fb3a6a --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/privileges_api_client.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { BuiltinESPrivileges, RawKibanaPrivileges } from '../../../common/model'; + +export class PrivilegesAPIClient { + constructor(private readonly http: HttpStart) {} + + async getAll({ includeActions }: { includeActions: boolean }) { + return await this.http.get('/api/security/privileges', { + query: { includeActions }, + }); + } + + async getBuiltIn() { + return await this.http.get('/internal/security/esPrivileges/builtin'); + } +} diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts new file mode 100644 index 00000000000000..c4d3724c0ecb5f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.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 const rolesAPIClientMock = { + create: () => ({ + getRoles: jest.fn(), + getRole: jest.fn(), + deleteRole: jest.fn(), + saveRole: jest.fn(), + }), +}; diff --git a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts similarity index 82% rename from x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts rename to x-pack/plugins/security/public/management/roles/roles_api_client.test.ts index 1ea19f2637305e..75611613684051 100644 --- a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts @@ -4,12 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Role } from '../../common/model'; -import { transformRoleForSave } from './transform_role_for_save'; +import { Role } from '../../../common/model'; +import { RolesAPIClient } from './roles_api_client'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +describe('RolesAPIClient', () => { + async function saveRole(role: Role, spacesEnabled: boolean) { + const httpMock = httpServiceMock.createStartContract(); + const rolesAPIClient = new RolesAPIClient(httpMock); + + await rolesAPIClient.saveRole({ role, spacesEnabled }); + expect(httpMock.put).toHaveBeenCalledTimes(1); + + return JSON.parse(httpMock.put.mock.calls[0][1]?.body as any); + } -describe('transformRoleForSave', () => { describe('spaces disabled', () => { - it('removes placeholder index privileges', () => { + it('removes placeholder index privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -20,7 +31,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -32,7 +43,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes placeholder query entries', () => { + it('removes placeholder query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -43,7 +54,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -55,7 +66,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes transient fields not required for save', () => { + it('removes transient fields not required for save', async () => { const role: Role = { name: 'my role', transient_metadata: { @@ -74,7 +85,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ metadata: { @@ -89,7 +100,7 @@ describe('transformRoleForSave', () => { }); }); - it('does not remove actual query entries', () => { + it('does not remove actual query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -100,7 +111,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -112,7 +123,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove feature privileges if a corresponding base privilege is defined', () => { + it('should remove feature privileges if a corresponding base privilege is defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -132,7 +143,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -150,7 +161,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -170,7 +181,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -191,7 +202,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove space privileges', () => { + it('should remove space privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -219,7 +230,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -242,7 +253,7 @@ describe('transformRoleForSave', () => { }); describe('spaces enabled', () => { - it('removes placeholder index privileges', () => { + it('removes placeholder index privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -253,7 +264,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -265,7 +276,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes placeholder query entries', () => { + it('removes placeholder query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -276,7 +287,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -288,7 +299,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes transient fields not required for save', () => { + it('removes transient fields not required for save', async () => { const role: Role = { name: 'my role', transient_metadata: { @@ -307,7 +318,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ metadata: { @@ -322,7 +333,7 @@ describe('transformRoleForSave', () => { }); }); - it('does not remove actual query entries', () => { + it('does not remove actual query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -333,7 +344,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -345,7 +356,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove feature privileges if a corresponding base privilege is defined', () => { + it('should remove feature privileges if a corresponding base privilege is defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -365,7 +376,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -383,7 +394,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -403,7 +414,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -424,7 +435,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove space privileges', () => { + it('should not remove space privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -452,7 +463,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts new file mode 100644 index 00000000000000..d7e98e03a965b7 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.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 { HttpStart } from 'src/core/public'; +import { Role, RoleIndexPrivilege, copyRole } from '../../../common/model'; +import { isGlobalPrivilegeDefinition } from './edit_role/privilege_utils'; + +export class RolesAPIClient { + constructor(private readonly http: HttpStart) {} + + public async getRoles() { + return await this.http.get('/api/security/role'); + } + + public async getRole(roleName: string) { + return await this.http.get(`/api/security/role/${encodeURIComponent(roleName)}`); + } + + public async deleteRole(roleName: string) { + await this.http.delete(`/api/security/role/${encodeURIComponent(roleName)}`); + } + + public async saveRole({ role, spacesEnabled }: { role: Role; spacesEnabled: boolean }) { + await this.http.put(`/api/security/role/${encodeURIComponent(role.name)}`, { + body: JSON.stringify(this.transformRoleForSave(copyRole(role), spacesEnabled)), + }); + } + + private transformRoleForSave(role: Role, spacesEnabled: boolean) { + // Remove any placeholder index privileges + const isPlaceholderPrivilege = (indexPrivilege: RoleIndexPrivilege) => + indexPrivilege.names.length === 0; + role.elasticsearch.indices = role.elasticsearch.indices.filter( + indexPrivilege => !isPlaceholderPrivilege(indexPrivilege) + ); + + // Remove any placeholder query entries + role.elasticsearch.indices.forEach(index => index.query || delete index.query); + + // If spaces are disabled, then do not persist any space privileges + if (!spacesEnabled) { + role.kibana = role.kibana.filter(isGlobalPrivilegeDefinition); + } + + role.kibana.forEach(kibanaPrivilege => { + // If a base privilege is defined, then do not persist feature privileges + if (kibanaPrivilege.base.length > 0) { + kibanaPrivilege.feature = {}; + } + }); + + delete role.name; + delete role.transient_metadata; + delete role._unrecognized_applications; + delete role._transform_error; + + return role; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx similarity index 72% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx index 34784b4b2accb6..37eed3357241d5 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { Component, Fragment } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -15,23 +16,24 @@ import { EuiOverlayMask, EuiText, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component, Fragment } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { RolesApi } from '../../../../../lib/roles_api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { RolesAPIClient } from '../../roles_api_client'; interface Props { rolesToDelete: string[]; - intl: InjectedIntl; callback: (rolesToDelete: string[], errors: string[]) => void; onCancel: () => void; + notifications: NotificationsStart; + rolesAPIClient: PublicMethodsOf; } interface State { deleteInProgress: boolean; } -class ConfirmDeleteUI extends Component { +export class ConfirmDelete extends Component { constructor(props: Props) { super(props); this.state = { @@ -40,15 +42,12 @@ class ConfirmDeleteUI extends Component { } public render() { - const { rolesToDelete, intl } = this.props; + const { rolesToDelete } = this.props; const moreThanOne = rolesToDelete.length > 1; - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.deleteRoleTitle', - defaultMessage: 'Delete role{value, plural, one {{roleName}} other {s}}', - }, - { value: rolesToDelete.length, roleName: ` ${rolesToDelete[0]}` } - ); + const title = i18n.translate('xpack.security.management.roles.deleteRoleTitle', { + defaultMessage: 'Delete role{value, plural, one {{roleName}} other {s}}', + values: { value: rolesToDelete.length, roleName: ` ${rolesToDelete[0]}` }, + }); // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability // to disable the buttons since this could be a long-running operation @@ -128,32 +127,24 @@ class ConfirmDeleteUI extends Component { }; private deleteRoles = async () => { - const { rolesToDelete, callback } = this.props; + const { rolesToDelete, callback, rolesAPIClient, notifications } = this.props; const errors: string[] = []; const deleteOperations = rolesToDelete.map(roleName => { const deleteRoleTask = async () => { try { - await RolesApi.deleteRole(roleName); - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.roles.confirmDelete.roleSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted role {roleName}', - }, - { roleName } + await rolesAPIClient.deleteRole(roleName); + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.roles.confirmDelete.roleSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted role {roleName}', values: { roleName } } ) ); } catch (e) { errors.push(roleName); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.roles.confirmDelete.roleDeletingErrorNotificationMessage', - defaultMessage: 'Error deleting role {roleName}', - }, - { roleName } + notifications.toasts.addDanger( + i18n.translate( + 'xpack.security.management.roles.confirmDelete.roleDeletingErrorNotificationMessage', + { defaultMessage: 'Error deleting role {roleName}', values: { roleName } } ) ); } @@ -167,5 +158,3 @@ class ConfirmDeleteUI extends Component { callback(rolesToDelete, errors); }; } - -export const ConfirmDelete = injectI18n(ConfirmDeleteUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/permission_denied.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/permission_denied.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/permission_denied.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/permission_denied.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx similarity index 63% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 6da2f2442d488b..63ace53420612d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -4,50 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -let mockSimulate403 = false; -const mock403 = () => ({ body: { statusCode: 403 } }); -jest.mock('../../../../lib/roles_api', () => { - return { - RolesApi: { - async getRoles() { - if (mockSimulate403) { - throw mock403(); - } - return [ - { - name: 'test-role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - }, - { - name: 'reserved-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - metadata: { - _reserved: true, - }, - }, - { - name: 'disabled-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - transient_metadata: { - enabled: false, - }, - }, - ]; - }, - }, - }; -}); - import { EuiIcon } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { RolesAPIClient } from '../roles_api_client'; import { PermissionDenied } from './permission_denied'; import { RolesGridPage } from './roles_grid_page'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { rolesAPIClientMock } from '../index.mock'; + +const mock403 = () => ({ body: { statusCode: 403 } }); + const waitForRender = async ( wrapper: ReactWrapper, condition: (wrapper: ReactWrapper) => boolean @@ -69,12 +38,37 @@ const waitForRender = async ( }; describe('', () => { + let apiClientMock: jest.Mocked>; beforeEach(() => { - mockSimulate403 = false; + apiClientMock = rolesAPIClientMock.create(); + apiClientMock.getRoles.mockResolvedValue([ + { + name: 'test-role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, + { + name: 'reserved-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + metadata: { _reserved: true }, + }, + { + name: 'disabled-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + transient_metadata: { enabled: false }, + }, + ]); }); it(`renders reserved roles as such`, async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const initialIconCount = wrapper.find(EuiIcon).length; await waitForRender(wrapper, updatedWrapper => { @@ -87,8 +81,14 @@ describe('', () => { }); it('renders permission denied if required', async () => { - mockSimulate403 = true; - const wrapper = mountWithIntl(); + apiClientMock.getRoles.mockRejectedValue(mock403()); + + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(PermissionDenied).length > 0; }); @@ -96,7 +96,12 @@ describe('', () => { }); it('renders role actions as appropriate', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const initialIconCount = wrapper.find(EuiIcon).length; await waitForRender(wrapper, updatedWrapper => { diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 2083a93f4b33c9..7c686bef391a79 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import React, { Component } from 'react'; import { EuiButton, EuiIcon, @@ -18,18 +20,17 @@ import { EuiButtonIcon, EuiBasicTableColumn, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import _ from 'lodash'; -import React, { Component } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { Role } from '../../../../../common/model'; -import { isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../lib/role_utils'; -import { RolesApi } from '../../../../lib/roles_api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { Role, isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../common/model'; +import { RolesAPIClient } from '../roles_api_client'; import { ConfirmDelete } from './confirm_delete'; import { PermissionDenied } from './permission_denied'; interface Props { - intl: InjectedIntl; + notifications: NotificationsStart; + rolesAPIClient: PublicMethodsOf; } interface State { @@ -44,7 +45,7 @@ const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { return `#/management/security/roles/${action}${roleName ? `/${roleName}` : ''}`; }; -class RolesGridPageUI extends Component { +export class RolesGridPage extends Component { constructor(props: Props) { super(props); this.state = { @@ -68,7 +69,6 @@ class RolesGridPageUI extends Component { private getPageContent = () => { const { roles } = this.state; - const { intl } = this.props; return ( @@ -105,6 +105,8 @@ class RolesGridPageUI extends Component { onCancel={this.onCancelDelete} rolesToDelete={this.state.selection.map(role => role.name)} callback={this.handleDelete} + notifications={this.props.notifications} + rolesAPIClient={this.props.rolesAPIClient} /> ) : null} @@ -112,7 +114,7 @@ class RolesGridPageUI extends Component { !role.metadata || !role.metadata._reserved, @@ -155,17 +157,16 @@ class RolesGridPageUI extends Component { ); }; - private getColumnConfig = (intl: InjectedIntl) => { - const reservedRoleDesc = intl.formatMessage({ - id: 'xpack.security.management.roles.reservedColumnDescription', - defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.', - }); + private getColumnConfig = () => { + const reservedRoleDesc = i18n.translate( + 'xpack.security.management.roles.reservedColumnDescription', + { defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.' } + ); return [ { field: 'name', - name: intl.formatMessage({ - id: 'xpack.security.management.roles.nameColumnName', + name: i18n.translate('xpack.security.management.roles.nameColumnName', { defaultMessage: 'Role', }), sortable: true, @@ -188,8 +189,7 @@ class RolesGridPageUI extends Component { }, { field: 'metadata', - name: intl.formatMessage({ - id: 'xpack.security.management.roles.reservedColumnName', + name: i18n.translate('xpack.security.management.roles.reservedColumnName', { defaultMessage: 'Reserved', }), sortable: ({ metadata }: Role) => Boolean(metadata && metadata._reserved), @@ -197,8 +197,7 @@ class RolesGridPageUI extends Component { align: 'right', description: reservedRoleDesc, render: (metadata: Role['metadata']) => { - const label = intl.formatMessage({ - id: 'xpack.security.management.roles.reservedRoleIconLabel', + const label = i18n.translate('xpack.security.management.roles.reservedRoleIconLabel', { defaultMessage: 'Reserved role', }); @@ -210,8 +209,7 @@ class RolesGridPageUI extends Component { }, }, { - name: intl.formatMessage({ - id: 'xpack.security.management.roles.actionsColumnName', + name: i18n.translate('xpack.security.management.roles.actionsColumnName', { defaultMessage: 'Actions', }), width: '150px', @@ -219,15 +217,10 @@ class RolesGridPageUI extends Component { { available: (role: Role) => !isReadOnlyRole(role), render: (role: Role) => { - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.editRoleActionName', - defaultMessage: `Edit {roleName}`, - }, - { - roleName: role.name, - } - ); + const title = i18n.translate('xpack.security.management.roles.editRoleActionName', { + defaultMessage: `Edit {roleName}`, + values: { roleName: role.name }, + }); return ( { { available: (role: Role) => !isReservedRole(role), render: (role: Role) => { - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.cloneRoleActionName', - defaultMessage: `Clone {roleName}`, - }, - { - roleName: role.name, - } - ); + const title = i18n.translate('xpack.security.management.roles.cloneRoleActionName', { + defaultMessage: `Clone {roleName}`, + values: { roleName: role.name }, + }); return ( { private async loadRoles() { try { - const roles = await RolesApi.getRoles(); + const roles = await this.props.rolesAPIClient.getRoles(); this.setState({ roles }); } catch (e) { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true }); } else { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.roles.fetchingRolesErrorMessage', - defaultMessage: 'Error fetching roles: {message}', - }, - { message: _.get(e, 'body.message', '') } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { + defaultMessage: 'Error fetching roles: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) ); } } @@ -339,5 +324,3 @@ class RolesGridPageUI extends Component { this.setState({ showDeleteConfirmation: false }); }; } - -export const RolesGridPage = injectI18n(RolesGridPageUI); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx new file mode 100644 index 00000000000000..48bc1a6580a93d --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -0,0 +1,160 @@ +/* + * 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 { licenseMock } from '../../../common/licensing/index.mock'; + +jest.mock('./roles_grid', () => ({ + RolesGridPage: (props: any) => `Roles Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_role', () => ({ + EditRolePage: (props: any) => `Role Edit Page: ${JSON.stringify(props)}`, +})); + +import { rolesManagementApp } from './roles_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +async function mountApp(basePath: string) { + const { fatalErrors } = coreMock.createSetup(); + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await rolesManagementApp + .create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: jest.fn().mockResolvedValue([coreMock.createStart(), { data: {} }]), + }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('rolesManagementApp', () => { + it('create() returns proper management app descriptor', () => { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + + expect( + rolesManagementApp.create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "roles", + "mount": [Function], + "order": 20, + "title": "Roles", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/roles'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Roles' }]); + expect(container).toMatchInlineSnapshot(` +
+ Roles Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create role` page', async () => { + const basePath = '/some-base-path/roles'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit role` page', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'someRoleName'; + window.location.hash = `${basePath}/edit/${roleName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { href: `#/some-base-path/roles/edit/${roleName}`, text: roleName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"edit","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `clone role` page', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'someRoleName'; + window.location.hash = `${basePath}/clone/${roleName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes role name in `edit role` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'some 安全性 role'; + window.location.hash = `${basePath}/edit/${roleName}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { + href: '#/some-base-path/roles/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role', + text: roleName, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx new file mode 100644 index 00000000000000..4e8c95b61c2f19 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, FatalErrorsSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { SecurityLicense } from '../../../common/licensing'; +import { PluginStartDependencies } from '../../plugin'; +import { UserAPIClient } from '../users'; +import { RolesAPIClient } from './roles_api_client'; +import { RolesGridPage } from './roles_grid'; +import { EditRolePage } from './edit_role'; +import { DocumentationLinksService } from './documentation_links'; +import { IndicesAPIClient } from './indices_api_client'; +import { PrivilegesAPIClient } from './privileges_api_client'; + +interface CreateParams { + fatalErrors: FatalErrorsSetup; + license: SecurityLicense; + getStartServices: CoreSetup['getStartServices']; +} + +export const rolesManagementApp = Object.freeze({ + id: 'roles', + create({ license, fatalErrors, getStartServices }: CreateParams) { + return { + id: this.id, + order: 20, + title: i18n.translate('xpack.security.management.rolesTitle', { defaultMessage: 'Roles' }), + async mount({ basePath, element, setBreadcrumbs }) { + const [ + { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, + { data }, + ] = await getStartServices(); + + const rolesBreadcrumbs = [ + { + text: i18n.translate('xpack.security.roles.breadcrumb', { defaultMessage: 'Roles' }), + href: `#${basePath}`, + }, + ]; + + const rolesAPIClient = new RolesAPIClient(http); + const RolesGridPageWithBreadcrumbs = () => { + setBreadcrumbs(rolesBreadcrumbs); + return ; + }; + + const EditRolePageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { + const { roleName } = useParams<{ roleName?: string }>(); + + setBreadcrumbs([ + ...rolesBreadcrumbs, + action === 'edit' && roleName + ? { text: roleName, href: `#${basePath}/edit/${encodeURIComponent(roleName)}` } + : { + text: i18n.translate('xpack.security.roles.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/management/users/_index.scss b/x-pack/plugins/security/public/management/users/_index.scss new file mode 100644 index 00000000000000..35df0c1b965837 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/_index.scss @@ -0,0 +1 @@ +@import './edit_user/index'; diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx similarity index 82% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx rename to x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx index 221120532318cb..be46612767a593 100644 --- a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx @@ -7,10 +7,12 @@ import { EuiFieldText } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { User } from '../../../../common/model'; -import { UserAPIClient } from '../../../lib/api'; +import { User } from '../../../../../common/model'; import { ChangePasswordForm } from './change_password_form'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { userAPIClientMock } from '../../index.mock'; + function getCurrentPasswordField(wrapper: ReactWrapper) { return wrapper.find(EuiFieldText).filter('[data-test-subj="currentPassword"]'); } @@ -23,8 +25,6 @@ function getConfirmPasswordField(wrapper: ReactWrapper) { return wrapper.find(EuiFieldText).filter('[data-test-subj="confirmNewPassword"]'); } -jest.mock('ui/kfetch'); - describe('', () => { describe('for the current user', () => { it('shows fields for current and new passwords', () => { @@ -40,7 +40,8 @@ describe('', () => { ); @@ -60,15 +61,15 @@ describe('', () => { const callback = jest.fn(); - const apiClient = new UserAPIClient(); - apiClient.changePassword = jest.fn(); + const apiClientMock = userAPIClientMock.create(); const wrapper = mountWithIntl( ); @@ -83,8 +84,8 @@ describe('', () => { wrapper.find('button[data-test-subj="changePasswordButton"]').simulate('click'); - expect(apiClient.changePassword).toHaveBeenCalledTimes(1); - expect(apiClient.changePassword).toHaveBeenCalledWith( + expect(apiClientMock.changePassword).toHaveBeenCalledTimes(1); + expect(apiClientMock.changePassword).toHaveBeenCalledWith( 'user', 'myNewPassword', 'myCurrentPassword' @@ -106,7 +107,8 @@ describe('', () => { ); diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx rename to x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 61c0b77decd565..6dcf330ec6f9e1 100644 --- a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -5,10 +5,7 @@ */ import { EuiButton, - // @ts-ignore EuiButtonEmpty, - // @ts-ignore - EuiDescribedFormGroup, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -18,15 +15,16 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { User } from '../../../../common/model'; -import { UserAPIClient } from '../../../lib/api'; +import { NotificationsStart } from 'src/core/public'; +import { User } from '../../../../../common/model'; +import { UserAPIClient } from '../..'; interface Props { user: User; isUserChangingOwnPassword: boolean; onChangePassword?: () => void; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } interface State { @@ -294,7 +292,7 @@ export class ChangePasswordForm extends Component { }; private handleChangePasswordSuccess = () => { - toastNotifications.addSuccess({ + this.props.notifications.toasts.addSuccess({ title: i18n.translate('xpack.security.account.changePasswordSuccess', { defaultMessage: 'Your password has been changed.', }), @@ -317,7 +315,7 @@ export class ChangePasswordForm extends Component { if (error.body && error.body.statusCode === 403) { this.setState({ currentPasswordError: true }); } else { - toastNotifications.addDanger( + this.props.notifications.toasts.addDanger( i18n.translate('xpack.security.management.users.editUser.settingPasswordErrorMessage', { defaultMessage: 'Error setting password: {message}', values: { message: _.get(error, 'body.message') }, diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/index.ts b/x-pack/plugins/security/public/management/users/components/change_password_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/index.ts rename to x-pack/plugins/security/public/management/users/components/change_password_form/index.ts diff --git a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx similarity index 58% rename from x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx index 9f69fc7a7551f0..bcec707b03f936 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx @@ -5,16 +5,21 @@ */ import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ConfirmDeleteUsers } from './confirm_delete'; +import { ConfirmDeleteUsers } from './confirm_delete_users'; import React from 'react'; -import { UserAPIClient } from '../../../lib/api'; -jest.mock('ui/kfetch'); +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { userAPIClientMock } from '../../index.mock'; describe('ConfirmDeleteUsers', () => { it('renders a warning for a single user', () => { const wrapper = mountWithIntl( - + ); expect(wrapper.find('EuiModalHeaderTitle').text()).toMatchInlineSnapshot(`"Delete user foo"`); @@ -23,7 +28,8 @@ describe('ConfirmDeleteUsers', () => { it('renders a warning for a multiple users', () => { const wrapper = mountWithIntl( @@ -35,7 +41,12 @@ describe('ConfirmDeleteUsers', () => { it('fires onCancel when the operation is cancelled', () => { const onCancel = jest.fn(); const wrapper = mountWithIntl( - + ); expect(onCancel).toBeCalledTimes(0); @@ -47,50 +58,48 @@ describe('ConfirmDeleteUsers', () => { it('deletes the requested users when confirmed', () => { const onCancel = jest.fn(); - const deleteUser = jest.fn(); - - const apiClient = new UserAPIClient(); - apiClient.deleteUser = deleteUser; + const apiClientMock = userAPIClientMock.create(); const wrapper = mountWithIntl( ); wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(deleteUser).toBeCalledTimes(2); - expect(deleteUser).toBeCalledWith('foo'); - expect(deleteUser).toBeCalledWith('bar'); + expect(apiClientMock.deleteUser).toBeCalledTimes(2); + expect(apiClientMock.deleteUser).toBeCalledWith('foo'); + expect(apiClientMock.deleteUser).toBeCalledWith('bar'); }); it('attempts to delete all users even if some fail', () => { const onCancel = jest.fn(); - const deleteUser = jest.fn().mockImplementation(user => { + + const apiClientMock = userAPIClientMock.create(); + apiClientMock.deleteUser.mockImplementation(user => { if (user === 'foo') { return Promise.reject('something terrible happened'); } return Promise.resolve(); }); - const apiClient = new UserAPIClient(); - apiClient.deleteUser = deleteUser; - const wrapper = mountWithIntl( ); wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(deleteUser).toBeCalledTimes(2); - expect(deleteUser).toBeCalledWith('foo'); - expect(deleteUser).toBeCalledWith('bar'); + expect(apiClientMock.deleteUser).toBeCalledTimes(2); + expect(apiClientMock.deleteUser).toBeCalledWith('foo'); + expect(apiClientMock.deleteUser).toBeCalledWith('bar'); }); }); diff --git a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx similarity index 50% rename from x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index 53bb022afb513f..b7269e0168d7d5 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -6,51 +6,46 @@ import React, { Component, Fragment } from 'react'; import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { UserAPIClient } from '../../../lib/api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { UserAPIClient } from '../..'; interface Props { - intl: InjectedIntl; usersToDelete: string[]; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; onCancel: () => void; callback?: (usersToDelete: string[], errors: string[]) => void; } -class ConfirmDeleteUI extends Component { +export class ConfirmDeleteUsers extends Component { public render() { - const { usersToDelete, onCancel, intl } = this.props; + const { usersToDelete, onCancel } = this.props; const moreThanOne = usersToDelete.length > 1; const title = moreThanOne - ? intl.formatMessage( - { - id: 'xpack.security.management.users.confirmDelete.deleteMultipleUsersTitle', - defaultMessage: 'Delete {userLength} users', - }, - { userLength: usersToDelete.length } - ) - : intl.formatMessage( - { - id: 'xpack.security.management.users.confirmDelete.deleteOneUserTitle', - defaultMessage: 'Delete user {userLength}', - }, - { userLength: usersToDelete[0] } - ); + ? i18n.translate('xpack.security.management.users.confirmDelete.deleteMultipleUsersTitle', { + defaultMessage: 'Delete {userLength} users', + values: { userLength: usersToDelete.length }, + }) + : i18n.translate('xpack.security.management.users.confirmDelete.deleteOneUserTitle', { + defaultMessage: 'Delete user {userLength}', + values: { userLength: usersToDelete[0] }, + }); return (
@@ -82,31 +77,23 @@ class ConfirmDeleteUI extends Component { } private deleteUsers = () => { - const { usersToDelete, callback, apiClient } = this.props; + const { usersToDelete, callback, apiClient, notifications } = this.props; const errors: string[] = []; usersToDelete.forEach(async username => { try { await apiClient.deleteUser(username); - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted user {username}', - }, - { username } + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted user {username}', values: { username } } ) ); } catch (e) { errors.push(username); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.users.confirmDelete.userDeletingErrorNotificationMessage', - defaultMessage: 'Error deleting user {username}', - }, - { username } + notifications.toasts.addDanger( + i18n.translate( + 'xpack.security.management.users.confirmDelete.userDeletingErrorNotificationMessage', + { defaultMessage: 'Error deleting user {username}', values: { username } } ) ); } @@ -116,5 +103,3 @@ class ConfirmDeleteUI extends Component { }); }; } - -export const ConfirmDeleteUsers = injectI18n(ConfirmDeleteUI); diff --git a/x-pack/legacy/plugins/security/public/components/management/users/index.ts b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts similarity index 79% rename from x-pack/legacy/plugins/security/public/components/management/users/index.ts rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts index 813e671c05ccf2..fde35ab0f0d02e 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/index.ts +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ConfirmDeleteUsers } from './confirm_delete'; +export { ConfirmDeleteUsers } from './confirm_delete_users'; diff --git a/x-pack/plugins/security/public/management/users/components/index.ts b/x-pack/plugins/security/public/management/users/components/index.ts new file mode 100644 index 00000000000000..54011a6a24cbd2 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ChangePasswordForm } from './change_password_form'; +export { ConfirmDeleteUsers } from './confirm_delete_users'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/_users.scss b/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/_users.scss rename to x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss diff --git a/x-pack/plugins/security/public/management/users/edit_user/_index.scss b/x-pack/plugins/security/public/management/users/edit_user/_index.scss new file mode 100644 index 00000000000000..734ba7882ba72c --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/_index.scss @@ -0,0 +1 @@ +@import './edit_user_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx similarity index 70% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 639646ce48e224..543d20bb92afe8 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -8,13 +8,14 @@ import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { EditUserPage } from './edit_user_page'; import React from 'react'; -import { securityMock } from '../../../../../../../../plugins/security/public/mocks'; -import { UserAPIClient } from '../../../../lib/api'; -import { User, Role } from '../../../../../common/model'; +import { User, Role } from '../../../../common/model'; import { ReactWrapper } from 'enzyme'; -import { mockAuthenticatedUser } from '../../../../../../../../plugins/security/common/model/authenticated_user.mock'; -jest.mock('ui/kfetch'); +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../../mocks'; +import { rolesAPIClientMock } from '../../roles/index.mock'; +import { userAPIClientMock } from '../index.mock'; const createUser = (username: string) => { const user: User = { @@ -34,14 +35,12 @@ const createUser = (username: string) => { return user; }; -const buildClient = () => { - const apiClient = new UserAPIClient(); +const buildClients = () => { + const apiClient = userAPIClientMock.create(); + apiClient.getUser.mockImplementation(async (username: string) => createUser(username)); - apiClient.getUser = jest - .fn() - .mockImplementation(async (username: string) => createUser(username)); - - apiClient.getRoles = jest.fn().mockImplementation(() => { + const rolesAPIClient = rolesAPIClientMock.create(); + rolesAPIClient.getRoles.mockImplementation(() => { return Promise.resolve([ { name: 'role 1', @@ -64,7 +63,7 @@ const buildClient = () => { ] as Role[]); }); - return apiClient; + return { apiClient, rolesAPIClient }; }; function buildSecuritySetup() { @@ -85,15 +84,15 @@ function expectMissingSaveButton(wrapper: ReactWrapper) { describe('EditUserPage', () => { it('allows reserved users to be viewed', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); @@ -106,15 +105,15 @@ describe('EditUserPage', () => { }); it('allows new users to be created', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); @@ -127,15 +126,15 @@ describe('EditUserPage', () => { }); it('allows existing users to be edited', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx similarity index 77% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index bbffe07485f8dc..576f3ff9e6008c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -26,22 +26,23 @@ import { EuiHorizontalRule, EuiSpacer, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { SecurityPluginSetup } from '../../../../../../../../plugins/security/public'; -import { UserValidator, UserValidationResult } from '../../../../lib/validate_user'; -import { User, EditUser, Role } from '../../../../../common/model'; -import { USERS_PATH } from '../../../../views/management/management_urls'; -import { ConfirmDeleteUsers } from '../../../../components/management/users'; -import { UserAPIClient } from '../../../../lib/api'; -import { ChangePasswordForm } from '../../../../components/management/change_password_form'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { User, EditUser, Role } from '../../../../common/model'; +import { AuthenticationServiceSetup } from '../../../authentication'; +import { USERS_PATH } from '../../management_urls'; +import { RolesAPIClient } from '../../roles'; +import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; +import { UserValidator, UserValidationResult } from './validate_user'; +import { UserAPIClient } from '..'; interface Props { - username: string; - intl: InjectedIntl; - changeUrl: (path: string) => void; - apiClient: UserAPIClient; - securitySetup: SecurityPluginSetup; + username?: string; + apiClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + authc: AuthenticationServiceSetup; + notifications: NotificationsStart; } interface State { @@ -56,7 +57,11 @@ interface State { formError: UserValidationResult | null; } -class EditUserPageUI extends Component { +function backToUserList() { + window.location.hash = USERS_PATH; +} + +export class EditUserPage extends Component { private validator: UserValidator; constructor(props: Props) { @@ -84,7 +89,17 @@ class EditUserPageUI extends Component { } public async componentDidMount() { - const { username, apiClient, securitySetup } = this.props; + await this.setCurrentUser(); + } + + public async componentDidUpdate(prevProps: Props) { + if (prevProps.username !== this.props.username) { + await this.setCurrentUser(); + } + } + + private async setCurrentUser() { + const { username, apiClient, rolesAPIClient, notifications, authc } = this.props; let { user, currentUser } = this.state; if (username) { try { @@ -93,26 +108,24 @@ class EditUserPageUI extends Component { password: '', confirmPassword: '', }; - currentUser = await securitySetup.authc.getCurrentUser(); + currentUser = await authc.getCurrentUser(); } catch (err) { - toastNotifications.addDanger({ - title: this.props.intl.formatMessage({ - id: 'xpack.security.management.users.editUser.errorLoadingUserTitle', + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.editUser.errorLoadingUserTitle', { defaultMessage: 'Error loading user', }), text: get(err, 'body.message') || err.message, }); - return; + return backToUserList(); } } let roles: Role[] = []; try { - roles = await apiClient.getRoles(); + roles = await rolesAPIClient.getRoles(); } catch (err) { - toastNotifications.addDanger({ - title: this.props.intl.formatMessage({ - id: 'xpack.security.management.users.editUser.errorLoadingRolesTitle', + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.editUser.errorLoadingRolesTitle', { defaultMessage: 'Error loading roles', }), text: get(err, 'body.message') || err.message, @@ -131,8 +144,7 @@ class EditUserPageUI extends Component { private handleDelete = (usernames: string[], errors: string[]) => { if (errors.length === 0) { - const { changeUrl } = this.props; - changeUrl(USERS_PATH); + backToUserList(); } }; @@ -148,7 +160,7 @@ class EditUserPageUI extends Component { this.setState({ formError: null, }); - const { changeUrl, apiClient } = this.props; + const { apiClient } = this.props; const { user, isNewUser, selectedRoles } = this.state; const userToSave: EditUser = { ...user }; if (!isNewUser) { @@ -160,26 +172,23 @@ class EditUserPageUI extends Component { }); try { await apiClient.saveUser(userToSave); - toastNotifications.addSuccess( - this.props.intl.formatMessage( + this.props.notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', { - id: - 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', defaultMessage: 'Saved user {message}', - }, - { message: user.username } + values: { message: user.username }, + } ) ); - changeUrl(USERS_PATH); + + backToUserList(); } catch (e) { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.users.editUser.savingUserErrorMessage', - defaultMessage: 'Error saving user: {message}', - }, - { message: get(e, 'body.message', 'Unknown error') } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.users.editUser.savingUserErrorMessage', { + defaultMessage: 'Error saving user: {message}', + values: { message: get(e, 'body.message', 'Unknown error') }, + }) ); } } @@ -189,8 +198,7 @@ class EditUserPageUI extends Component { return ( { /> { {user.username === 'kibana' ? ( @@ -260,6 +268,7 @@ class EditUserPageUI extends Component { isUserChangingOwnPassword={userIsLoggedInUser} onChangePassword={this.toggleChangePasswordForm} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ); @@ -352,7 +361,6 @@ class EditUserPageUI extends Component { }; public render() { - const { changeUrl, intl } = this.props; const { user, roles, @@ -417,6 +425,7 @@ class EditUserPageUI extends Component { usersToDelete={[user.username]} callback={this.handleDelete} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ) : null} @@ -425,17 +434,16 @@ class EditUserPageUI extends Component { {...this.validator.validateUsername(this.state.user)} helpText={ !isNewUser && !reserved - ? intl.formatMessage({ - id: - 'xpack.security.management.users.editUser.changingUserNameAfterCreationDescription', - defaultMessage: `Usernames can't be changed after creation.`, - }) + ? i18n.translate( + 'xpack.security.management.users.editUser.changingUserNameAfterCreationDescription', + { defaultMessage: `Usernames can't be changed after creation.` } + ) : null } - label={intl.formatMessage({ - id: 'xpack.security.management.users.editUser.usernameFormRowLabel', - defaultMessage: 'Username', - })} + label={i18n.translate( + 'xpack.security.management.users.editUser.usernameFormRowLabel', + { defaultMessage: 'Username' } + )} > { {reserved ? null : ( { { )} { @@ -513,7 +521,7 @@ class EditUserPageUI extends Component { {reserved && ( - changeUrl(USERS_PATH)}> + { - changeUrl(USERS_PATH)} - > + { ); } } - -export const EditUserPage = injectI18n(EditUserPageUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/index.ts b/x-pack/plugins/security/public/management/users/edit_user/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/index.ts rename to x-pack/plugins/security/public/management/users/edit_user/index.ts diff --git a/x-pack/legacy/plugins/security/public/lib/validate_user.test.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/lib/validate_user.test.ts rename to x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts index 0535248fede88d..6050e1868a759a 100644 --- a/x-pack/legacy/plugins/security/public/lib/validate_user.test.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts @@ -5,7 +5,7 @@ */ import { UserValidator, UserValidationResult } from './validate_user'; -import { User, EditUser } from '../../common/model'; +import { User, EditUser } from '../../../../common/model'; function expectValid(result: UserValidationResult) { expect(result.isInvalid).toBe(false); diff --git a/x-pack/legacy/plugins/security/public/lib/validate_user.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/lib/validate_user.ts rename to x-pack/plugins/security/public/management/users/edit_user/validate_user.ts index 113aaacdcbf966..5edd96c68bf0dc 100644 --- a/x-pack/legacy/plugins/security/public/lib/validate_user.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { User, EditUser } from '../../common/model'; +import { User, EditUser } from '../../../../common/model'; interface UserValidatorOptions { shouldValidate?: boolean; diff --git a/x-pack/legacy/plugins/security/public/views/management/index.js b/x-pack/plugins/security/public/management/users/index.mock.ts similarity index 80% rename from x-pack/legacy/plugins/security/public/views/management/index.js rename to x-pack/plugins/security/public/management/users/index.mock.ts index 0ed6fe09ef80a4..f090f88da500de 100644 --- a/x-pack/legacy/plugins/security/public/views/management/index.js +++ b/x-pack/plugins/security/public/management/users/index.mock.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './management'; +export { userAPIClientMock } from './user_api_client.mock'; diff --git a/x-pack/legacy/plugins/security/public/objects/index.ts b/x-pack/plugins/security/public/management/users/index.ts similarity index 68% rename from x-pack/legacy/plugins/security/public/objects/index.ts rename to x-pack/plugins/security/public/management/users/index.ts index a6238ca879901c..c8b4d41973da6a 100644 --- a/x-pack/legacy/plugins/security/public/objects/index.ts +++ b/x-pack/plugins/security/public/management/users/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { saveRole, deleteRole } from './lib/roles'; - -export { getFields } from './lib/get_fields'; +export { UserAPIClient } from './user_api_client'; +export { usersManagementApp } from './users_management_app'; diff --git a/x-pack/plugins/security/public/management/users/user_api_client.mock.ts b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts new file mode 100644 index 00000000000000..7223f78d57fdc0 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const userAPIClientMock = { + create: () => ({ + getUsers: jest.fn(), + getUser: jest.fn(), + deleteUser: jest.fn(), + saveUser: jest.fn(), + changePassword: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/users/user_api_client.ts b/x-pack/plugins/security/public/management/users/user_api_client.ts new file mode 100644 index 00000000000000..61dd09d2c5e3de --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_api_client.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 { HttpStart } from 'src/core/public'; +import { User, EditUser } from '../../../common/model'; + +const usersUrl = '/internal/security/users'; + +export class UserAPIClient { + constructor(private readonly http: HttpStart) {} + + public async getUsers() { + return await this.http.get(usersUrl); + } + + public async getUser(username: string) { + return await this.http.get(`${usersUrl}/${encodeURIComponent(username)}`); + } + + public async deleteUser(username: string) { + await this.http.delete(`${usersUrl}/${encodeURIComponent(username)}`); + } + + public async saveUser(user: EditUser) { + await this.http.post(`${usersUrl}/${encodeURIComponent(user.username)}`, { + body: JSON.stringify(user), + }); + } + + public async changePassword(username: string, password: string, currentPassword: string) { + const data: Record = { + newPassword: password, + }; + if (currentPassword) { + data.password = currentPassword; + } + + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/password`, { + body: JSON.stringify(data), + }); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts b/x-pack/plugins/security/public/management/users/users_grid/index.ts similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts rename to x-pack/plugins/security/public/management/users/users_grid/index.ts index 03721f5ce93b1f..90e16248e19c36 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts +++ b/x-pack/plugins/security/public/management/users/users_grid/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { UsersListPage } from './users_list_page'; +export { UsersGridPage } from './users_grid_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx similarity index 63% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx rename to x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx index bdc0df9bae67c3..def0649953437d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UserAPIClient } from '../../../../lib/api'; -import { User } from '../../../../../common/model'; +import { User } from '../../../../common/model'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UsersListPage } from './users_list_page'; +import { UsersGridPage } from './users_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; +import { userAPIClientMock } from '../index.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; -jest.mock('ui/kfetch'); - -describe('UsersListPage', () => { +describe('UsersGridPage', () => { it('renders the list of users', async () => { - const apiClient = new UserAPIClient(); - apiClient.getUsers = jest.fn().mockImplementation(() => { + const apiClientMock = userAPIClientMock.create(); + apiClientMock.getUsers.mockImplementation(() => { return Promise.resolve([ { username: 'foo', @@ -38,22 +37,27 @@ describe('UsersListPage', () => { ]); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper); - expect(apiClient.getUsers).toBeCalledTimes(1); + expect(apiClientMock.getUsers).toBeCalledTimes(1); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); }); it('renders a forbidden message if user is not authorized', async () => { - const apiClient = new UserAPIClient(); - apiClient.getUsers = jest.fn().mockImplementation(() => { - return Promise.reject({ body: { statusCode: 403 } }); - }); + const apiClient = userAPIClientMock.create(); + apiClient.getUsers.mockRejectedValue({ body: { statusCode: 403 } }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper); diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx similarity index 85% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx rename to x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index df8522e5f32f91..fa15c3388fcc99 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -19,15 +19,16 @@ import { EuiEmptyPrompt, EuiBasicTableColumn, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import { ConfirmDeleteUsers } from '../../../../components/management/users'; -import { User } from '../../../../../common/model'; -import { UserAPIClient } from '../../../../lib/api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { User } from '../../../../common/model'; +import { ConfirmDeleteUsers } from '../components'; +import { UserAPIClient } from '..'; interface Props { - intl: InjectedIntl; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } interface State { @@ -38,7 +39,7 @@ interface State { filter: string; } -class UsersListPageUI extends Component { +export class UsersGridPage extends Component { constructor(props: Props) { super(props); this.state = { @@ -56,7 +57,6 @@ class UsersListPageUI extends Component { public render() { const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state; - const { intl } = this.props; if (permissionDenied) { return ( @@ -88,8 +88,7 @@ class UsersListPageUI extends Component { const columns: Array> = [ { field: 'full_name', - name: intl.formatMessage({ - id: 'xpack.security.management.users.fullNameColumnName', + name: i18n.translate('xpack.security.management.users.fullNameColumnName', { defaultMessage: 'Full Name', }), sortable: true, @@ -100,8 +99,7 @@ class UsersListPageUI extends Component { }, { field: 'username', - name: intl.formatMessage({ - id: 'xpack.security.management.users.userNameColumnName', + name: i18n.translate('xpack.security.management.users.userNameColumnName', { defaultMessage: 'User Name', }), sortable: true, @@ -114,8 +112,7 @@ class UsersListPageUI extends Component { }, { field: 'email', - name: intl.formatMessage({ - id: 'xpack.security.management.users.emailAddressColumnName', + name: i18n.translate('xpack.security.management.users.emailAddressColumnName', { defaultMessage: 'Email Address', }), sortable: true, @@ -126,8 +123,7 @@ class UsersListPageUI extends Component { }, { field: 'roles', - name: intl.formatMessage({ - id: 'xpack.security.management.users.rolesColumnName', + name: i18n.translate('xpack.security.management.users.rolesColumnName', { defaultMessage: 'Roles', }), render: (rolenames: string[]) => { @@ -144,15 +140,13 @@ class UsersListPageUI extends Component { }, { field: 'metadata', - name: intl.formatMessage({ - id: 'xpack.security.management.users.reservedColumnName', + name: i18n.translate('xpack.security.management.users.reservedColumnName', { defaultMessage: 'Reserved', }), sortable: ({ metadata }: User) => Boolean(metadata && metadata._reserved), width: '100px', align: 'right', - description: intl.formatMessage({ - id: 'xpack.security.management.users.reservedColumnDescription', + description: i18n.translate('xpack.security.management.users.reservedColumnDescription', { defaultMessage: 'Reserved users are built-in and cannot be removed. Only the password can be changed.', }), @@ -233,6 +227,7 @@ class UsersListPageUI extends Component { usersToDelete={selection.map(user => user.username)} callback={this.handleDelete} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ) : null} @@ -275,14 +270,11 @@ class UsersListPageUI extends Component { if (e.body.statusCode === 403) { this.setState({ permissionDenied: true }); } else { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.users.fetchingUsersErrorMessage', - defaultMessage: 'Error fetching users: {message}', - }, - { message: e.body.message } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.users.fetchingUsersErrorMessage', { + defaultMessage: 'Error fetching users: {message}', + values: { message: e.body.message }, + }) ); } } @@ -315,5 +307,3 @@ class UsersListPageUI extends Component { this.setState({ showDeleteConfirmation: false }); }; } - -export const UsersListPage = injectI18n(UsersListPageUI); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx new file mode 100644 index 00000000000000..48ffcfc550a849 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx @@ -0,0 +1,131 @@ +/* + * 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. + */ + +jest.mock('./users_grid', () => ({ + UsersGridPage: (props: any) => `Users Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_user', () => ({ + EditUserPage: (props: any) => `User Edit Page: ${JSON.stringify(props)}`, +})); + +import { usersManagementApp } from './users_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { securityMock } from '../../mocks'; + +async function mountApp(basePath: string) { + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await usersManagementApp + .create({ + authc: securityMock.createSetup().authc, + getStartServices: coreMock.createSetup().getStartServices as any, + }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('usersManagementApp', () => { + it('create() returns proper management app descriptor', () => { + expect( + usersManagementApp.create({ + authc: securityMock.createSetup().authc, + getStartServices: coreMock.createSetup().getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "users", + "mount": [Function], + "order": 10, + "title": "Users", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/users'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Users' }]); + expect(container).toMatchInlineSnapshot(` +
+ Users Page: {"notifications":{"toasts":{}},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create user` page', async () => { + const basePath = '/some-base-path/users'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit user` page', async () => { + const basePath = '/some-base-path/users'; + const userName = 'someUserName'; + window.location.hash = `${basePath}/edit/${userName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { href: `#/some-base-path/users/edit/${userName}`, text: userName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes user name in `edit user` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/users'; + const username = 'some 安全性 user'; + window.location.hash = `${basePath}/edit/${username}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { + href: '#/some-base-path/users/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20user', + text: username, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx new file mode 100644 index 00000000000000..9aebb396ce9a92 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { AuthenticationServiceSetup } from '../../authentication'; +import { PluginStartDependencies } from '../../plugin'; +import { RolesAPIClient } from '../roles'; +import { UserAPIClient } from './user_api_client'; +import { UsersGridPage } from './users_grid'; +import { EditUserPage } from './edit_user'; + +interface CreateParams { + authc: AuthenticationServiceSetup; + getStartServices: CoreSetup['getStartServices']; +} + +export const usersManagementApp = Object.freeze({ + id: 'users', + create({ authc, getStartServices }: CreateParams) { + return { + id: this.id, + order: 10, + title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ http, notifications, i18n: i18nStart }] = await getStartServices(); + const usersBreadcrumbs = [ + { + text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), + href: `#${basePath}`, + }, + ]; + + const userAPIClient = new UserAPIClient(http); + const UsersGridPageWithBreadcrumbs = () => { + setBreadcrumbs(usersBreadcrumbs); + return ; + }; + + const EditUserPageWithBreadcrumbs = () => { + const { username } = useParams<{ username?: string }>(); + + setBreadcrumbs([ + ...usersBreadcrumbs, + username + ? { text: username, href: `#${basePath}/edit/${encodeURIComponent(username)}` } + : { + text: i18n.translate('xpack.security.users.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts deleted file mode 100644 index 50e0b838c750fc..00000000000000 --- a/x-pack/plugins/security/public/plugin.ts +++ /dev/null @@ -1,74 +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 { Plugin, CoreSetup, CoreStart } from 'src/core/public'; -import { LicensingPluginSetup } from '../../licensing/public'; -import { - SessionExpired, - SessionTimeout, - ISessionTimeout, - SessionTimeoutHttpInterceptor, - UnauthorizedResponseHttpInterceptor, -} from './session'; -import { SecurityLicenseService } from '../common/licensing'; -import { SecurityNavControlService } from './nav_control'; -import { AuthenticationService } from './authentication'; - -export interface PluginSetupDependencies { - licensing: LicensingPluginSetup; -} - -export class SecurityPlugin implements Plugin { - private sessionTimeout!: ISessionTimeout; - - private navControlService!: SecurityNavControlService; - - private securityLicenseService!: SecurityLicenseService; - - public setup(core: CoreSetup, { licensing }: PluginSetupDependencies) { - const { http, notifications, injectedMetadata } = core; - const { basePath, anonymousPaths } = http; - anonymousPaths.register('/login'); - anonymousPaths.register('/logout'); - anonymousPaths.register('/logged_out'); - - const tenant = `${injectedMetadata.getInjectedVar('session.tenant', '')}`; - const sessionExpired = new SessionExpired(basePath, tenant); - http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); - this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); - - this.navControlService = new SecurityNavControlService(); - this.securityLicenseService = new SecurityLicenseService(); - const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); - - const authc = new AuthenticationService().setup({ http: core.http }); - - this.navControlService.setup({ - securityLicense: license, - authc, - }); - - return { - authc, - sessionTimeout: this.sessionTimeout, - }; - } - - public start(core: CoreStart) { - this.sessionTimeout.start(); - this.navControlService.start({ core }); - } - - public stop() { - this.sessionTimeout.stop(); - this.navControlService.stop(); - this.securityLicenseService.stop(); - } -} - -export type SecurityPluginSetup = ReturnType; -export type SecurityPluginStart = ReturnType; diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx new file mode 100644 index 00000000000000..394e23cbbf646c --- /dev/null +++ b/x-pack/plugins/security/public/plugin.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; +import { + SessionExpired, + SessionTimeout, + ISessionTimeout, + SessionTimeoutHttpInterceptor, + UnauthorizedResponseHttpInterceptor, +} from './session'; +import { SecurityLicenseService } from '../common/licensing'; +import { SecurityNavControlService } from './nav_control'; +import { AccountManagementPage } from './account_management'; +import { AuthenticationService, AuthenticationServiceSetup } from './authentication'; +import { ManagementService, UserAPIClient } from './management'; + +export interface PluginSetupDependencies { + licensing: LicensingPluginSetup; + home?: HomePublicPluginSetup; + management?: ManagementSetup; +} + +export interface PluginStartDependencies { + data: DataPublicPluginStart; + management?: ManagementStart; +} + +export class SecurityPlugin + implements + Plugin< + SecurityPluginSetup, + SecurityPluginStart, + PluginSetupDependencies, + PluginStartDependencies + > { + private sessionTimeout!: ISessionTimeout; + private readonly navControlService = new SecurityNavControlService(); + private readonly securityLicenseService = new SecurityLicenseService(); + private readonly managementService = new ManagementService(); + private authc!: AuthenticationServiceSetup; + + public setup( + core: CoreSetup, + { home, licensing, management }: PluginSetupDependencies + ) { + const { http, notifications, injectedMetadata } = core; + const { basePath, anonymousPaths } = http; + anonymousPaths.register('/login'); + anonymousPaths.register('/logout'); + anonymousPaths.register('/logged_out'); + + const tenant = `${injectedMetadata.getInjectedVar('session.tenant', '')}`; + const sessionExpired = new SessionExpired(basePath, tenant); + http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); + this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); + http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); + + const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); + + this.authc = new AuthenticationService().setup({ http: core.http }); + + this.navControlService.setup({ + securityLicense: license, + authc: this.authc, + }); + + if (management) { + this.managementService.setup({ + license, + management, + authc: this.authc, + fatalErrors: core.fatalErrors, + getStartServices: core.getStartServices, + }); + } + + if (management && home) { + home.featureCatalogue.register({ + id: 'security', + title: i18n.translate('xpack.security.registerFeature.securitySettingsTitle', { + defaultMessage: 'Security Settings', + }), + description: i18n.translate('xpack.security.registerFeature.securitySettingsDescription', { + defaultMessage: + 'Protect your data and easily manage who has access to what with users and roles.', + }), + icon: 'securityApp', + path: '/app/kibana#/management/security/users', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + } + + return { + authc: this.authc, + sessionTimeout: this.sessionTimeout, + }; + } + + public start(core: CoreStart, { data, management }: PluginStartDependencies) { + this.sessionTimeout.start(); + this.navControlService.start({ core }); + + if (management) { + this.managementService.start({ management }); + } + + return { + __legacyCompat: { + account_management: { + AccountManagementPage: () => ( + + + + ), + }, + }, + }; + } + + public stop() { + this.sessionTimeout.stop(); + this.navControlService.stop(); + this.securityLicenseService.stop(); + this.managementService.stop(); + } +} + +export type SecurityPluginSetup = ReturnType; +export type SecurityPluginStart = ReturnType; diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 9cd5cf83092e18..def6fabc0e3222 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; -import { RoleMapping } from '../../../../../legacy/plugins/security/common/model'; +import { RoleMapping } from '../../../common/model'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 65baa1bd991022..c1f0f8bd3ece41 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -5,5 +5,5 @@ */ export { isReservedSpace } from './is_reserved_space'; -export { MAX_SPACE_INITIALS } from './constants'; +export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD } from './constants'; export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ce3edbbb598280..c3e46a9c04e1cc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10595,17 +10595,6 @@ "xpack.security.management.apiKeys.table.userFilterLabel": "ユーザー", "xpack.security.management.apiKeys.table.userNameColumnName": "ユーザー", "xpack.security.management.apiKeysTitle": "API キー", - "xpack.security.management.changePasswordForm.cancelButtonLabel": "キャンセル", - "xpack.security.management.changePasswordForm.changePasswordLinkLabel": "パスワードを変更", - "xpack.security.management.changePasswordForm.confirmPasswordLabel": "パスワードの確認", - "xpack.security.management.changePasswordForm.currentPasswordLabel": "現在のパスワード", - "xpack.security.management.changePasswordForm.incorrectPasswordDescription": "入力された現在のパスワードが正しくありません。", - "xpack.security.management.changePasswordForm.newPasswordLabel": "新しいパスワード", - "xpack.security.management.changePasswordForm.passwordDontMatchDescription": "パスワードが一致しません", - "xpack.security.management.changePasswordForm.passwordLabel": "パスワード", - "xpack.security.management.changePasswordForm.passwordLengthDescription": "パスワードは最低 6 文字必要です", - "xpack.security.management.changePasswordForm.saveChangesButtonLabel": "変更を保存", - "xpack.security.management.changePasswordForm.updateAndRestartKibanaDescription": "Kibana ユーザーのパスワードを変更後、kibana.yml ファイルを更新し Kibana を再起動する必要があります。", "xpack.security.management.editRole.cancelButtonLabel": "キャンセル", "xpack.security.management.editRole.changeAllPrivilegesLink": "(すべて変更)", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "非表示", @@ -10730,10 +10719,6 @@ "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "スペース権限を作成", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "グローバル特権を更新", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "スペース権限を更新", - "xpack.security.management.passwordForm.confirmPasswordLabel": "パスワードの確認", - "xpack.security.management.passwordForm.passwordDontMatchDescription": "パスワードが一致しません", - "xpack.security.management.passwordForm.passwordLabel": "パスワード", - "xpack.security.management.passwordForm.passwordLengthDescription": "パスワードは最低 6 文字必要です", "xpack.security.management.roles.actionsColumnName": "アクション", "xpack.security.management.roles.cloneRoleActionName": "{roleName} を複製", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dc72e83ebe3c30..c40b8c8d0393c8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10594,17 +10594,6 @@ "xpack.security.management.apiKeys.table.userFilterLabel": "用户", "xpack.security.management.apiKeys.table.userNameColumnName": "用户", "xpack.security.management.apiKeysTitle": "API 密钥", - "xpack.security.management.changePasswordForm.cancelButtonLabel": "取消", - "xpack.security.management.changePasswordForm.changePasswordLinkLabel": "更改密码", - "xpack.security.management.changePasswordForm.confirmPasswordLabel": "确认密码", - "xpack.security.management.changePasswordForm.currentPasswordLabel": "当前密码", - "xpack.security.management.changePasswordForm.incorrectPasswordDescription": "您输入的当前密码不正确。", - "xpack.security.management.changePasswordForm.newPasswordLabel": "新密码", - "xpack.security.management.changePasswordForm.passwordDontMatchDescription": "密码不匹配", - "xpack.security.management.changePasswordForm.passwordLabel": "密码", - "xpack.security.management.changePasswordForm.passwordLengthDescription": "密码长度必须至少为 6 个字符", - "xpack.security.management.changePasswordForm.saveChangesButtonLabel": "保存更改", - "xpack.security.management.changePasswordForm.updateAndRestartKibanaDescription": "更改 Kibana 用户的密码后,必须更新 kibana.yml 文件并重新启动 Kibana", "xpack.security.management.editRole.cancelButtonLabel": "取消", "xpack.security.management.editRole.changeAllPrivilegesLink": "(全部更改)", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "隐藏", @@ -10729,10 +10718,6 @@ "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "创建工作区权限", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "更新全局权限", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "更新工作区权限", - "xpack.security.management.passwordForm.confirmPasswordLabel": "确认密码", - "xpack.security.management.passwordForm.passwordDontMatchDescription": "密码不匹配", - "xpack.security.management.passwordForm.passwordLabel": "密码", - "xpack.security.management.passwordForm.passwordLengthDescription": "密码长度必须至少为 6 个字符", "xpack.security.management.roles.actionsColumnName": "鎿嶄綔", "xpack.security.management.roles.cloneRoleActionName": "克隆 {roleName}", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "取消", diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 45a35029ffba2e..8ab84126b2b30a 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -11,7 +11,7 @@ import { ROLES_PATH, EDIT_ROLES_PATH, CLONE_ROLES_PATH, -} from '../../../../legacy/plugins/security/public/views/management/management_urls'; +} from '../../../../plugins/security/public/management/management_urls'; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 5fed56ee79e3dd..a1517e1934a286 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -93,7 +93,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const url = parse(await browser.getCurrentUrl()); - expect(url.hash).to.eql('#/management/security/role_mappings?_g=()'); + expect(url.hash).to.eql('#/management/security/role_mappings'); }); describe('with role mappings', () => { From a131f1dbcfa4838653d587e948ef0365036e5dd6 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 21 Jan 2020 13:50:18 +0100 Subject: [PATCH 06/19] [ML] Formatting for additional timing and model size stats (#55062) * [ML] formatting for additional timing and model size stats * [ML] roundToDecimalPlace only average search time * [ML] adjust functional tests * [ML] remove debug tag, fix assert value * [ML] check for no decimal place * [ML] fix functional tests Co-authored-by: Elastic Machine --- .../formatters/round_to_decimal_place.test.ts | 2 ++ .../formatters/round_to_decimal_place.ts | 7 ++++++- .../components/job_details/format_values.js | 10 ++++++++++ .../anomaly_detection/advanced_job.ts | 8 ++++---- .../anomaly_detection/multi_metric_job.ts | 4 ++-- .../anomaly_detection/population_job.ts | 4 ++-- .../anomaly_detection/saved_search_job.ts | 20 +++++++++---------- .../anomaly_detection/single_metric_job.ts | 4 ++-- 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts index 99b9aceab3696f..663a3f3d8f9556 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts @@ -20,6 +20,7 @@ describe('ML - roundToDecimalPlace formatter', () => { expect(roundToDecimalPlace(0.0005)).toBe('5.00e-4'); expect(roundToDecimalPlace(-0.0005)).toBe('-5.00e-4'); expect(roundToDecimalPlace(-12.045)).toBe(-12.04); + expect(roundToDecimalPlace(0)).toBe(0); }); it('returns the correct format using specified decimal place', () => { @@ -31,5 +32,6 @@ describe('ML - roundToDecimalPlace formatter', () => { expect(roundToDecimalPlace(0.0005, 4)).toBe(0.0005); expect(roundToDecimalPlace(0.00005, 4)).toBe('5.00e-5'); expect(roundToDecimalPlace(-0.00005, 4)).toBe('-5.00e-5'); + expect(roundToDecimalPlace(0, 4)).toBe(0); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts index f863fe6d76e57d..5a030d7619e986 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export function roundToDecimalPlace(num: number, dp: number = 2) { +export function roundToDecimalPlace(num: number, dp: number = 2): number | string { + if (num % 1 === 0) { + // no decimal place + return num; + } + if (Math.abs(num) < Math.pow(10, -dp)) { return Number.parseFloat(String(num)).toExponential(2); } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index eb0a905725d750..9984f3be299ae8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -6,6 +6,7 @@ import numeral from '@elastic/numeral'; import { formatDate } from '@elastic/eui/lib/services/format'; +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; import { toLocaleString } from '../../../../util/string_utils'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; @@ -35,6 +36,8 @@ export function formatValues([key, value]) { case 'established_model_memory': case 'input_bytes': case 'model_bytes': + case 'model_bytes_exceeded': + case 'model_bytes_memory_limit': value = formatData(value); break; @@ -53,9 +56,16 @@ export function formatValues([key, value]) { case 'total_over_field_count': case 'total_partition_field_count': case 'bucket_allocation_failures_count': + case 'search_count': value = toLocaleString(value); break; + // numbers rounded to 3 decimal places + case 'average_search_time_per_bucket_ms': + case 'exponential_average_search_time_per_hour_ms': + value = typeof value === 'number' ? roundToDecimalPlace(value, 3).toLocaleString() : value; + break; + default: break; } diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts index 386914b735554c..acb92b270c4a12 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts @@ -194,8 +194,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '10485760', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '10.0 MB', total_by_field_count: '37', total_over_field_count: '92', total_partition_field_count: '8', @@ -261,8 +261,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '104857600', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '100.0 MB', total_by_field_count: '994', total_over_field_count: '0', total_partition_field_count: '2', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index d41d96e40e2bee..6a12a28e8ac490 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -60,8 +60,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '59', total_over_field_count: '0', total_partition_field_count: '58', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index 296af3179ce3ee..6593dd10928b4b 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -74,8 +74,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '8388608', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '8.0 MB', total_by_field_count: '25', total_over_field_count: '92', total_partition_field_count: '3', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index 7d989bc6244b81..348910a2a8f84a 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -53,8 +53,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -104,8 +104,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -155,8 +155,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -207,8 +207,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -258,8 +258,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index f6cd7b40bc7b1d..13cac36d99a1ba 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -59,8 +59,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '15728640', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '15.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', From c1960583500d28fc3002b4959804ad52cf723b4a Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 21 Jan 2020 14:04:52 +0100 Subject: [PATCH 07/19] [SIEM] Update ml_conditional_links cypress tests (#55373) --- .../ml_conditional_links/ml_conditional_links.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index afeb8c3c13a4fd..142729189e49b3 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -99,7 +99,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/siem#/network/ip/127.0.0.1?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/siem#/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -107,7 +107,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - "/app/siem#/network/ip/127.0.0.1?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/ip/127.0.0.1/source?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); From c88aa5a505ced73ffabd4a4136f6172ee23073c8 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 21 Jan 2020 14:49:30 +0100 Subject: [PATCH 08/19] Migration: Separate legacy and index entrypoint (#54124) --- src/legacy/core_plugins/kibana/index.js | 7 +- .../core_plugins/kibana/public/.eslintrc.js | 1 + .../kibana/public/dashboard/index.ts | 40 ++---------- .../kibana/public/dashboard/legacy.ts | 56 ++++++++++++++++ .../__tests__/directives/discover_field.js | 2 +- .../__tests__/directives/field_calculator.js | 2 +- .../__tests__/directives/field_chooser.js | 2 +- .../discover/__tests__/doc_table/doc_table.js | 2 +- .../__tests__/doc_table/lib/rows_headers.js | 2 +- .../query_parameters/action_add_filter.js | 2 +- .../action_set_predecessor_count.js | 2 +- .../action_set_query_parameters.js | 2 +- .../action_set_successor_count.js | 2 +- .../kibana/public/discover/index.ts | 17 +---- .../kibana/public/discover/legacy.ts | 32 +++++++++ .../angular/context/api/__tests__/anchor.js | 2 +- .../context/api/__tests__/predecessors.js | 2 +- .../context/api/__tests__/successors.js | 2 +- .../core_plugins/kibana/public/home/index.ts | 5 -- .../kibana/public/home/kibana_services.ts | 1 - .../home/np_ready/components/welcome.test.tsx | 4 -- .../home/np_ready/components/welcome.tsx | 7 +- .../core_plugins/kibana/public/home/plugin.ts | 8 ++- .../core_plugins/kibana/public/kibana.js | 6 +- .../kibana/public/visualize/index.ts | 49 ++------------ .../kibana/public/visualize/legacy.ts | 65 +++++++++++++++++++ .../directives/__tests__/css_truncate.js | 2 +- .../dashboard_mode/public/dashboard_viewer.js | 1 + 28 files changed, 202 insertions(+), 123 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/dashboard/legacy.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/legacy.ts create mode 100644 src/legacy/core_plugins/kibana/public/visualize/legacy.ts diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index e6a0420534ef25..0366d8b27f2118 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -60,7 +60,12 @@ export default function(kibana) { }, uiExports: { - hacks: ['plugins/kibana/discover', 'plugins/kibana/dev_tools', 'plugins/kibana/visualize'], + hacks: [ + 'plugins/kibana/discover/legacy', + 'plugins/kibana/dev_tools', + 'plugins/kibana/visualize/legacy', + 'plugins/kibana/dashboard/legacy', + ], savedObjectTypes: ['plugins/kibana/dashboard/saved_dashboard/saved_dashboard_register'], app: { id: 'kibana', diff --git a/src/legacy/core_plugins/kibana/public/.eslintrc.js b/src/legacy/core_plugins/kibana/public/.eslintrc.js index 160adcc5b63f1b..9b45217287dc8c 100644 --- a/src/legacy/core_plugins/kibana/public/.eslintrc.js +++ b/src/legacy/core_plugins/kibana/public/.eslintrc.js @@ -51,6 +51,7 @@ function buildRestrictedPaths(shimmedPlugins) { from: [ `src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`, + `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`, ], allowSameFolder: false, errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/index.ts index fd39f28a7673a1..4a8decab6b00e0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.ts @@ -17,40 +17,12 @@ * under the License. */ -import { npSetup, npStart, legacyChrome } from './legacy_imports'; -import { DashboardPlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { start as data } from '../../../data/public/legacy'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; -import './saved_dashboard/saved_dashboard_register'; -import './dashboard_config'; +import { PluginInitializerContext } from 'kibana/public'; +import { DashboardPlugin } from './plugin'; export * from './np_ready/dashboard_constants'; -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularDependencies(): Promise { - const injector = await legacyChrome.dangerouslyGetActiveInjector(); - - return { - dashboardConfig: injector.get('dashboardConfig'), - }; -} - -(async () => { - const instance = new DashboardPlugin(); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - getAngularDependencies, - }, - }); - instance.start(npStart.core, { - ...npStart.plugins, - data, - npData: npStart.plugins.data, - embeddables, - navigation: npStart.plugins.navigation, - }); -})(); +// Core will be looking for this when loading our plugin in the new platform +export const plugin = (context: PluginInitializerContext) => { + return new DashboardPlugin(); +}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts new file mode 100644 index 00000000000000..068a8378f936a3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts @@ -0,0 +1,56 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart, legacyChrome } from './legacy_imports'; +import { LegacyAngularInjectedDependencies } from './plugin'; +import { start as data } from '../../../data/public/legacy'; +import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; +import './saved_dashboard/saved_dashboard_register'; +import './dashboard_config'; +import { plugin } from './index'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularDependencies(): Promise { + const injector = await legacyChrome.dangerouslyGetActiveInjector(); + + return { + dashboardConfig: injector.get('dashboardConfig'), + }; +} + +(async () => { + const instance = plugin({} as PluginInitializerContext); + instance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + getAngularDependencies, + }, + }); + instance.start(npStart.core, { + ...npStart.plugins, + data, + npData: npStart.plugins.data, + embeddables, + navigation: npStart.plugins.navigation, + }); +})(); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js index 18d83595f8fa30..6ffda87ac2be8d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js @@ -22,7 +22,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; // Load the kibana app dependencies. diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js index 378a9e93256557..f302d684135f6c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import ngMock from 'ng_mock'; import { fieldCalculator } from '../../np_ready/components/field_chooser/lib/field_calculator'; import expect from '@kbn/expect'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index 5f6898ae2bd164..f74e145865475d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -23,7 +23,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import expect from '@kbn/expect'; import $ from 'jquery'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesHitsProvider from 'fixtures/hits'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { SimpleSavedObject } from '../../../../../../../core/public'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js index b57f452b637afa..6b97da79fc5893 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; import _ from 'lodash'; import ngMock from 'ng_mock'; import 'ui/private'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import hits from 'fixtures/real_hits'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js index 012f2b6061ee42..c19e033ccb72df 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js @@ -24,7 +24,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { getFakeRow, getFakeRowVals } from 'fixtures/fake_row'; import $ from 'jquery'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('Doc Table', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js index 90614cf3c132c2..f2acbf363d825d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js @@ -22,7 +22,7 @@ import ngMock from 'ng_mock'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; import { createIndexPatternsStub } from '../../np_ready/angular/context/api/__tests__/_stubs'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { npStart } from 'ui/new_platform'; describe('context app', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js index 1ad4bdbea210db..9ba425bb0e489e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js index e9ec2c300faa11..39dde2d8bb7cf0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js index 15f3eefac3fd1a..c05f5b4aff3bc7 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.ts b/src/legacy/core_plugins/kibana/public/discover/index.ts index 7bde30e0d0f0ef..d851cb96a18c48 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/index.ts @@ -16,24 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import chrome from 'ui/chrome'; + import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; import { DiscoverPlugin } from './plugin'; +export { createSavedSearchesService } from './saved_searches/saved_searches'; + // Core will be looking for this when loading our plugin in the new platform export const plugin = (context: PluginInitializerContext) => { return new DiscoverPlugin(); }; - -// Legacy compatiblity part - to be removed at cutover, replaced by a kibana.json file -export const pluginInstance = plugin({} as PluginInitializerContext); -export const setup = pluginInstance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - chrome, - }, -}); -export const start = pluginInstance.start(npStart.core, npStart.plugins); - -export { createSavedSearchesService } from './saved_searches/saved_searches'; diff --git a/src/legacy/core_plugins/kibana/public/discover/legacy.ts b/src/legacy/core_plugins/kibana/public/discover/legacy.ts new file mode 100644 index 00000000000000..2ec64177156f9e --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/legacy.ts @@ -0,0 +1,32 @@ +/* + * 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 chrome from 'ui/chrome'; +import { PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart } from 'ui/new_platform'; +import { plugin } from './index'; + +// Legacy compatiblity part - to be removed at cutover, replaced by a kibana.json file +export const pluginInstance = plugin({} as PluginInitializerContext); +export const setup = pluginInstance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + chrome, + }, +}); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js index 45ce6cc9d0af2e..debcccebbd11c0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js index 266a505f6be14b..c24b6ac6307ffd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import moment from 'moment'; import * as _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js index e06d414ba260ce..d4c00930c93839 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import moment from 'moment'; import * as _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index b2d90f1444654a..27d09a53ba20dc 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -22,11 +22,8 @@ import { npSetup, npStart } from 'ui/new_platform'; import chrome from 'ui/chrome'; import { IPrivate } from 'ui/private'; import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; import { TelemetryOptInProvider } from '../../../telemetry/public/services'; -export const trackUiMetric = createUiStatsReporter('Kibana_home'); - /** * Get dependencies relying on the global angular context. * They also have to get resolved together with the legacy imports above @@ -54,9 +51,7 @@ let copiedLegacyCatalogue = false; instance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { - trackUiMetric, metadata: npStart.core.injectedMetadata.getLegacyMetadata(), - METRIC_TYPE, getFeatureCatalogueEntries: async () => { if (!copiedLegacyCatalogue) { const injector = await chrome.dangerouslyGetActiveInjector(); diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 0eb55a3902edac..4d9177735556df 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -55,7 +55,6 @@ export interface HomeKibanaServices { savedObjectsClient: SavedObjectsClientContract; toastNotifications: NotificationsSetup['toasts']; banners: OverlayStart['banners']; - METRIC_TYPE: any; trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; getBasePath: () => string; shouldShowTelemetryOptIn: boolean; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx index 28bdab14193c40..55c469fa58fc61 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx @@ -25,10 +25,6 @@ jest.mock('../../kibana_services', () => ({ getServices: () => ({ addBasePath: (path: string) => `root${path}`, trackUiMetric: () => {}, - METRIC_TYPE: { - LOADED: 'loaded', - CLICK: 'click', - }, }), })); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx index 9bbb7aaceb915b..1b7761d068d2f2 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx @@ -35,6 +35,7 @@ import { EuiIcon, EuiPortal, } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../../kibana_services'; @@ -64,17 +65,17 @@ export class Welcome extends React.Component { } private onSampleDataDecline = () => { - this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, 'sampleDataDecline'); + this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataDecline'); this.props.onSkip(); }; private onSampleDataConfirm = () => { - this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, 'sampleDataConfirm'); + this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataConfirm'); this.redirecToSampleData(); }; componentDidMount() { - this.services.trackUiMetric(this.services.METRIC_TYPE.LOADED, 'welcomeScreenMount'); + this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); this.props.onOptInSeen(); document.addEventListener('keydown', this.hideOnEsc); } diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 42ab049eb5b3a8..502c8f45646cf0 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -18,11 +18,11 @@ */ import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'kibana/public'; -import { UiStatsMetricType } from '@kbn/analytics'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { setServices } from './kibana_services'; import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; +import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { Environment, FeatureCatalogueEntry, @@ -41,8 +41,6 @@ export interface HomePluginStartDependencies { export interface HomePluginSetupDependencies { __LEGACY: { - trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; - METRIC_TYPE: any; metadata: { app: unknown; bundleId: string; @@ -59,6 +57,7 @@ export interface HomePluginSetupDependencies { getFeatureCatalogueEntries: () => Promise; getAngularDependencies: () => Promise; }; + usageCollection: UsageCollectionSetup; kibana_legacy: KibanaLegacySetup; } @@ -71,6 +70,7 @@ export class HomePlugin implements Plugin { core: CoreSetup, { kibana_legacy, + usageCollection, __LEGACY: { getAngularDependencies, ...legacyServices }, }: HomePluginSetupDependencies ) { @@ -78,9 +78,11 @@ export class HomePlugin implements Plugin { id: 'home', title: 'Home', mount: async ({ core: contextCore }, params) => { + const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home'); const angularDependencies = await getAngularDependencies(); setServices({ ...legacyServices, + trackUiMetric, http: contextCore.http, toastNotifications: core.notifications.toasts, banners: contextCore.overlays.banners, diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 4100ae72058699..bd947b9cb9d7f1 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -47,9 +47,9 @@ import 'uiExports/interpreter'; import 'ui/autoload/all'; import 'ui/kbn_top_nav'; import './home'; -import './discover'; -import './visualize'; -import './dashboard'; +import './discover/legacy'; +import './visualize/legacy'; +import './dashboard/legacy'; import './management'; import './dev_tools'; import 'ui/color_maps'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.ts b/src/legacy/core_plugins/kibana/public/visualize/index.ts index f113c81256f8e4..a39779792b83a7 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/index.ts @@ -17,50 +17,15 @@ * under the License. */ -import { - IPrivate, - legacyChrome, - npSetup, - npStart, - VisEditorTypesRegistryProvider, -} from './legacy_imports'; -import { VisualizePlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; -import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; +import { PluginInitializerContext } from 'kibana/public'; +import { VisualizePlugin } from './plugin'; export * from './np_ready/visualize_constants'; export { showNewVisModal } from './np_ready/wizard'; -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularDependencies(): Promise { - const injector = await legacyChrome.dangerouslyGetActiveInjector(); - - const Private = injector.get('Private'); - - const editorTypes = Private(VisEditorTypesRegistryProvider); - - return { - legacyChrome, - editorTypes, - }; -} - -(() => { - const instance = new VisualizePlugin(); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - getAngularDependencies, - }, - }); - instance.start(npStart.core, { - ...npStart.plugins, - embeddables, - visualizations, - }); -})(); - export { createSavedVisLoader } from './saved_visualizations/saved_visualizations'; + +// Core will be looking for this when loading our plugin in the new platform +export const plugin = (context: PluginInitializerContext) => { + return new VisualizePlugin(); +}; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts new file mode 100644 index 00000000000000..2a1b6130fac899 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts @@ -0,0 +1,65 @@ +/* + * 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 'ui/collapsible_sidebar'; // used in default editor + +import { PluginInitializerContext } from 'kibana/public'; +import { + IPrivate, + legacyChrome, + npSetup, + npStart, + VisEditorTypesRegistryProvider, +} from './legacy_imports'; +import { LegacyAngularInjectedDependencies } from './plugin'; +import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; +import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; +import { plugin } from './index'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularDependencies(): Promise { + const injector = await legacyChrome.dangerouslyGetActiveInjector(); + + const Private = injector.get('Private'); + + const editorTypes = Private(VisEditorTypesRegistryProvider); + + return { + legacyChrome, + editorTypes, + }; +} + +(() => { + const instance = plugin({} as PluginInitializerContext); + instance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + getAngularDependencies, + }, + }); + instance.start(npStart.core, { + ...npStart.plugins, + embeddables, + visualizations, + }); +})(); diff --git a/src/legacy/ui/public/directives/__tests__/css_truncate.js b/src/legacy/ui/public/directives/__tests__/css_truncate.js index 9d470c10358ccf..bf102f5a29fdb9 100644 --- a/src/legacy/ui/public/directives/__tests__/css_truncate.js +++ b/src/legacy/ui/public/directives/__tests__/css_truncate.js @@ -20,7 +20,7 @@ import angular from 'angular'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import 'plugins/kibana/discover/index'; +import 'plugins/kibana/discover/legacy'; let $parentScope; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 391973f6d909b4..fbf917054edbff 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -34,6 +34,7 @@ import 'ui/color_maps'; import 'ui/agg_response'; import 'ui/agg_types'; import 'leaflet'; +import 'plugins/kibana/dashboard/legacy'; import { npStart } from 'ui/new_platform'; import { localApplicationService } from 'plugins/kibana/local_application_service'; From 6b02ed804fa858447f1e69627ccf6c121358dcab Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 21 Jan 2020 14:52:32 +0100 Subject: [PATCH 09/19] Migrate session storage short url handling (#55021) --- .../state_session_storage_redirect/index.js | 32 ------- .../package.json | 5 -- src/legacy/server/kbn_server.js | 4 - src/legacy/server/url_shortening/index.js | 20 ----- .../routes/lib/short_url_assert_valid.js | 41 --------- .../routes/lib/short_url_assert_valid.test.js | 63 -------------- .../routes/lib/short_url_error.js | 26 ------ .../routes/lib/short_url_error.test.js | 67 --------------- .../routes/lib/short_url_lookup.js | 43 ---------- .../routes/lib/short_url_lookup.test.js | 84 ------------------- .../url_shortening/url_shortening_mixin.js | 23 ----- src/plugins/newsfeed/public/plugin.tsx | 12 ++- .../share/common/short_url_routes.ts} | 16 ++-- src/plugins/share/public/lib/url_shortener.ts | 5 +- src/plugins/share/public/plugin.test.ts | 17 +++- src/plugins/share/public/plugin.ts | 6 +- .../services/short_url_redirect_app.test.ts | 46 ++++++++++ .../services/short_url_redirect_app.ts} | 39 +++++---- .../share/server/routes/create_routes.ts | 2 + .../share/server/routes/get.ts} | 47 +++++++---- src/plugins/share/server/routes/goto.ts | 10 ++- .../share/server/routes/shorten_url.ts | 3 +- 22 files changed, 151 insertions(+), 460 deletions(-) delete mode 100644 src/legacy/core_plugins/state_session_storage_redirect/index.js delete mode 100644 src/legacy/core_plugins/state_session_storage_redirect/package.json delete mode 100644 src/legacy/server/url_shortening/index.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.test.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_error.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_error.test.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_lookup.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js delete mode 100644 src/legacy/server/url_shortening/url_shortening_mixin.js rename src/{legacy/server/url_shortening/routes/create_routes.js => plugins/share/common/short_url_routes.ts} (67%) create mode 100644 src/plugins/share/public/services/short_url_redirect_app.test.ts rename src/{legacy/core_plugins/state_session_storage_redirect/public/index.js => plugins/share/public/services/short_url_redirect_app.ts} (50%) rename src/{legacy/server/url_shortening/routes/goto.js => plugins/share/server/routes/get.ts} (55%) diff --git a/src/legacy/core_plugins/state_session_storage_redirect/index.js b/src/legacy/core_plugins/state_session_storage_redirect/index.js deleted file mode 100644 index 2d4d7c97232c04..00000000000000 --- a/src/legacy/core_plugins/state_session_storage_redirect/index.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - require: ['kibana'], - title: 'Redirecting', - id: 'stateSessionStorageRedirect', - main: 'plugins/state_session_storage_redirect', - hidden: true, - }, - }, - }); -} diff --git a/src/legacy/core_plugins/state_session_storage_redirect/package.json b/src/legacy/core_plugins/state_session_storage_redirect/package.json deleted file mode 100644 index 21956e5d76d5b1..00000000000000 --- a/src/legacy/core_plugins/state_session_storage_redirect/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "state_session_storage_redirect", - "version": "kibana", - "description": "When using the state:storeInSessionStorage setting with the short-urls, we need some way to get the full URL's hashed states into sessionStorage, this app will grab the URL from the injected state and and put the URL hashed states into sessionStorage before redirecting the user." -} diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 8ab55d4c517b59..6991527a9503c6 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -36,7 +36,6 @@ import * as Plugins from './plugins'; import { indexPatternsMixin } from './index_patterns'; import { savedObjectsMixin } from './saved_objects/saved_objects_mixin'; import { capabilitiesMixin } from './capabilities'; -import { urlShorteningMixin } from './url_shortening'; import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { sassMixin } from './sass'; @@ -123,9 +122,6 @@ export default class KbnServer { // setup capabilities routes capabilitiesMixin, - // setup routes for short urls - urlShorteningMixin, - // ensure that all bundles are built, or that the // watch bundle server is running optimizeMixin, diff --git a/src/legacy/server/url_shortening/index.js b/src/legacy/server/url_shortening/index.js deleted file mode 100644 index 031af0618d7bcb..00000000000000 --- a/src/legacy/server/url_shortening/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { urlShorteningMixin } from './url_shortening_mixin'; diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.js b/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.js deleted file mode 100644 index ff2b0f214e5eed..00000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { parse } from 'url'; -import { trim } from 'lodash'; -import Boom from 'boom'; - -export function shortUrlAssertValid(url) { - const { protocol, hostname, pathname } = parse(url); - - if (protocol) { - throw Boom.notAcceptable(`Short url targets cannot have a protocol, found "${protocol}"`); - } - - if (hostname) { - throw Boom.notAcceptable(`Short url targets cannot have a hostname, found "${hostname}"`); - } - - const pathnameParts = trim(pathname, '/').split('/'); - if (pathnameParts.length !== 2) { - throw Boom.notAcceptable( - `Short url target path must be in the format "/app/{{appId}}", found "${pathname}"` - ); - } -} diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.test.js deleted file mode 100644 index f83073e6aefe90..00000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { shortUrlAssertValid } from './short_url_assert_valid'; - -describe('shortUrlAssertValid()', () => { - const invalid = [ - ['protocol', 'http://localhost:5601/app/kibana'], - ['protocol', 'https://localhost:5601/app/kibana'], - ['protocol', 'mailto:foo@bar.net'], - ['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url - ['hostname', 'localhost/app/kibana'], - ['hostname and port', 'local.host:5601/app/kibana'], - ['hostname and auth', 'user:pass@localhost.net/app/kibana'], - ['path traversal', '/app/../../not-kibana'], - ['deep path', '/app/kibana/foo'], - ['deep path', '/app/kibana/foo/bar'], - ['base path', '/base/app/kibana'], - ]; - - invalid.forEach(([desc, url]) => { - it(`fails when url has ${desc}`, () => { - try { - shortUrlAssertValid(url); - throw new Error(`expected assertion to throw`); - } catch (err) { - if (!err || !err.isBoom) { - throw err; - } - } - }); - }); - - const valid = [ - '/app/kibana', - '/app/monitoring#angular/route', - '/app/text#document-id', - '/app/some?with=query', - '/app/some?with=query#and-a-hash', - ]; - - valid.forEach(url => { - it(`allows ${url}`, () => { - shortUrlAssertValid(url); - }); - }); -}); diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_error.js b/src/legacy/server/url_shortening/routes/lib/short_url_error.js deleted file mode 100644 index ed44ba21aa4c41..00000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_error.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 Boom from 'boom'; - -export function handleShortUrlError(error) { - return Boom.boomify(error, { - statusCode: error.statusCode || error.status || 500, - }); -} diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_error.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_error.test.js deleted file mode 100644 index 4eca6320ec834e..00000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_error.test.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { handleShortUrlError } from './short_url_error'; - -function createErrorWithStatus(status) { - const error = new Error(); - error.status = status; - return error; -} - -function createErrorWithStatusCode(statusCode) { - const error = new Error(); - error.statusCode = statusCode; - return error; -} - -describe('handleShortUrlError()', () => { - const caughtErrorsWithStatus = [ - createErrorWithStatus(401), - createErrorWithStatus(403), - createErrorWithStatus(404), - ]; - - const caughtErrorsWithStatusCode = [ - createErrorWithStatusCode(401), - createErrorWithStatusCode(403), - createErrorWithStatusCode(404), - ]; - - const uncaughtErrors = [new Error(), createErrorWithStatus(500), createErrorWithStatusCode(500)]; - - caughtErrorsWithStatus.forEach(err => { - it(`should handle errors with status of ${err.status}`, function() { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.status); - }); - }); - - caughtErrorsWithStatusCode.forEach(err => { - it(`should handle errors with statusCode of ${err.statusCode}`, function() { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.statusCode); - }); - }); - - uncaughtErrors.forEach(err => { - it(`should not handle unknown errors`, function() { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(500); - }); - }); -}); diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js deleted file mode 100644 index a8a01d1433a7a3..00000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { get } from 'lodash'; - -export function shortUrlLookupProvider(server) { - async function updateMetadata(doc, req) { - try { - await req.getSavedObjectsClient().update('url', doc.id, { - accessDate: new Date(), - accessCount: get(doc, 'attributes.accessCount', 0) + 1, - }); - } catch (err) { - server.log('Warning: Error updating url metadata', err); - //swallow errors. It isn't critical if there is no update. - } - } - - return { - async getUrl(id, req) { - const doc = await req.getSavedObjectsClient().get('url', id); - updateMetadata(doc, req); - - return doc.attributes.url; - }, - }; -} diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js deleted file mode 100644 index e8bf72a142d117..00000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 sinon from 'sinon'; -import { shortUrlLookupProvider } from './short_url_lookup'; -import { SavedObjectsClient } from '../../../../../core/server'; - -describe('shortUrlLookupProvider', () => { - const ID = 'bf00ad16941fc51420f91a93428b27a0'; - const TYPE = 'url'; - const URL = 'http://elastic.co'; - const server = { log: sinon.stub() }; - const sandbox = sinon.createSandbox(); - - let savedObjectsClient; - let req; - let shortUrl; - - beforeEach(() => { - savedObjectsClient = { - get: sandbox.stub(), - create: sandbox.stub().returns(Promise.resolve({ id: ID })), - update: sandbox.stub(), - errors: SavedObjectsClient.errors, - }; - - req = { getSavedObjectsClient: () => savedObjectsClient }; - shortUrl = shortUrlLookupProvider(server); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('getUrl', () => { - beforeEach(() => { - const attributes = { accessCount: 2, url: URL }; - savedObjectsClient.get.returns({ id: ID, attributes }); - }); - - it('provides the ID to savedObjectsClient', async () => { - await shortUrl.getUrl(ID, req); - - sinon.assert.calledOnce(savedObjectsClient.get); - const [type, id] = savedObjectsClient.get.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(id).toEqual(ID); - }); - - it('returns the url', async () => { - const response = await shortUrl.getUrl(ID, req); - expect(response).toEqual(URL); - }); - - it('increments accessCount', async () => { - await shortUrl.getUrl(ID, req); - - sinon.assert.calledOnce(savedObjectsClient.update); - const [type, id, attributes] = savedObjectsClient.update.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(id).toEqual(ID); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); - expect(attributes.accessCount).toEqual(3); - }); - }); -}); diff --git a/src/legacy/server/url_shortening/url_shortening_mixin.js b/src/legacy/server/url_shortening/url_shortening_mixin.js deleted file mode 100644 index 867898cac845a3..00000000000000 --- a/src/legacy/server/url_shortening/url_shortening_mixin.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { createRoutes } from './routes/create_routes'; - -export function urlShorteningMixin(kbnServer, server) { - createRoutes(server); -} diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index 5ea5e5b324717d..c4e042fe452f9c 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -23,7 +23,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { NewsfeedPluginInjectedConfig } from '../types'; +import { FetchResult, NewsfeedPluginInjectedConfig } from '../types'; import { NewsfeedNavButton, NewsfeedApiFetchResult } from './components/newsfeed_header_nav_button'; import { getApi } from './lib/api'; @@ -54,10 +54,14 @@ export class NewsfeedPublicPlugin implements Plugin { private fetchNewsfeed(core: CoreStart) { const { http, injectedMetadata } = core; - const config = injectedMetadata.getInjectedVar( - 'newsfeed' - ) as NewsfeedPluginInjectedConfig['newsfeed']; + const config = injectedMetadata.getInjectedVar('newsfeed') as + | NewsfeedPluginInjectedConfig['newsfeed'] + | undefined; + if (!config) { + // running in new platform, injected metadata not available + return new Rx.Observable(); + } return getApi(http, config, this.kibanaVersion).pipe( takeUntil(this.stop$), // stop the interval when stop method is called catchError(() => Rx.of(null)) // do not throw error diff --git a/src/legacy/server/url_shortening/routes/create_routes.js b/src/plugins/share/common/short_url_routes.ts similarity index 67% rename from src/legacy/server/url_shortening/routes/create_routes.js rename to src/plugins/share/common/short_url_routes.ts index 9540e7441a268b..7b42534de2ab15 100644 --- a/src/legacy/server/url_shortening/routes/create_routes.js +++ b/src/plugins/share/common/short_url_routes.ts @@ -17,11 +17,15 @@ * under the License. */ -import { shortUrlLookupProvider } from './lib/short_url_lookup'; -import { createGotoRoute } from './goto'; +export const GOTO_PREFIX = '/goto'; -export function createRoutes(server) { - const shortUrlLookup = shortUrlLookupProvider(server); +export const getUrlIdFromGotoRoute = (path: string) => + path.match(new RegExp(`${GOTO_PREFIX}/(.*)$`))?.[1]; - server.route(createGotoRoute({ server, shortUrlLookup })); -} +export const getGotoPath = (urlId: string) => `${GOTO_PREFIX}/${urlId}`; + +export const GETTER_PREFIX = '/api/short_url'; + +export const getUrlPath = (urlId: string) => `${GETTER_PREFIX}/${urlId}`; + +export const CREATE_PATH = '/api/shorten_url'; diff --git a/src/plugins/share/public/lib/url_shortener.ts b/src/plugins/share/public/lib/url_shortener.ts index 29d91bdb1aae6a..f2259f1fabf7d8 100644 --- a/src/plugins/share/public/lib/url_shortener.ts +++ b/src/plugins/share/public/lib/url_shortener.ts @@ -19,6 +19,7 @@ import url from 'url'; import { HttpStart } from 'kibana/public'; +import { CREATE_PATH, getGotoPath } from '../../common/short_url_routes'; export async function shortenUrl( absoluteUrl: string, @@ -34,10 +35,10 @@ export async function shortenUrl( const body = JSON.stringify({ url: relativeUrl }); - const resp = await post('/api/shorten_url', { body }); + const resp = await post(CREATE_PATH, { body }); return url.format({ protocol: parsedUrl.protocol, host: parsedUrl.host, - pathname: `${basePath}/goto/${resp.urlId}`, + pathname: `${basePath}${getGotoPath(resp.urlId)}`, }); } diff --git a/src/plugins/share/public/plugin.test.ts b/src/plugins/share/public/plugin.test.ts index 5610490be33b33..730814fe9ed237 100644 --- a/src/plugins/share/public/plugin.test.ts +++ b/src/plugins/share/public/plugin.test.ts @@ -20,6 +20,7 @@ import { registryMock, managerMock } from './plugin.test.mocks'; import { SharePlugin } from './plugin'; import { CoreStart } from 'kibana/public'; +import { coreMock } from '../../../core/public/mocks'; describe('SharePlugin', () => { beforeEach(() => { @@ -30,16 +31,28 @@ describe('SharePlugin', () => { describe('setup', () => { test('wires up and returns registry', async () => { - const setup = await new SharePlugin().setup(); + const coreSetup = coreMock.createSetup(); + const setup = await new SharePlugin().setup(coreSetup); expect(registryMock.setup).toHaveBeenCalledWith(); expect(setup.register).toBeDefined(); }); + + test('registers redirect app', async () => { + const coreSetup = coreMock.createSetup(); + await new SharePlugin().setup(coreSetup); + expect(coreSetup.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'short_url_redirect', + }) + ); + }); }); describe('start', () => { test('wires up and returns show function, but not registry', async () => { + const coreSetup = coreMock.createSetup(); const service = new SharePlugin(); - await service.setup(); + await service.setup(coreSetup); const start = await service.start({} as CoreStart); expect(registryMock.start).toHaveBeenCalled(); expect(managerMock.start).toHaveBeenCalledWith( diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 6d78211cf99544..01c248624950ab 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -17,15 +17,17 @@ * under the License. */ -import { CoreStart, Plugin } from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ShareMenuManager, ShareMenuManagerStart } from './services'; import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services'; +import { createShortUrlRedirectApp } from './services/short_url_redirect_app'; export class SharePlugin implements Plugin { private readonly shareMenuRegistry = new ShareMenuRegistry(); private readonly shareContextMenu = new ShareMenuManager(); - public async setup() { + public async setup(core: CoreSetup) { + core.application.register(createShortUrlRedirectApp(core, window.location)); return { ...this.shareMenuRegistry.setup(), }; diff --git a/src/plugins/share/public/services/short_url_redirect_app.test.ts b/src/plugins/share/public/services/short_url_redirect_app.test.ts new file mode 100644 index 00000000000000..206e637451ec07 --- /dev/null +++ b/src/plugins/share/public/services/short_url_redirect_app.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { createShortUrlRedirectApp } from './short_url_redirect_app'; +import { coreMock } from '../../../../core/public/mocks'; +import { hashUrl } from '../../../kibana_utils/public'; + +jest.mock('../../../kibana_utils/public', () => ({ hashUrl: jest.fn(x => `${x}/hashed`) })); + +describe('short_url_redirect_app', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch url and redirect to hashed version', async () => { + const coreSetup = coreMock.createSetup({ basePath: 'base' }); + coreSetup.http.get.mockResolvedValueOnce({ url: '/app/abc' }); + const locationMock = { pathname: '/base/goto/12345', href: '' } as Location; + + const { mount } = createShortUrlRedirectApp(coreSetup, locationMock); + await mount(); + + // check for fetching the complete URL + expect(coreSetup.http.get).toHaveBeenCalledWith('/api/short_url/12345'); + // check for hashing the URL returned from the server + expect(hashUrl).toHaveBeenCalledWith('/app/abc'); + // check for redirecting to the prepended path + expect(locationMock.href).toEqual('base/app/abc/hashed'); + }); +}); diff --git a/src/legacy/core_plugins/state_session_storage_redirect/public/index.js b/src/plugins/share/public/services/short_url_redirect_app.ts similarity index 50% rename from src/legacy/core_plugins/state_session_storage_redirect/public/index.js rename to src/plugins/share/public/services/short_url_redirect_app.ts index 701a5736c7d3b8..6f72b711f66020 100644 --- a/src/legacy/core_plugins/state_session_storage_redirect/public/index.js +++ b/src/plugins/share/public/services/short_url_redirect_app.ts @@ -17,24 +17,29 @@ * under the License. */ -import chrome from 'ui/chrome'; -import { hashUrl } from '../../../../plugins/kibana_utils/public'; -import uiRoutes from 'ui/routes'; -import { fatalError } from 'ui/notify'; +import { CoreSetup } from 'kibana/public'; +import { getUrlIdFromGotoRoute, getUrlPath, GOTO_PREFIX } from '../../common/short_url_routes'; +import { hashUrl } from '../../../kibana_utils/public'; -uiRoutes.enable(); -uiRoutes.when('/', { - resolve: { - url: function(AppState, globalState, $window) { - const redirectUrl = chrome.getInjected('redirectUrl'); - try { - const hashedUrl = hashUrl(redirectUrl); - const url = chrome.addBasePath(hashedUrl); +export const createShortUrlRedirectApp = (core: CoreSetup, location: Location) => ({ + id: 'short_url_redirect', + appRoute: GOTO_PREFIX, + chromeless: true, + title: 'Short URL Redirect', + async mount() { + const urlId = getUrlIdFromGotoRoute(location.pathname); - $window.location = url; - } catch (e) { - fatalError(e); - } - }, + if (!urlId) { + throw new Error('Url id not present in path'); + } + + const response = await core.http.get<{ url: string }>(getUrlPath(urlId)); + const redirectUrl = response.url; + const hashedUrl = hashUrl(redirectUrl); + const url = core.http.basePath.prepend(hashedUrl); + + location.href = url; + + return () => {}; }, }); diff --git a/src/plugins/share/server/routes/create_routes.ts b/src/plugins/share/server/routes/create_routes.ts index bd4b6fdb08791d..22d10c25197c90 100644 --- a/src/plugins/share/server/routes/create_routes.ts +++ b/src/plugins/share/server/routes/create_routes.ts @@ -22,11 +22,13 @@ import { CoreSetup, Logger } from 'kibana/server'; import { shortUrlLookupProvider } from './lib/short_url_lookup'; import { createGotoRoute } from './goto'; import { createShortenUrlRoute } from './shorten_url'; +import { createGetterRoute } from './get'; export function createRoutes({ http }: CoreSetup, logger: Logger) { const shortUrlLookup = shortUrlLookupProvider({ logger }); const router = http.createRouter(); createGotoRoute({ router, shortUrlLookup, http }); + createGetterRoute({ router, shortUrlLookup, http }); createShortenUrlRoute({ router, shortUrlLookup }); } diff --git a/src/legacy/server/url_shortening/routes/goto.js b/src/plugins/share/server/routes/get.ts similarity index 55% rename from src/legacy/server/url_shortening/routes/goto.js rename to src/plugins/share/server/routes/get.ts index 7a874786423d81..d6b191341dbe14 100644 --- a/src/legacy/server/url_shortening/routes/goto.js +++ b/src/plugins/share/server/routes/get.ts @@ -17,23 +17,40 @@ * under the License. */ -import { handleShortUrlError } from './lib/short_url_error'; +import { CoreSetup, IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { getUrlPath } from '../../common/short_url_routes'; -export const createGotoRoute = ({ server, shortUrlLookup }) => ({ - method: 'GET', - path: '/goto_LP/{urlId}', - handler: async function(request, h) { - try { - const url = await shortUrlLookup.getUrl(request.params.urlId, request); +export const createGetterRoute = ({ + router, + shortUrlLookup, + http, +}: { + router: IRouter; + shortUrlLookup: ShortUrlLookupService; + http: CoreSetup['http']; +}) => { + router.get( + { + path: getUrlPath('{urlId}'), + validate: { + params: schema.object({ urlId: schema.string() }), + }, + }, + router.handleLegacyErrors(async function(context, request, response) { + const url = await shortUrlLookup.getUrl(request.params.urlId, { + savedObjects: context.core.savedObjects.client, + }); shortUrlAssertValid(url); - const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); - return h.renderApp(app, { - redirectUrl: url, + return response.ok({ + body: { + url, + }, }); - } catch (err) { - throw handleShortUrlError(err); - } - }, -}); + }) + ); +}; diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 7343dc1bd34a26..5c3a4e441099fb 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -22,6 +22,7 @@ import { schema } from '@kbn/config-schema'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { getGotoPath } from '../../common/short_url_routes'; export const createGotoRoute = ({ router, @@ -34,7 +35,7 @@ export const createGotoRoute = ({ }) => { router.get( { - path: '/goto/{urlId}', + path: getGotoPath('{urlId}'), validate: { params: schema.object({ urlId: schema.string() }), }, @@ -54,10 +55,13 @@ export const createGotoRoute = ({ }, }); } - return response.redirected({ + const body = await context.core.rendering.render(); + + return response.ok({ headers: { - location: http.basePath.prepend('/goto_LP/' + request.params.urlId), + 'content-security-policy': http.csp.header, }, + body, }); }) ); diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts index 116b90c6971c5c..41570f8a5f453a 100644 --- a/src/plugins/share/server/routes/shorten_url.ts +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -22,6 +22,7 @@ import { schema } from '@kbn/config-schema'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { CREATE_PATH } from '../../common/short_url_routes'; export const createShortenUrlRoute = ({ shortUrlLookup, @@ -32,7 +33,7 @@ export const createShortenUrlRoute = ({ }) => { router.post( { - path: '/api/shorten_url', + path: CREATE_PATH, validate: { body: schema.object({ url: schema.string() }), }, From d3cef4791ec88da7d364596051e6f86310a800dd Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 21 Jan 2020 08:54:42 -0500 Subject: [PATCH 10/19] [Maps] fix warning about missing key in react element (#55372) --- .../legacy/plugins/maps/public/layers/styles/color_utils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js index df212f23cd8942..b04f8ff56e5ee4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js @@ -113,7 +113,11 @@ export const COLOR_PALETTES = COLOR_PALETTES_CONFIGS.map(palette => { height: '100%', display: 'inline-block', }; - return
 
; + return ( +
+   +
+ ); }); return { value: palette.id, From 6feabcd53342482d403247487dfc7bb6ca6e74d9 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 21 Jan 2020 17:38:28 +0300 Subject: [PATCH 11/19] Generate a static parser, move tests to vis_type_timelion (#55299) * Use generated parser, move tests to vis_type_timelion * Remove legacy tests * Create a grunt task for generating a parser --- .eslintignore | 1 + .../core_plugins/vis_type_timelion/README.md | 10 + .../public/_generated_/chain.js | 1780 +++++++++++++++++ ...timelion_expression_input_helpers.test.ts} | 172 +- .../timelion_expression_input_helpers.ts | 13 +- tasks/config/peg.js | 4 + 6 files changed, 1843 insertions(+), 137 deletions(-) create mode 100644 src/legacy/core_plugins/vis_type_timelion/README.md create mode 100644 src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js rename src/legacy/core_plugins/{timelion/public/directives/__tests__/timelion_expression_input_helpers.js => vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts} (70%) diff --git a/.eslintignore b/.eslintignore index c4fb806b6d394c..86a01b68ecab1f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,7 @@ bower_components /built_assets /html_docs /src/plugins/data/common/es_query/kuery/ast/_generated_/** +/src/legacy/core_plugins/vis_type_timelion/public/_generated_/** src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data /src/legacy/ui/public/angular-bootstrap /src/legacy/ui/public/flot-charts diff --git a/src/legacy/core_plugins/vis_type_timelion/README.md b/src/legacy/core_plugins/vis_type_timelion/README.md new file mode 100644 index 00000000000000..c306e03abf2c60 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timelion/README.md @@ -0,0 +1,10 @@ +# Vis type Timelion + +# Generate a parser +If your grammar was changed in `public/chain.peg` you need to re-generate the static parser. You could use a grunt task: + +``` +grunt peg:timelion_chain +``` + +The generated parser will be appeared at `public/_generated_` folder, which is included in `.eslintignore` \ No newline at end of file diff --git a/src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js b/src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js new file mode 100644 index 00000000000000..f812b94238d43d --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js @@ -0,0 +1,1780 @@ +module.exports = (function() { + "use strict"; + + /* + * Generated by PEG.js 0.9.0. + * + * http://pegjs.org/ + */ + + function peg$subclass(child, parent) { + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + } + + function peg$SyntaxError(message, expected, found, location) { + this.message = message; + this.expected = expected; + this.found = found; + this.location = location; + this.name = "SyntaxError"; + + if (typeof Error.captureStackTrace === "function") { + Error.captureStackTrace(this, peg$SyntaxError); + } + } + + peg$subclass(peg$SyntaxError, Error); + + function peg$parse(input) { + var options = arguments.length > 1 ? arguments[1] : {}, + parser = this, + + peg$FAILED = {}, + + peg$startRuleFunctions = { start: peg$parsestart }, + peg$startRuleFunction = peg$parsestart, + + peg$c0 = function(tree) { + return { + tree: tree.filter(function (o) {return o != null}), + functions: functions, + args: args, + variables: variables + } + }, + peg$c1 = ",", + peg$c2 = { type: "literal", value: ",", description: "\",\"" }, + peg$c3 = function(first, arg) {return arg}, + peg$c4 = function(first, rest) { + return [first].concat(rest); + }, + peg$c5 = "=", + peg$c6 = { type: "literal", value: "=", description: "\"=\"" }, + peg$c7 = function(name, value) { + var arg = { + type: 'namedArg', + name: name, + value: value, + location: simpleLocation(location()), + text: text() + }; + currentArgs.push(arg); + return arg; + }, + peg$c8 = function(value) { + var exception = { + type: 'incompleteArgument', + currentArgs: currentArgs, + currentFunction: currentFunction, + location: simpleLocation(location()), + text: text() + } + error(JSON.stringify(exception)); + }, + peg$c9 = function(name) { + var exception = { + type: 'incompleteArgumentValue', + currentArgs: currentArgs, + currentFunction: currentFunction, + name: name, + location: simpleLocation(location()), + text: text() + } + error(JSON.stringify(exception)); + }, + peg$c10 = function(element) {return element}, + peg$c11 = function(literal) { + var result = ltoo(literal); + result.location = simpleLocation(location()), + result.text = text(); + return result; + }, + peg$c12 = "$", + peg$c13 = { type: "literal", value: "$", description: "\"$\"" }, + peg$c14 = function(name) { + if (variables[name]) { + return variables[name]; + } else { + error('$' + name + ' is not defined') + } + }, + peg$c15 = function(name, value) { + variables[name] = value; + }, + peg$c16 = function(first, series) {return series}, + peg$c17 = function(first, rest) { + return [first].concat(rest) + }, + peg$c18 = /^[a-zA-Z]/, + peg$c19 = { type: "class", value: "[a-zA-Z]", description: "[a-zA-Z]" }, + peg$c20 = /^[.a-zA-Z0-9_\-]/, + peg$c21 = { type: "class", value: "[.a-zA-Z0-9_-]", description: "[.a-zA-Z0-9_-]" }, + peg$c22 = function(first, rest) { + currentFunction = first.join('') + rest.join(''); + currentArgs = []; + return currentFunction; + }, + peg$c23 = function(first, rest) { return first.join('') + rest.join('') }, + peg$c24 = { type: "other", description: "function" }, + peg$c25 = ".", + peg$c26 = { type: "literal", value: ".", description: "\".\"" }, + peg$c27 = "(", + peg$c28 = { type: "literal", value: "(", description: "\"(\"" }, + peg$c29 = ")", + peg$c30 = { type: "literal", value: ")", description: "\")\"" }, + peg$c31 = function(name, arg_list) { + var result = { + type: 'function', + function: name, + arguments: arg_list || [], + location: simpleLocation(location()), + text: text() + } + + result.arguments.forEach(function (arg) { + arg.function = name; + args.push(arg); + }) + + functions.push(result) + return result; + }, + peg$c32 = function(func) { + var exception = { + type: 'incompleteFunction', + function: func, + location: simpleLocation(location()), + text: text() + } + error(JSON.stringify(exception)); + }, + peg$c33 = "@", + peg$c34 = { type: "literal", value: "@", description: "\"@\"" }, + peg$c35 = ":", + peg$c36 = { type: "literal", value: ":", description: "\":\"" }, + peg$c37 = function(plot, series) { + return { + type: 'reference', + plot: plot, + series: series + } + }, + peg$c38 = function(plot) { + return { + type: 'reference', + plot: plot + } + }, + peg$c39 = function(func, rest) {return {type: 'chain', chain: [func].concat(rest)}}, + peg$c40 = function(grouped, functions) { + var first = { + type: 'chainList', + list: grouped + } + first.label = text(); + + return {type: "chain", chain: [first].concat(functions)}; + }, + peg$c41 = { type: "other", description: "literal" }, + peg$c42 = "\"", + peg$c43 = { type: "literal", value: "\"", description: "\"\\\"\"" }, + peg$c44 = function(chars) { return chars.join(''); }, + peg$c45 = "'", + peg$c46 = { type: "literal", value: "'", description: "\"'\"" }, + peg$c47 = "true", + peg$c48 = { type: "literal", value: "true", description: "\"true\"" }, + peg$c49 = function() { return true; }, + peg$c50 = "false", + peg$c51 = { type: "literal", value: "false", description: "\"false\"" }, + peg$c52 = function() { return false; }, + peg$c53 = "null", + peg$c54 = { type: "literal", value: "null", description: "\"null\"" }, + peg$c55 = function() { return null; }, + peg$c56 = /^[^()"',= \t]/, + peg$c57 = { type: "class", value: "[^()\"',=\\ \\t]", description: "[^()\"',=\\ \\t]" }, + peg$c58 = function(string) { // this also matches numbers via Number() + var result = string.join(''); + // Sort of hacky, but PEG doesn't have backtracking so + // a number rule is hard to read, and performs worse + if (isNaN(Number(result))) return result; + return Number(result) + }, + peg$c59 = /^[ \t\r\n]/, + peg$c60 = { type: "class", value: "[\\ \\t\\r\\n]", description: "[\\ \\t\\r\\n]" }, + peg$c61 = "\\", + peg$c62 = { type: "literal", value: "\\", description: "\"\\\\\"" }, + peg$c63 = function(sequence) { return sequence; }, + peg$c64 = /^[^"]/, + peg$c65 = { type: "class", value: "[^\"]", description: "[^\"]" }, + peg$c66 = /^[^']/, + peg$c67 = { type: "class", value: "[^']", description: "[^']" }, + peg$c68 = /^[0-9]/, + peg$c69 = { type: "class", value: "[0-9]", description: "[0-9]" }, + peg$c70 = function(digits) {return parseInt(digits.join(''))}, + + peg$currPos = 0, + peg$savedPos = 0, + peg$posDetailsCache = [{ line: 1, column: 1, seenCR: false }], + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + + peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$savedPos, peg$currPos); + } + + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); + } + + function expected(description) { + throw peg$buildException( + null, + [{ type: "other", description: description }], + input.substring(peg$savedPos, peg$currPos), + peg$computeLocation(peg$savedPos, peg$currPos) + ); + } + + function error(message) { + throw peg$buildException( + message, + null, + input.substring(peg$savedPos, peg$currPos), + peg$computeLocation(peg$savedPos, peg$currPos) + ); + } + + function peg$computePosDetails(pos) { + var details = peg$posDetailsCache[pos], + p, ch; + + if (details) { + return details; + } else { + p = pos - 1; + while (!peg$posDetailsCache[p]) { + p--; + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column, + seenCR: details.seenCR + }; + + while (p < pos) { + ch = input.charAt(p); + if (ch === "\n") { + if (!details.seenCR) { details.line++; } + details.column = 1; + details.seenCR = false; + } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { + details.line++; + details.column = 1; + details.seenCR = true; + } else { + details.column++; + details.seenCR = false; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + return details; + } + } + + function peg$computeLocation(startPos, endPos) { + var startPosDetails = peg$computePosDetails(startPos), + endPosDetails = peg$computePosDetails(endPos); + + return { + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column + } + }; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildException(message, expected, found, location) { + function cleanupExpected(expected) { + var i = 1; + + expected.sort(function(a, b) { + if (a.description < b.description) { + return -1; + } else if (a.description > b.description) { + return 1; + } else { + return 0; + } + }); + + while (i < expected.length) { + if (expected[i - 1] === expected[i]) { + expected.splice(i, 1); + } else { + i++; + } + } + } + + function buildMessage(expected, found) { + function stringEscape(s) { + function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } + + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\x08/g, '\\b') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\f/g, '\\f') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); }) + .replace(/[\u0100-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); }) + .replace(/[\u1000-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); }); + } + + var expectedDescs = new Array(expected.length), + expectedDesc, foundDesc, i; + + for (i = 0; i < expected.length; i++) { + expectedDescs[i] = expected[i].description; + } + + expectedDesc = expected.length > 1 + ? expectedDescs.slice(0, -1).join(", ") + + " or " + + expectedDescs[expected.length - 1] + : expectedDescs[0]; + + foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; + + return "Expected " + expectedDesc + " but " + foundDesc + " found."; + } + + if (expected !== null) { + cleanupExpected(expected); + } + + return new peg$SyntaxError( + message !== null ? message : buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parsestart() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parsespace(); + if (s1 === peg$FAILED) { + s1 = null; + } + if (s1 !== peg$FAILED) { + s2 = peg$parseseries(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c0(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsearg_list() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseargument(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parseargument(); + if (s7 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c3(s1, s7); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parseargument(); + if (s7 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c3(s1, s7); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + if (s2 !== peg$FAILED) { + s3 = peg$parsespace(); + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c1; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c4(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseargument() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseargument_name(); + if (s1 !== peg$FAILED) { + s2 = peg$parsespace(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s3 = peg$c5; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + s5 = peg$parsearg_type(); + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c7(s1, s5); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parsespace(); + if (s1 === peg$FAILED) { + s1 = null; + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s2 = peg$c5; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s2 !== peg$FAILED) { + s3 = peg$parsespace(); + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + s4 = peg$parsearg_type(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c8(s4); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseargument_name(); + if (s1 !== peg$FAILED) { + s2 = peg$parsespace(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s3 = peg$c5; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c9(s1); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parsearg_type(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c10(s1); + } + s0 = s1; + } + } + } + + return s0; + } + + function peg$parsearg_type() { + var s0, s1; + + s0 = peg$parsevariable_get(); + if (s0 === peg$FAILED) { + s0 = peg$parseseries_type(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseliteral(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c11(s1); + } + s0 = s1; + } + } + + return s0; + } + + function peg$parsevariable_get() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c12; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseargument_name(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c14(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsevariable_set() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c12; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseargument_name(); + if (s2 !== peg$FAILED) { + s3 = peg$parsespace(); + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s4 = peg$c5; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s4 !== peg$FAILED) { + s5 = peg$parsespace(); + if (s5 === peg$FAILED) { + s5 = null; + } + if (s5 !== peg$FAILED) { + s6 = peg$parsearg_type(); + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c15(s2, s6); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseseries_type() { + var s0; + + s0 = peg$parsevariable_set(); + if (s0 === peg$FAILED) { + s0 = peg$parsevariable_get(); + if (s0 === peg$FAILED) { + s0 = peg$parsegroup(); + if (s0 === peg$FAILED) { + s0 = peg$parsechain(); + if (s0 === peg$FAILED) { + s0 = peg$parsereference(); + } + } + } + } + + return s0; + } + + function peg$parseseries() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseseries_type(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parseseries_type(); + if (s7 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c16(s1, s7); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parseseries_type(); + if (s7 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c16(s1, s7); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s3 = peg$c1; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c17(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefunction_name() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = []; + if (peg$c18.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c18.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = []; + if (peg$c20.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + while (s3 !== peg$FAILED) { + s2.push(s3); + if (peg$c20.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c22(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseargument_name() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = []; + if (peg$c18.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c18.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = []; + if (peg$c20.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + while (s3 !== peg$FAILED) { + s2.push(s3); + if (peg$c20.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c23(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefunction() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parsespace(); + if (s1 === peg$FAILED) { + s1 = null; + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 46) { + s2 = peg$c25; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c26); } + } + if (s2 !== peg$FAILED) { + s3 = peg$parsefunction_name(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 40) { + s5 = peg$c27; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c28); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parsearg_list(); + if (s7 === peg$FAILED) { + s7 = null; + } + if (s7 !== peg$FAILED) { + s8 = peg$parsespace(); + if (s8 === peg$FAILED) { + s8 = null; + } + if (s8 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s9 = peg$c29; + peg$currPos++; + } else { + s9 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s9 !== peg$FAILED) { + s10 = peg$parsespace(); + if (s10 === peg$FAILED) { + s10 = null; + } + if (s10 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c31(s3, s7); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s1 = peg$c25; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c26); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsefunction_name(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c32(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c24); } + } + + return s0; + } + + function peg$parsereference() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 64) { + s1 = peg$c33; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c34); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseinteger(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c35; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseinteger(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c37(s2, s4); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 64) { + s1 = peg$c33; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c34); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseinteger(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c38(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parsechain() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parsefunction(); + if (s1 !== peg$FAILED) { + s2 = peg$parsespace(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$parsefunction(); + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parsefunction(); + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c39(s1, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsegroup() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c27; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c28); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsespace(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseseries(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s5 = peg$c29; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s5 !== peg$FAILED) { + s6 = []; + s7 = peg$parsefunction(); + while (s7 !== peg$FAILED) { + s6.push(s7); + s7 = peg$parsefunction(); + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c40(s3, s6); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseliteral() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c42; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsedq_char(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsedq_char(); + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c42; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c44(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 39) { + s1 = peg$c45; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c46); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsesq_char(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsesq_char(); + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 39) { + s3 = peg$c45; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c46); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c44(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c47) { + s1 = peg$c47; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c48); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c49(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c50) { + s1 = peg$c50; + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c51); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c52(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c53) { + s1 = peg$c53; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c54); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c55(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = []; + if (peg$c56.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c56.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c58(s1); + } + s0 = s1; + } + } + } + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + + return s0; + } + + function peg$parsespace() { + var s0, s1; + + s0 = []; + if (peg$c59.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c60); } + } + if (s1 !== peg$FAILED) { + while (s1 !== peg$FAILED) { + s0.push(s1); + if (peg$c59.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c60); } + } + } + } else { + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsedq_char() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c61; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c62); } + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s2 = peg$c42; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s2 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 92) { + s2 = peg$c61; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c62); } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c63(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + if (peg$c64.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c65); } + } + } + + return s0; + } + + function peg$parsesq_char() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c61; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c62); } + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 39) { + s2 = peg$c45; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c46); } + } + if (s2 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 92) { + s2 = peg$c61; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c62); } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c63(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + if (peg$c66.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c67); } + } + } + + return s0; + } + + function peg$parseinteger() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = []; + if (peg$c68.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c69); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c68.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c69); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c70(s1); + } + s0 = s1; + + return s0; + } + + + function ltoo (literal) { + return {type: 'literal', value: literal} + } + + function simpleLocation (location) { + // Returns an object representing the position of the function within the expression, + // demarcated by the position of its first character and last character. We calculate these values + // using the offset because the expression could span multiple lines, and we don't want to deal + // with column and line values. + return { + min: location.start.offset, + max: location.end.offset + } + } + + var currentFunction; + var currentArgs = []; + + var functions = []; + var args = []; + var variables = {}; + + + + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail({ type: "end", description: "end of input" }); + } + + throw peg$buildException( + null, + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } + } + + return { + SyntaxError: peg$SyntaxError, + parse: peg$parse + }; +})(); \ No newline at end of file diff --git a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts similarity index 70% rename from src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js rename to src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts index ea2d44bcaefe07..18a0c0872dc031 100644 --- a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js +++ b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts @@ -17,19 +17,16 @@ * under the License. */ -import expect from '@kbn/expect'; -import PEG from 'pegjs'; -import grammar from 'raw-loader!../../../../vis_type_timelion/public/chain.peg'; -import { SUGGESTION_TYPE, suggest } from '../timelion_expression_input_helpers'; -import { getArgValueSuggestions } from '../../../../vis_type_timelion/public/helpers/arg_value_suggestions'; -import { - setIndexPatterns, - setSavedObjectsClient, -} from '../../../../vis_type_timelion/public/helpers/plugin_services'; +import { SUGGESTION_TYPE, suggest } from './timelion_expression_input_helpers'; +import { getArgValueSuggestions } from '../helpers/arg_value_suggestions'; +import { setIndexPatterns, setSavedObjectsClient } from '../helpers/plugin_services'; +import { IndexPatterns } from 'src/plugins/data/public'; +import { SavedObjectsClient } from 'kibana/public'; +import { ITimelionFunction } from '../../common/types'; describe('Timelion expression suggestions', () => { - setIndexPatterns({}); - setSavedObjectsClient({}); + setIndexPatterns({} as IndexPatterns); + setSavedObjectsClient({} as SavedObjectsClient); const argValueSuggestions = getArgValueSuggestions(); @@ -45,17 +42,13 @@ describe('Timelion expression suggestions', () => { suggestions: [{ name: 'value1' }], }, ], - }; + } as ITimelionFunction; const myFunc2 = { name: 'myFunc2', chainable: false, args: [{ name: 'argA' }, { name: 'argAB' }, { name: 'argABC' }], - }; + } as ITimelionFunction; const functionList = [func1, myFunc2]; - let Parser; - beforeEach(function() { - Parser = PEG.generate(grammar); - }); describe('parse exception', () => { describe('incompleteFunction', () => { @@ -65,17 +58,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [func1, myFunc2], - location: { - min: 0, - max: 1, - }, - type: 'functions', + type: SUGGESTION_TYPE.FUNCTIONS, }); }); it('should filter function suggestions by function name', async () => { @@ -84,17 +72,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [myFunc2], - location: { - min: 0, - max: 4, - }, - type: 'functions', + type: SUGGESTION_TYPE.FUNCTIONS, }); }); }); @@ -106,17 +89,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [], - location: { - min: 11, - max: 12, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); @@ -126,17 +104,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: myFunc2.args, - location: { - min: 9, - max: 10, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); @@ -146,17 +119,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: myFunc2.args, - location: { - min: 9, - max: 25, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); @@ -166,17 +134,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argA' }, { name: 'argAB', suggestions: [{ name: 'value1' }] }], - location: { - min: 7, - max: 8, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); @@ -186,17 +149,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argA' }, { name: 'argABC' }], - location: { - min: 24, - max: 25, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); }); @@ -208,17 +166,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [], - location: { - min: 11, - max: 11, - }, - type: 'argument_value', + type: SUGGESTION_TYPE.ARGUMENT_VALUE, }); }); @@ -228,17 +181,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'value1' }], - location: { - min: 11, - max: 11, - }, - type: 'argument_value', + type: SUGGESTION_TYPE.ARGUMENT_VALUE, }); }); }); @@ -252,17 +200,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [func1], - location: { - min: 0, - max: 8, - }, - type: 'functions', + type: SUGGESTION_TYPE.FUNCTIONS, }); }); }); @@ -275,17 +218,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: myFunc2.args, - location: { - min: 9, - max: 9, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); it('should not provide argument suggestions for argument that is all ready set in function def', async () => { @@ -294,18 +232,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions.type).to.equal(SUGGESTION_TYPE.ARGUMENTS); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argA' }, { name: 'argABC' }], - location: { - min: 24, - max: 24, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); it('should filter argument suggestions by argument name', async () => { @@ -314,17 +246,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argAB' }, { name: 'argABC' }], - location: { - min: 9, - max: 14, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); it('should not show first argument for chainable functions', async () => { @@ -333,17 +260,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argA' }, { name: 'argAB', suggestions: [{ name: 'value1' }] }], - location: { - min: 7, - max: 7, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); }); @@ -354,17 +276,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [], - location: { - min: 14, - max: 16, - }, - type: 'argument_value', + type: SUGGESTION_TYPE.ARGUMENT_VALUE, }); }); @@ -374,17 +291,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'value1' }], - location: { - min: 13, - max: 16, - }, - type: 'argument_value', + type: SUGGESTION_TYPE.ARGUMENT_VALUE, }); }); }); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts index 5aa05fb16466bb..d7818680c9543e 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts +++ b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts @@ -18,18 +18,17 @@ */ import { get, startsWith } from 'lodash'; -import PEG from 'pegjs'; import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { i18n } from '@kbn/i18n'; +import { Parser } from 'pegjs'; + // @ts-ignore -import grammar from 'raw-loader!../chain.peg'; +import { parse } from '../_generated_/chain'; -import { i18n } from '@kbn/i18n'; import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types'; import { ArgValueSuggestions, FunctionArg, Location } from '../helpers/arg_value_suggestions'; -const Parser = PEG.generate(grammar); - export enum SUGGESTION_TYPE { ARGUMENTS = 'arguments', ARGUMENT_VALUE = 'argument_value', @@ -57,7 +56,7 @@ function getArgumentsHelp( } async function extractSuggestionsFromParsedResult( - result: ReturnType, + result: ReturnType, cursorPosition: number, functionList: ITimelionFunction[], argValueSuggestions: ArgValueSuggestions @@ -141,7 +140,7 @@ export async function suggest( argValueSuggestions: ArgValueSuggestions ) { try { - const result = await Parser.parse(expression); + const result = await parse(expression); return await extractSuggestionsFromParsedResult( result, diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 0c57c6139d2984..1d8667840faba0 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -25,4 +25,8 @@ module.exports = { allowedStartRules: ['start', 'Literal'], }, }, + timelion_chain: { + src: 'src/legacy/core_plugins/vis_type_timelion/public/chain.peg', + dest: 'src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js', + }, }; From be31198d21c1693c6d67a4cc47726325ca192a45 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 21 Jan 2020 15:55:54 +0100 Subject: [PATCH 12/19] Re-enable Kerberos + anonymous access test. (#55377) --- .../kerberos_api_integration/apis/security/kerberos_login.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index bd35f21d8f428b..0346da334d2f2d 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -344,8 +344,7 @@ export default function({ getService }: FtrProviderContext) { }); }); - // FAILING: https://github.com/elastic/kibana/issues/52969 - describe.skip('API access with missing access token document or expired refresh token.', () => { + describe('API access with missing access token document or expired refresh token.', () => { let sessionCookie: Cookie; beforeEach(async () => { From ce286f543e4bd4d72ec7ffefac3a410cac2eaf0f Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 21 Jan 2020 15:25:58 +0000 Subject: [PATCH 13/19] [ML] Adding missing job groups to recognizer wizard (#55392) --- .../application/jobs/new_job/recognize/page.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 141ed5d1bbb8ff..c4a96d9e373c8d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -77,6 +77,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { const [kibanaObjects, setKibanaObjects] = useState({}); const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); const [resultsUrl, setResultsUrl] = useState(''); + const [existingGroups, setExistingGroups] = useState(existingGroupIds); // #endregion const { @@ -109,6 +110,10 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { setKibanaObjects(kibanaObjectsResult); setSaveState(SAVE_STATE.NOT_SAVED); + + // mix existing groups from the server with the groups used across all jobs in the module. + const moduleGroups = [...response.jobs.map(j => j.config.groups || [])].flat(); + setExistingGroups([...new Set([...existingGroups, ...moduleGroups])]); } catch (e) { // eslint-disable-next-line no-console console.error(e); @@ -222,6 +227,12 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { ...jobOverrides, [job.job_id as string]: job, }); + if (job.groups !== undefined) { + // add newly added jobs to the list of existing groups + // for use when editing other jobs in the module + const groups = [...new Set([...existingGroups, ...job.groups])]; + setExistingGroups(groups); + } }; const isFormVisible = [SAVE_STATE.NOT_SAVED, SAVE_STATE.SAVING].includes(saveState); @@ -304,7 +315,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { jobs={jobs} jobPrefix={jobPrefix} saveState={saveState} - existingGroupIds={existingGroupIds} + existingGroupIds={existingGroups} jobOverrides={jobOverrides} onJobOverridesChange={onJobOverridesChange} /> From 2bf111c50fc211454a6f658b488323e22a8ad4e3 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 21 Jan 2020 10:58:57 -0500 Subject: [PATCH 14/19] [Uptime] Fix flaky uptime overview page test (#54767) * Fix flaky uptime overview page test. * Increase timeout for url checks. * Prefer standard `retry.try` to custom retry implementation. * Remove unneeded symbol. * Remove unnecessary type annotation. Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/uptime/overview.ts | 9 +++------ x-pack/test/functional/page_objects/uptime_page.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index ba701da6e517dd..2ef6a381a6a30e 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -50,13 +50,11 @@ export default ({ getPageObjects }: FtrProviderContext) => { ]); }); - // flakey see https://github.com/elastic/kibana/issues/54527 - it.skip('pagination is cleared when filter criteria changes', async () => { + it('pagination is cleared when filter criteria changes', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await pageObjects.uptime.changePage('next'); // there should now be pagination data in the URL - const contains = await pageObjects.uptime.pageUrlContains('pagination'); - expect(contains).to.be(true); + await pageObjects.uptime.pageUrlContains('pagination'); await pageObjects.uptime.pageHasExpectedIds([ '0010-down', '0011-up', @@ -71,8 +69,7 @@ export default ({ getPageObjects }: FtrProviderContext) => { ]); await pageObjects.uptime.setStatusFilter('up'); // ensure that pagination is removed from the URL - const doesNotContain = await pageObjects.uptime.pageUrlContains('pagination'); - expect(doesNotContain).to.be(false); + await pageObjects.uptime.pageUrlContains('pagination', false); await pageObjects.uptime.pageHasExpectedIds([ '0000-intermittent', '0001-up', diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index f04f96148583fa..2ae0ea38c957bb 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function UptimePageProvider({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'timePicker']); const uptimeService = getService('uptime'); + const retry = getService('retry'); return new (class UptimePage { public async goToUptimePageAndSetDateRange( @@ -51,8 +53,10 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo await Promise.all(monitorIdsToCheck.map(id => uptimeService.monitorPageLinkExists(id))); } - public async pageUrlContains(value: string) { - return await uptimeService.urlContains(value); + public async pageUrlContains(value: string, expected: boolean = true) { + retry.try(async () => { + expect(await uptimeService.urlContains(value)).to.eql(expected); + }); } public async changePage(direction: 'next' | 'prev') { From b0af1bf95c26a03135a382c0c758b54782e514fa Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 21 Jan 2020 09:19:08 -0700 Subject: [PATCH 15/19] Clear core loading indicator just before UI is rendered (#55242) Co-authored-by: Elastic Machine --- src/core/public/core_system.test.ts | 7 +++---- src/core/public/core_system.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 1ee41fe64418ec..94fa74f4bd861f 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -429,15 +429,14 @@ describe('Notifications targetDomElement', () => { rootDomElement, }); - let targetDomElementParentInStart: HTMLElement | null; + let targetDomElementInStart: HTMLElement | null; MockNotificationsService.start.mockImplementation(({ targetDomElement }): any => { - expect(targetDomElement.parentElement).not.toBeNull(); - targetDomElementParentInStart = targetDomElement.parentElement; + targetDomElementInStart = targetDomElement; }); // Starting the core system should pass the targetDomElement as a child of the rootDomElement await core.setup(); await core.start(); - expect(targetDomElementParentInStart!).toBe(rootDomElement); + expect(targetDomElementInStart!.parentElement).toBe(rootDomElement); }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 97bd49416f7052..be69cacdd271a7 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -222,13 +222,6 @@ export class CoreSystem { const notificationsTargetDomElement = document.createElement('div'); const overlayTargetDomElement = document.createElement('div'); - // ensure the rootDomElement is empty - this.rootDomElement.textContent = ''; - this.rootDomElement.classList.add('coreSystemRootDomElement'); - this.rootDomElement.appendChild(coreUiTargetDomElement); - this.rootDomElement.appendChild(notificationsTargetDomElement); - this.rootDomElement.appendChild(overlayTargetDomElement); - const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement, @@ -276,6 +269,14 @@ export class CoreSystem { }; const plugins = await this.plugins.start(core); + + // ensure the rootDomElement is empty + this.rootDomElement.textContent = ''; + this.rootDomElement.classList.add('coreSystemRootDomElement'); + this.rootDomElement.appendChild(coreUiTargetDomElement); + this.rootDomElement.appendChild(notificationsTargetDomElement); + this.rootDomElement.appendChild(overlayTargetDomElement); + const rendering = this.rendering.start({ application, chrome, From ce2930ec5154ef4ccba8420744a522225cd24210 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Tue, 21 Jan 2020 10:46:56 -0600 Subject: [PATCH 16/19] De-angularize vis tooltips (#54954) * Remove angular dependencey from vis/tooltip * Move tooltip logic into vislib * Remove and fix all ngMock refs in vislib tests * Add numeral to renovate config * Add vis_type_vislib to codeowners * Move vis_legend into vislib and fix errors * vis_type_vislib/public imports to be only top-level --- .github/CODEOWNERS | 1 + .i18nrc.json | 4 +- package.json | 1 + renovate.json5 | 8 + .../public/components/region_map_options.tsx | 6 +- .../public/components/tile_map_options.tsx | 2 +- .../components/wms_internal_options.tsx | 2 +- .../public/components/wms_options.tsx | 2 +- .../public/settings_options.tsx | 2 +- .../public/components/metric_vis_options.tsx | 4 +- .../vis_type_metric/public/metric_vis_fn.ts | 2 +- .../vis_type_metric/public/metric_vis_type.ts | 2 +- .../vis_type_metric/public/types.ts | 3 +- .../public/components/table_vis_options.tsx | 6 +- .../public/components/tag_cloud_options.tsx | 2 +- .../vis_type_vislib/public/area.ts | 14 +- .../components/common/basic_options.tsx | 4 +- .../public/components/common/color_ranges.tsx | 2 +- .../public/components/common/color_schema.tsx | 8 +- .../public/components/common/range.tsx | 2 +- .../components/common/truncate_labels.tsx | 2 +- .../components/options/gauge/labels_panel.tsx | 10 +- .../components/options/gauge/ranges_panel.tsx | 14 +- .../components/options/gauge/style_panel.tsx | 6 +- .../components/options/heatmap/index.tsx | 23 +- .../options/heatmap/labels_panel.tsx | 10 +- .../category_axis_panel.test.tsx.snap | 2 +- .../__snapshots__/label_options.test.tsx.snap | 2 +- .../value_axes_panel.test.tsx.snap | 2 +- .../metrics_axes/category_axis_panel.tsx | 6 +- .../options/metrics_axes/chart_options.tsx | 10 +- .../metrics_axes/custom_extents_options.tsx | 15 +- .../options/metrics_axes/label_options.tsx | 29 +- .../options/metrics_axes/line_options.tsx | 8 +- .../options/metrics_axes/series_panel.tsx | 4 +- .../options/metrics_axes/value_axes_panel.tsx | 8 +- .../metrics_axes/value_axis_options.tsx | 14 +- .../options/metrics_axes/y_extents.tsx | 20 +- .../public/components/options/pie.tsx | 12 +- .../options/point_series/grid_panel.tsx | 10 +- .../options/point_series/point_series.tsx | 8 +- .../options/point_series/threshold_panel.tsx | 40 +- .../vis_type_vislib/public/gauge.ts | 8 +- .../vis_type_vislib/public/goal.ts | 8 +- .../vis_type_vislib/public/heatmap.ts | 12 +- .../vis_type_vislib/public/histogram.ts | 14 +- .../vis_type_vislib/public/horizontal_bar.ts | 14 +- .../vis_type_vislib/public/index.ts | 14 + .../vis_type_vislib/public/legacy.ts | 14 - .../vis_type_vislib/public/legacy_imports.ts | 5 +- .../vis_type_vislib/public/line.ts | 14 +- .../vis_type_vislib/public/pie.ts | 10 +- .../vis_type_vislib/public/pie_fn.ts | 2 +- .../vis_type_vislib/public/plugin.ts | 4 - .../public/utils/collections.ts | 60 +-- .../public/utils/common_config.tsx | 6 +- .../vis_type_vislib/public/vis_controller.tsx | 6 +- .../public/vis_type_vislib_vis_fn.ts | 2 +- .../components}/_tooltip_formatter.js | 13 +- .../__tests__/components/heatmap_color.js | 3 - .../vislib/__tests__/components/labels.js | 74 ++-- .../__tests__/components}/positioning.js | 3 +- .../public/vislib/__tests__/index.js | 11 +- .../public/vislib/__tests__/lib/axis/axis.js | 60 ++- .../public/vislib/__tests__/lib/axis_title.js | 102 +++-- .../vislib/__tests__/lib/chart_title.js | 54 ++- .../public/vislib/__tests__/lib/data.js | 28 +- .../public/vislib/__tests__/lib/dispatch.js | 90 ++--- .../vislib/__tests__/lib/error_handler.js | 11 +- .../__tests__/lib/fixtures/_vis_fixture.js | 51 ++- .../vislib/__tests__/lib/handler/handler.js | 27 +- .../vislib/__tests__/lib/layout/layout.js | 28 +- .../__tests__/lib/layout/layout_types.js | 11 +- .../lib/layout/splits/column_chart/splits.js | 103 +++-- .../lib/layout/splits/gauge_chart/splits.js | 43 +- .../lib/layout/types/column_layout.js | 19 +- .../__tests__/lib/types/point_series.js | 7 +- .../public/vislib/__tests__/lib/vis_config.js | 40 +- .../public/vislib/__tests__/lib/x_axis.js | 54 ++- .../public/vislib/__tests__/lib/y_axis.js | 23 +- .../public/vislib/__tests__/vis.js | 38 +- .../__tests__/visualizations/area_chart.js | 73 ++-- .../vislib/__tests__/visualizations/chart.js | 42 +- .../__tests__/visualizations/column_chart.js | 86 ++-- .../__tests__/visualizations/gauge_chart.js | 24 +- .../__tests__/visualizations/heatmap_chart.js | 31 +- .../__tests__/visualizations/line_chart.js | 70 ++-- .../__tests__/visualizations/pie_chart.js | 90 ++--- .../__tests__/visualizations/time_marker.js | 41 +- .../__tests__/visualizations/vis_types.js | 13 +- .../vis_type_vislib/public/vislib/_index.scss | 3 + .../public/vislib}/_vislib_vis_type.scss | 3 + .../__snapshots__/legend.test.tsx.snap} | 0 .../vislib/components/legend/_index.scss | 1 + .../vislib/components/legend/_legend.scss} | 9 +- .../public/vislib/components/legend}/index.ts | 2 +- .../vislib/components/legend/legend.test.tsx} | 6 +- .../vislib/components/legend/legend.tsx} | 12 +- .../vislib/components/legend/legend_item.tsx} | 10 +- .../vislib/components/legend}/models.ts | 0 .../vislib/components/legend}/pie_utils.ts | 0 .../components/tooltip/_collect_branch.js | 0 .../tooltip/_collect_branch.test.js | 0 .../_hierarchical_tooltip_formatter.js | 93 +++++ .../vislib}/components/tooltip/_index.scss | 0 .../tooltip/_pointseries_tooltip_formatter.js | 79 ++++ .../vislib}/components/tooltip/_tooltip.scss | 3 +- .../vislib}/components/tooltip/index.js | 4 +- .../components/tooltip/position_tooltip.js | 0 .../vislib}/components/tooltip/tooltip.js | 11 +- .../public/vislib/lib/chart_title.js | 2 +- .../public/vislib/lib/types/point_series.js | 2 +- .../public/vislib/visualizations/_chart.js | 11 +- .../vislib/visualizations/point_series.js | 18 +- src/legacy/ui/public/vis/_index.scss | 2 - .../ui/public/vis/components/_index.scss | 1 - .../tooltip/_hierarchical_tooltip.html | 28 -- .../_hierarchical_tooltip_formatter.js | 82 ---- .../tooltip/_pointseries_tooltip.html | 12 - .../tooltip/_pointseries_tooltip_formatter.js | 89 ----- .../ui/public/vis/vis_types/_index.scss | 2 - .../translations/translations/ja-JP.json | 372 +++++++++--------- .../translations/translations/zh-CN.json | 372 +++++++++--------- yarn.lock | 5 + 124 files changed, 1415 insertions(+), 1707 deletions(-) rename src/legacy/{ui/public/vis/components/tooltip/__tests__ => core_plugins/vis_type_vislib/public/vislib/__tests__/components}/_tooltip_formatter.js (88%) rename src/legacy/{ui/public/vis/components/tooltip/__tests__ => core_plugins/vis_type_vislib/public/vislib/__tests__/components}/positioning.js (99%) rename src/legacy/{ui/public/vis/vis_types => core_plugins/vis_type_vislib/public/vislib}/_vislib_vis_type.scss (99%) rename src/legacy/{ui/public/vis/vis_types/vislib_vis_legend/__snapshots__/vislib_vis_legend.test.tsx.snap => core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap} (100%) create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_index.scss rename src/legacy/{ui/public/vis/vis_types/_vislib_vis_legend.scss => core_plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss} (85%) rename src/legacy/{ui/public/vis/vis_types/vislib_vis_legend => core_plugins/vis_type_vislib/public/vislib/components/legend}/index.ts (94%) rename src/legacy/{ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx => core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx} (96%) rename src/legacy/{ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.tsx => core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx} (93%) rename src/legacy/{ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend_item.tsx => core_plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx} (92%) rename src/legacy/{ui/public/vis/vis_types/vislib_vis_legend => core_plugins/vis_type_vislib/public/vislib/components/legend}/models.ts (100%) rename src/legacy/{ui/public/vis/vis_types/vislib_vis_legend => core_plugins/vis_type_vislib/public/vislib/components/legend}/pie_utils.ts (100%) rename src/legacy/{ui/public/vis => core_plugins/vis_type_vislib/public/vislib}/components/tooltip/_collect_branch.js (100%) rename src/legacy/{ui/public/vis => core_plugins/vis_type_vislib/public/vislib}/components/tooltip/_collect_branch.test.js (100%) create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js rename src/legacy/{ui/public/vis => core_plugins/vis_type_vislib/public/vislib}/components/tooltip/_index.scss (100%) create mode 100644 src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js rename src/legacy/{ui/public/vis => core_plugins/vis_type_vislib/public/vislib}/components/tooltip/_tooltip.scss (97%) rename src/legacy/{ui/public/vis => core_plugins/vis_type_vislib/public/vislib}/components/tooltip/index.js (80%) rename src/legacy/{ui/public/vis => core_plugins/vis_type_vislib/public/vislib}/components/tooltip/position_tooltip.js (100%) rename src/legacy/{ui/public/vis => core_plugins/vis_type_vislib/public/vislib}/components/tooltip/tooltip.js (97%) delete mode 100644 src/legacy/ui/public/vis/components/_index.scss delete mode 100644 src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip.html delete mode 100644 src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip_formatter.js delete mode 100644 src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip.html delete mode 100644 src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip_formatter.js delete mode 100644 src/legacy/ui/public/vis/vis_types/_index.scss diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ed5721e8756e88..7276a726fd6d1f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,7 @@ /src/legacy/core_plugins/kibana/public/home/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/legacy/core_plugins/metrics/ @elastic/kibana-app +/src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app /src/plugins/home/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app diff --git a/.i18nrc.json b/.i18nrc.json index 73acf92cda1491..907310b32e35cc 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -21,7 +21,6 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "kbnVislibVisTypes": "src/legacy/core_plugins/vis_type_vislib", "management": ["src/legacy/core_plugins/management", "src/plugins/management"], "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", @@ -41,6 +40,7 @@ "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud", "visTypeTimeseries": "src/legacy/core_plugins/vis_type_timeseries", "visTypeVega": "src/legacy/core_plugins/vis_type_vega", + "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", "visualizations": [ "src/plugins/visualizations", "src/legacy/core_plugins/visualizations" @@ -50,4 +50,4 @@ "src/legacy/ui/ui_render/ui_render_mixin.js" ], "translations": [] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 430ab9e1ba77d7..8b96fcc9d396e7 100644 --- a/package.json +++ b/package.json @@ -347,6 +347,7 @@ "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", + "@types/numeral": "^0.0.26", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", "@types/pngjs": "^3.3.2", diff --git a/renovate.json5 b/renovate.json5 index 5af62d0acef854..6764ed38ba4cf0 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -657,6 +657,14 @@ '@types/nodemailer', ], }, + { + groupSlug: 'numeral', + groupName: 'numeral related packages', + packageNames: [ + 'numeral', + '@types/numeral', + ], + }, { groupSlug: 'object-hash', groupName: 'object-hash related packages', diff --git a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx index 73fe07ec60102d..287fb87e735a25 100644 --- a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx +++ b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx @@ -24,11 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { FileLayerField, VectorLayer, ServiceSettings } from 'ui/vis/map/service_settings'; import { VisOptionsProps } from 'ui/vis/editors/default'; -import { - NumberInputOption, - SelectOption, - SwitchOption, -} from '../../../vis_type_vislib/public/components'; +import { NumberInputOption, SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { WmsOptions } from '../../../tile_map/public/components/wms_options'; import { RegionMapVisParams } from '../types'; diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx index e57cea8467d124..4ab9f95ee4c3c2 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx @@ -27,7 +27,7 @@ import { RangeOption, SelectOption, SwitchOption, -} from '../../../vis_type_vislib/public/components'; +} from '../../../vis_type_vislib/public'; import { WmsOptions } from './wms_options'; import { TileMapVisParams } from '../types'; import { MapTypes } from '../map_types'; diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx b/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx index 2989f6ce7ebd58..b81667400303d5 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TextInputOption } from '../../../vis_type_vislib/public/components'; +import { TextInputOption } from '../../../vis_type_vislib/public'; import { WMSOptions } from '../types'; interface WmsInternalOptions { diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx index d9dca5afd73776..a0b7a0a844f55f 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx @@ -25,7 +25,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { TmsLayer } from 'ui/vis/map/service_settings'; import { Vis } from 'ui/vis'; import { RegionMapVisParams } from '../../../region_map/public/types'; -import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public/components'; +import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { WmsInternalOptions } from './wms_internal_options'; import { WMSOptions, TileMapVisParams } from '../types'; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx b/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx index 125577815c207b..18852b549b1ed0 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx @@ -22,7 +22,7 @@ import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { VisOptionsProps } from 'ui/vis/editors/default'; -import { RangeOption, SwitchOption } from '../../vis_type_vislib/public/components'; +import { RangeOption, SwitchOption } from '../../vis_type_vislib/public'; import { MarkdownVisParams } from './types'; function SettingsOptions({ stateParams, setValue }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx index 032f66d92624cc..e144c055d80238 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -31,13 +31,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from '../legacy_imports'; import { + ColorModes, ColorRanges, ColorSchemaOptions, SwitchOption, RangeOption, SetColorSchemaOptionsValue, -} from '../../../vis_type_vislib/public/components'; -import { ColorModes } from '../../../vis_type_vislib/public/utils/collections'; +} from '../../../vis_type_vislib/public'; import { MetricVisParam, VisParams } from '../types'; import { SetColorRangeValue } from '../../../vis_type_vislib/public/components/common/color_ranges'; diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts index 45110ca1130030..4ebd8bf97c55a0 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts @@ -27,7 +27,7 @@ import { Render, Style, } from '../../../../plugins/expressions/public'; -import { ColorModes } from '../../vis_type_vislib/public/utils/collections'; +import { ColorModes } from '../../vis_type_vislib/public'; import { visType, DimensionsVisParam, VisParams } from './types'; export type Context = KibanaDatatable; diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts index 0d9019ee0579c1..ee7ead0b7331b4 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts @@ -21,8 +21,8 @@ import { i18n } from '@kbn/i18n'; import { MetricVisComponent } from './components/metric_vis_component'; import { MetricVisOptions } from './components/metric_vis_options'; +import { ColorModes } from '../../vis_type_vislib/public'; import { Schemas, AggGroupNames, colorSchemas, ColorSchemas } from './legacy_imports'; -import { ColorModes } from '../../vis_type_vislib/public/utils/collections'; export const metricVisTypeDefinition = { name: 'metric', diff --git a/src/legacy/core_plugins/vis_type_metric/public/types.ts b/src/legacy/core_plugins/vis_type_metric/public/types.ts index 71c1c12b4f8f02..34cb1b209a3aed 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/types.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/types.ts @@ -20,8 +20,7 @@ import { ColorSchemas } from './legacy_imports'; import { Range } from '../../../../plugins/expressions/public'; import { SchemaConfig } from '../../visualizations/public'; -import { ColorModes } from '../../vis_type_vislib/public/utils/collections'; -import { Labels, Style } from '../../vis_type_vislib/public/types'; +import { ColorModes, Labels, Style } from '../../vis_type_vislib/public'; export const visType = 'metric'; diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index 33d7480de5a8e3..529439a8006823 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -24,11 +24,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { tabifyGetColumns, VisOptionsProps } from '../legacy_imports'; -import { - NumberInputOption, - SwitchOption, - SelectOption, -} from '../../../vis_type_vislib/public/components/common'; +import { NumberInputOption, SwitchOption, SelectOption } from '../../../vis_type_vislib/public'; import { TableVisParams } from '../types'; import { totalAggregations, isAggConfigNumeric } from './utils'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index c500b5d888b053..76117c8b6b3987 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { ValidatedDualRange } from 'ui/validated_range'; import { VisOptionsProps } from 'ui/vis/editors/default'; -import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public/components'; +import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { TagCloudVisParams } from '../types'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/area.ts b/src/legacy/core_plugins/vis_type_vislib/public/area.ts index 9484ddc16fe625..a96fb19a9321ca 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/area.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/area.ts @@ -42,9 +42,9 @@ import { KbnVislibVisTypesDependencies } from './plugin'; export const createAreaVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({ name: 'area', - title: i18n.translate('kbnVislibVisTypes.area.areaTitle', { defaultMessage: 'Area' }), + title: i18n.translate('visTypeVislib.area.areaTitle', { defaultMessage: 'Area' }), icon: 'visArea', - description: i18n.translate('kbnVislibVisTypes.area.areaDescription', { + description: i18n.translate('visTypeVislib.area.areaDescription', { defaultMessage: 'Emphasize the quantity beneath a line chart', }), visualization: createVislibVisController(deps), @@ -136,7 +136,7 @@ export const createAreaVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Metrics, name: 'metric', - title: i18n.translate('kbnVislibVisTypes.area.metricsTitle', { + title: i18n.translate('visTypeVislib.area.metricsTitle', { defaultMessage: 'Y-axis', }), aggFilter: ['!geo_centroid', '!geo_bounds'], @@ -146,7 +146,7 @@ export const createAreaVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Metrics, name: 'radius', - title: i18n.translate('kbnVislibVisTypes.area.radiusTitle', { + title: i18n.translate('visTypeVislib.area.radiusTitle', { defaultMessage: 'Dot size', }), min: 0, @@ -156,7 +156,7 @@ export const createAreaVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'segment', - title: i18n.translate('kbnVislibVisTypes.area.segmentTitle', { + title: i18n.translate('visTypeVislib.area.segmentTitle', { defaultMessage: 'X-axis', }), min: 0, @@ -166,7 +166,7 @@ export const createAreaVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'group', - title: i18n.translate('kbnVislibVisTypes.area.groupTitle', { + title: i18n.translate('visTypeVislib.area.groupTitle', { defaultMessage: 'Split series', }), min: 0, @@ -176,7 +176,7 @@ export const createAreaVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'split', - title: i18n.translate('kbnVislibVisTypes.area.splitTitle', { + title: i18n.translate('visTypeVislib.area.splitTitle', { defaultMessage: 'Split chart', }), min: 0, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx index 81174d63060e5c..229945621fe76d 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx @@ -37,7 +37,7 @@ function BasicOptions({ return ( <> ({ setValue={setValue} /> @@ -79,11 +79,11 @@ function ColorSchemaOptions({ disabled={disabled} helpText={ showHelpText && - i18n.translate('kbnVislibVisTypes.controls.colorSchema.howToChangeColorsDescription', { + i18n.translate('visTypeVislib.controls.colorSchema.howToChangeColorsDescription', { defaultMessage: 'Individual colors can be changed in the legend.', }) } - label={i18n.translate('kbnVislibVisTypes.controls.colorSchema.colorSchemaLabel', { + label={i18n.translate('visTypeVislib.controls.colorSchema.colorSchemaLabel', { defaultMessage: 'Color schema', })} labelAppend={isCustomColors && resetColorsButton} @@ -95,7 +95,7 @@ function ColorSchemaOptions({ ({ const [stateValue, setStateValue] = useState(value); const [isValidState, setIsValidState] = useState(true); - const error = i18n.translate('kbnVislibVisTypes.controls.rangeErrorMessage', { + const error = i18n.translate('visTypeVislib.controls.rangeErrorMessage', { defaultMessage: 'Values must be on or between {min} and {max}', values: { min, max }, }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/truncate_labels.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/common/truncate_labels.tsx index 81772107bc729c..2f0cb701848d0c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/truncate_labels.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/truncate_labels.tsx @@ -33,7 +33,7 @@ function TruncateLabelsOption({ disabled, value = null, setValue }: TruncateLabe return (

@@ -39,7 +39,7 @@ function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInter

@@ -76,10 +76,10 @@ function RangesPanel({

@@ -44,7 +44,7 @@ function StylePanel({ aggs, setGaugeValue, stateParams, vis }: GaugeOptionsInter ) {

@@ -78,13 +78,13 @@ function HeatmapOptions(props: VisOptionsProps) { ) {

@@ -114,7 +114,7 @@ function HeatmapOptions(props: VisOptionsProps) { ) { /> ) { ) { data-test-subj="heatmapColorsNumber" disabled={stateParams.setColorRange} isInvalid={isColorsNumberInvalid} - label={i18n.translate('kbnVislibVisTypes.controls.heatmapOptions.colorsNumberLabel', { + label={i18n.translate('visTypeVislib.controls.heatmapOptions.colorsNumberLabel', { defaultMessage: 'Number of colors', })} max={10} @@ -160,7 +159,7 @@ function HeatmapOptions(props: VisOptionsProps) {

@@ -73,7 +73,7 @@ function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap index d88654cfdc0c43..124b7d7004c0df 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap @@ -10,7 +10,7 @@ exports[`CategoryAxisPanel component should init with the default set of props 1

diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap index 027c074176ef85..8972cdbd82fc3a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap @@ -11,7 +11,7 @@ exports[`LabelOptions component should init with the default set of props 1`] =

diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap index 8d20765fe35918..f589a69eecbc3b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap @@ -18,7 +18,7 @@ exports[`ValueAxesPanel component should init with the default set of props 1`]

diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx index b83508f3f08964..a698d70d972324 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx @@ -62,7 +62,7 @@ function CategoryAxisPanel(props: CategoryAxisPanelProps) {

@@ -70,7 +70,7 @@ function CategoryAxisPanel(props: CategoryAxisPanelProps) {

@@ -73,12 +73,9 @@ function LabelOptions({ stateParams, setValue, axis, axesName, index }: LabelOpt

@@ -57,7 +57,7 @@ function SeriesPanel(props: SeriesPanelProps) { buttonContent={chart.data.label} buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" aria-label={i18n.translate( - 'kbnVislibVisTypes.controls.pointSeries.seriesAccordionAriaLabel', + 'visTypeVislib.controls.pointSeries.seriesAccordionAriaLabel', { defaultMessage: 'Toggle {agg} options', values: { agg: chart.data.label }, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx index eb0ab4333af599..b94f5ebbcce44f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx @@ -60,7 +60,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { const removeButtonTooltip = useMemo( () => - i18n.translate('kbnVislibVisTypes.controls.pointSeries.valueAxes.removeButtonTooltip', { + i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.removeButtonTooltip', { defaultMessage: 'Remove Y-axis', }), [] @@ -83,7 +83,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { const addButtonTooltip = useMemo( () => - i18n.translate('kbnVislibVisTypes.controls.pointSeries.valueAxes.addButtonTooltip', { + i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.addButtonTooltip', { defaultMessage: 'Add Y-axis', }), [] @@ -111,7 +111,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) {

@@ -142,7 +142,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { buttonClassName="eui-textTruncate" buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" aria-label={i18n.translate( - 'kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleOptionsAriaLabel', + 'visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel', { defaultMessage: 'Toggle {axisName} options', values: { axisName: axis.name }, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx index b4ea4cb42ee60c..5daea193b06c1b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx @@ -119,7 +119,7 @@ function ValueAxisOptions(props: ValueAxisOptionsParams) { return ( <> ) {

) {

) { setValue={setLabels} /> ) { setValue={setLabels} />

@@ -72,14 +72,14 @@ function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps)

@@ -46,7 +46,7 @@ function PointSeriesOptions(props: ValidationVisOptionsProps) {vis.hasSchemaAgg('segment', 'date_histogram') ? ( ) /> ) : ( ) {vis.type.type === 'histogram' && (

@@ -63,7 +63,7 @@ function ThresholdPanel({ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts b/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts index 5dcc8ad16918d0..f235ed4bb04e40 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts @@ -56,9 +56,9 @@ export interface GaugeVisParams { export const createGaugeVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({ name: 'gauge', - title: i18n.translate('kbnVislibVisTypes.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), + title: i18n.translate('visTypeVislib.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), icon: 'visGauge', - description: i18n.translate('kbnVislibVisTypes.gauge.gaugeDescription', { + description: i18n.translate('visTypeVislib.gauge.gaugeDescription', { defaultMessage: "Gauges indicate the status of a metric. Use it to show how a metric's value relates to reference threshold values.", }), @@ -116,7 +116,7 @@ export const createGaugeVisTypeDefinition = (deps: KbnVislibVisTypesDependencies { group: AggGroupNames.Metrics, name: 'metric', - title: i18n.translate('kbnVislibVisTypes.gauge.metricTitle', { defaultMessage: 'Metric' }), + title: i18n.translate('visTypeVislib.gauge.metricTitle', { defaultMessage: 'Metric' }), min: 1, aggFilter: [ '!std_dev', @@ -134,7 +134,7 @@ export const createGaugeVisTypeDefinition = (deps: KbnVislibVisTypesDependencies { group: AggGroupNames.Buckets, name: 'group', - title: i18n.translate('kbnVislibVisTypes.gauge.groupTitle', { + title: i18n.translate('visTypeVislib.gauge.groupTitle', { defaultMessage: 'Split group', }), min: 0, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts b/src/legacy/core_plugins/vis_type_vislib/public/goal.ts index 302d5f6393ef90..94262629669bec 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/goal.ts @@ -27,9 +27,9 @@ import { KbnVislibVisTypesDependencies } from './plugin'; export const createGoalVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({ name: 'goal', - title: i18n.translate('kbnVislibVisTypes.goal.goalTitle', { defaultMessage: 'Goal' }), + title: i18n.translate('visTypeVislib.goal.goalTitle', { defaultMessage: 'Goal' }), icon: 'visGoal', - description: i18n.translate('kbnVislibVisTypes.goal.goalDescription', { + description: i18n.translate('visTypeVislib.goal.goalDescription', { defaultMessage: 'A goal chart indicates how close you are to your final goal.', }), visualization: createVislibVisController(deps), @@ -80,7 +80,7 @@ export const createGoalVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Metrics, name: 'metric', - title: i18n.translate('kbnVislibVisTypes.goal.metricTitle', { defaultMessage: 'Metric' }), + title: i18n.translate('visTypeVislib.goal.metricTitle', { defaultMessage: 'Metric' }), min: 1, aggFilter: [ '!std_dev', @@ -98,7 +98,7 @@ export const createGoalVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'group', - title: i18n.translate('kbnVislibVisTypes.goal.groupTitle', { + title: i18n.translate('visTypeVislib.goal.groupTitle', { defaultMessage: 'Split group', }), min: 0, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts b/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts index eb5f84b409838a..470e978027ca2b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts @@ -41,9 +41,9 @@ export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaVislibP export const createHeatmapVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({ name: 'heatmap', - title: i18n.translate('kbnVislibVisTypes.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), + title: i18n.translate('visTypeVislib.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), icon: 'visHeatmap', - description: i18n.translate('kbnVislibVisTypes.heatmap.heatmapDescription', { + description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix', }), visualization: createVislibVisController(deps), @@ -90,7 +90,7 @@ export const createHeatmapVisTypeDefinition = (deps: KbnVislibVisTypesDependenci { group: AggGroupNames.Metrics, name: 'metric', - title: i18n.translate('kbnVislibVisTypes.heatmap.metricTitle', { defaultMessage: 'Value' }), + title: i18n.translate('visTypeVislib.heatmap.metricTitle', { defaultMessage: 'Value' }), min: 1, max: 1, aggFilter: [ @@ -109,7 +109,7 @@ export const createHeatmapVisTypeDefinition = (deps: KbnVislibVisTypesDependenci { group: AggGroupNames.Buckets, name: 'segment', - title: i18n.translate('kbnVislibVisTypes.heatmap.segmentTitle', { + title: i18n.translate('visTypeVislib.heatmap.segmentTitle', { defaultMessage: 'X-axis', }), min: 0, @@ -119,7 +119,7 @@ export const createHeatmapVisTypeDefinition = (deps: KbnVislibVisTypesDependenci { group: AggGroupNames.Buckets, name: 'group', - title: i18n.translate('kbnVislibVisTypes.heatmap.groupTitle', { defaultMessage: 'Y-axis' }), + title: i18n.translate('visTypeVislib.heatmap.groupTitle', { defaultMessage: 'Y-axis' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], @@ -127,7 +127,7 @@ export const createHeatmapVisTypeDefinition = (deps: KbnVislibVisTypesDependenci { group: AggGroupNames.Buckets, name: 'split', - title: i18n.translate('kbnVislibVisTypes.heatmap.splitTitle', { + title: i18n.translate('visTypeVislib.heatmap.splitTitle', { defaultMessage: 'Split chart', }), min: 0, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts b/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts index f92875a62cfd7d..490b557b7bafb9 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts @@ -42,11 +42,11 @@ import { KbnVislibVisTypesDependencies } from './plugin'; export const createHistogramVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({ name: 'histogram', - title: i18n.translate('kbnVislibVisTypes.histogram.histogramTitle', { + title: i18n.translate('visTypeVislib.histogram.histogramTitle', { defaultMessage: 'Vertical Bar', }), icon: 'visBarVertical', - description: i18n.translate('kbnVislibVisTypes.histogram.histogramDescription', { + description: i18n.translate('visTypeVislib.histogram.histogramDescription', { defaultMessage: 'Assign a continuous variable to each axis', }), visualization: createVislibVisController(deps), @@ -139,7 +139,7 @@ export const createHistogramVisTypeDefinition = (deps: KbnVislibVisTypesDependen { group: AggGroupNames.Metrics, name: 'metric', - title: i18n.translate('kbnVislibVisTypes.histogram.metricTitle', { + title: i18n.translate('visTypeVislib.histogram.metricTitle', { defaultMessage: 'Y-axis', }), min: 1, @@ -149,7 +149,7 @@ export const createHistogramVisTypeDefinition = (deps: KbnVislibVisTypesDependen { group: AggGroupNames.Metrics, name: 'radius', - title: i18n.translate('kbnVislibVisTypes.histogram.radiusTitle', { + title: i18n.translate('visTypeVislib.histogram.radiusTitle', { defaultMessage: 'Dot size', }), min: 0, @@ -159,7 +159,7 @@ export const createHistogramVisTypeDefinition = (deps: KbnVislibVisTypesDependen { group: AggGroupNames.Buckets, name: 'segment', - title: i18n.translate('kbnVislibVisTypes.histogram.segmentTitle', { + title: i18n.translate('visTypeVislib.histogram.segmentTitle', { defaultMessage: 'X-axis', }), min: 0, @@ -169,7 +169,7 @@ export const createHistogramVisTypeDefinition = (deps: KbnVislibVisTypesDependen { group: AggGroupNames.Buckets, name: 'group', - title: i18n.translate('kbnVislibVisTypes.histogram.groupTitle', { + title: i18n.translate('visTypeVislib.histogram.groupTitle', { defaultMessage: 'Split series', }), min: 0, @@ -179,7 +179,7 @@ export const createHistogramVisTypeDefinition = (deps: KbnVislibVisTypesDependen { group: AggGroupNames.Buckets, name: 'split', - title: i18n.translate('kbnVislibVisTypes.histogram.splitTitle', { + title: i18n.translate('visTypeVislib.histogram.splitTitle', { defaultMessage: 'Split chart', }), min: 0, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts index ada0c6b44ff701..e8d51fe037a63e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts @@ -42,11 +42,11 @@ import { KbnVislibVisTypesDependencies } from './plugin'; export const createHorizontalBarVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({ name: 'horizontal_bar', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.horizontalBarTitle', { + title: i18n.translate('visTypeVislib.horizontalBar.horizontalBarTitle', { defaultMessage: 'Horizontal Bar', }), icon: 'visBarHorizontal', - description: i18n.translate('kbnVislibVisTypes.horizontalBar.horizontalBarDescription', { + description: i18n.translate('visTypeVislib.horizontalBar.horizontalBarDescription', { defaultMessage: 'Assign a continuous variable to each axis', }), visualization: createVislibVisController(deps), @@ -138,7 +138,7 @@ export const createHorizontalBarVisTypeDefinition = (deps: KbnVislibVisTypesDepe { group: AggGroupNames.Metrics, name: 'metric', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.metricTitle', { + title: i18n.translate('visTypeVislib.horizontalBar.metricTitle', { defaultMessage: 'Y-axis', }), min: 1, @@ -148,7 +148,7 @@ export const createHorizontalBarVisTypeDefinition = (deps: KbnVislibVisTypesDepe { group: AggGroupNames.Metrics, name: 'radius', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.radiusTitle', { + title: i18n.translate('visTypeVislib.horizontalBar.radiusTitle', { defaultMessage: 'Dot size', }), min: 0, @@ -158,7 +158,7 @@ export const createHorizontalBarVisTypeDefinition = (deps: KbnVislibVisTypesDepe { group: AggGroupNames.Buckets, name: 'segment', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.segmentTitle', { + title: i18n.translate('visTypeVislib.horizontalBar.segmentTitle', { defaultMessage: 'X-axis', }), min: 0, @@ -168,7 +168,7 @@ export const createHorizontalBarVisTypeDefinition = (deps: KbnVislibVisTypesDepe { group: AggGroupNames.Buckets, name: 'group', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.groupTitle', { + title: i18n.translate('visTypeVislib.horizontalBar.groupTitle', { defaultMessage: 'Split series', }), min: 0, @@ -178,7 +178,7 @@ export const createHorizontalBarVisTypeDefinition = (deps: KbnVislibVisTypesDepe { group: AggGroupNames.Buckets, name: 'split', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.splitTitle', { + title: i18n.translate('visTypeVislib.horizontalBar.splitTitle', { defaultMessage: 'Split chart', }), min: 0, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/index.ts b/src/legacy/core_plugins/vis_type_vislib/public/index.ts index 3b4bcb6bc3a7e2..80c078de1a10b4 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/index.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/index.ts @@ -24,4 +24,18 @@ export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } +export { + BasicOptions, + RangeOption, + ColorRanges, + SelectOption, + SetColorSchemaOptionsValue, + ColorSchemaOptions, + NumberInputOption, + SwitchOption, + TextInputOption, +} from './components'; + export { ColorModes } from './utils/collections'; + +export * from './types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts index 3d4cf55adc5e0c..775cf63b96f68a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts @@ -21,16 +21,6 @@ import { npSetup, npStart } from 'ui/new_platform'; import { PluginInitializerContext } from 'kibana/public'; /* eslint-disable prettier/prettier */ -import { - initializeHierarchicalTooltipFormatter, - getHierarchicalTooltipFormatter, - // @ts-ignore -} from 'ui/vis/components/tooltip/_hierarchical_tooltip_formatter'; -import { - initializePointSeriesTooltipFormatter, - getPointSeriesTooltipFormatter, - // @ts-ignore -} from 'ui/vis/components/tooltip/_pointseries_tooltip_formatter'; import { vislibSeriesResponseHandlerProvider, vislibSlicesResponseHandlerProvider, @@ -53,10 +43,6 @@ const setupPlugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: visualizationsSetup, __LEGACY: { - initializeHierarchicalTooltipFormatter, - getHierarchicalTooltipFormatter, - initializePointSeriesTooltipFormatter, - getPointSeriesTooltipFormatter, vislibSeriesResponseHandlerProvider, vislibSlicesResponseHandlerProvider, vislibColor, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts index 2970942f221e80..6b10fb0d412234 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts @@ -23,13 +23,10 @@ export { RangeValues, RangesParamEditor } from 'ui/vis/editors/default/controls/ export { ColorSchema, ColorSchemas, colorSchemas, getHeatmapColors } from 'ui/color_maps'; export { AggConfig, Vis, VisParams } from 'ui/vis'; export { AggType } from 'ui/agg_types'; -export { CUSTOM_LEGEND_VIS_TYPES, VisLegend } from 'ui/vis/vis_types/vislib_vis_legend'; -// @ts-ignore -export { Tooltip } from 'ui/vis/components/tooltip'; // @ts-ignore export { SimpleEmitter } from 'ui/utils/simple_emitter'; // @ts-ignore export { Binder } from 'ui/binder'; -export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; +export { getFormat, getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; // @ts-ignore export { tabifyAggResponse } from 'ui/agg_response/tabify'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/line.ts b/src/legacy/core_plugins/vis_type_vislib/public/line.ts index 35a059fadddcb9..d3f2bc1bb684ca 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/line.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/line.ts @@ -42,9 +42,9 @@ import { KbnVislibVisTypesDependencies } from './plugin'; export const createLineVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({ name: 'line', - title: i18n.translate('kbnVislibVisTypes.line.lineTitle', { defaultMessage: 'Line' }), + title: i18n.translate('visTypeVislib.line.lineTitle', { defaultMessage: 'Line' }), icon: 'visLine', - description: i18n.translate('kbnVislibVisTypes.line.lineDescription', { + description: i18n.translate('visTypeVislib.line.lineDescription', { defaultMessage: 'Emphasize trends', }), visualization: createVislibVisController(deps), @@ -136,7 +136,7 @@ export const createLineVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Metrics, name: 'metric', - title: i18n.translate('kbnVislibVisTypes.line.metricTitle', { defaultMessage: 'Y-axis' }), + title: i18n.translate('visTypeVislib.line.metricTitle', { defaultMessage: 'Y-axis' }), min: 1, aggFilter: ['!geo_centroid', '!geo_bounds'], defaults: [{ schema: 'metric', type: 'count' }], @@ -144,7 +144,7 @@ export const createLineVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Metrics, name: 'radius', - title: i18n.translate('kbnVislibVisTypes.line.radiusTitle', { defaultMessage: 'Dot size' }), + title: i18n.translate('visTypeVislib.line.radiusTitle', { defaultMessage: 'Dot size' }), min: 0, max: 1, aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'], @@ -152,7 +152,7 @@ export const createLineVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'segment', - title: i18n.translate('kbnVislibVisTypes.line.segmentTitle', { defaultMessage: 'X-axis' }), + title: i18n.translate('visTypeVislib.line.segmentTitle', { defaultMessage: 'X-axis' }), min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], @@ -160,7 +160,7 @@ export const createLineVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'group', - title: i18n.translate('kbnVislibVisTypes.line.groupTitle', { + title: i18n.translate('visTypeVislib.line.groupTitle', { defaultMessage: 'Split series', }), min: 0, @@ -170,7 +170,7 @@ export const createLineVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'split', - title: i18n.translate('kbnVislibVisTypes.line.splitTitle', { + title: i18n.translate('visTypeVislib.line.splitTitle', { defaultMessage: 'Split chart', }), min: 0, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts b/src/legacy/core_plugins/vis_type_vislib/public/pie.ts index 32307b7a117a1c..3c30030ca45f5b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/pie.ts @@ -40,9 +40,9 @@ export interface PieVisParams extends CommonVislibParams { export const createPieVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) => ({ name: 'pie', - title: i18n.translate('kbnVislibVisTypes.pie.pieTitle', { defaultMessage: 'Pie' }), + title: i18n.translate('visTypeVislib.pie.pieTitle', { defaultMessage: 'Pie' }), icon: 'visPie', - description: i18n.translate('kbnVislibVisTypes.pie.pieDescription', { + description: i18n.translate('visTypeVislib.pie.pieDescription', { defaultMessage: 'Compare parts of a whole', }), visualization: createVislibVisController(deps), @@ -70,7 +70,7 @@ export const createPieVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Metrics, name: 'metric', - title: i18n.translate('kbnVislibVisTypes.pie.metricTitle', { + title: i18n.translate('visTypeVislib.pie.metricTitle', { defaultMessage: 'Slice size', }), min: 1, @@ -81,7 +81,7 @@ export const createPieVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'segment', - title: i18n.translate('kbnVislibVisTypes.pie.segmentTitle', { + title: i18n.translate('visTypeVislib.pie.segmentTitle', { defaultMessage: 'Split slices', }), min: 0, @@ -91,7 +91,7 @@ export const createPieVisTypeDefinition = (deps: KbnVislibVisTypesDependencies) { group: AggGroupNames.Buckets, name: 'split', - title: i18n.translate('kbnVislibVisTypes.pie.splitTitle', { + title: i18n.translate('visTypeVislib.pie.splitTitle', { defaultMessage: 'Split chart', }), mustBeFirst: true, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts b/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts index 4b536caedb1212..af9842fa94fdab 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts @@ -53,7 +53,7 @@ export const createPieVisFn = (deps: KbnVislibVisTypesDependencies) => (): Expre context: { types: ['kibana_datatable'], }, - help: i18n.translate('kbnVislibVisTypes.functions.pie.help', { + help: i18n.translate('visTypeVislib.functions.pie.help', { defaultMessage: 'Pie visualization', }), args: { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index a2e8512b2201b2..0ab2b2120382b1 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -46,10 +46,6 @@ type ResponseHandlerProvider = () => { type KbnVislibVisTypesCoreSetup = CoreSetup; export interface LegacyDependencies { - initializeHierarchicalTooltipFormatter: () => Promise; - getHierarchicalTooltipFormatter: () => Promise; - initializePointSeriesTooltipFormatter: () => void; - getPointSeriesTooltipFormatter: () => void; vislibSeriesResponseHandlerProvider: ResponseHandlerProvider; vislibSlicesResponseHandlerProvider: ResponseHandlerProvider; vislibColor: (colors: Array, mappings: any) => (value: any) => any; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts b/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts index 810ddeea738341..d8f9fbf7d756ae 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts @@ -31,25 +31,25 @@ export type Positions = $Values; const getPositions = () => [ { - text: i18n.translate('kbnVislibVisTypes.legendPositions.topText', { + text: i18n.translate('visTypeVislib.legendPositions.topText', { defaultMessage: 'Top', }), value: Positions.TOP, }, { - text: i18n.translate('kbnVislibVisTypes.legendPositions.leftText', { + text: i18n.translate('visTypeVislib.legendPositions.leftText', { defaultMessage: 'Left', }), value: Positions.LEFT, }, { - text: i18n.translate('kbnVislibVisTypes.legendPositions.rightText', { + text: i18n.translate('visTypeVislib.legendPositions.rightText', { defaultMessage: 'Right', }), value: Positions.RIGHT, }, { - text: i18n.translate('kbnVislibVisTypes.legendPositions.bottomText', { + text: i18n.translate('visTypeVislib.legendPositions.bottomText', { defaultMessage: 'Bottom', }), value: Positions.BOTTOM, @@ -65,19 +65,19 @@ export type ChartTypes = $Values; const getChartTypes = () => [ { - text: i18n.translate('kbnVislibVisTypes.chartTypes.lineText', { + text: i18n.translate('visTypeVislib.chartTypes.lineText', { defaultMessage: 'Line', }), value: ChartTypes.LINE, }, { - text: i18n.translate('kbnVislibVisTypes.chartTypes.areaText', { + text: i18n.translate('visTypeVislib.chartTypes.areaText', { defaultMessage: 'Area', }), value: ChartTypes.AREA, }, { - text: i18n.translate('kbnVislibVisTypes.chartTypes.barText', { + text: i18n.translate('visTypeVislib.chartTypes.barText', { defaultMessage: 'Bar', }), value: ChartTypes.HISTOGRAM, @@ -92,13 +92,13 @@ export type ChartModes = $Values; const getChartModes = () => [ { - text: i18n.translate('kbnVislibVisTypes.chartModes.normalText', { + text: i18n.translate('visTypeVislib.chartModes.normalText', { defaultMessage: 'Normal', }), value: ChartModes.NORMAL, }, { - text: i18n.translate('kbnVislibVisTypes.chartModes.stackedText', { + text: i18n.translate('visTypeVislib.chartModes.stackedText', { defaultMessage: 'Stacked', }), value: ChartModes.STACKED, @@ -114,19 +114,19 @@ export type InterpolationModes = $Values; const getInterpolationModes = () => [ { - text: i18n.translate('kbnVislibVisTypes.interpolationModes.straightText', { + text: i18n.translate('visTypeVislib.interpolationModes.straightText', { defaultMessage: 'Straight', }), value: InterpolationModes.LINEAR, }, { - text: i18n.translate('kbnVislibVisTypes.interpolationModes.smoothedText', { + text: i18n.translate('visTypeVislib.interpolationModes.smoothedText', { defaultMessage: 'Smoothed', }), value: InterpolationModes.CARDINAL, }, { - text: i18n.translate('kbnVislibVisTypes.interpolationModes.steppedText', { + text: i18n.translate('visTypeVislib.interpolationModes.steppedText', { defaultMessage: 'Stepped', }), value: InterpolationModes.STEP_AFTER, @@ -148,19 +148,19 @@ export type ScaleTypes = $Values; const getScaleTypes = () => [ { - text: i18n.translate('kbnVislibVisTypes.scaleTypes.linearText', { + text: i18n.translate('visTypeVislib.scaleTypes.linearText', { defaultMessage: 'Linear', }), value: ScaleTypes.LINEAR, }, { - text: i18n.translate('kbnVislibVisTypes.scaleTypes.logText', { + text: i18n.translate('visTypeVislib.scaleTypes.logText', { defaultMessage: 'Log', }), value: ScaleTypes.LOG, }, { - text: i18n.translate('kbnVislibVisTypes.scaleTypes.squareRootText', { + text: i18n.translate('visTypeVislib.scaleTypes.squareRootText', { defaultMessage: 'Square root', }), value: ScaleTypes.SQUARE_ROOT, @@ -177,25 +177,25 @@ export type AxisModes = $Values; const getAxisModes = () => [ { - text: i18n.translate('kbnVislibVisTypes.axisModes.normalText', { + text: i18n.translate('visTypeVislib.axisModes.normalText', { defaultMessage: 'Normal', }), value: AxisModes.NORMAL, }, { - text: i18n.translate('kbnVislibVisTypes.axisModes.percentageText', { + text: i18n.translate('visTypeVislib.axisModes.percentageText', { defaultMessage: 'Percentage', }), value: AxisModes.PERCENTAGE, }, { - text: i18n.translate('kbnVislibVisTypes.axisModes.wiggleText', { + text: i18n.translate('visTypeVislib.axisModes.wiggleText', { defaultMessage: 'Wiggle', }), value: AxisModes.WIGGLE, }, { - text: i18n.translate('kbnVislibVisTypes.axisModes.silhouetteText', { + text: i18n.translate('visTypeVislib.axisModes.silhouetteText', { defaultMessage: 'Silhouette', }), value: AxisModes.SILHOUETTE, @@ -219,19 +219,19 @@ export type ThresholdLineStyles = $Values; const getThresholdLineStyles = () => [ { value: ThresholdLineStyles.FULL, - text: i18n.translate('kbnVislibVisTypes.thresholdLine.style.fullText', { + text: i18n.translate('visTypeVislib.thresholdLine.style.fullText', { defaultMessage: 'Full', }), }, { value: ThresholdLineStyles.DASHED, - text: i18n.translate('kbnVislibVisTypes.thresholdLine.style.dashedText', { + text: i18n.translate('visTypeVislib.thresholdLine.style.dashedText', { defaultMessage: 'Dashed', }), }, { value: ThresholdLineStyles.DOT_DASHED, - text: i18n.translate('kbnVislibVisTypes.thresholdLine.style.dotdashedText', { + text: i18n.translate('visTypeVislib.thresholdLine.style.dotdashedText', { defaultMessage: 'Dot-dashed', }), }, @@ -239,19 +239,19 @@ const getThresholdLineStyles = () => [ const getRotateOptions = () => [ { - text: i18n.translate('kbnVislibVisTypes.categoryAxis.rotate.horizontalText', { + text: i18n.translate('visTypeVislib.categoryAxis.rotate.horizontalText', { defaultMessage: 'Horizontal', }), value: Rotates.HORIZONTAL, }, { - text: i18n.translate('kbnVislibVisTypes.categoryAxis.rotate.verticalText', { + text: i18n.translate('visTypeVislib.categoryAxis.rotate.verticalText', { defaultMessage: 'Vertical', }), value: Rotates.VERTICAL, }, { - text: i18n.translate('kbnVislibVisTypes.categoryAxis.rotate.angledText', { + text: i18n.translate('visTypeVislib.categoryAxis.rotate.angledText', { defaultMessage: 'Angled', }), value: Rotates.ANGLED, @@ -273,13 +273,13 @@ export type ColorModes = $Values; const getGaugeTypes = () => [ { - text: i18n.translate('kbnVislibVisTypes.gauge.gaugeTypes.arcText', { + text: i18n.translate('visTypeVislib.gauge.gaugeTypes.arcText', { defaultMessage: 'Arc', }), value: GaugeTypes.ARC, }, { - text: i18n.translate('kbnVislibVisTypes.gauge.gaugeTypes.circleText', { + text: i18n.translate('visTypeVislib.gauge.gaugeTypes.circleText', { defaultMessage: 'Circle', }), value: GaugeTypes.CIRCLE, @@ -295,19 +295,19 @@ export type Alignments = $Values; const getAlignments = () => [ { - text: i18n.translate('kbnVislibVisTypes.gauge.alignmentAutomaticTitle', { + text: i18n.translate('visTypeVislib.gauge.alignmentAutomaticTitle', { defaultMessage: 'Automatic', }), value: Alignments.AUTOMATIC, }, { - text: i18n.translate('kbnVislibVisTypes.gauge.alignmentHorizontalTitle', { + text: i18n.translate('visTypeVislib.gauge.alignmentHorizontalTitle', { defaultMessage: 'Horizontal', }), value: Alignments.HORIZONTAL, }, { - text: i18n.translate('kbnVislibVisTypes.gauge.alignmentVerticalTitle', { + text: i18n.translate('visTypeVislib.gauge.alignmentVerticalTitle', { defaultMessage: 'Vertical', }), value: Alignments.VERTICAL, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx b/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx index adb93ca8011b2e..e2f6ba0d8b5620 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx @@ -29,7 +29,7 @@ function getAreaOptionTabs() { return [ { name: 'advanced', - title: i18n.translate('kbnVislibVisTypes.area.tabs.metricsAxesTitle', { + title: i18n.translate('visTypeVislib.area.tabs.metricsAxesTitle', { defaultMessage: 'Metrics & axes', }), editor: (props: VisOptionsProps) => ( @@ -38,7 +38,7 @@ function getAreaOptionTabs() { }, { name: 'options', - title: i18n.translate('kbnVislibVisTypes.area.tabs.panelSettingsTitle', { + title: i18n.translate('visTypeVislib.area.tabs.panelSettingsTitle', { defaultMessage: 'Panel settings', }), editor: (props: VisOptionsProps) => ( @@ -48,7 +48,7 @@ function getAreaOptionTabs() { ]; } -const countLabel = i18n.translate('kbnVislibVisTypes.area.countText', { +const countLabel = i18n.translate('visTypeVislib.area.countText', { defaultMessage: 'Count', }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx index cff9a0a2e85519..ca49f17e7c264d 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx @@ -20,12 +20,13 @@ import $ from 'jquery'; import React, { RefObject } from 'react'; -import { CUSTOM_LEGEND_VIS_TYPES, VisLegend, Vis, VisParams } from './legacy_imports'; +import { Vis, VisParams } from './legacy_imports'; // @ts-ignore import { Vis as Vislib } from './vislib/vis'; import { Positions } from './utils/collections'; import { KbnVislibVisTypesDependencies } from './plugin'; import { mountReactNode } from '../../../../core/public/utils'; +import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend'; const legendClassName = { top: 'visLib--legend-top', @@ -76,9 +77,6 @@ export const createVislibVisController = (deps: KbnVislibVisTypesDependencies) = return resolve(); } - await deps.initializeHierarchicalTooltipFormatter(); - await deps.initializePointSeriesTooltipFormatter(); - this.vislibVis = new Vislib(this.chartEl, visParams, deps); this.vislibVis.on('brush', this.vis.API.events.brush); this.vislibVis.on('click', this.vis.API.events.filter); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts b/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts index 0a685cd70e0895..8a35fe3a0f6f82 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -52,7 +52,7 @@ export const createKbnVislibVisTypesFn = ( context: { types: ['kibana_datatable'], }, - help: i18n.translate('kbnVislibVisTypes.functions.vislib.help', { + help: i18n.translate('visTypeVislib.functions.vislib.help', { defaultMessage: 'Vislib visualization', }), args: { diff --git a/src/legacy/ui/public/vis/components/tooltip/__tests__/_tooltip_formatter.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js similarity index 88% rename from src/legacy/ui/public/vis/components/tooltip/__tests__/_tooltip_formatter.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js index d0cfab0b54f794..a3aabcb90be624 100644 --- a/src/legacy/ui/public/vis/components/tooltip/__tests__/_tooltip_formatter.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js @@ -20,18 +20,11 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { PointSeriesTooltipFormatterProvider } from '../_pointseries_tooltip_formatter'; -describe('tooltipFormatter', function() { - let tooltipFormatter; +import { pointSeriesTooltipFormatter } from '../../components/tooltip'; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - tooltipFormatter = Private(PointSeriesTooltipFormatterProvider)(); - }) - ); +describe('tooltipFormatter', function() { + const tooltipFormatter = pointSeriesTooltipFormatter(); function cell($row, i) { return $row diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/heatmap_color.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/heatmap_color.js index 36c5b60abf5c6b..c7ffc843876e37 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/heatmap_color.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/heatmap_color.js @@ -18,7 +18,6 @@ */ import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; import { getHeatmapColors } from '../../../legacy_imports'; @@ -27,8 +26,6 @@ describe('Vislib Heatmap Color Module Test Suite', function() { const nullValue = null; let notAValue; - beforeEach(ngMock.module('kibana')); - it('should throw an error if schema is invalid', function() { expect(function() { getHeatmapColors(4, 'invalid schema'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js index 55d629adaf2459..db99b881a6e380 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js @@ -19,7 +19,7 @@ import _ from 'lodash'; import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; + import { labels } from '../../components/labels/labels'; import { dataArray } from '../../components/labels/data_array'; import { uniqLabels } from '../../components/labels/uniq_labels'; @@ -161,21 +161,18 @@ const columnsData = { describe('Vislib Labels Module Test Suite', function() { let uniqSeriesLabels; describe('Labels (main)', function() { - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - seriesLabels = labels(seriesData); - rowsLabels = labels(rowsData); - seriesArr = Array.isArray(seriesLabels); - rowsArr = Array.isArray(rowsLabels); - uniqSeriesLabels = _.chain(rowsData.rows) - .pluck('series') - .flattenDeep() - .pluck('label') - .uniq() - .value(); - }) - ); + beforeEach(() => { + seriesLabels = labels(seriesData); + rowsLabels = labels(rowsData); + seriesArr = Array.isArray(seriesLabels); + rowsArr = Array.isArray(rowsLabels); + uniqSeriesLabels = _.chain(rowsData.rows) + .pluck('series') + .flattenDeep() + .pluck('label') + .uniq() + .value(); + }); it('should be a function', function() { expect(typeof labels).to.be('function'); @@ -224,15 +221,12 @@ describe('Vislib Labels Module Test Suite', function() { let testSeries; let testRows; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - seriesLabels = dataArray(seriesData); - rowsLabels = dataArray(rowsData); - testSeries = Array.isArray(seriesLabels); - testRows = Array.isArray(rowsLabels); - }) - ); + beforeEach(() => { + seriesLabels = dataArray(seriesData); + rowsLabels = dataArray(rowsData); + testSeries = Array.isArray(seriesLabels); + testRows = Array.isArray(rowsLabels); + }); it('should throw an error if the input is not an object', function() { expect(function() { @@ -333,15 +327,12 @@ describe('Vislib Labels Module Test Suite', function() { let uniq; let testArr; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - uniq = uniqLabels(arrObj, function(d) { - return d; - }); - testArr = Array.isArray(uniq); - }) - ); + beforeEach(() => { + uniq = uniqLabels(arrObj, function(d) { + return d; + }); + testArr = Array.isArray(uniq); + }); it('should throw an error if input is not an array', function() { expect(function() { @@ -407,15 +398,12 @@ describe('Vislib Labels Module Test Suite', function() { let columnsArr; let rowsArr; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - columnsLabels = getSeries(columnsData); - rowsLabels = getSeries(rowsData); - columnsArr = Array.isArray(columnsLabels); - rowsArr = Array.isArray(rowsLabels); - }) - ); + beforeEach(() => { + columnsLabels = getSeries(columnsData); + rowsLabels = getSeries(rowsData); + columnsArr = Array.isArray(columnsLabels); + rowsArr = Array.isArray(rowsLabels); + }); it('should throw an error if input is not an object', function() { expect(function() { diff --git a/src/legacy/ui/public/vis/components/tooltip/__tests__/positioning.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js similarity index 99% rename from src/legacy/ui/public/vis/components/tooltip/__tests__/positioning.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js index 89d547f2377bab..f1c80c99810204 100644 --- a/src/legacy/ui/public/vis/components/tooltip/__tests__/positioning.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js @@ -21,7 +21,8 @@ import expect from '@kbn/expect'; import $ from 'jquery'; import _ from 'lodash'; import sinon from 'sinon'; -import { positionTooltip } from '../position_tooltip'; + +import { positionTooltip } from '../../components/tooltip/position_tooltip'; describe('Tooltip Positioning', function() { const sandbox = sinon.createSandbox(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js index e30ca17a1fce7a..734c6d003278fd 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js @@ -19,18 +19,15 @@ import _ from 'lodash'; import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; + import VislibProvider from '..'; describe('Vislib Index Test Suite', function() { let vislib; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - vislib = Private(VislibProvider); - }) - ); + beforeEach(() => { + vislib = new VislibProvider(); + }); it('should return an object', function() { expect(_.isObject(vislib)).to.be(true); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js index 3081c124150765..bc4a4f9925513c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js @@ -19,17 +19,15 @@ import d3 from 'd3'; import _ from 'lodash'; -import ngMock from 'ng_mock'; -import 'ui/persisted_state'; - import expect from '@kbn/expect'; import $ from 'jquery'; import { Axis } from '../../../lib/axis'; import { VisConfig } from '../../../lib/vis_config'; +import { getMockUiState } from '../fixtures/_vis_fixture'; describe('Vislib Axis Class Test Suite', function() { - let persistedState; + let mockUiState; let yAxis; let el; let fixture; @@ -102,38 +100,34 @@ describe('Vislib Axis Class Test Suite', function() { yAxisLabel: 'Count', }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - persistedState = new ($injector.get('PersistedState'))(); - - el = d3 - .select('body') - .append('div') - .attr('class', 'visAxis--x') - .style('height', '40px'); + beforeEach(() => { + mockUiState = getMockUiState(); + el = d3 + .select('body') + .append('div') + .attr('class', 'visAxis--x') + .style('height', '40px'); - fixture = el.append('div').attr('class', 'x-axis-div'); + fixture = el.append('div').attr('class', 'x-axis-div'); - const visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - persistedState, - $('.x-axis-div')[0], - () => undefined - ); - yAxis = new Axis(visConfig, { - type: 'value', - id: 'ValueAxis-1', - }); + const visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + mockUiState, + $('.x-axis-div')[0], + () => undefined + ); + yAxis = new Axis(visConfig, { + type: 'value', + id: 'ValueAxis-1', + }); - seriesData = data.series.map(series => { - return series.values; - }); - }) - ); + seriesData = data.series.map(series => { + return series.values; + }); + }); afterEach(function() { fixture.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js index cbb294c3b44e4e..fd25335dd2cd4c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js @@ -20,18 +20,15 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import ngMock from 'ng_mock'; - import expect from '@kbn/expect'; -import 'ui/persisted_state'; import { AxisTitle } from '../../lib/axis/axis_title'; import { AxisConfig } from '../../lib/axis/axis_config'; import { VisConfig } from '../../lib/vis_config'; import { Data } from '../../lib/data'; +import { getMockUiState } from './fixtures/_vis_fixture'; describe('Vislib AxisTitle Class Test Suite', function() { - let PersistedState; let el; let dataObj; let xTitle; @@ -96,56 +93,53 @@ describe('Vislib AxisTitle Class Test Suite', function() { yAxisLabel: 'Count', }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - PersistedState = $injector.get('PersistedState'); - - el = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper'); - - el.append('div') - .attr('class', 'visAxis__column--bottom') - .append('div') - .attr('class', 'axis-title y-axis-title') - .style('height', '20px') - .style('width', '20px'); - - el.append('div') - .attr('class', 'visAxis__column--left') - .append('div') - .attr('class', 'axis-title x-axis-title') - .style('height', '20px') - .style('width', '20px'); - - dataObj = new Data(data, new PersistedState(), () => undefined); - visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - new PersistedState(), - el.node(), - () => undefined - ); - const xAxisConfig = new AxisConfig(visConfig, { - position: 'bottom', - title: { - text: dataObj.get('xAxisLabel'), - }, - }); - const yAxisConfig = new AxisConfig(visConfig, { - position: 'left', - title: { - text: dataObj.get('yAxisLabel'), - }, - }); - xTitle = new AxisTitle(xAxisConfig); - yTitle = new AxisTitle(yAxisConfig); - }) - ); + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper'); + + el.append('div') + .attr('class', 'visAxis__column--bottom') + .append('div') + .attr('class', 'axis-title y-axis-title') + .style('height', '20px') + .style('width', '20px'); + + el.append('div') + .attr('class', 'visAxis__column--left') + .append('div') + .attr('class', 'axis-title x-axis-title') + .style('height', '20px') + .style('width', '20px'); + + const uiState = getMockUiState(); + uiState.set('vis.colors', []); + dataObj = new Data(data, getMockUiState(), () => undefined); + visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + getMockUiState(), + el.node(), + () => undefined + ); + const xAxisConfig = new AxisConfig(visConfig, { + position: 'bottom', + title: { + text: dataObj.get('xAxisLabel'), + }, + }); + const yAxisConfig = new AxisConfig(visConfig, { + position: 'left', + title: { + text: dataObj.get('yAxisLabel'), + }, + }); + xTitle = new AxisTitle(xAxisConfig); + yTitle = new AxisTitle(yAxisConfig); + }); afterEach(function() { el.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js index b2086d0749a419..b65571becd83c6 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js @@ -19,14 +19,14 @@ import d3 from 'd3'; import _ from 'lodash'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import { ChartTitle } from '../../lib/chart_title'; import { VisConfig } from '../../lib/vis_config'; +import { getMockUiState } from './fixtures/_vis_fixture'; describe('Vislib ChartTitle Class Test Suite', function() { - let persistedState; + let mockUiState; let chartTitle; let el; const data = { @@ -88,36 +88,32 @@ describe('Vislib ChartTitle Class Test Suite', function() { yAxisLabel: 'Count', }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - persistedState = new ($injector.get('PersistedState'))(); + beforeEach(() => { + mockUiState = getMockUiState(); + el = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper') + .datum(data); - el = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper') - .datum(data); + el.append('div') + .attr('class', 'chart-title') + .style('height', '20px'); - el.append('div') - .attr('class', 'chart-title') - .style('height', '20px'); - - const visConfig = new VisConfig( - { - type: 'histogram', - title: { - text: 'rows', - }, + const visConfig = new VisConfig( + { + type: 'histogram', + title: { + text: 'rows', }, - data, - persistedState, - el.node(), - () => undefined - ); - chartTitle = new ChartTitle(visConfig); - }) - ); + }, + data, + mockUiState, + el.node(), + () => undefined + ); + chartTitle = new ChartTitle(visConfig); + }); afterEach(function() { el.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js index 5811b1d238163a..d4ec6f363a75b0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js @@ -18,11 +18,10 @@ */ import _ from 'lodash'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import 'ui/persisted_state'; import { Data } from '../../lib/data'; +import { getMockUiState } from './fixtures/_vis_fixture'; const seriesData = { label: '', @@ -153,14 +152,11 @@ const colsData = { }; describe('Vislib Data Class Test Suite', function() { - let persistedState; + let mockUiState; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - persistedState = new ($injector.get('PersistedState'))(); - }) - ); + beforeEach(() => { + mockUiState = getMockUiState(); + }); describe('Data Class (main)', function() { it('should be a function', function() { @@ -168,7 +164,7 @@ describe('Vislib Data Class Test Suite', function() { }); it('should return an object', function() { - const rowIn = new Data(rowsData, persistedState, () => undefined); + const rowIn = new Data(rowsData, mockUiState, () => undefined); expect(_.isObject(rowIn)).to.be(true); }); }); @@ -182,7 +178,7 @@ describe('Vislib Data Class Test Suite', function() { }; beforeEach(function() { - data = new Data(pieData, persistedState, () => undefined); + data = new Data(pieData, mockUiState, () => undefined); }); it('should remove zero values', function() { @@ -196,7 +192,7 @@ describe('Vislib Data Class Test Suite', function() { let serOut; beforeEach(function() { - serIn = new Data(seriesData, persistedState, () => undefined); + serIn = new Data(seriesData, mockUiState, () => undefined); serOut = serIn.flatten(); }); @@ -210,7 +206,7 @@ describe('Vislib Data Class Test Suite', function() { function testLength(inputData) { return function() { - const data = new Data(inputData, persistedState, () => undefined); + const data = new Data(inputData, mockUiState, () => undefined); const len = _.reduce( data.chartData(), function(sum, chart) { @@ -266,7 +262,7 @@ describe('Vislib Data Class Test Suite', function() { }; beforeEach(function() { - data = new Data(geohashGridData, persistedState, () => undefined); + data = new Data(geohashGridData, mockUiState, () => undefined); }); describe('getVisData', function() { @@ -287,7 +283,7 @@ describe('Vislib Data Class Test Suite', function() { describe('null value check', function() { it('should return false', function() { - const data = new Data(rowsData, persistedState, () => undefined); + const data = new Data(rowsData, mockUiState, () => undefined); expect(data.hasNullValues()).to.be(false); }); @@ -307,7 +303,7 @@ describe('Vislib Data Class Test Suite', function() { ], }); - const data = new Data(nullRowData, persistedState, () => undefined); + const data = new Data(nullRowData, mockUiState, () => undefined); expect(data.hasNullValues()).to.be(true); }); }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js index a93db5637c89d9..760c2e80d84284 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js @@ -19,14 +19,12 @@ import _ from 'lodash'; import d3 from 'd3'; -import ngMock from 'ng_mock'; - import expect from '@kbn/expect'; -import 'ui/persisted_state'; // Data import data from './fixtures/mock_data/date_histogram/_series'; -import getFixturesVislibVisFixtureProvider from './fixtures/_vis_fixture'; + +import { getVis, getMockUiState } from './fixtures/_vis_fixture'; import { SimpleEmitter } from '../../../legacy_imports'; describe('Vislib Dispatch Class Test Suite', function() { @@ -44,17 +42,13 @@ describe('Vislib Dispatch Class Test Suite', function() { describe('', function() { let vis; - let persistedState; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(); - persistedState = new ($injector.get('PersistedState'))(); - vis.render(data, persistedState); - }) - ); + let mockUiState; + + beforeEach(() => { + vis = getVis(); + mockUiState = getMockUiState(); + vis.render(data, mockUiState); + }); afterEach(function() { destroyVis(vis); @@ -71,18 +65,14 @@ describe('Vislib Dispatch Class Test Suite', function() { describe('Stock event handlers', function() { let vis; - let persistedState; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - persistedState = new ($injector.get('PersistedState'))(); - vis = getVis(); - vis.on('brush', _.noop); - vis.render(data, persistedState); - }) - ); + let mockUiState; + + beforeEach(() => { + mockUiState = getMockUiState(); + vis = getVis(); + vis.on('brush', _.noop); + vis.render(data, mockUiState); + }); afterEach(function() { destroyVis(vis); @@ -187,42 +177,30 @@ describe('Vislib Dispatch Class Test Suite', function() { describe('Custom event handlers', function() { it('should attach whatever gets passed on vis.on() to chart.events', function(done) { - let vis; - let persistedState; - ngMock.module('kibana'); - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(); - persistedState = new ($injector.get('PersistedState'))(); - vis.on('someEvent', _.noop); - vis.render(data, persistedState); - - vis.handler.charts.forEach(function(chart) { - expect(chart.events.listenerCount('someEvent')).to.be(1); - }); + const vis = getVis(); + const mockUiState = getMockUiState(); + vis.on('someEvent', _.noop); + vis.render(data, mockUiState); - destroyVis(vis); - done(); + vis.handler.charts.forEach(function(chart) { + expect(chart.events.listenerCount('someEvent')).to.be(1); }); + + destroyVis(vis); + done(); }); it('can be added after rendering', function() { - let vis; - let persistedState; - ngMock.module('kibana'); - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(); - persistedState = new ($injector.get('PersistedState'))(); - vis.render(data, persistedState); - vis.on('someEvent', _.noop); - - vis.handler.charts.forEach(function(chart) { - expect(chart.events.listenerCount('someEvent')).to.be(1); - }); + const vis = getVis(); + const mockUiState = getMockUiState(); + vis.render(data, mockUiState); + vis.on('someEvent', _.noop); - destroyVis(vis); + vis.handler.charts.forEach(function(chart) { + expect(chart.events.listenerCount('someEvent')).to.be(1); }); + + destroyVis(vis); }); }); }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js index 244386c1b51e67..4523e70ccbb4cb 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js @@ -18,18 +18,15 @@ */ import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; + import { ErrorHandler } from '../../lib/_error_handler'; describe('Vislib ErrorHandler Test Suite', function() { let errorHandler; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - errorHandler = new ErrorHandler(); - }) - ); + beforeEach(() => { + errorHandler = new ErrorHandler(); + }); describe('validateWidthandHeight Method', function() { it('should throw an error when width and/or height is 0', function() { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/_vis_fixture.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/_vis_fixture.js index c49ca732f09159..c001a04f4b6e8c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/_vis_fixture.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/_vis_fixture.js @@ -22,15 +22,6 @@ import $ from 'jquery'; import { Vis } from '../../../vis'; -// TODO: remove legacy imports when/of converting tests to jest -import { - setHierarchicalTooltipFormatter, - getHierarchicalTooltipFormatter, -} from 'ui/vis/components/tooltip/_hierarchical_tooltip_formatter'; -import { - setPointSeriesTooltipFormatter, - getPointSeriesTooltipFormatter, -} from 'ui/vis/components/tooltip/_pointseries_tooltip_formatter'; import { vislibSeriesResponseHandlerProvider, vislibSlicesResponseHandlerProvider, @@ -73,29 +64,33 @@ const getDeps = () => { return { uiSettings, vislibColor, - getHierarchicalTooltipFormatter, - getPointSeriesTooltipFormatter, vislibSeriesResponseHandlerProvider, vislibSlicesResponseHandlerProvider, }; }; -export default function getVislibFixtures(Private) { - setHierarchicalTooltipFormatter(Private); - setPointSeriesTooltipFormatter(Private); +export const getMockUiState = () => { + const map = new Map(); - return function(visLibParams, element) { - return new Vis( - element || $visCanvas.new(), - _.defaults({}, visLibParams || {}, { - addTooltip: true, - addLegend: true, - defaultYExtents: false, - setYExtents: false, - yAxis: {}, - type: 'histogram', - }), - getDeps() - ); - }; + return (() => ({ + get: (...args) => map.get(...args), + set: (...args) => map.set(...args), + setSilent: (...args) => map.set(...args), + on: () => undefined, + }))(); +}; + +export function getVis(visLibParams, element) { + return new Vis( + element || $visCanvas.new(), + _.defaults({}, visLibParams || {}, { + addTooltip: true, + addLegend: true, + defaultYExtents: false, + setYExtents: false, + yAxis: {}, + type: 'histogram', + }), + getDeps() + ); } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js index b309c97d240006..8e25015c101860 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js @@ -17,36 +17,29 @@ * under the License. */ -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; +import $ from 'jquery'; // Data import series from '../fixtures/mock_data/date_histogram/_series'; import columns from '../fixtures/mock_data/date_histogram/_columns'; import rows from '../fixtures/mock_data/date_histogram/_rows'; import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series'; -import $ from 'jquery'; -import 'ui/persisted_state'; -import getFixturesVislibVisFixtureProvider from '../fixtures/_vis_fixture'; +import { getVis, getMockUiState } from '../fixtures/_vis_fixture'; + const dateHistogramArray = [series, columns, rows, stackedSeries]; const names = ['series', 'columns', 'rows', 'stackedSeries']; dateHistogramArray.forEach(function(data, i) { describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function() { - let vis; - let persistedState; const events = ['click', 'brush']; + let vis; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(); - persistedState = new ($injector.get('PersistedState'))(); - vis.render(data, persistedState); - }) - ); + beforeEach(() => { + vis = getVis(); + vis.render(data, getMockUiState()); + }); afterEach(function() { vis.destroy(); @@ -107,9 +100,7 @@ dateHistogramArray.forEach(function(data, i) { describe('removeAll Method', function() { beforeEach(function() { - ngMock.inject(function() { - vis.handler.removeAll(vis.element); - }); + vis.handler.removeAll(vis.element); }); it('should remove all DOM elements from the el', function() { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js index c8636f34ce6f8f..f72794e27e834a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js @@ -18,18 +18,17 @@ */ import d3 from 'd3'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import 'ui/persisted_state'; +import $ from 'jquery'; // Data import series from '../fixtures/mock_data/date_histogram/_series'; import columns from '../fixtures/mock_data/date_histogram/_columns'; import rows from '../fixtures/mock_data/date_histogram/_rows'; import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series'; -import $ from 'jquery'; + import { Layout } from '../../../lib/layout/layout'; -import getFixturesVislibVisFixtureProvider from '../fixtures/_vis_fixture'; +import { getVis, getMockUiState } from '../fixtures/_vis_fixture'; import { VisConfig } from '../../../lib/vis_config'; const dateHistogramArray = [series, columns, rows, stackedSeries]; @@ -38,23 +37,18 @@ const names = ['series', 'columns', 'rows', 'stackedSeries']; dateHistogramArray.forEach(function(data, i) { describe('Vislib Layout Class Test Suite for ' + names[i] + ' Data', function() { let vis; - let persistedState; + let mockUiState; let numberOfCharts; let testLayout; - beforeEach(ngMock.module('kibana')); - - beforeEach(function() { - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(); - persistedState = new ($injector.get('PersistedState'))(); - vis.render(data, persistedState); - numberOfCharts = vis.handler.charts.length; - }); + beforeEach(() => { + vis = getVis(); + mockUiState = getMockUiState(); + vis.render(data, mockUiState); + numberOfCharts = vis.handler.charts.length; }); - afterEach(function() { + afterEach(() => { vis.destroy(); }); @@ -81,7 +75,7 @@ dateHistogramArray.forEach(function(data, i) { type: 'histogram', }, data, - persistedState, + mockUiState, vis.element, () => undefined ); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js index 4dc52309c76fde..cc6d33a2d98daf 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js @@ -18,19 +18,16 @@ */ import _ from 'lodash'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; + import { layoutTypes as layoutType } from '../../../lib/layout/layout_types'; describe('Vislib Layout Types Test Suite', function() { let layoutFunc; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - layoutFunc = layoutType.point_series; - }) - ); + beforeEach(() => { + layoutFunc = layoutType.point_series; + }); it('should be an object', function() { expect(_.isObject(layoutType)).to.be(true); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js index 2626fd3ace2b0d..3942aa18891b84 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js @@ -18,9 +18,9 @@ */ import d3 from 'd3'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import $ from 'jquery'; + import { chartSplit } from '../../../../../lib/layout/splits/column_chart/chart_split'; import { chartTitleSplit } from '../../../../../lib/layout/splits/column_chart/chart_title_split'; import { xAxisSplit } from '../../../../../lib/layout/splits/column_chart/x_axis_split'; @@ -150,16 +150,13 @@ describe('Vislib Split Function Test Suite', function() { ], }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization') - .datum(data); - }) - ); + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization') + .datum(data); + }); afterEach(function() { el.remove(); @@ -168,11 +165,9 @@ describe('Vislib Split Function Test Suite', function() { describe('chart split function', function() { let fixture; - beforeEach( - ngMock.inject(function() { - fixture = d3.select('.visualization').call(chartSplit); - }) - ); + beforeEach(function() { + fixture = d3.select('.visualization').call(chartSplit); + }); afterEach(function() { fixture.remove(); @@ -192,28 +187,26 @@ describe('Vislib Split Function Test Suite', function() { let newEl; let fixture; - beforeEach( - ngMock.inject(function() { - visEl = el.append('div').attr('class', 'visWrapper'); - visEl.append('div').attr('class', 'visAxis__splitTitles--x'); - visEl.append('div').attr('class', 'visAxis__splitTitles--y'); - visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + beforeEach(function() { + visEl = el.append('div').attr('class', 'visWrapper'); + visEl.append('div').attr('class', 'visAxis__splitTitles--x'); + visEl.append('div').attr('class', 'visAxis__splitTitles--y'); + visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - newEl = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper') - .datum({ series: [] }); + newEl = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper') + .datum({ series: [] }); - newEl.append('div').attr('class', 'visAxis__splitTitles--x'); - newEl.append('div').attr('class', 'visAxis__splitTitles--y'); - newEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - newEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + newEl.append('div').attr('class', 'visAxis__splitTitles--x'); + newEl.append('div').attr('class', 'visAxis__splitTitles--y'); + newEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + newEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - fixture = newEl.selectAll(this.childNodes)[0].length; - }) - ); + fixture = newEl.selectAll(this.childNodes)[0].length; + }); afterEach(function() { newEl.remove(); @@ -237,17 +230,15 @@ describe('Vislib Split Function Test Suite', function() { let fixture; let divs; - beforeEach( - ngMock.inject(function() { - fixture = d3 - .select('body') - .append('div') - .attr('class', 'columns') - .datum({ columns: [{}, {}] }); - d3.select('.columns').call(xAxisSplit); - divs = d3.selectAll('.x-axis-div')[0]; - }) - ); + beforeEach(function() { + fixture = d3 + .select('body') + .append('div') + .attr('class', 'columns') + .datum({ columns: [{}, {}] }); + d3.select('.columns').call(xAxisSplit); + divs = d3.selectAll('.x-axis-div')[0]; + }); afterEach(function() { fixture.remove(); @@ -263,19 +254,17 @@ describe('Vislib Split Function Test Suite', function() { let fixture; let divs; - beforeEach( - ngMock.inject(function() { - fixture = d3 - .select('body') - .append('div') - .attr('class', 'rows') - .datum({ rows: [{}, {}] }); + beforeEach(function() { + fixture = d3 + .select('body') + .append('div') + .attr('class', 'rows') + .datum({ rows: [{}, {}] }); - d3.select('.rows').call(yAxisSplit); + d3.select('.rows').call(yAxisSplit); - divs = d3.selectAll('.y-axis-div')[0]; - }) - ); + divs = d3.selectAll('.y-axis-div')[0]; + }); afterEach(function() { fixture.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js index 0c744e4fa2b573..8978f80f58dde8 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js @@ -18,9 +18,9 @@ */ import d3 from 'd3'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import $ from 'jquery'; + import { chartSplit } from '../../../../../lib/layout/splits/gauge_chart/chart_split'; import { chartTitleSplit } from '../../../../../lib/layout/splits/gauge_chart/chart_title_split'; @@ -148,16 +148,13 @@ describe('Vislib Gauge Split Function Test Suite', function() { ], }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization') - .datum(data); - }) - ); + beforeEach(function() { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization') + .datum(data); + }); afterEach(function() { el.remove(); @@ -166,11 +163,9 @@ describe('Vislib Gauge Split Function Test Suite', function() { describe('chart split function', function() { let fixture; - beforeEach( - ngMock.inject(function() { - fixture = d3.select('.visualization').call(chartSplit); - }) - ); + beforeEach(function() { + fixture = d3.select('.visualization').call(chartSplit); + }); afterEach(function() { fixture.remove(); @@ -188,15 +183,13 @@ describe('Vislib Gauge Split Function Test Suite', function() { describe('chart title split function', function() { let visEl; - beforeEach( - ngMock.inject(function() { - visEl = el.append('div').attr('class', 'visWrapper'); - visEl.append('div').attr('class', 'visAxis__splitTitles--x'); - visEl.append('div').attr('class', 'visAxis__splitTitles--y'); - visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - }) - ); + beforeEach(function() { + visEl = el.append('div').attr('class', 'visWrapper'); + visEl.append('div').attr('class', 'visAxis__splitTitles--x'); + visEl.append('div').attr('class', 'visAxis__splitTitles--y'); + visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + }); afterEach(function() { visEl.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js index bdf886033d8d6f..e9c2ff0d2fa078 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js @@ -19,8 +19,8 @@ import d3 from 'd3'; import _ from 'lodash'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; + import { layoutTypes } from '../../../../lib/layout/layout_types'; describe('Vislib Column Layout Test Suite', function() { @@ -85,16 +85,13 @@ describe('Vislib Column Layout Test Suite', function() { yAxisLabel: 'Count', }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization'); - columnLayout = layoutTypes.point_series(el, data); - }) - ); + beforeEach(function() { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization'); + columnLayout = layoutTypes.point_series(el, data); + }); afterEach(function() { el.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js index ee3f5a476cd9ac..03646d08298dd6 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js @@ -17,12 +17,13 @@ * under the License. */ -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { vislibPointSeriesTypes as pointSeriesConfig } from '../../../lib/types/point_series'; + import percentileTestdata from './testdata_linechart_percentile.json'; import percentileTestdataResult from './testdata_linechart_percentile_result.json'; +import { vislibPointSeriesTypes as pointSeriesConfig } from '../../../lib/types/point_series'; + describe('Point Series Config Type Class Test Suite', function() { let parsedConfig; const histogramConfig = { @@ -95,8 +96,6 @@ describe('Point Series Config Type Class Test Suite', function() { }, }; - beforeEach(ngMock.module('kibana')); - describe('histogram chart', function() { beforeEach(function() { parsedConfig = pointSeriesConfig.column(histogramConfig, data); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js index 3f0253b4a46702..7dfd2ded36a667 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js @@ -18,10 +18,10 @@ */ import d3 from 'd3'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import 'ui/persisted_state'; + import { VisConfig } from '../../lib/vis_config'; +import { getMockUiState } from './fixtures/_vis_fixture'; describe('Vislib VisConfig Class Test Suite', function() { let el; @@ -85,27 +85,23 @@ describe('Vislib VisConfig Class Test Suite', function() { yAxisLabel: 'Count', }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - const PersistedState = $injector.get('PersistedState'); - el = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper') - .node(); + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper') + .node(); - visConfig = new VisConfig( - { - type: 'point_series', - }, - data, - new PersistedState(), - el, - () => undefined - ); - }) - ); + visConfig = new VisConfig( + { + type: 'point_series', + }, + data, + getMockUiState(), + el, + () => undefined + ); + }); afterEach(() => { el.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js index 09fe067537c7f7..d42562a87b825a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js @@ -19,15 +19,15 @@ import d3 from 'd3'; import _ from 'lodash'; -import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import 'ui/persisted_state'; import $ from 'jquery'; + import { Axis } from '../../lib/axis'; import { VisConfig } from '../../lib/vis_config'; +import { getMockUiState } from './fixtures/_vis_fixture'; describe('Vislib xAxis Class Test Suite', function() { - let persistedState; + let mockUiState; let xAxis; let el; let fixture; @@ -105,34 +105,30 @@ describe('Vislib xAxis Class Test Suite', function() { yAxisLabel: 'Count', }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - persistedState = new ($injector.get('PersistedState'))(); - - el = d3 - .select('body') - .append('div') - .attr('class', 'visAxis--x') - .style('height', '40px'); + beforeEach(() => { + mockUiState = getMockUiState(); + el = d3 + .select('body') + .append('div') + .attr('class', 'visAxis--x') + .style('height', '40px'); - fixture = el.append('div').attr('class', 'x-axis-div'); + fixture = el.append('div').attr('class', 'x-axis-div'); - const visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - persistedState, - $('.x-axis-div')[0], - () => undefined - ); - xAxis = new Axis(visConfig, { - type: 'category', - id: 'CategoryAxis-1', - }); - }) - ); + const visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + mockUiState, + $('.x-axis-div')[0], + () => undefined + ); + xAxis = new Axis(visConfig, { + type: 'category', + id: 'CategoryAxis-1', + }); + }); afterEach(function() { fixture.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js index e857aca3bf3edc..f73011d6616451 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js @@ -19,15 +19,15 @@ import _ from 'lodash'; import d3 from 'd3'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import 'ui/persisted_state'; import $ from 'jquery'; +import expect from '@kbn/expect'; + import { Axis } from '../../lib/axis'; import { VisConfig } from '../../lib/vis_config'; +import { getMockUiState } from './fixtures/_vis_fixture'; const YAxis = Axis; -let persistedState; +let mockUiState; let el; let buildYAxis; let yAxis; @@ -96,7 +96,7 @@ function createData(seriesData) { type: 'histogram', }, data, - persistedState, + mockUiState, node, () => undefined ); @@ -121,15 +121,10 @@ function createData(seriesData) { } describe('Vislib yAxis Class Test Suite', function() { - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(function($injector) { - persistedState = new ($injector.get('PersistedState'))(); - - expect($('.y-axis-wrapper')).to.have.length(0); - }) - ); + beforeEach(() => { + mockUiState = getMockUiState(); + expect($('.y-axis-wrapper')).to.have.length(0); + }); afterEach(function() { if (el) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js index a6d1c4daf5d2cf..4852f71d8c45b3 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js @@ -19,19 +19,15 @@ import _ from 'lodash'; import $ from 'jquery'; -import ngMock from 'ng_mock'; - import expect from '@kbn/expect'; -import 'ui/persisted_state'; import series from './lib/fixtures/mock_data/date_histogram/_series'; import columns from './lib/fixtures/mock_data/date_histogram/_columns'; import rows from './lib/fixtures/mock_data/date_histogram/_rows'; import stackedSeries from './lib/fixtures/mock_data/date_histogram/_stacked_series'; -import getFixturesVislibVisFixtureProvider from './lib/fixtures/_vis_fixture'; +import { getVis, getMockUiState } from './lib/fixtures/_vis_fixture'; const dataArray = [series, columns, rows, stackedSeries]; - const names = ['series', 'columns', 'rows', 'stackedSeries']; dataArray.forEach(function(data, i) { @@ -39,19 +35,15 @@ dataArray.forEach(function(data, i) { const beforeEvent = 'click'; const afterEvent = 'brush'; let vis; - let persistedState; + let mockUiState; let secondVis; let numberOfCharts; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(); - persistedState = new ($injector.get('PersistedState'))(); - secondVis = getVis(); - }) - ); + beforeEach(() => { + vis = getVis(); + secondVis = getVis(); + mockUiState = getMockUiState(); + }); afterEach(function() { vis.destroy(); @@ -60,7 +52,7 @@ dataArray.forEach(function(data, i) { describe('render Method', function() { beforeEach(function() { - vis.render(data, persistedState); + vis.render(data, mockUiState); numberOfCharts = vis.handler.charts.length; }); @@ -78,7 +70,7 @@ dataArray.forEach(function(data, i) { it('should throw an error if no data is provided', function() { expect(function() { - vis.render(null, persistedState); + vis.render(null, mockUiState); }).to.throwError(); }); }); @@ -91,8 +83,8 @@ dataArray.forEach(function(data, i) { describe('destroy Method', function() { beforeEach(function() { - vis.render(data, persistedState); - secondVis.render(data, persistedState); + vis.render(data, mockUiState); + secondVis.render(data, mockUiState); secondVis.destroy(); }); @@ -107,7 +99,7 @@ dataArray.forEach(function(data, i) { describe('set Method', function() { beforeEach(function() { - vis.render(data, persistedState); + vis.render(data, mockUiState); vis.set('addLegend', false); vis.set('offset', 'wiggle'); }); @@ -120,7 +112,7 @@ dataArray.forEach(function(data, i) { describe('get Method', function() { beforeEach(function() { - vis.render(data, persistedState); + vis.render(data, mockUiState); }); it('should get attribute values', function() { @@ -142,7 +134,7 @@ dataArray.forEach(function(data, i) { }); // Render chart - vis.render(data, persistedState); + vis.render(data, mockUiState); // Add event after charts have rendered listeners.forEach(function(listener) { @@ -196,7 +188,7 @@ dataArray.forEach(function(data, i) { vis.off(beforeEvent, listener1); // Render chart - vis.render(data, persistedState); + vis.render(data, mockUiState); // Add event after charts have rendered listeners.forEach(function(listener) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js index 7fe350bd85e052..c3f5859eb454cc 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js @@ -18,14 +18,11 @@ */ import d3 from 'd3'; -import ngMock from 'ng_mock'; import _ from 'lodash'; import $ from 'jquery'; - import expect from '@kbn/expect'; -import 'ui/persisted_state'; -import getFixturesVislibVisFixtureProvider from '../lib/fixtures/_vis_fixture'; +import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; const dataTypesArray = { 'series pos': require('../lib/fixtures/mock_data/date_histogram/_series'), @@ -46,18 +43,14 @@ const visLibParams = { _.forOwn(dataTypesArray, function(dataType, dataTypeName) { describe('Vislib Area Chart Test Suite for ' + dataTypeName + ' Data', function() { let vis; - let persistedState; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(visLibParams); - persistedState = new ($injector.get('PersistedState'))(); - vis.on('brush', _.noop); - vis.render(dataType, persistedState); - }) - ); + let mockUiState; + + beforeEach(() => { + vis = getVis(visLibParams); + mockUiState = getMockUiState(); + vis.on('brush', _.noop); + vis.render(dataType, mockUiState); + }); afterEach(function() { vis.destroy(); @@ -97,17 +90,15 @@ _.forOwn(dataTypesArray, function(dataType, dataTypeName) { let d3selectedPath; let onMouseOver; - beforeEach( - ngMock.inject(function() { - vis.handler.charts.forEach(function(chart) { - path = $(chart.chartEl).find('path')[0]; - d3selectedPath = d3.select(path)[0][0]; + beforeEach(function() { + vis.handler.charts.forEach(function(chart) { + path = $(chart.chartEl).find('path')[0]; + d3selectedPath = d3.select(path)[0][0]; - // d3 instance of click and hover - onMouseOver = !!d3selectedPath.__onmouseover; - }); - }) - ); + // d3 instance of click and hover + onMouseOver = !!d3selectedPath.__onmouseover; + }); + }); it('should attach a hover event', function() { vis.handler.charts.forEach(function() { @@ -124,20 +115,18 @@ _.forOwn(dataTypesArray, function(dataType, dataTypeName) { let onClick; let onMouseOver; - beforeEach( - ngMock.inject(function() { - vis.handler.charts.forEach(function(chart) { - circle = $(chart.chartEl).find('circle')[0]; - brush = $(chart.chartEl).find('.brush'); - d3selectedCircle = d3.select(circle)[0][0]; - - // d3 instance of click and hover - onBrush = !!brush; - onClick = !!d3selectedCircle.__onclick; - onMouseOver = !!d3selectedCircle.__onmouseover; - }); - }) - ); + beforeEach(() => { + vis.handler.charts.forEach(function(chart) { + circle = $(chart.chartEl).find('circle')[0]; + brush = $(chart.chartEl).find('.brush'); + d3selectedCircle = d3.select(circle)[0][0]; + + // d3 instance of click and hover + onBrush = !!brush; + onClick = !!d3selectedCircle.__onclick; + onMouseOver = !!d3selectedCircle.__onmouseover; + }); + }); // D3 brushing requires that a g element is appended that // listens for mousedown events. This g element includes @@ -219,7 +208,7 @@ _.forOwn(dataTypesArray, function(dataType, dataTypeName) { describe('defaultYExtents is true', function() { beforeEach(function() { vis.visConfigArgs.defaultYExtents = true; - vis.render(dataType, persistedState); + vis.render(dataType, mockUiState); }); it('should return yAxis extents equal to data extents', function() { @@ -238,7 +227,7 @@ _.forOwn(dataTypesArray, function(dataType, dataTypeName) { beforeEach(function() { vis.visConfigArgs.defaultYExtents = true; vis.visConfigArgs.boundsMargin = boundsMarginValue; - vis.render(dataType, persistedState); + vis.render(dataType, mockUiState); }); it('should return yAxis extents equal to data extents with boundsMargin', function() { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js index 088d3377af4dd5..9653f9abab6fb8 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js @@ -18,16 +18,12 @@ */ import d3 from 'd3'; -import ngMock from 'ng_mock'; - import expect from '@kbn/expect'; -import 'ui/persisted_state'; import { Chart } from '../../visualizations/_chart'; -import getFixturesVislibVisFixtureProvider from '../lib/fixtures/_vis_fixture'; +import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; describe('Vislib _chart Test Suite', function() { - let persistedState; let vis; let el; let myChart; @@ -109,30 +105,24 @@ describe('Vislib _chart Test Suite', function() { yAxisLabel: 'Count', }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - persistedState = new ($injector.get('PersistedState'))(); - - el = d3 - .select('body') - .append('div') - .attr('class', 'column-chart'); + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'column-chart'); - config = { - type: 'histogram', - addTooltip: true, - addLegend: true, - zeroFill: true, - }; + config = { + type: 'histogram', + addTooltip: true, + addLegend: true, + zeroFill: true, + }; - vis = getVis(config, el[0][0]); - vis.render(data, persistedState); + vis = getVis(config, el[0][0]); + vis.render(data, getMockUiState()); - myChart = vis.handler.charts[0]; - }) - ); + myChart = vis.handler.charts[0]; + }); afterEach(function() { el.remove(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js index d02060ef29bdd4..2216294fcbac11 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js @@ -17,12 +17,10 @@ * under the License. */ -import ngMock from 'ng_mock'; import _ from 'lodash'; import d3 from 'd3'; - +import $ from 'jquery'; import expect from '@kbn/expect'; -import 'ui/persisted_state'; // Data import series from '../lib/fixtures/mock_data/date_histogram/_series'; @@ -31,11 +29,11 @@ import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; import termsColumns from '../lib/fixtures/mock_data/terms/_columns'; import histogramRows from '../lib/fixtures/mock_data/histogram/_rows'; import stackedSeries from '../lib/fixtures/mock_data/date_histogram/_stacked_series'; + import { seriesMonthlyInterval } from '../lib/fixtures/mock_data/date_histogram/_series_monthly_interval'; import { rowsSeriesWithHoles } from '../lib/fixtures/mock_data/date_histogram/_rows_series_with_holes'; import rowsWithZeros from '../lib/fixtures/mock_data/date_histogram/_rows'; -import $ from 'jquery'; -import getFixturesVislibVisFixtureProvider from '../lib/fixtures/_vis_fixture'; +import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; // tuple, with the format [description, mode, data] const dataTypesArray = [ @@ -54,7 +52,7 @@ dataTypesArray.forEach(function(dataType) { describe('Vislib Column Chart Test Suite for ' + name + ' Data', function() { let vis; - let persistedState; + let mockUiState; const visLibParams = { type: 'histogram', addLegend: true, @@ -67,16 +65,12 @@ dataTypesArray.forEach(function(dataType) { }, }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(visLibParams); - persistedState = new ($injector.get('PersistedState'))(); - vis.on('brush', _.noop); - vis.render(data, persistedState); - }) - ); + beforeEach(() => { + vis = getVis(visLibParams); + mockUiState = getMockUiState(); + vis.on('brush', _.noop); + vis.render(data, mockUiState); + }); afterEach(function() { vis.destroy(); @@ -200,7 +194,7 @@ dataTypesArray.forEach(function(dataType) { describe('defaultYExtents is true', function() { beforeEach(function() { vis.visConfigArgs.defaultYExtents = true; - vis.render(data, persistedState); + vis.render(data, mockUiState); }); it('should return yAxis extents equal to data extents', function() { @@ -219,7 +213,7 @@ dataTypesArray.forEach(function(dataType) { beforeEach(function() { vis.visConfigArgs.defaultYExtents = true; vis.visConfigArgs.boundsMargin = boundsMarginValue; - vis.render(data, persistedState); + vis.render(data, mockUiState); }); it('should return yAxis extents equal to data extents with boundsMargin', function() { @@ -247,7 +241,7 @@ dataTypesArray.forEach(function(dataType) { describe('stackData method - data set with zeros in percentage mode', function() { let vis; - let persistedState; + let mockUiState; const visLibParams = { type: 'histogram', addLegend: true, @@ -256,22 +250,18 @@ describe('stackData method - data set with zeros in percentage mode', function() zeroFill: true, }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(visLibParams); - persistedState = new ($injector.get('PersistedState'))(); - vis.on('brush', _.noop); - }) - ); + beforeEach(() => { + vis = getVis(visLibParams); + mockUiState = getMockUiState(); + vis.on('brush', _.noop); + }); afterEach(function() { vis.destroy(); }); it('should not mutate the injected zeros', function() { - vis.render(seriesMonthlyInterval, persistedState); + vis.render(seriesMonthlyInterval, mockUiState); expect(vis.handler.charts).to.have.length(1); const chart = vis.handler.charts[0]; @@ -284,7 +274,7 @@ describe('stackData method - data set with zeros in percentage mode', function() }); it('should not mutate zeros that exist in the data', function() { - vis.render(rowsWithZeros, persistedState); + vis.render(rowsWithZeros, mockUiState); expect(vis.handler.charts).to.have.length(2); const chart = vis.handler.charts[0]; @@ -298,7 +288,7 @@ describe('stackData method - data set with zeros in percentage mode', function() describe('datumWidth - split chart data set with holes', function() { let vis; - let persistedState; + let mockUiState; const visLibParams = { type: 'histogram', addLegend: true, @@ -307,16 +297,12 @@ describe('datumWidth - split chart data set with holes', function() { zeroFill: true, }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(visLibParams); - persistedState = new ($injector.get('PersistedState'))(); - vis.on('brush', _.noop); - vis.render(rowsSeriesWithHoles, persistedState); - }) - ); + beforeEach(() => { + vis = getVis(visLibParams); + mockUiState = getMockUiState(); + vis.on('brush', _.noop); + vis.render(rowsSeriesWithHoles, mockUiState); + }); afterEach(function() { vis.destroy(); @@ -336,7 +322,7 @@ describe('datumWidth - split chart data set with holes', function() { describe('datumWidth - monthly interval', function() { let vis; - let persistedState; + let mockUiState; const visLibParams = { type: 'histogram', addLegend: true, @@ -345,16 +331,12 @@ describe('datumWidth - monthly interval', function() { zeroFill: true, }; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(visLibParams); - persistedState = new ($injector.get('PersistedState'))(); - vis.on('brush', _.noop); - vis.render(seriesMonthlyInterval, persistedState); - }) - ); + beforeEach(() => { + vis = getVis(visLibParams); + mockUiState = getMockUiState(); + vis.on('brush', _.noop); + vis.render(seriesMonthlyInterval, mockUiState); + }); afterEach(function() { vis.destroy(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js index 074b34e1c03c43..fe25734fcbfde5 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js @@ -17,21 +17,15 @@ * under the License. */ -import ngMock from 'ng_mock'; import $ from 'jquery'; import _ from 'lodash'; - import expect from '@kbn/expect'; -import 'ui/persisted_state'; import data from '../lib/fixtures/mock_data/terms/_seriesMultiple'; -import getFixturesVislibVisFixtureProvider from '../lib/fixtures/_vis_fixture'; +import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; describe('Vislib Gauge Chart Test Suite', function() { - let PersistedState; - let vislibVis; let vis; - let persistedState; let chartEl; const visLibParams = { type: 'gauge', @@ -81,21 +75,15 @@ describe('Vislib Gauge Chart Test Suite', function() { vis.destroy(); $('.visChart').remove(); } - vis = vislibVis(config); - persistedState = new PersistedState(); + vis = getVis(config); vis.on('brush', _.noop); - vis.render(data, persistedState); + vis.render(data, getMockUiState()); chartEl = vis.handler.charts[0].chartEl; } - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - vislibVis = getFixturesVislibVisFixtureProvider(Private); - PersistedState = $injector.get('PersistedState'); - generateVis(); - }) - ); + beforeEach(() => { + generateVis(); + }); afterEach(function() { vis.destroy(); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js index bf1dbad0b44cf1..f4c952be191deb 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js @@ -17,12 +17,10 @@ * under the License. */ -import ngMock from 'ng_mock'; import _ from 'lodash'; +import $ from 'jquery'; import d3 from 'd3'; - import expect from '@kbn/expect'; -import 'ui/persisted_state'; // Data import series from '../lib/fixtures/mock_data/date_histogram/_series'; @@ -30,8 +28,8 @@ import seriesPosNeg from '../lib/fixtures/mock_data/date_histogram/_series_pos_n import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; import termsColumns from '../lib/fixtures/mock_data/terms/_columns'; import stackedSeries from '../lib/fixtures/mock_data/date_histogram/_stacked_series'; -import $ from 'jquery'; -import getFixturesVislibVisFixtureProvider from '../lib/fixtures/_vis_fixture'; + +import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; // tuple, with the format [description, mode, data] const dataTypesArray = [ @@ -48,10 +46,8 @@ describe('Vislib Heatmap Chart Test Suite', function() { const data = dataType[1]; describe('for ' + name + ' Data', function() { - let PersistedState; - let vislibVis; let vis; - let persistedState; + let mockUiState; const visLibParams = { type: 'heatmap', addLegend: true, @@ -66,20 +62,15 @@ describe('Vislib Heatmap Chart Test Suite', function() { function generateVis(opts = {}) { const config = _.defaultsDeep({}, opts, visLibParams); - vis = vislibVis(config); - persistedState = new PersistedState(); + vis = getVis(config); + mockUiState = getMockUiState(); vis.on('brush', _.noop); - vis.render(data, persistedState); + vis.render(data, mockUiState); } - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - vislibVis = getFixturesVislibVisFixtureProvider(Private); - PersistedState = $injector.get('PersistedState'); - generateVis(); - }) - ); + beforeEach(() => { + generateVis(); + }); afterEach(function() { vis.destroy(); @@ -174,7 +165,7 @@ describe('Vislib Heatmap Chart Test Suite', function() { }); it('should define default colors', function() { - expect(persistedState.get('vis.defaultColors')).to.not.be(undefined); + expect(mockUiState.get('vis.defaultColors')).to.not.be(undefined); }); it('should set custom range', function() { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js index d010944a19e47d..1269fe7bcf62e9 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js @@ -19,12 +19,9 @@ import d3 from 'd3'; import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; import $ from 'jquery'; import _ from 'lodash'; -import 'ui/persisted_state'; - // Data import seriesPos from '../lib/fixtures/mock_data/date_histogram/_series'; import seriesPosNeg from '../lib/fixtures/mock_data/date_histogram/_series_pos_neg'; @@ -32,7 +29,8 @@ import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; import histogramColumns from '../lib/fixtures/mock_data/histogram/_columns'; import rangeRows from '../lib/fixtures/mock_data/range/_rows'; import termSeries from '../lib/fixtures/mock_data/terms/_series'; -import getFixturesVislibVisFixtureProvider from '../lib/fixtures/_vis_fixture'; + +import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; const dataTypes = [ ['series pos', seriesPos], @@ -50,25 +48,21 @@ describe('Vislib Line Chart', function() { describe(name + ' Data', function() { let vis; - let persistedState; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - const visLibParams = { - type: 'line', - addLegend: true, - addTooltip: true, - drawLinesBetweenPoints: true, - }; - - vis = getVis(visLibParams); - persistedState = new ($injector.get('PersistedState'))(); - vis.render(data, persistedState); - vis.on('brush', _.noop); - }) - ); + let mockUiState; + + beforeEach(() => { + const visLibParams = { + type: 'line', + addLegend: true, + addTooltip: true, + drawLinesBetweenPoints: true, + }; + + vis = getVis(visLibParams); + mockUiState = getMockUiState(); + vis.render(data, mockUiState); + vis.on('brush', _.noop); + }); afterEach(function() { vis.destroy(); @@ -82,20 +76,18 @@ describe('Vislib Line Chart', function() { let onClick; let onMouseOver; - beforeEach( - ngMock.inject(function() { - vis.handler.charts.forEach(function(chart) { - circle = $(chart.chartEl).find('.circle')[0]; - brush = $(chart.chartEl).find('.brush'); - d3selectedCircle = d3.select(circle)[0][0]; - - // d3 instance of click and hover - onBrush = !!brush; - onClick = !!d3selectedCircle.__onclick; - onMouseOver = !!d3selectedCircle.__onmouseover; - }); - }) - ); + beforeEach(function() { + vis.handler.charts.forEach(function(chart) { + circle = $(chart.chartEl).find('.circle')[0]; + brush = $(chart.chartEl).find('.brush'); + d3selectedCircle = d3.select(circle)[0][0]; + + // d3 instance of click and hover + onBrush = !!brush; + onClick = !!d3selectedCircle.__onclick; + onMouseOver = !!d3selectedCircle.__onmouseover; + }); + }); // D3 brushing requires that a g element is appended that // listens for mousedown events. This g element includes @@ -177,7 +169,7 @@ describe('Vislib Line Chart', function() { describe('defaultYExtents is true', function() { beforeEach(function() { vis.visConfigArgs.defaultYExtents = true; - vis.render(data, persistedState); + vis.render(data, mockUiState); }); it('should return yAxis extents equal to data extents', function() { @@ -196,7 +188,7 @@ describe('Vislib Line Chart', function() { beforeEach(function() { vis.visConfigArgs.defaultYExtents = true; vis.visConfigArgs.boundsMargin = boundsMarginValue; - vis.render(data, persistedState); + vis.render(data, mockUiState); }); it('should return yAxis extents equal to data extents with boundsMargin', function() { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js index 381dfcd387cc20..f38fa47393a143 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js @@ -18,19 +18,16 @@ */ import d3 from 'd3'; -import ngMock from 'ng_mock'; import _ from 'lodash'; import $ from 'jquery'; - import expect from '@kbn/expect'; -// TODO: Remove ui imports once converting to jest -import 'ui/persisted_state'; + import { vislibSlicesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; import fixtures from 'fixtures/fake_hierarchical_data'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import getFixturesVislibVisFixtureProvider from '../lib/fixtures/_vis_fixture'; +import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; import { Vis, tabifyAggResponse } from '../../../legacy_imports'; const rowAgg = [ @@ -123,35 +120,31 @@ describe('No global chart settings', function() { addTooltip: true, }; let chart1; - let persistedState; + let mockUiState; let indexPattern; let responseHandler; let data1; let stubVis1; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - chart1 = getVis(visLibParams1); - persistedState = new ($injector.get('PersistedState'))(); - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - responseHandler = vislibSlicesResponseHandlerProvider().handler; - - let id1 = 1; - stubVis1 = new Vis(indexPattern, { - type: 'pie', - aggs: rowAgg, - }); + beforeEach(() => { + chart1 = getVis(visLibParams1); + mockUiState = getMockUiState(); + indexPattern = new FixturesStubbedLogstashIndexPatternProvider(); + responseHandler = vislibSlicesResponseHandlerProvider().handler; - stubVis1.isHierarchical = () => true; + let id1 = 1; + stubVis1 = new Vis(indexPattern, { + type: 'pie', + aggs: rowAgg, + }); - // We need to set the aggs to a known value. - _.each(stubVis1.aggs.aggs, function(agg) { - agg.id = 'agg_' + id1++; - }); - }) - ); + stubVis1.isHierarchical = () => true; + + // We need to set the aggs to a known value. + _.each(stubVis1.aggs.aggs, function(agg) { + agg.id = 'agg_' + id1++; + }); + }); beforeEach(async () => { const table1 = tabifyAggResponse(stubVis1.aggs, fixtures.threeTermBuckets, { @@ -159,7 +152,7 @@ describe('No global chart settings', function() { }); data1 = await responseHandler(table1, rowAggDimensions); - chart1.render(data1, persistedState); + chart1.render(data1, mockUiState); }); afterEach(function() { @@ -216,40 +209,37 @@ describe('Vislib PieChart Class Test Suite', function() { addTooltip: true, }; let vis; - let persistedState; + let mockUiState; let indexPattern; let data; let stubVis; let responseHandler; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private, $injector) { - const getVis = getFixturesVislibVisFixtureProvider(Private); - vis = getVis(visLibParams); - persistedState = new ($injector.get('PersistedState'))(); - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - responseHandler = vislibSlicesResponseHandlerProvider().handler; - - let id = 1; - stubVis = new Vis(indexPattern, { - type: 'pie', - aggs: dataAgg, - }); + beforeEach(() => { + vis = getVis(visLibParams); + mockUiState = getMockUiState(); + indexPattern = new FixturesStubbedLogstashIndexPatternProvider(); + responseHandler = vislibSlicesResponseHandlerProvider().handler; - // We need to set the aggs to a known value. - _.each(stubVis.aggs.aggs, function(agg) { - agg.id = 'agg_' + id++; - }); - }) - ); + let id = 1; + stubVis = new Vis(indexPattern, { + type: 'pie', + aggs: dataAgg, + }); + + // We need to set the aggs to a known value. + _.each(stubVis.aggs.aggs, function(agg) { + agg.id = 'agg_' + id++; + }); + }); beforeEach(async () => { const table = tabifyAggResponse(stubVis.aggs, fixtures.threeTermBuckets, { metricsAtAllLevels: true, }); data = await responseHandler(table, dataDimensions); - vis.render(data, persistedState); + + vis.render(data, mockUiState); }); afterEach(function() { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js index ec22d43c08cb2b..d69f952325ed02 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js @@ -18,11 +18,11 @@ */ import d3 from 'd3'; +import $ from 'jquery'; import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; + import series from '../lib/fixtures/mock_data/date_histogram/_series'; import terms from '../lib/fixtures/mock_data/terms/_columns'; -import $ from 'jquery'; import { TimeMarker } from '../../visualizations/time_marker'; describe('Vislib Time Marker Test Suite', function() { @@ -57,26 +57,23 @@ describe('Vislib Time Marker Test Suite', function() { let maxDomain; let domain; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - minDomain = getExtent(series.series, d3.min); - maxDomain = getExtent(series.series, d3.max); - domain = [minDomain, maxDomain]; - xScale = d3.time - .scale() - .domain(domain) - .range([0, 500]); - defaultMarker = new TimeMarker(times, xScale, height); - customMarker = new TimeMarker(myTimes, xScale, height); - - selection = d3 - .select('body') - .append('div') - .attr('class', 'marker'); - selection.datum(series); - }) - ); + beforeEach(function() { + minDomain = getExtent(series.series, d3.min); + maxDomain = getExtent(series.series, d3.max); + domain = [minDomain, maxDomain]; + xScale = d3.time + .scale() + .domain(domain) + .range([0, 500]); + defaultMarker = new TimeMarker(times, xScale, height); + customMarker = new TimeMarker(myTimes, xScale, height); + + selection = d3 + .select('body') + .append('div') + .attr('class', 'marker'); + selection.datum(series); + }); afterEach(function() { selection.remove('*'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js index e150ca58c15cfd..c8f0faf8dcca5d 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js @@ -17,20 +17,17 @@ * under the License. */ -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; import _ from 'lodash'; +import expect from '@kbn/expect'; + import { visTypes } from '../../visualizations/vis_types'; describe('Vislib Vis Types Test Suite', function() { let visFunc; - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function() { - visFunc = visTypes.point_series; - }) - ); + beforeEach(function() { + visFunc = visTypes.point_series; + }); it('should be an object', function() { expect(_.isObject(visTypes)).to.be(true); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/_index.scss b/src/legacy/core_plugins/vis_type_vislib/public/vislib/_index.scss index 0344fbb5359ece..78e16224a67a3c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/_index.scss +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/_index.scss @@ -1,6 +1,9 @@ @import './variables'; +@import './vislib_vis_type'; @import './lib/index'; +@import './components/tooltip/index'; +@import './components/legend/index'; @import './visualizations/point_series/index'; @import './visualizations/gauges/index'; diff --git a/src/legacy/ui/public/vis/vis_types/_vislib_vis_type.scss b/src/legacy/core_plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss similarity index 99% rename from src/legacy/ui/public/vis/vis_types/_vislib_vis_type.scss rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss index c1f369d3e3536f..c03aa19140de06 100644 --- a/src/legacy/ui/public/vis/vis_types/_vislib_vis_type.scss +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss @@ -7,12 +7,15 @@ &.visLib--legend-left { flex-direction: row-reverse; } + &.visLib--legend-right { flex-direction: row; } + &.visLib--legend-top { flex-direction: column-reverse; } + &.visLib--legend-bottom { flex-direction: column; } diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/__snapshots__/vislib_vis_legend.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap similarity index 100% rename from src/legacy/ui/public/vis/vis_types/vislib_vis_legend/__snapshots__/vislib_vis_legend.test.tsx.snap rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_index.scss b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_index.scss new file mode 100644 index 00000000000000..53617a984dcf51 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_index.scss @@ -0,0 +1 @@ +@import './_legend'; diff --git a/src/legacy/ui/public/vis/vis_types/_vislib_vis_legend.scss b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss similarity index 85% rename from src/legacy/ui/public/vis/vis_types/_vislib_vis_legend.scss rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss index 62050ce4e99fd3..b1a59f88a348ad 100644 --- a/src/legacy/ui/public/vis/vis_types/_vislib_vis_legend.scss +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss @@ -1,4 +1,4 @@ -@import '../../../../core_plugins/vis_type_vislib/public/vislib/variables'; +@import '../../variables'; // NOTE: Some of the styles attempt to align with the TSVB legend @@ -14,17 +14,16 @@ $visLegendLineHeight: $euiSize; display: flex; padding: $euiSizeXS; background-color: $euiColorEmptyShade; - transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance, - background-color $euiAnimSpeedFast $euiAnimSlightResistance $euiAnimSpeedExtraSlow; + transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance, background-color $euiAnimSpeedFast $euiAnimSlightResistance $euiAnimSpeedExtraSlow; &:focus { box-shadow: none; - background-color: $euiFocusBackgroundColor !important; + background-color: $euiFocusBackgroundColor !important; // sass-lint:disable-line no-important } } .visLegend__toggle--isOpen { - background-color: transparentize($euiColorDarkestShade, 0.9); + background-color: transparentize($euiColorDarkestShade, .9); opacity: 1; } diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/index.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/index.ts similarity index 94% rename from src/legacy/ui/public/vis/vis_types/vislib_vis_legend/index.ts rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/index.ts index ebf132f0ab697f..230cda4c35ff8d 100644 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/index.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { VisLegend } from './vislib_vis_legend'; +export { VisLegend } from './legend'; export { CUSTOM_LEGEND_VIS_TYPES } from './models'; diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx similarity index 96% rename from src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 839dc0024bbea5..6f0a5a3784b070 100644 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -23,7 +23,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; import { EuiButtonGroup } from '@elastic/eui'; -import { VisLegend, VisLegendProps } from '../vislib_vis_legend/vislib_vis_legend'; +import { VisLegend, VisLegendProps } from './legend'; import { legendColors } from './models'; jest.mock('@elastic/eui', () => ({ @@ -31,10 +31,10 @@ jest.mock('@elastic/eui', () => ({ htmlIdGenerator: jest.fn().mockReturnValue(() => 'legendId'), })); -jest.mock('../../../visualize/loader/pipeline_helpers/utilities', () => ({ +jest.mock('../../../legacy_imports', () => ({ getTableAggs: jest.fn(), })); -jest.mock('../../../../../core_plugins/visualizations/public', () => ({ +jest.mock('../../../../../visualizations/public', () => ({ createFiltersFromEvent: jest.fn().mockReturnValue(['yes']), })); diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx similarity index 93% rename from src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.tsx rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index d98590f9885b9d..0eec557dd334ee 100644 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -24,11 +24,11 @@ import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; // @ts-ignore -import { createFiltersFromEvent } from '../../../../../core_plugins/visualizations/public'; +import { createFiltersFromEvent } from '../../../../../visualizations/public'; import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; -import { VisLegendItem } from './vislib_vis_legend_item'; +import { VisLegendItem } from './legend_item'; import { getPieNames } from './pie_utils'; -import { getTableAggs } from '../../../visualize/loader/pipeline_helpers/utilities'; +import { getTableAggs } from '../../../legacy_imports'; export interface VisLegendProps { vis: any; @@ -138,7 +138,7 @@ export class VisLegend extends PureComponent { this.setState({ labels: [ { - label: i18n.translate('common.ui.vis.visTypes.legend.loadingLabel', { + label: i18n.translate('visTypeVislib.vislib.legend.loadingLabel', { defaultMessage: 'loading…', }), }, @@ -244,13 +244,13 @@ export class VisLegend extends PureComponent { className={classNames('visLegend__toggle kbn-resetFocusState', { 'visLegend__toggle--isOpen': open, })} - aria-label={i18n.translate('common.ui.vis.visTypes.legend.toggleLegendButtonAriaLabel', { + aria-label={i18n.translate('visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel', { defaultMessage: 'Toggle legend', })} aria-expanded={Boolean(open)} aria-controls={this.legendId} data-test-subj="vislibToggleLegend" - title={i18n.translate('common.ui.vis.visTypes.legend.toggleLegendButtonTitle', { + title={i18n.translate('visTypeVislib.vislib.legend.toggleLegendButtonTitle', { defaultMessage: 'Toggle legend', })} > diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend_item.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx similarity index 92% rename from src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend_item.tsx rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx index 7376fabfe738be..09c8a838532bf6 100644 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend_item.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx @@ -77,7 +77,7 @@ const VisLegendItemComponent = ({ const filterOptions: EuiButtonGroupOption[] = [ { id: 'filterIn', - label: i18n.translate('common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel', { + label: i18n.translate('visTypeVislib.vislib.legend.filterForValueButtonAriaLabel', { defaultMessage: 'Filter for value {legendDataLabel}', values: { legendDataLabel: item.label }, }), @@ -86,7 +86,7 @@ const VisLegendItemComponent = ({ }, { id: 'filterOut', - label: i18n.translate('common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel', { + label: i18n.translate('visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel', { defaultMessage: 'Filter out value {legendDataLabel}', values: { legendDataLabel: item.label }, }), @@ -105,7 +105,7 @@ const VisLegendItemComponent = ({ type="multi" isIconOnly isFullWidth - legend={i18n.translate('common.ui.vis.visTypes.legend.filterOptionsLegend', { + legend={i18n.translate('visTypeVislib.vislib.legend.filterOptionsLegend', { defaultMessage: '{legendDataLabel}, filter options', values: { legendDataLabel: item.label }, })} @@ -131,7 +131,7 @@ const VisLegendItemComponent = ({ onBlur={onUnhighlight} data-label={item.label} title={item.label} - aria-label={i18n.translate('common.ui.vis.visTypes.legend.toggleOptionsButtonAriaLabel', { + aria-label={i18n.translate('visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel', { defaultMessage: '{legendDataLabel}, toggle options', values: { legendDataLabel: item.label }, })} @@ -163,7 +163,7 @@ const VisLegendItemComponent = ({
diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/models.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/models.ts similarity index 100% rename from src/legacy/ui/public/vis/vis_types/vislib_vis_legend/models.ts rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/models.ts diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/pie_utils.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts similarity index 100% rename from src/legacy/ui/public/vis/vis_types/vislib_vis_legend/pie_utils.ts rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts diff --git a/src/legacy/ui/public/vis/components/tooltip/_collect_branch.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js similarity index 100% rename from src/legacy/ui/public/vis/components/tooltip/_collect_branch.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js diff --git a/src/legacy/ui/public/vis/components/tooltip/_collect_branch.test.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js similarity index 100% rename from src/legacy/ui/public/vis/components/tooltip/_collect_branch.test.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js new file mode 100644 index 00000000000000..22fff9cf6a317f --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import _ from 'lodash'; +import numeral from 'numeral'; +import { renderToStaticMarkup } from 'react-dom/server'; + +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; + +import { collectBranch } from './_collect_branch'; + +export function hierarchicalTooltipFormatter(metricFieldFormatter) { + return function({ datum }) { + // Collect the current leaf and parents into an array of values + const rows = collectBranch(datum); + + // Map those values to what the tooltipSource.rows format. + _.forEachRight(rows, function(row) { + row.spacer = _.escape(_.repeat(' ', row.depth)); + + let percent; + if (row.item.percentOfGroup !== null && row.item.percentOfGroup !== undefined) { + percent = row.item.percentOfGroup; + } + + row.metric = metricFieldFormatter ? metricFieldFormatter.convert(row.metric) : row.metric; + + if (percent !== null && percent !== undefined) { + row.metric += ' (' + numeral(percent).format('0.[00]%') + ')'; + } + + return row; + }); + + return renderToStaticMarkup( + + + + + + + + + + + + {rows.map((row, index) => ( + + + + + + ))} + +
{/* {metricCol.label} */}
+
+ + {row.field} +
+
+
{row.bucket}
+
{row.metric}
+
+ ); + }; +} diff --git a/src/legacy/ui/public/vis/components/tooltip/_index.scss b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss similarity index 100% rename from src/legacy/ui/public/vis/components/tooltip/_index.scss rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js new file mode 100644 index 00000000000000..13c9b8024aac3d --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js @@ -0,0 +1,79 @@ +/* + * 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 { renderToStaticMarkup } from 'react-dom/server'; + +export function pointSeriesTooltipFormatter() { + return function tooltipFormatter({ datum, data }) { + if (!datum) return ''; + + const details = []; + + const currentSeries = data.series && data.series.find(serie => serie.rawId === datum.seriesId); + const addDetail = (label, value) => details.push({ label, value }); + + if (datum.extraMetrics) { + datum.extraMetrics.forEach(metric => { + addDetail(metric.label, metric.value); + }); + } + + if (datum.x !== null && datum.x !== undefined) { + addDetail(data.xAxisLabel, data.xAxisFormatter(datum.x)); + } + + if (datum.y !== null && datum.y !== undefined) { + const value = datum.yScale ? datum.yScale * datum.y : datum.y; + addDetail(currentSeries.label, currentSeries.yAxisFormatter(value)); + } + + if (datum.z !== null && datum.z !== undefined) { + addDetail(currentSeries.zLabel, currentSeries.zAxisFormatter(datum.z)); + } + if (datum.series && datum.parent) { + const dimension = datum.parent; + addDetail(dimension.title, datum.series); + } + if (datum.tableRaw) { + addDetail(datum.tableRaw.title, datum.tableRaw.value); + } + + return renderToStaticMarkup( + + + {details.map((detail, index) => ( + + + + + + ))} + +
+
{detail.label}
+
+
+ {detail.value} + {detail.percent && ({detail.percent})} +
+
+ ); + }; +} diff --git a/src/legacy/ui/public/vis/components/tooltip/_tooltip.scss b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss similarity index 97% rename from src/legacy/ui/public/vis/components/tooltip/_tooltip.scss rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss index 5a30ded7c2e5bd..bafec7edf3b943 100644 --- a/src/legacy/ui/public/vis/components/tooltip/_tooltip.scss +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss @@ -28,7 +28,7 @@ } .visTooltip__header { - margin: 0 0 $euiSizeS 0; + margin: 0 0 $euiSizeS; padding: $euiSizeXS $euiSizeS; display: flex; align-items: center; @@ -41,6 +41,7 @@ margin-top: $euiSizeS; } } + .visTooltip__labelContainer, .visTooltip__valueContainer { overflow-wrap: break-word; diff --git a/src/legacy/ui/public/vis/components/tooltip/index.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/index.js similarity index 80% rename from src/legacy/ui/public/vis/components/tooltip/index.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/index.js index e596a8af865331..e394981125a1f5 100644 --- a/src/legacy/ui/public/vis/components/tooltip/index.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/index.js @@ -17,4 +17,6 @@ * under the License. */ -export { Tooltip, TooltipProvider } from './tooltip'; +export { Tooltip } from './tooltip'; +export { hierarchicalTooltipFormatter } from './_hierarchical_tooltip_formatter'; +export { pointSeriesTooltipFormatter } from './_pointseries_tooltip_formatter'; diff --git a/src/legacy/ui/public/vis/components/tooltip/position_tooltip.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js similarity index 100% rename from src/legacy/ui/public/vis/components/tooltip/position_tooltip.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js diff --git a/src/legacy/ui/public/vis/components/tooltip/tooltip.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js similarity index 97% rename from src/legacy/ui/public/vis/components/tooltip/tooltip.js rename to src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js index 764ab24da67402..f7d29164eec6f1 100644 --- a/src/legacy/ui/public/vis/components/tooltip/tooltip.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js @@ -19,9 +19,10 @@ import d3 from 'd3'; import _ from 'lodash'; -import { Binder } from '../../../binder'; -import { positionTooltip } from './position_tooltip'; import $ from 'jquery'; + +import { Binder } from '../../../legacy_imports'; +import { positionTooltip } from './position_tooltip'; import theme from '@elastic/eui/dist/eui_theme_light.json'; let allContents = []; @@ -217,7 +218,7 @@ Tooltip.prototype.render = function() { return content.id !== id; }); - if (html) allContents.push({ id: id, html: html, order: order }); + if (html) allContents.push({ id, html, order }); const allHtml = _(allContents) .sortBy('order') @@ -253,7 +254,3 @@ Tooltip.prototype.destroy = function() { this.hide(); this.binder.destroy(); }; - -export function TooltipProvider() { - return Tooltip; -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_title.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_title.js index 1c84f98614b05b..3caaf99cbb5c12 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_title.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_title.js @@ -21,7 +21,7 @@ import d3 from 'd3'; import _ from 'lodash'; import { ErrorHandler } from './_error_handler'; -import { Tooltip } from '../../legacy_imports'; +import { Tooltip } from '../components/tooltip'; export class ChartTitle extends ErrorHandler { constructor(visConfig) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.js index eab3bc02f4eec9..ebaf64874d7298 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.js @@ -229,7 +229,7 @@ export const vislibPointSeriesTypes = { const tooManySeries = defaults.charts.length && defaults.charts[0].series.length > cfg.heatmapMaxBuckets; if (hasCharts && tooManySeries) { - defaults.error = i18n.translate('kbnVislibVisTypes.vislib.heatmap.maxBucketsText', { + defaults.error = i18n.translate('visTypeVislib.vislib.heatmap.maxBucketsText', { defaultMessage: 'There are too many series defined ({nr}). The configured maximum is {max}.', values: { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/_chart.js index ac6e8130a846ad..a36c7c4774dadf 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/_chart.js @@ -22,7 +22,12 @@ import _ from 'lodash'; import { dataLabel } from '../lib/_data_label'; import { Dispatch } from '../lib/dispatch'; -import { Tooltip, getFormat } from '../../legacy_imports'; +import { getFormat } from '../../legacy_imports'; +import { + Tooltip, + hierarchicalTooltipFormatter, + pointSeriesTooltipFormatter, +} from '../components/tooltip'; /** * The Base Class for all visualizations. @@ -45,8 +50,8 @@ export class Chart { const fieldFormatter = getFormat(this.handler.data.get('tooltipFormatter')); const tooltipFormatterProvider = this.handler.visConfig.get('type') === 'pie' - ? deps.getHierarchicalTooltipFormatter() - : deps.getPointSeriesTooltipFormatter(); + ? hierarchicalTooltipFormatter + : pointSeriesTooltipFormatter; const tooltipFormatter = tooltipFormatterProvider(fieldFormatter); if (this.handler.visConfig && this.handler.visConfig.get('addTooltip', false)) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series.js index c838c51d34bf5a..53f06c79d178c2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series.js @@ -21,7 +21,7 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import { Tooltip } from '../../legacy_imports'; +import { Tooltip } from '../components/tooltip'; import { Chart } from './_chart'; import { TimeMarker } from './time_marker'; import { seriesTypes } from './point_series/series_types'; @@ -153,18 +153,10 @@ export class PointSeries extends Chart { .attr('class', 'endzone') .append('rect') .attr('class', 'zone') - .attr('x', function(d) { - return isHorizontal ? d.x : 0; - }) - .attr('y', function(d) { - return isHorizontal ? 0 : d.x; - }) - .attr('height', function(d) { - return isHorizontal ? height : d.w; - }) - .attr('width', function(d) { - return isHorizontal ? d.w : width; - }); + .attr('x', d => (isHorizontal ? d.x : 0)) + .attr('y', d => (isHorizontal ? 0 : d.x)) + .attr('height', d => (isHorizontal ? height : d.w)) + .attr('width', d => (isHorizontal ? d.w : width)); function callPlay(event) { const boundData = event.target.__data__; diff --git a/src/legacy/ui/public/vis/_index.scss b/src/legacy/ui/public/vis/_index.scss index 4fb07557977d31..36d586abdb147b 100644 --- a/src/legacy/ui/public/vis/_index.scss +++ b/src/legacy/ui/public/vis/_index.scss @@ -1,4 +1,2 @@ -@import './components/index'; @import './editors/index'; @import './map/index'; -@import './vis_types/index'; diff --git a/src/legacy/ui/public/vis/components/_index.scss b/src/legacy/ui/public/vis/components/_index.scss deleted file mode 100644 index 0d79aa9c458aca..00000000000000 --- a/src/legacy/ui/public/vis/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './tooltip/index'; diff --git a/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip.html b/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip.html deleted file mode 100644 index 43c4793384bf13..00000000000000 --- a/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - -
{{metricCol.label}}
-
{{row.field}}
-
-
{{row.bucket}} -
{{row.metric}}
diff --git a/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip_formatter.js b/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip_formatter.js deleted file mode 100644 index aef7bc3913a49c..00000000000000 --- a/src/legacy/ui/public/vis/components/tooltip/_hierarchical_tooltip_formatter.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 _ from 'lodash'; -import $ from 'jquery'; - -import chrome from 'ui/chrome'; - -import { collectBranch } from './_collect_branch'; -import numeral from 'numeral'; -import template from './_hierarchical_tooltip.html'; - -export function HierarchicalTooltipFormatterProvider($rootScope, $compile, $sce) { - const $tooltip = $(template); - const $tooltipScope = $rootScope.$new(); - - $compile($tooltip)($tooltipScope); - - return function(metricFieldFormatter) { - return function(event) { - const datum = event.datum; - - // Collect the current leaf and parents into an array of values - $tooltipScope.rows = collectBranch(datum); - - // Map those values to what the tooltipSource.rows format. - _.forEachRight($tooltipScope.rows, function(row) { - row.spacer = $sce.trustAsHtml(_.repeat(' ', row.depth)); - - let percent; - if (row.item.percentOfGroup != null) { - percent = row.item.percentOfGroup; - } - - row.metric = metricFieldFormatter ? metricFieldFormatter.convert(row.metric) : row.metric; - - if (percent != null) { - row.metric += ' (' + numeral(percent).format('0.[00]%') + ')'; - } - - return row; - }); - - $tooltipScope.$apply(); - return $tooltip[0].outerHTML; - }; - }; -} - -let _tooltipFormatter; -export const getHierarchicalTooltipFormatter = () => { - if (!_tooltipFormatter) { - throw new Error('tooltip formatter not initialized'); - } - return _tooltipFormatter; -}; - -export const initializeHierarchicalTooltipFormatter = async () => { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private'); - _tooltipFormatter = Private(HierarchicalTooltipFormatterProvider); -}; - -export const setHierarchicalTooltipFormatter = Private => { - _tooltipFormatter = Private(HierarchicalTooltipFormatterProvider); -}; diff --git a/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip.html b/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip.html deleted file mode 100644 index 9e82739a57f0f2..00000000000000 --- a/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - -
-
{{detail.label}}
-
-
{{detail.value}} ({{detail.percent}})
-
diff --git a/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip_formatter.js b/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip_formatter.js deleted file mode 100644 index 88c9e3d67b4a93..00000000000000 --- a/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip_formatter.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 $ from 'jquery'; - -import chrome from 'ui/chrome'; - -import template from './_pointseries_tooltip.html'; - -export function PointSeriesTooltipFormatterProvider($compile, $rootScope) { - const $tooltipScope = $rootScope.$new(); - const $tooltip = $(template); - $compile($tooltip)($tooltipScope); - - return function() { - return function tooltipFormatter(event) { - const data = event.data; - const datum = event.datum; - if (!datum) return ''; - - const details = ($tooltipScope.details = []); - - const currentSeries = - data.series && data.series.find(serie => serie.rawId === datum.seriesId); - const addDetail = (label, value) => details.push({ label, value }); - - if (datum.extraMetrics) { - datum.extraMetrics.forEach(metric => { - addDetail(metric.label, metric.value); - }); - } - - if (datum.x) { - addDetail(data.xAxisLabel, data.xAxisFormatter(datum.x)); - } - if (datum.y) { - const value = datum.yScale ? datum.yScale * datum.y : datum.y; - addDetail(currentSeries.label, currentSeries.yAxisFormatter(value)); - } - if (datum.z) { - addDetail(currentSeries.zLabel, currentSeries.zAxisFormatter(datum.z)); - } - if (datum.series && datum.parent) { - const dimension = datum.parent; - addDetail(dimension.title, datum.series); - } - if (datum.tableRaw) { - addDetail(datum.tableRaw.title, datum.tableRaw.value); - } - - $tooltipScope.$apply(); - return $tooltip[0].outerHTML; - }; - }; -} - -let _tooltipFormatter; -export const getPointSeriesTooltipFormatter = () => { - if (!_tooltipFormatter) { - throw new Error('tooltip formatter not initialized'); - } - return _tooltipFormatter; -}; - -export const initializePointSeriesTooltipFormatter = async () => { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private'); - _tooltipFormatter = Private(PointSeriesTooltipFormatterProvider); -}; - -export const setPointSeriesTooltipFormatter = Private => { - _tooltipFormatter = Private(PointSeriesTooltipFormatterProvider); -}; diff --git a/src/legacy/ui/public/vis/vis_types/_index.scss b/src/legacy/ui/public/vis/vis_types/_index.scss deleted file mode 100644 index 9d86383ec40b2e..00000000000000 --- a/src/legacy/ui/public/vis/vis_types/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './vislib_vis_type'; -@import './vislib_vis_legend'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c3e46a9c04e1cc..88fd8360ec728f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -77,8 +77,6 @@ }, "messages": { "common.ui.aggResponse.allDocsTitle": "すべてのドキュメント", - "common.ui.aggResponse.fieldLabel": "フィールド", - "common.ui.aggResponse.valueLabel": "値", "common.ui.aggTypes.aggNotValidLabel": "- 無効な集約 -", "common.ui.aggTypes.aggregateWith.noAggsErrorTooltip": "選択されたフィールドには互換性のある集約がありません。", "common.ui.aggTypes.aggregateWithLabel": "アグリゲーション:", @@ -509,13 +507,6 @@ "common.ui.vis.editors.sidebar.tabs.optionsLabel": "オプション", "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", "common.ui.vis.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、Elasticsearch と Kibana の {defaultDistribution} にアップグレードしてください。{ems} でより多くのズームレベルが利用できます。または、独自のマップサーバーを構成できます。詳細は { wms } または { configSettings} をご覧ください。", - "common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", - "common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", - "common.ui.vis.visTypes.legend.loadingLabel": "読み込み中…", - "common.ui.vis.visTypes.legend.setColorScreenReaderDescription": "値 {legendDataLabel} の色を設定", - "common.ui.vis.visTypes.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", - "common.ui.vis.visTypes.legend.toggleLegendButtonTitle": "凡例を切り替える", - "common.ui.vis.visTypes.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", "common.ui.vislib.colormaps.bluesText": "青", "common.ui.vislib.colormaps.greensText": "緑", "common.ui.vislib.colormaps.greenToRedText": "緑から赤", @@ -2483,183 +2474,192 @@ "kbn.visualize.wizard.step1Breadcrumb": "作成", "kbn.visualize.wizard.step2Breadcrumb": "作成", "kbn.visualizeTitle": "可視化", - "kbnVislibVisTypes.area.areaDescription": "折れ線グラフの下の数量を強調します。", - "kbnVislibVisTypes.area.areaTitle": "エリア", - "kbnVislibVisTypes.area.countText": "カウント", - "kbnVislibVisTypes.area.groupTitle": "系列を分割", - "kbnVislibVisTypes.area.metricsTitle": "Y 軸", - "kbnVislibVisTypes.area.radiusTitle": "点のサイズ", - "kbnVislibVisTypes.area.segmentTitle": "X 軸", - "kbnVislibVisTypes.area.splitTitle": "チャートを分割", - "kbnVislibVisTypes.area.tabs.metricsAxesTitle": "メトリックと軸", - "kbnVislibVisTypes.area.tabs.panelSettingsTitle": "パネル設定", - "kbnVislibVisTypes.axisModes.normalText": "標準", - "kbnVislibVisTypes.axisModes.percentageText": "パーセンテージ", - "kbnVislibVisTypes.axisModes.silhouetteText": "シルエット", - "kbnVislibVisTypes.axisModes.wiggleText": "振動", - "kbnVislibVisTypes.categoryAxis.rotate.angledText": "傾斜", - "kbnVislibVisTypes.categoryAxis.rotate.horizontalText": "横", - "kbnVislibVisTypes.categoryAxis.rotate.verticalText": "縦", - "kbnVislibVisTypes.chartModes.normalText": "標準", - "kbnVislibVisTypes.chartModes.stackedText": "スタック", - "kbnVislibVisTypes.chartTypes.areaText": "エリア", - "kbnVislibVisTypes.chartTypes.barText": "バー", - "kbnVislibVisTypes.chartTypes.lineText": "折れ線", - "kbnVislibVisTypes.controls.colorRanges.errorText": "各範囲は前の範囲よりも大きくなければなりません。", - "kbnVislibVisTypes.controls.colorSchema.colorSchemaLabel": "カラー図表", - "kbnVislibVisTypes.controls.colorSchema.howToChangeColorsDescription": "それぞれの色は凡例で変更できます。", - "kbnVislibVisTypes.controls.colorSchema.resetColorsButtonLabel": "色をリセット", - "kbnVislibVisTypes.controls.colorSchema.reverseColorSchemaLabel": "図表を反転", - "kbnVislibVisTypes.controls.gaugeOptions.alignmentLabel": "アラインメント", - "kbnVislibVisTypes.controls.gaugeOptions.autoExtendRangeLabel": "範囲を自動拡張", - "kbnVislibVisTypes.controls.gaugeOptions.displayWarningsLabel": "警告を表示", - "kbnVislibVisTypes.controls.gaugeOptions.extendRangeTooltip": "範囲をデータの最高値に広げます。", - "kbnVislibVisTypes.controls.gaugeOptions.gaugeTypeLabel": "ゲージタイプ", - "kbnVislibVisTypes.controls.gaugeOptions.labelsTitle": "ラベル", - "kbnVislibVisTypes.controls.gaugeOptions.percentageModeLabel": "パーセンテージモード", - "kbnVislibVisTypes.controls.gaugeOptions.rangesTitle": "範囲", - "kbnVislibVisTypes.controls.gaugeOptions.showLabelsLabel": "ラベルを表示", - "kbnVislibVisTypes.controls.gaugeOptions.showLegendLabel": "凡例を表示", - "kbnVislibVisTypes.controls.gaugeOptions.showScaleLabel": "縮尺を表示", - "kbnVislibVisTypes.controls.gaugeOptions.styleTitle": "スタイル", - "kbnVislibVisTypes.controls.gaugeOptions.subTextLabel": "サブラベル", - "kbnVislibVisTypes.controls.gaugeOptions.switchWarningsTooltip": "警告のオン・オフを切り替えます。オンにすると、すべてのラベルを表示できない際に警告が表示されます。", - "kbnVislibVisTypes.controls.heatmapOptions.colorLabel": "色", - "kbnVislibVisTypes.controls.heatmapOptions.colorScaleLabel": "カラースケール", - "kbnVislibVisTypes.controls.heatmapOptions.colorsNumberLabel": "色の数", - "kbnVislibVisTypes.controls.heatmapOptions.labelsTitle": "ラベル", - "kbnVislibVisTypes.controls.heatmapOptions.overwriteAutomaticColorLabel": "自動からーを上書きする", - "kbnVislibVisTypes.controls.heatmapOptions.percentageModeLabel": "パーセンテージモード", - "kbnVislibVisTypes.controls.heatmapOptions.rotateLabel": "回転", - "kbnVislibVisTypes.controls.heatmapOptions.scaleToDataBoundsLabel": "データバウンドに合わせる", - "kbnVislibVisTypes.controls.heatmapOptions.showLabelsTitle": "ラベルを表示", - "kbnVislibVisTypes.controls.heatmapOptions.useCustomRangesLabel": "カスタム範囲を使用", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.alignLabel": "配置", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.filterLabelsLabel": "フィルターラベル", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.labelsTitle": "ラベル", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.positionLabel": "配置", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.showLabel": "表示", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.showLabelsLabel": "ラベルを表示", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.xAxisTitle": "X 軸", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.dontShowLabel": "非表示", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.gridText": "グリッド", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.xAxisLinesLabel": "X 軸線を表示", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "ヒストグラムに X 軸線は表示できません。", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 軸線を表示", - "kbnVislibVisTypes.controls.pointSeries.series.chartTypeLabel": "チャートタイプ", - "kbnVislibVisTypes.controls.pointSeries.series.lineModeLabel": "線のモード", - "kbnVislibVisTypes.controls.pointSeries.series.lineWidthLabel": "線の幅", - "kbnVislibVisTypes.controls.pointSeries.series.metricsTitle": "メトリック", - "kbnVislibVisTypes.controls.pointSeries.series.modeLabel": "モード", - "kbnVislibVisTypes.controls.pointSeries.series.newAxisLabel": "新規軸…", - "kbnVislibVisTypes.controls.pointSeries.series.showDotsLabel": "点を表示", - "kbnVislibVisTypes.controls.pointSeries.series.showLineLabel": "線を表示", - "kbnVislibVisTypes.controls.pointSeries.series.valueAxisLabel": "値軸", - "kbnVislibVisTypes.controls.pointSeries.seriesAccordionAriaLabel": "{agg} オプションを切り替える", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.addButtonTooltip": "Y 軸を追加します", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.customExtentsLabel": "カスタム範囲", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.maxLabel": "最高", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minErrorMessage": "最低値は最高値よりも低く設定する必要があります", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minLabel": "最低", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minNeededScaleText": "ログスケールが選択されている場合、最低値は 0 よりも大きいものである必要があります", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.modeLabel": "モード", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.positionLabel": "配置", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.removeButtonTooltip": "Y 軸を削除します", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "境界マージン", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "境界マージンは 0 以上でなければなりません", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "データバウンドに合わせる", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleTypeLabel": "スケールタイプ", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.setAxisExtentsLabel": "軸の範囲の設定", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.showLabel": "表示", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.titleLabel": "タイトル", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "カスタム範囲を切り替える", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "{axisName} オプションを切り替える", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.yAxisTitle": "Y 軸", - "kbnVislibVisTypes.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません", - "kbnVislibVisTypes.controls.truncateLabel": "切り捨て", - "kbnVislibVisTypes.controls.vislibBasicOptions.legendPositionLabel": "凡例の配置", - "kbnVislibVisTypes.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", - "kbnVislibVisTypes.editors.heatmap.basicSettingsTitle": "基本設定", - "kbnVislibVisTypes.editors.heatmap.heatmapSettingsTitle": "ヒートマップ設定", - "kbnVislibVisTypes.editors.heatmap.highlightLabel": "ハイライト範囲", - "kbnVislibVisTypes.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr}).構成されている最高値は {max} です。", - "kbnVislibVisTypes.editors.heatmap.highlightLabelTooltip": "チャートのカーソルを当てた部分と凡例の対応するラベルをハイライトします。", - "kbnVislibVisTypes.editors.pie.donutLabel": "ドーナッツ", - "kbnVislibVisTypes.editors.pie.labelsSettingsTitle": "ラベル設定", - "kbnVislibVisTypes.editors.pie.pieSettingsTitle": "パイ設定", - "kbnVislibVisTypes.editors.pie.showLabelsLabel": "ラベルを表示", - "kbnVislibVisTypes.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", - "kbnVislibVisTypes.editors.pie.showValuesLabel": "値を表示", - "kbnVislibVisTypes.editors.pointSeries.currentTimeMarkerLabel": "現在時刻マーカー", - "kbnVislibVisTypes.editors.pointSeries.orderBucketsBySumLabel": "バケットを合計で並べ替え", - "kbnVislibVisTypes.editors.pointSeries.settingsTitle": "設定", - "kbnVislibVisTypes.editors.pointSeries.showLabels": "チャートに値を表示", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.colorLabel": "ラインカラー", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.showLabel": "しきい線を表示", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.styleLabel": "ラインスタイル", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.valueLabel": "しきい値", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.widthLabel": "線の幅", - "kbnVislibVisTypes.editors.pointSeries.thresholdLineSettingsTitle": "しきい線", - "kbnVislibVisTypes.functions.pie.help": "パイビジュアライゼーション", - "kbnVislibVisTypes.functions.vislib.help": "Vislib ビジュアライゼーション", - "kbnVislibVisTypes.gauge.alignmentAutomaticTitle": "自動", - "kbnVislibVisTypes.gauge.alignmentHorizontalTitle": "横", - "kbnVislibVisTypes.gauge.alignmentVerticalTitle": "縦", - "kbnVislibVisTypes.gauge.gaugeDescription": "ゲージはメトリックのステータスを示します。メトリックの値としきい値との関連性を示すのに使用します。", - "kbnVislibVisTypes.gauge.gaugeTitle": "ゲージ", - "kbnVislibVisTypes.gauge.gaugeTypes.arcText": "弧形", - "kbnVislibVisTypes.gauge.gaugeTypes.circleText": "円", - "kbnVislibVisTypes.gauge.groupTitle": "グループを分割", - "kbnVislibVisTypes.gauge.metricTitle": "メトリック", - "kbnVislibVisTypes.goal.goalDescription": "ゴールチャートは、最終目標にどれだけ近いかを示します。", - "kbnVislibVisTypes.goal.goalTitle": "ゴール", - "kbnVislibVisTypes.goal.groupTitle": "グループを分割", - "kbnVislibVisTypes.goal.metricTitle": "メトリック", - "kbnVislibVisTypes.heatmap.groupTitle": "Y 軸", - "kbnVislibVisTypes.heatmap.heatmapDescription": "マトリックス内のセルに影をつける。", - "kbnVislibVisTypes.heatmap.heatmapTitle": "ヒートマップ", - "kbnVislibVisTypes.heatmap.metricTitle": "値", - "kbnVislibVisTypes.heatmap.segmentTitle": "X 軸", - "kbnVislibVisTypes.heatmap.splitTitle": "チャートを分割", - "kbnVislibVisTypes.histogram.groupTitle": "系列を分割", - "kbnVislibVisTypes.histogram.histogramDescription": "連続変数を各軸に割り当てる。", - "kbnVislibVisTypes.histogram.histogramTitle": "縦棒", - "kbnVislibVisTypes.histogram.metricTitle": "Y 軸", - "kbnVislibVisTypes.histogram.radiusTitle": "点のサイズ", - "kbnVislibVisTypes.histogram.segmentTitle": "X 軸", - "kbnVislibVisTypes.histogram.splitTitle": "チャートを分割", - "kbnVislibVisTypes.horizontalBar.groupTitle": "系列を分割", - "kbnVislibVisTypes.horizontalBar.horizontalBarDescription": "連続変数を各軸に割り当てる。", - "kbnVislibVisTypes.horizontalBar.horizontalBarTitle": "横棒", - "kbnVislibVisTypes.horizontalBar.metricTitle": "Y 軸", - "kbnVislibVisTypes.horizontalBar.radiusTitle": "点のサイズ", - "kbnVislibVisTypes.horizontalBar.segmentTitle": "X 軸", - "kbnVislibVisTypes.horizontalBar.splitTitle": "チャートを分割", - "kbnVislibVisTypes.interpolationModes.smoothedText": "スムーズ", - "kbnVislibVisTypes.interpolationModes.steppedText": "ステップ", - "kbnVislibVisTypes.interpolationModes.straightText": "直線", - "kbnVislibVisTypes.legendPositions.bottomText": "一番下", - "kbnVislibVisTypes.legendPositions.leftText": "左", - "kbnVislibVisTypes.legendPositions.rightText": "右", - "kbnVislibVisTypes.legendPositions.topText": "一番上", - "kbnVislibVisTypes.line.groupTitle": "系列を分割", - "kbnVislibVisTypes.line.lineDescription": "トレンドを強調します。", - "kbnVislibVisTypes.line.lineTitle": "折れ線", - "kbnVislibVisTypes.line.metricTitle": "Y 軸", - "kbnVislibVisTypes.line.radiusTitle": "点のサイズ", - "kbnVislibVisTypes.line.segmentTitle": "X 軸", - "kbnVislibVisTypes.line.splitTitle": "チャートを分割", - "kbnVislibVisTypes.pie.metricTitle": "サイズのスライス", - "kbnVislibVisTypes.pie.pieDescription": "全体に対する内訳を表現する。", - "kbnVislibVisTypes.pie.pieTitle": "パイ", - "kbnVislibVisTypes.pie.segmentTitle": "スライスの分割", - "kbnVislibVisTypes.pie.splitTitle": "チャートを分割", - "kbnVislibVisTypes.scaleTypes.linearText": "直線", - "kbnVislibVisTypes.scaleTypes.logText": "ログ", - "kbnVislibVisTypes.scaleTypes.squareRootText": "平方根", - "kbnVislibVisTypes.thresholdLine.style.dashedText": "鎖線", - "kbnVislibVisTypes.thresholdLine.style.dotdashedText": "点線", - "kbnVislibVisTypes.thresholdLine.style.fullText": "完全", + "visTypeVislib.area.areaDescription": "折れ線グラフの下の数量を強調します。", + "visTypeVislib.area.areaTitle": "エリア", + "visTypeVislib.area.countText": "カウント", + "visTypeVislib.area.groupTitle": "系列を分割", + "visTypeVislib.area.metricsTitle": "Y 軸", + "visTypeVislib.area.radiusTitle": "点のサイズ", + "visTypeVislib.area.segmentTitle": "X 軸", + "visTypeVislib.area.splitTitle": "チャートを分割", + "visTypeVislib.area.tabs.metricsAxesTitle": "メトリックと軸", + "visTypeVislib.area.tabs.panelSettingsTitle": "パネル設定", + "visTypeVislib.axisModes.normalText": "標準", + "visTypeVislib.axisModes.percentageText": "パーセンテージ", + "visTypeVislib.axisModes.silhouetteText": "シルエット", + "visTypeVislib.axisModes.wiggleText": "振動", + "visTypeVislib.categoryAxis.rotate.angledText": "傾斜", + "visTypeVislib.categoryAxis.rotate.horizontalText": "横", + "visTypeVislib.categoryAxis.rotate.verticalText": "縦", + "visTypeVislib.chartModes.normalText": "標準", + "visTypeVislib.chartModes.stackedText": "スタック", + "visTypeVislib.chartTypes.areaText": "エリア", + "visTypeVislib.chartTypes.barText": "バー", + "visTypeVislib.chartTypes.lineText": "折れ線", + "visTypeVislib.controls.colorRanges.errorText": "各範囲は前の範囲よりも大きくなければなりません。", + "visTypeVislib.controls.colorSchema.colorSchemaLabel": "カラー図表", + "visTypeVislib.controls.colorSchema.howToChangeColorsDescription": "それぞれの色は凡例で変更できます。", + "visTypeVislib.controls.colorSchema.resetColorsButtonLabel": "色をリセット", + "visTypeVislib.controls.colorSchema.reverseColorSchemaLabel": "図表を反転", + "visTypeVislib.controls.gaugeOptions.alignmentLabel": "アラインメント", + "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "範囲を自動拡張", + "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "警告を表示", + "visTypeVislib.controls.gaugeOptions.extendRangeTooltip": "範囲をデータの最高値に広げます。", + "visTypeVislib.controls.gaugeOptions.gaugeTypeLabel": "ゲージタイプ", + "visTypeVislib.controls.gaugeOptions.labelsTitle": "ラベル", + "visTypeVislib.controls.gaugeOptions.percentageModeLabel": "パーセンテージモード", + "visTypeVislib.controls.gaugeOptions.rangesTitle": "範囲", + "visTypeVislib.controls.gaugeOptions.showLabelsLabel": "ラベルを表示", + "visTypeVislib.controls.gaugeOptions.showLegendLabel": "凡例を表示", + "visTypeVislib.controls.gaugeOptions.showScaleLabel": "縮尺を表示", + "visTypeVislib.controls.gaugeOptions.styleTitle": "スタイル", + "visTypeVislib.controls.gaugeOptions.subTextLabel": "サブラベル", + "visTypeVislib.controls.gaugeOptions.switchWarningsTooltip": "警告のオン・オフを切り替えます。オンにすると、すべてのラベルを表示できない際に警告が表示されます。", + "visTypeVislib.controls.heatmapOptions.colorLabel": "色", + "visTypeVislib.controls.heatmapOptions.colorScaleLabel": "カラースケール", + "visTypeVislib.controls.heatmapOptions.colorsNumberLabel": "色の数", + "visTypeVislib.controls.heatmapOptions.labelsTitle": "ラベル", + "visTypeVislib.controls.heatmapOptions.overwriteAutomaticColorLabel": "自動からーを上書きする", + "visTypeVislib.controls.heatmapOptions.percentageModeLabel": "パーセンテージモード", + "visTypeVislib.controls.heatmapOptions.rotateLabel": "回転", + "visTypeVislib.controls.heatmapOptions.scaleToDataBoundsLabel": "データバウンドに合わせる", + "visTypeVislib.controls.heatmapOptions.showLabelsTitle": "ラベルを表示", + "visTypeVislib.controls.heatmapOptions.useCustomRangesLabel": "カスタム範囲を使用", + "visTypeVislib.controls.pointSeries.categoryAxis.alignLabel": "配置", + "visTypeVislib.controls.pointSeries.categoryAxis.filterLabelsLabel": "フィルターラベル", + "visTypeVislib.controls.pointSeries.categoryAxis.labelsTitle": "ラベル", + "visTypeVislib.controls.pointSeries.categoryAxis.positionLabel": "配置", + "visTypeVislib.controls.pointSeries.categoryAxis.showLabel": "表示", + "visTypeVislib.controls.pointSeries.categoryAxis.showLabelsLabel": "ラベルを表示", + "visTypeVislib.controls.pointSeries.categoryAxis.xAxisTitle": "X 軸", + "visTypeVislib.controls.pointSeries.gridAxis.dontShowLabel": "非表示", + "visTypeVislib.controls.pointSeries.gridAxis.gridText": "グリッド", + "visTypeVislib.controls.pointSeries.gridAxis.xAxisLinesLabel": "X 軸線を表示", + "visTypeVislib.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "ヒストグラムに X 軸線は表示できません。", + "visTypeVislib.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 軸線を表示", + "visTypeVislib.controls.pointSeries.series.chartTypeLabel": "チャートタイプ", + "visTypeVislib.controls.pointSeries.series.lineModeLabel": "線のモード", + "visTypeVislib.controls.pointSeries.series.lineWidthLabel": "線の幅", + "visTypeVislib.controls.pointSeries.series.metricsTitle": "メトリック", + "visTypeVislib.controls.pointSeries.series.modeLabel": "モード", + "visTypeVislib.controls.pointSeries.series.newAxisLabel": "新規軸…", + "visTypeVislib.controls.pointSeries.series.showDotsLabel": "点を表示", + "visTypeVislib.controls.pointSeries.series.showLineLabel": "線を表示", + "visTypeVislib.controls.pointSeries.series.valueAxisLabel": "値軸", + "visTypeVislib.controls.pointSeries.seriesAccordionAriaLabel": "{agg} オプションを切り替える", + "visTypeVislib.controls.pointSeries.valueAxes.addButtonTooltip": "Y 軸を追加します", + "visTypeVislib.controls.pointSeries.valueAxes.customExtentsLabel": "カスタム範囲", + "visTypeVislib.controls.pointSeries.valueAxes.maxLabel": "最高", + "visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage": "最低値は最高値よりも低く設定する必要があります", + "visTypeVislib.controls.pointSeries.valueAxes.minLabel": "最低", + "visTypeVislib.controls.pointSeries.valueAxes.minNeededScaleText": "ログスケールが選択されている場合、最低値は 0 よりも大きいものである必要があります", + "visTypeVislib.controls.pointSeries.valueAxes.modeLabel": "モード", + "visTypeVislib.controls.pointSeries.valueAxes.positionLabel": "配置", + "visTypeVislib.controls.pointSeries.valueAxes.removeButtonTooltip": "Y 軸を削除します", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "境界マージン", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "境界マージンは 0 以上でなければなりません", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "データバウンドに合わせる", + "visTypeVislib.controls.pointSeries.valueAxes.scaleTypeLabel": "スケールタイプ", + "visTypeVislib.controls.pointSeries.valueAxes.setAxisExtentsLabel": "軸の範囲の設定", + "visTypeVislib.controls.pointSeries.valueAxes.showLabel": "表示", + "visTypeVislib.controls.pointSeries.valueAxes.titleLabel": "タイトル", + "visTypeVislib.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "カスタム範囲を切り替える", + "visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "{axisName} オプションを切り替える", + "visTypeVislib.controls.pointSeries.valueAxes.yAxisTitle": "Y 軸", + "visTypeVislib.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません", + "visTypeVislib.controls.truncateLabel": "切り捨て", + "visTypeVislib.controls.vislibBasicOptions.legendPositionLabel": "凡例の配置", + "visTypeVislib.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", + "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本設定", + "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "ヒートマップ設定", + "visTypeVislib.editors.heatmap.highlightLabel": "ハイライト範囲", + "visTypeVislib.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr}).構成されている最高値は {max} です。", + "visTypeVislib.editors.heatmap.highlightLabelTooltip": "チャートのカーソルを当てた部分と凡例の対応するラベルをハイライトします。", + "visTypeVislib.editors.pie.donutLabel": "ドーナッツ", + "visTypeVislib.editors.pie.labelsSettingsTitle": "ラベル設定", + "visTypeVislib.editors.pie.pieSettingsTitle": "パイ設定", + "visTypeVislib.editors.pie.showLabelsLabel": "ラベルを表示", + "visTypeVislib.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", + "visTypeVislib.editors.pie.showValuesLabel": "値を表示", + "visTypeVislib.editors.pointSeries.currentTimeMarkerLabel": "現在時刻マーカー", + "visTypeVislib.editors.pointSeries.orderBucketsBySumLabel": "バケットを合計で並べ替え", + "visTypeVislib.editors.pointSeries.settingsTitle": "設定", + "visTypeVislib.editors.pointSeries.showLabels": "チャートに値を表示", + "visTypeVislib.editors.pointSeries.thresholdLine.colorLabel": "ラインカラー", + "visTypeVislib.editors.pointSeries.thresholdLine.showLabel": "しきい線を表示", + "visTypeVislib.editors.pointSeries.thresholdLine.styleLabel": "ラインスタイル", + "visTypeVislib.editors.pointSeries.thresholdLine.valueLabel": "しきい値", + "visTypeVislib.editors.pointSeries.thresholdLine.widthLabel": "線の幅", + "visTypeVislib.editors.pointSeries.thresholdLineSettingsTitle": "しきい線", + "visTypeVislib.functions.pie.help": "パイビジュアライゼーション", + "visTypeVislib.functions.vislib.help": "Vislib ビジュアライゼーション", + "visTypeVislib.gauge.alignmentAutomaticTitle": "自動", + "visTypeVislib.gauge.alignmentHorizontalTitle": "横", + "visTypeVislib.gauge.alignmentVerticalTitle": "縦", + "visTypeVislib.gauge.gaugeDescription": "ゲージはメトリックのステータスを示します。メトリックの値としきい値との関連性を示すのに使用します。", + "visTypeVislib.gauge.gaugeTitle": "ゲージ", + "visTypeVislib.gauge.gaugeTypes.arcText": "弧形", + "visTypeVislib.gauge.gaugeTypes.circleText": "円", + "visTypeVislib.gauge.groupTitle": "グループを分割", + "visTypeVislib.gauge.metricTitle": "メトリック", + "visTypeVislib.goal.goalDescription": "ゴールチャートは、最終目標にどれだけ近いかを示します。", + "visTypeVislib.goal.goalTitle": "ゴール", + "visTypeVislib.goal.groupTitle": "グループを分割", + "visTypeVislib.goal.metricTitle": "メトリック", + "visTypeVislib.heatmap.groupTitle": "Y 軸", + "visTypeVislib.heatmap.heatmapDescription": "マトリックス内のセルに影をつける。", + "visTypeVislib.heatmap.heatmapTitle": "ヒートマップ", + "visTypeVislib.heatmap.metricTitle": "値", + "visTypeVislib.heatmap.segmentTitle": "X 軸", + "visTypeVislib.heatmap.splitTitle": "チャートを分割", + "visTypeVislib.histogram.groupTitle": "系列を分割", + "visTypeVislib.histogram.histogramDescription": "連続変数を各軸に割り当てる。", + "visTypeVislib.histogram.histogramTitle": "縦棒", + "visTypeVislib.histogram.metricTitle": "Y 軸", + "visTypeVislib.histogram.radiusTitle": "点のサイズ", + "visTypeVislib.histogram.segmentTitle": "X 軸", + "visTypeVislib.histogram.splitTitle": "チャートを分割", + "visTypeVislib.horizontalBar.groupTitle": "系列を分割", + "visTypeVislib.horizontalBar.horizontalBarDescription": "連続変数を各軸に割り当てる。", + "visTypeVislib.horizontalBar.horizontalBarTitle": "横棒", + "visTypeVislib.horizontalBar.metricTitle": "Y 軸", + "visTypeVislib.horizontalBar.radiusTitle": "点のサイズ", + "visTypeVislib.horizontalBar.segmentTitle": "X 軸", + "visTypeVislib.horizontalBar.splitTitle": "チャートを分割", + "visTypeVislib.interpolationModes.smoothedText": "スムーズ", + "visTypeVislib.interpolationModes.steppedText": "ステップ", + "visTypeVislib.interpolationModes.straightText": "直線", + "visTypeVislib.legendPositions.bottomText": "一番下", + "visTypeVislib.legendPositions.leftText": "左", + "visTypeVislib.legendPositions.rightText": "右", + "visTypeVislib.legendPositions.topText": "一番上", + "visTypeVislib.line.groupTitle": "系列を分割", + "visTypeVislib.line.lineDescription": "トレンドを強調します。", + "visTypeVislib.line.lineTitle": "折れ線", + "visTypeVislib.line.metricTitle": "Y 軸", + "visTypeVislib.line.radiusTitle": "点のサイズ", + "visTypeVislib.line.segmentTitle": "X 軸", + "visTypeVislib.line.splitTitle": "チャートを分割", + "visTypeVislib.pie.metricTitle": "サイズのスライス", + "visTypeVislib.pie.pieDescription": "全体に対する内訳を表現する。", + "visTypeVislib.pie.pieTitle": "パイ", + "visTypeVislib.pie.segmentTitle": "スライスの分割", + "visTypeVislib.pie.splitTitle": "チャートを分割", + "visTypeVislib.scaleTypes.linearText": "直線", + "visTypeVislib.scaleTypes.logText": "ログ", + "visTypeVislib.scaleTypes.squareRootText": "平方根", + "visTypeVislib.thresholdLine.style.dashedText": "鎖線", + "visTypeVislib.thresholdLine.style.dotdashedText": "点線", + "visTypeVislib.thresholdLine.style.fullText": "完全", + "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", + "visTypeVislib.vislib.tooltip.valueLabel": "値", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", + "visTypeVislib.vislib.legend.loadingLabel": "読み込み中…", + "visTypeVislib.vislib.legend.setColorScreenReaderDescription": "値 {legendDataLabel} の色を設定", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "凡例を切り替える", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "全画面を終了", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "ESC キーで全画面モードを終了します。", "kibana-react.savedObjects.finder.filterButtonLabel": "タイプ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c40b8c8d0393c8..ad62993d50f062 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -77,8 +77,6 @@ }, "messages": { "common.ui.aggResponse.allDocsTitle": "所有文档", - "common.ui.aggResponse.fieldLabel": "字段", - "common.ui.aggResponse.valueLabel": "值", "common.ui.aggTypes.aggNotValidLabel": "- 聚合无效 -", "common.ui.aggTypes.aggregateWith.noAggsErrorTooltip": "选择的字段没有兼容的聚合。", "common.ui.aggTypes.aggregateWithLabel": "聚合对象", @@ -509,13 +507,6 @@ "common.ui.vis.editors.sidebar.tabs.optionsLabel": "选项", "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", "common.ui.vis.kibanaMap.zoomWarning": "已达到缩放级别最大数目。要一直放大,请升级到 Elasticsearch 和 Kibana 的 {defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", - "common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", - "common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", - "common.ui.vis.visTypes.legend.loadingLabel": "正在加载……", - "common.ui.vis.visTypes.legend.setColorScreenReaderDescription": "为值 {legendDataLabel} 设置颜色", - "common.ui.vis.visTypes.legend.toggleLegendButtonAriaLabel": "切换图例", - "common.ui.vis.visTypes.legend.toggleLegendButtonTitle": "切换图例", - "common.ui.vis.visTypes.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}切换选项", "common.ui.vislib.colormaps.bluesText": "蓝色", "common.ui.vislib.colormaps.greensText": "绿色", "common.ui.vislib.colormaps.greenToRedText": "绿到红", @@ -2483,183 +2474,192 @@ "kbn.visualize.wizard.step1Breadcrumb": "创建", "kbn.visualize.wizard.step2Breadcrumb": "创建", "kbn.visualizeTitle": "可视化", - "kbnVislibVisTypes.area.areaDescription": "突出折线图下方的数量", - "kbnVislibVisTypes.area.areaTitle": "面积图", - "kbnVislibVisTypes.area.countText": "计数", - "kbnVislibVisTypes.area.groupTitle": "拆分序列", - "kbnVislibVisTypes.area.metricsTitle": "Y 轴", - "kbnVislibVisTypes.area.radiusTitle": "点大小", - "kbnVislibVisTypes.area.segmentTitle": "X 轴", - "kbnVislibVisTypes.area.splitTitle": "拆分图表", - "kbnVislibVisTypes.area.tabs.metricsAxesTitle": "指标和轴", - "kbnVislibVisTypes.area.tabs.panelSettingsTitle": "面板设置", - "kbnVislibVisTypes.axisModes.normalText": "正常", - "kbnVislibVisTypes.axisModes.percentageText": "百分比", - "kbnVislibVisTypes.axisModes.silhouetteText": "剪影", - "kbnVislibVisTypes.axisModes.wiggleText": "扭动", - "kbnVislibVisTypes.categoryAxis.rotate.angledText": "带角度", - "kbnVislibVisTypes.categoryAxis.rotate.horizontalText": "水平", - "kbnVislibVisTypes.categoryAxis.rotate.verticalText": "垂直", - "kbnVislibVisTypes.chartModes.normalText": "正常", - "kbnVislibVisTypes.chartModes.stackedText": "堆叠", - "kbnVislibVisTypes.chartTypes.areaText": "面积图", - "kbnVislibVisTypes.chartTypes.barText": "条形图", - "kbnVislibVisTypes.chartTypes.lineText": "折线图", - "kbnVislibVisTypes.controls.colorRanges.errorText": "每个范围应大于前一范围。", - "kbnVislibVisTypes.controls.colorSchema.colorSchemaLabel": "颜色模式", - "kbnVislibVisTypes.controls.colorSchema.howToChangeColorsDescription": "可以更改图例中的各个颜色。", - "kbnVislibVisTypes.controls.colorSchema.resetColorsButtonLabel": "重置颜色", - "kbnVislibVisTypes.controls.colorSchema.reverseColorSchemaLabel": "反转模式", - "kbnVislibVisTypes.controls.gaugeOptions.alignmentLabel": "对齐方式", - "kbnVislibVisTypes.controls.gaugeOptions.autoExtendRangeLabel": "自动扩展范围", - "kbnVislibVisTypes.controls.gaugeOptions.displayWarningsLabel": "显示警告", - "kbnVislibVisTypes.controls.gaugeOptions.extendRangeTooltip": "将数据范围扩展到最大值。", - "kbnVislibVisTypes.controls.gaugeOptions.gaugeTypeLabel": "仪表类型", - "kbnVislibVisTypes.controls.gaugeOptions.labelsTitle": "标签", - "kbnVislibVisTypes.controls.gaugeOptions.percentageModeLabel": "百分比模式", - "kbnVislibVisTypes.controls.gaugeOptions.rangesTitle": "范围", - "kbnVislibVisTypes.controls.gaugeOptions.showLabelsLabel": "显示标签", - "kbnVislibVisTypes.controls.gaugeOptions.showLegendLabel": "显示图例", - "kbnVislibVisTypes.controls.gaugeOptions.showScaleLabel": "显示比例", - "kbnVislibVisTypes.controls.gaugeOptions.styleTitle": "样式", - "kbnVislibVisTypes.controls.gaugeOptions.subTextLabel": "子标签", - "kbnVislibVisTypes.controls.gaugeOptions.switchWarningsTooltip": "打开/关闭警告。打开时,如果标签没有全部显示,则显示警告。", - "kbnVislibVisTypes.controls.heatmapOptions.colorLabel": "颜色", - "kbnVislibVisTypes.controls.heatmapOptions.colorScaleLabel": "色阶", - "kbnVislibVisTypes.controls.heatmapOptions.colorsNumberLabel": "颜色个数", - "kbnVislibVisTypes.controls.heatmapOptions.labelsTitle": "标签", - "kbnVislibVisTypes.controls.heatmapOptions.overwriteAutomaticColorLabel": "覆盖自动配色", - "kbnVislibVisTypes.controls.heatmapOptions.percentageModeLabel": "百分比模式", - "kbnVislibVisTypes.controls.heatmapOptions.rotateLabel": "旋转", - "kbnVislibVisTypes.controls.heatmapOptions.scaleToDataBoundsLabel": "缩放到数据边界", - "kbnVislibVisTypes.controls.heatmapOptions.showLabelsTitle": "显示标签", - "kbnVislibVisTypes.controls.heatmapOptions.useCustomRangesLabel": "使用定制范围", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.alignLabel": "对齐", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.filterLabelsLabel": "筛选标签", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.labelsTitle": "标签", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.positionLabel": "位置", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.showLabel": "显示", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.showLabelsLabel": "显示标签", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.xAxisTitle": "X 轴", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.dontShowLabel": "不显示", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.gridText": "网格", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.xAxisLinesLabel": "显示 X 轴线", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "直方图的 X 轴线无法显示。", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 轴线", - "kbnVislibVisTypes.controls.pointSeries.series.chartTypeLabel": "图表类型", - "kbnVislibVisTypes.controls.pointSeries.series.lineModeLabel": "线条模式", - "kbnVislibVisTypes.controls.pointSeries.series.lineWidthLabel": "线条宽度", - "kbnVislibVisTypes.controls.pointSeries.series.metricsTitle": "指标", - "kbnVislibVisTypes.controls.pointSeries.series.modeLabel": "模式", - "kbnVislibVisTypes.controls.pointSeries.series.newAxisLabel": "新建轴…...", - "kbnVislibVisTypes.controls.pointSeries.series.showDotsLabel": "显示点线", - "kbnVislibVisTypes.controls.pointSeries.series.showLineLabel": "显示为线条", - "kbnVislibVisTypes.controls.pointSeries.series.valueAxisLabel": "值轴", - "kbnVislibVisTypes.controls.pointSeries.seriesAccordionAriaLabel": "切换 {agg} 选项", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.addButtonTooltip": "添加 Y 轴", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.customExtentsLabel": "定制范围", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.maxLabel": "最大值", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minErrorMessage": "最小值应小于最大值。", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minLabel": "最小值", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minNeededScaleText": "如果选择了对数刻度,最小值必须大于 0。", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.modeLabel": "模式", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.positionLabel": "位置", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.removeButtonTooltip": "移除 Y 轴", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "边界边距", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "边界边距必须大于或等于 0。", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "缩放到数据边界", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleTypeLabel": "缩放类型", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.setAxisExtentsLabel": "设置轴范围", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.showLabel": "显示", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.titleLabel": "标题", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "切换定制范围", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "切换 {axisName} 选项", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.yAxisTitle": "Y 轴", - "kbnVislibVisTypes.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", - "kbnVislibVisTypes.controls.truncateLabel": "截断", - "kbnVislibVisTypes.controls.vislibBasicOptions.legendPositionLabel": "图例位置", - "kbnVislibVisTypes.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", - "kbnVislibVisTypes.editors.heatmap.basicSettingsTitle": "基本设置", - "kbnVislibVisTypes.editors.heatmap.heatmapSettingsTitle": "热图设置", - "kbnVislibVisTypes.editors.heatmap.highlightLabel": "高亮范围", - "kbnVislibVisTypes.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", - "kbnVislibVisTypes.editors.heatmap.highlightLabelTooltip": "高亮显示图表中鼠标悬停的范围以及图例中对应的标签。", - "kbnVislibVisTypes.editors.pie.donutLabel": "圆环图", - "kbnVislibVisTypes.editors.pie.labelsSettingsTitle": "标签设置", - "kbnVislibVisTypes.editors.pie.pieSettingsTitle": "饼图设置", - "kbnVislibVisTypes.editors.pie.showLabelsLabel": "显示标签", - "kbnVislibVisTypes.editors.pie.showTopLevelOnlyLabel": "仅显示顶级", - "kbnVislibVisTypes.editors.pie.showValuesLabel": "显示值", - "kbnVislibVisTypes.editors.pointSeries.currentTimeMarkerLabel": "当前时间标记", - "kbnVislibVisTypes.editors.pointSeries.orderBucketsBySumLabel": "按总计值排序存储桶", - "kbnVislibVisTypes.editors.pointSeries.settingsTitle": "设置", - "kbnVislibVisTypes.editors.pointSeries.showLabels": "在图表上显示值", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.colorLabel": "线条颜色", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.showLabel": "显示阈值线条", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.styleLabel": "线条样式", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.valueLabel": "阈值", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.widthLabel": "线条宽度", - "kbnVislibVisTypes.editors.pointSeries.thresholdLineSettingsTitle": "阈值线条", - "kbnVislibVisTypes.functions.pie.help": "饼图可视化", - "kbnVislibVisTypes.functions.vislib.help": "Vislib 可视化", - "kbnVislibVisTypes.gauge.alignmentAutomaticTitle": "自动", - "kbnVislibVisTypes.gauge.alignmentHorizontalTitle": "水平", - "kbnVislibVisTypes.gauge.alignmentVerticalTitle": "垂直", - "kbnVislibVisTypes.gauge.gaugeDescription": "仪表盘图指示指标的状态。用于显示指标值与参考阈值的相关程度。", - "kbnVislibVisTypes.gauge.gaugeTitle": "仪表盘图", - "kbnVislibVisTypes.gauge.gaugeTypes.arcText": "弧形", - "kbnVislibVisTypes.gauge.gaugeTypes.circleText": "圆形", - "kbnVislibVisTypes.gauge.groupTitle": "拆分组", - "kbnVislibVisTypes.gauge.metricTitle": "指标", - "kbnVislibVisTypes.goal.goalDescription": "目标图指示与最终目标的接近程度。", - "kbnVislibVisTypes.goal.goalTitle": "目标图", - "kbnVislibVisTypes.goal.groupTitle": "拆分组", - "kbnVislibVisTypes.goal.metricTitle": "指标", - "kbnVislibVisTypes.heatmap.groupTitle": "Y 轴", - "kbnVislibVisTypes.heatmap.heatmapDescription": "为矩阵中的单元格添加阴影", - "kbnVislibVisTypes.heatmap.heatmapTitle": "热力图", - "kbnVislibVisTypes.heatmap.metricTitle": "值", - "kbnVislibVisTypes.heatmap.segmentTitle": "X 轴", - "kbnVislibVisTypes.heatmap.splitTitle": "拆分图表", - "kbnVislibVisTypes.histogram.groupTitle": "拆分序列", - "kbnVislibVisTypes.histogram.histogramDescription": "向每个轴赋予连续变量", - "kbnVislibVisTypes.histogram.histogramTitle": "垂直条形图", - "kbnVislibVisTypes.histogram.metricTitle": "Y 轴", - "kbnVislibVisTypes.histogram.radiusTitle": "点大小", - "kbnVislibVisTypes.histogram.segmentTitle": "X 轴", - "kbnVislibVisTypes.histogram.splitTitle": "拆分图表", - "kbnVislibVisTypes.horizontalBar.groupTitle": "拆分序列", - "kbnVislibVisTypes.horizontalBar.horizontalBarDescription": "向每个轴赋予连续变量", - "kbnVislibVisTypes.horizontalBar.horizontalBarTitle": "水平条形图", - "kbnVislibVisTypes.horizontalBar.metricTitle": "Y 轴", - "kbnVislibVisTypes.horizontalBar.radiusTitle": "点大小", - "kbnVislibVisTypes.horizontalBar.segmentTitle": "X 轴", - "kbnVislibVisTypes.horizontalBar.splitTitle": "拆分图表", - "kbnVislibVisTypes.interpolationModes.smoothedText": "平滑", - "kbnVislibVisTypes.interpolationModes.steppedText": "渐变", - "kbnVislibVisTypes.interpolationModes.straightText": "直线", - "kbnVislibVisTypes.legendPositions.bottomText": "下", - "kbnVislibVisTypes.legendPositions.leftText": "左", - "kbnVislibVisTypes.legendPositions.rightText": "右", - "kbnVislibVisTypes.legendPositions.topText": "上", - "kbnVislibVisTypes.line.groupTitle": "拆分序列", - "kbnVislibVisTypes.line.lineDescription": "突出趋势", - "kbnVislibVisTypes.line.lineTitle": "折线图", - "kbnVislibVisTypes.line.metricTitle": "Y 轴", - "kbnVislibVisTypes.line.radiusTitle": "点大小", - "kbnVislibVisTypes.line.segmentTitle": "X 轴", - "kbnVislibVisTypes.line.splitTitle": "拆分图表", - "kbnVislibVisTypes.pie.metricTitle": "切片大小", - "kbnVislibVisTypes.pie.pieDescription": "比较整体的各个部分", - "kbnVislibVisTypes.pie.pieTitle": "饼图", - "kbnVislibVisTypes.pie.segmentTitle": "拆分切片", - "kbnVislibVisTypes.pie.splitTitle": "拆分图表", - "kbnVislibVisTypes.scaleTypes.linearText": "线性", - "kbnVislibVisTypes.scaleTypes.logText": "对数", - "kbnVislibVisTypes.scaleTypes.squareRootText": "平方根", - "kbnVislibVisTypes.thresholdLine.style.dashedText": "虚线", - "kbnVislibVisTypes.thresholdLine.style.dotdashedText": "点虚线", - "kbnVislibVisTypes.thresholdLine.style.fullText": "实线", + "visTypeVislib.area.areaDescription": "突出折线图下方的数量", + "visTypeVislib.area.areaTitle": "面积图", + "visTypeVislib.area.countText": "计数", + "visTypeVislib.area.groupTitle": "拆分序列", + "visTypeVislib.area.metricsTitle": "Y 轴", + "visTypeVislib.area.radiusTitle": "点大小", + "visTypeVislib.area.segmentTitle": "X 轴", + "visTypeVislib.area.splitTitle": "拆分图表", + "visTypeVislib.area.tabs.metricsAxesTitle": "指标和轴", + "visTypeVislib.area.tabs.panelSettingsTitle": "面板设置", + "visTypeVislib.axisModes.normalText": "正常", + "visTypeVislib.axisModes.percentageText": "百分比", + "visTypeVislib.axisModes.silhouetteText": "剪影", + "visTypeVislib.axisModes.wiggleText": "扭动", + "visTypeVislib.categoryAxis.rotate.angledText": "带角度", + "visTypeVislib.categoryAxis.rotate.horizontalText": "水平", + "visTypeVislib.categoryAxis.rotate.verticalText": "垂直", + "visTypeVislib.chartModes.normalText": "正常", + "visTypeVislib.chartModes.stackedText": "堆叠", + "visTypeVislib.chartTypes.areaText": "面积图", + "visTypeVislib.chartTypes.barText": "条形图", + "visTypeVislib.chartTypes.lineText": "折线图", + "visTypeVislib.controls.colorRanges.errorText": "每个范围应大于前一范围。", + "visTypeVislib.controls.colorSchema.colorSchemaLabel": "颜色模式", + "visTypeVislib.controls.colorSchema.howToChangeColorsDescription": "可以更改图例中的各个颜色。", + "visTypeVislib.controls.colorSchema.resetColorsButtonLabel": "重置颜色", + "visTypeVislib.controls.colorSchema.reverseColorSchemaLabel": "反转模式", + "visTypeVislib.controls.gaugeOptions.alignmentLabel": "对齐方式", + "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "自动扩展范围", + "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "显示警告", + "visTypeVislib.controls.gaugeOptions.extendRangeTooltip": "将数据范围扩展到最大值。", + "visTypeVislib.controls.gaugeOptions.gaugeTypeLabel": "仪表类型", + "visTypeVislib.controls.gaugeOptions.labelsTitle": "标签", + "visTypeVislib.controls.gaugeOptions.percentageModeLabel": "百分比模式", + "visTypeVislib.controls.gaugeOptions.rangesTitle": "范围", + "visTypeVislib.controls.gaugeOptions.showLabelsLabel": "显示标签", + "visTypeVislib.controls.gaugeOptions.showLegendLabel": "显示图例", + "visTypeVislib.controls.gaugeOptions.showScaleLabel": "显示比例", + "visTypeVislib.controls.gaugeOptions.styleTitle": "样式", + "visTypeVislib.controls.gaugeOptions.subTextLabel": "子标签", + "visTypeVislib.controls.gaugeOptions.switchWarningsTooltip": "打开/关闭警告。打开时,如果标签没有全部显示,则显示警告。", + "visTypeVislib.controls.heatmapOptions.colorLabel": "颜色", + "visTypeVislib.controls.heatmapOptions.colorScaleLabel": "色阶", + "visTypeVislib.controls.heatmapOptions.colorsNumberLabel": "颜色个数", + "visTypeVislib.controls.heatmapOptions.labelsTitle": "标签", + "visTypeVislib.controls.heatmapOptions.overwriteAutomaticColorLabel": "覆盖自动配色", + "visTypeVislib.controls.heatmapOptions.percentageModeLabel": "百分比模式", + "visTypeVislib.controls.heatmapOptions.rotateLabel": "旋转", + "visTypeVislib.controls.heatmapOptions.scaleToDataBoundsLabel": "缩放到数据边界", + "visTypeVislib.controls.heatmapOptions.showLabelsTitle": "显示标签", + "visTypeVislib.controls.heatmapOptions.useCustomRangesLabel": "使用定制范围", + "visTypeVislib.controls.pointSeries.categoryAxis.alignLabel": "对齐", + "visTypeVislib.controls.pointSeries.categoryAxis.filterLabelsLabel": "筛选标签", + "visTypeVislib.controls.pointSeries.categoryAxis.labelsTitle": "标签", + "visTypeVislib.controls.pointSeries.categoryAxis.positionLabel": "位置", + "visTypeVislib.controls.pointSeries.categoryAxis.showLabel": "显示", + "visTypeVislib.controls.pointSeries.categoryAxis.showLabelsLabel": "显示标签", + "visTypeVislib.controls.pointSeries.categoryAxis.xAxisTitle": "X 轴", + "visTypeVislib.controls.pointSeries.gridAxis.dontShowLabel": "不显示", + "visTypeVislib.controls.pointSeries.gridAxis.gridText": "网格", + "visTypeVislib.controls.pointSeries.gridAxis.xAxisLinesLabel": "显示 X 轴线", + "visTypeVislib.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "直方图的 X 轴线无法显示。", + "visTypeVislib.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 轴线", + "visTypeVislib.controls.pointSeries.series.chartTypeLabel": "图表类型", + "visTypeVislib.controls.pointSeries.series.lineModeLabel": "线条模式", + "visTypeVislib.controls.pointSeries.series.lineWidthLabel": "线条宽度", + "visTypeVislib.controls.pointSeries.series.metricsTitle": "指标", + "visTypeVislib.controls.pointSeries.series.modeLabel": "模式", + "visTypeVislib.controls.pointSeries.series.newAxisLabel": "新建轴…...", + "visTypeVislib.controls.pointSeries.series.showDotsLabel": "显示点线", + "visTypeVislib.controls.pointSeries.series.showLineLabel": "显示为线条", + "visTypeVislib.controls.pointSeries.series.valueAxisLabel": "值轴", + "visTypeVislib.controls.pointSeries.seriesAccordionAriaLabel": "切换 {agg} 选项", + "visTypeVislib.controls.pointSeries.valueAxes.addButtonTooltip": "添加 Y 轴", + "visTypeVislib.controls.pointSeries.valueAxes.customExtentsLabel": "定制范围", + "visTypeVislib.controls.pointSeries.valueAxes.maxLabel": "最大值", + "visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage": "最小值应小于最大值。", + "visTypeVislib.controls.pointSeries.valueAxes.minLabel": "最小值", + "visTypeVislib.controls.pointSeries.valueAxes.minNeededScaleText": "如果选择了对数刻度,最小值必须大于 0。", + "visTypeVislib.controls.pointSeries.valueAxes.modeLabel": "模式", + "visTypeVislib.controls.pointSeries.valueAxes.positionLabel": "位置", + "visTypeVislib.controls.pointSeries.valueAxes.removeButtonTooltip": "移除 Y 轴", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "边界边距", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "边界边距必须大于或等于 0。", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "缩放到数据边界", + "visTypeVislib.controls.pointSeries.valueAxes.scaleTypeLabel": "缩放类型", + "visTypeVislib.controls.pointSeries.valueAxes.setAxisExtentsLabel": "设置轴范围", + "visTypeVislib.controls.pointSeries.valueAxes.showLabel": "显示", + "visTypeVislib.controls.pointSeries.valueAxes.titleLabel": "标题", + "visTypeVislib.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "切换定制范围", + "visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "切换 {axisName} 选项", + "visTypeVislib.controls.pointSeries.valueAxes.yAxisTitle": "Y 轴", + "visTypeVislib.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", + "visTypeVislib.controls.truncateLabel": "截断", + "visTypeVislib.controls.vislibBasicOptions.legendPositionLabel": "图例位置", + "visTypeVislib.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", + "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本设置", + "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "热图设置", + "visTypeVislib.editors.heatmap.highlightLabel": "高亮范围", + "visTypeVislib.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", + "visTypeVislib.editors.heatmap.highlightLabelTooltip": "高亮显示图表中鼠标悬停的范围以及图例中对应的标签。", + "visTypeVislib.editors.pie.donutLabel": "圆环图", + "visTypeVislib.editors.pie.labelsSettingsTitle": "标签设置", + "visTypeVislib.editors.pie.pieSettingsTitle": "饼图设置", + "visTypeVislib.editors.pie.showLabelsLabel": "显示标签", + "visTypeVislib.editors.pie.showTopLevelOnlyLabel": "仅显示顶级", + "visTypeVislib.editors.pie.showValuesLabel": "显示值", + "visTypeVislib.editors.pointSeries.currentTimeMarkerLabel": "当前时间标记", + "visTypeVislib.editors.pointSeries.orderBucketsBySumLabel": "按总计值排序存储桶", + "visTypeVislib.editors.pointSeries.settingsTitle": "设置", + "visTypeVislib.editors.pointSeries.showLabels": "在图表上显示值", + "visTypeVislib.editors.pointSeries.thresholdLine.colorLabel": "线条颜色", + "visTypeVislib.editors.pointSeries.thresholdLine.showLabel": "显示阈值线条", + "visTypeVislib.editors.pointSeries.thresholdLine.styleLabel": "线条样式", + "visTypeVislib.editors.pointSeries.thresholdLine.valueLabel": "阈值", + "visTypeVislib.editors.pointSeries.thresholdLine.widthLabel": "线条宽度", + "visTypeVislib.editors.pointSeries.thresholdLineSettingsTitle": "阈值线条", + "visTypeVislib.functions.pie.help": "饼图可视化", + "visTypeVislib.functions.vislib.help": "Vislib 可视化", + "visTypeVislib.gauge.alignmentAutomaticTitle": "自动", + "visTypeVislib.gauge.alignmentHorizontalTitle": "水平", + "visTypeVislib.gauge.alignmentVerticalTitle": "垂直", + "visTypeVislib.gauge.gaugeDescription": "仪表盘图指示指标的状态。用于显示指标值与参考阈值的相关程度。", + "visTypeVislib.gauge.gaugeTitle": "仪表盘图", + "visTypeVislib.gauge.gaugeTypes.arcText": "弧形", + "visTypeVislib.gauge.gaugeTypes.circleText": "圆形", + "visTypeVislib.gauge.groupTitle": "拆分组", + "visTypeVislib.gauge.metricTitle": "指标", + "visTypeVislib.goal.goalDescription": "目标图指示与最终目标的接近程度。", + "visTypeVislib.goal.goalTitle": "目标图", + "visTypeVislib.goal.groupTitle": "拆分组", + "visTypeVislib.goal.metricTitle": "指标", + "visTypeVislib.heatmap.groupTitle": "Y 轴", + "visTypeVislib.heatmap.heatmapDescription": "为矩阵中的单元格添加阴影", + "visTypeVislib.heatmap.heatmapTitle": "热力图", + "visTypeVislib.heatmap.metricTitle": "值", + "visTypeVislib.heatmap.segmentTitle": "X 轴", + "visTypeVislib.heatmap.splitTitle": "拆分图表", + "visTypeVislib.histogram.groupTitle": "拆分序列", + "visTypeVislib.histogram.histogramDescription": "向每个轴赋予连续变量", + "visTypeVislib.histogram.histogramTitle": "垂直条形图", + "visTypeVislib.histogram.metricTitle": "Y 轴", + "visTypeVislib.histogram.radiusTitle": "点大小", + "visTypeVislib.histogram.segmentTitle": "X 轴", + "visTypeVislib.histogram.splitTitle": "拆分图表", + "visTypeVislib.horizontalBar.groupTitle": "拆分序列", + "visTypeVislib.horizontalBar.horizontalBarDescription": "向每个轴赋予连续变量", + "visTypeVislib.horizontalBar.horizontalBarTitle": "水平条形图", + "visTypeVislib.horizontalBar.metricTitle": "Y 轴", + "visTypeVislib.horizontalBar.radiusTitle": "点大小", + "visTypeVislib.horizontalBar.segmentTitle": "X 轴", + "visTypeVislib.horizontalBar.splitTitle": "拆分图表", + "visTypeVislib.interpolationModes.smoothedText": "平滑", + "visTypeVislib.interpolationModes.steppedText": "渐变", + "visTypeVislib.interpolationModes.straightText": "直线", + "visTypeVislib.legendPositions.bottomText": "下", + "visTypeVislib.legendPositions.leftText": "左", + "visTypeVislib.legendPositions.rightText": "右", + "visTypeVislib.legendPositions.topText": "上", + "visTypeVislib.line.groupTitle": "拆分序列", + "visTypeVislib.line.lineDescription": "突出趋势", + "visTypeVislib.line.lineTitle": "折线图", + "visTypeVislib.line.metricTitle": "Y 轴", + "visTypeVislib.line.radiusTitle": "点大小", + "visTypeVislib.line.segmentTitle": "X 轴", + "visTypeVislib.line.splitTitle": "拆分图表", + "visTypeVislib.pie.metricTitle": "切片大小", + "visTypeVislib.pie.pieDescription": "比较整体的各个部分", + "visTypeVislib.pie.pieTitle": "饼图", + "visTypeVislib.pie.segmentTitle": "拆分切片", + "visTypeVislib.pie.splitTitle": "拆分图表", + "visTypeVislib.scaleTypes.linearText": "线性", + "visTypeVislib.scaleTypes.logText": "对数", + "visTypeVislib.scaleTypes.squareRootText": "平方根", + "visTypeVislib.thresholdLine.style.dashedText": "虚线", + "visTypeVislib.thresholdLine.style.dotdashedText": "点虚线", + "visTypeVislib.thresholdLine.style.fullText": "实线", + "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", + "visTypeVislib.vislib.tooltip.valueLabel": "値", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", + "visTypeVislib.vislib.legend.loadingLabel": "正在加载……", + "visTypeVislib.vislib.legend.setColorScreenReaderDescription": "为值 {legendDataLabel} 设置颜色", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "切换图例", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "切换图例", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}切换选项", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "退出全屏", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "在全屏模式下,按 ESC 键可退出。", "kibana-react.savedObjects.finder.filterButtonLabel": "类型", diff --git a/yarn.lock b/yarn.lock index b534a7a4fcb799..198174d0132fae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4828,6 +4828,11 @@ resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.25.tgz#b6f55062827a4787fe4ab151cf3412a468e65271" integrity sha512-ShHzHkYD+Ldw3eyttptCpUhF1/mkInWwasQkCNXZHOsJMJ/UMa8wXrxSrTJaVk0r4pLK/VnESVM0wFsfQzNEKQ== +"@types/numeral@^0.0.26": + version "0.0.26" + resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.26.tgz#cfab9842ef9349ce714b06722940ca7ebf8a6298" + integrity sha512-DwCsRqeOWopdEsm5KLTxKVKDSDoj+pzZD1vlwu1GQJ6IF3RhjuleYlRwyRH6MJLGaf3v8wFTnC6wo3yYfz0bnA== + "@types/object-hash@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.0.tgz#b20db2074129f71829d61ff404e618c4ac3d73cf" From f26596145ece8e8cb8a5e7fb671bb9953f23eb52 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 21 Jan 2020 18:49:57 +0200 Subject: [PATCH 17/19] Clean up search service (#53766) * deprecate msearch * Missing export * adjust tests, revert loading method of esaggs/boot * getInjectedMetadata * Fix jest tests * update default strategy abort test * notice update * Allow running discover errors test independently * Remove batchSearches * Detect painless script error * don't show notifications for aborted requests * Fix jest tests * Restore loader indicator * Decreace loading count on error * update search test * Trigger digest after fetching fresh index patterns * Revert isEqual * accurate revert * Return full error details to client from search endpoint * Re-throw AbortError from http when user aborts request. * fix typo * typo * Adjust routes jest test * Restore msearch using a separate es connection * typescript fixes * set http service mock * Move es client to dat aplugin, for follow up PR * Add karma mock * krma mock * fix tests * ts * Pass in version dynamically * add headers to esClient host * Restored fetch soon test Use tap for loadingCount side effects * Cleanup search params * Cleanup search params test * Revert "Cleanup search params" This reverts commit ca9dea01d59fddd10b43513450707421248b96d5. * Revert "Cleanup search params test" This reverts commit 30b94786122181f41ac44606899655ed85c0dc52. * Revert code to use old es client until #44302 is resolved * Revert changes to getPainlessError * Fix jest test * Refactor esClient to trigger loadingIndicator * fixing tests * use esClient from searchService * git remove comment * fix jest Co-authored-by: Elastic Machine --- src/core/public/http/fetch.ts | 6 +- src/legacy/core_plugins/data/public/plugin.ts | 19 +++- .../build_tabular_inspector_data.ts | 4 +- .../data/public/search/expressions/esaggs.ts | 10 +- .../public/search/fetch/call_client.test.ts | 2 +- .../data/public/search/fetch/call_client.ts | 6 +- .../data/public/search/fetch/fetch_soon.ts | 10 +- .../data/public/search/fetch/types.ts | 3 +- .../search_source/search_source.test.ts | 31 +++++-- .../search/search_source/search_source.ts | 34 ++++--- .../default_search_strategy.test.ts | 44 ++++++--- .../default_search_strategy.ts | 35 ++++++- .../np_ready/angular/get_painless_error.ts | 3 +- .../visualizations/public/legacy_imports.ts | 1 - .../components/visualization_requesterror.tsx | 2 +- .../public/np_ready/public/index.ts | 2 + .../new_platform/new_platform.karma_mock.js | 8 ++ src/plugins/data/public/mocks.ts | 17 +++- src/plugins/data/public/plugin.ts | 21 ++++- .../create_app_mount_context_search.test.ts | 50 +++++----- .../search/create_app_mount_context_search.ts | 15 ++- .../public/search/es_client/get_es_client.ts | 93 +++++++++++++++++++ .../data/public/search/es_client/index.ts | 20 ++++ .../data/public/search/search_service.ts | 22 ++++- src/plugins/data/public/services.ts | 16 +++- .../query_string_input.test.tsx.snap | 72 ++++++++++++++ src/plugins/data/server/search/routes.test.ts | 15 ++- src/plugins/data/server/search/routes.ts | 10 +- test/functional/apps/discover/_errors.js | 3 +- .../maps/public/angular/map_controller.js | 4 +- 30 files changed, 474 insertions(+), 104 deletions(-) rename src/legacy/{ui/public/inspector => core_plugins/data/public/search/expressions}/build_tabular_inspector_data.ts (95%) create mode 100644 src/plugins/data/public/search/es_client/get_es_client.ts create mode 100644 src/plugins/data/public/search/es_client/index.ts diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index b86f1f5c08029b..b7ceaed6e56a76 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -133,7 +133,11 @@ export class Fetch { try { response = await window.fetch(request); } catch (err) { - throw new HttpFetchError(err.message, request); + if (err.name === 'AbortError') { + throw err; + } else { + throw new HttpFetchError(err.message, request); + } } const contentType = response.headers.get('Content-Type') || ''; diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 893e477b38583d..5329702348207f 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -20,15 +20,24 @@ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { SearchService, SearchStart } from './search'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; +import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { setFieldFormats, setNotifications, setIndexPatterns, setQueryService, + setSearchService, + setUiSettings, + setInjectedMetadata, + setHttp, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; +export interface DataPluginSetupDependencies { + expressions: ExpressionsSetup; +} + export interface DataPluginStartDependencies { data: DataPublicPluginStart; } @@ -54,18 +63,24 @@ export interface DataStart { * or static code. */ -export class DataPlugin implements Plugin { +export class DataPlugin + implements Plugin { private readonly search = new SearchService(); - public setup(core: CoreSetup) {} + public setup(core: CoreSetup) { + setInjectedMetadata(core.injectedMetadata); + } public start(core: CoreStart, { data }: DataPluginStartDependencies): DataStart { // This is required for when Angular code uses Field and FieldList. setFieldFormats(data.fieldFormats); setQueryService(data.query); + setSearchService(data.search); setIndexPatterns(data.indexPatterns); setFieldFormats(data.fieldFormats); setNotifications(core.notifications); + setUiSettings(core.uiSettings); + setHttp(core.http); return { search: this.search.start(core), diff --git a/src/legacy/ui/public/inspector/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts similarity index 95% rename from src/legacy/ui/public/inspector/build_tabular_inspector_data.ts rename to src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index b09ed60e7186f9..6e6d2a15fa2ac8 100644 --- a/src/legacy/ui/public/inspector/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -19,8 +19,8 @@ import { set } from 'lodash'; // @ts-ignore -import { createFilter } from '../../../core_plugins/visualizations/public'; -import { FormattedData } from './adapters'; +import { createFilter } from '../../../../visualizations/public'; +import { FormattedData } from '../../../../../../plugins/inspector/public'; interface Column { id: string; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 4ec4dbd7f88d69..889c747c9a62e3 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -34,14 +34,8 @@ import { getTime, FilterManager, } from '../../../../../../plugins/data/public'; -import { - SearchSource, - ISearchSource, - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../../../../ui/public/courier'; -import { buildTabularInspectorData } from '../../../../../ui/public/inspector/build_tabular_inspector_data'; +import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { calculateObjectHash } from '../../../../visualizations/public'; // @ts-ignore import { tabifyAggResponse } from '../../../../../ui/public/agg_response/tabify/tabify'; @@ -49,6 +43,8 @@ import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getQueryService, getIndexPatterns } from '../../../../../../plugins/data/public/services'; +import { ISearchSource, getRequestInspectorStats, getResponseInspectorStats } from '../..'; +import { SearchSource } from '../search_source'; export interface RequestHandlerParams { searchSource: ISearchSource; diff --git a/src/legacy/core_plugins/data/public/search/fetch/call_client.test.ts b/src/legacy/core_plugins/data/public/search/fetch/call_client.test.ts index 74c87d77dd4fd0..24a36c9db9df76 100644 --- a/src/legacy/core_plugins/data/public/search/fetch/call_client.test.ts +++ b/src/legacy/core_plugins/data/public/search/fetch/call_client.test.ts @@ -76,7 +76,7 @@ describe('callClient', () => { test('Passes the additional arguments it is given to the search strategy', () => { const searchRequests = [{ _searchStrategyId: 0 }]; - const args = { es: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; + const args = { searchService: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; callClient(searchRequests, [], args); diff --git a/src/legacy/core_plugins/data/public/search/fetch/call_client.ts b/src/legacy/core_plugins/data/public/search/fetch/call_client.ts index 43da27f941e4e2..ad18775d5f1448 100644 --- a/src/legacy/core_plugins/data/public/search/fetch/call_client.ts +++ b/src/legacy/core_plugins/data/public/search/fetch/call_client.ts @@ -26,7 +26,7 @@ import { SearchRequest } from '../types'; export function callClient( searchRequests: SearchRequest[], requestsOptions: FetchOptions[] = [], - { es, config, esShardTimeout }: FetchHandlers + fetchHandlers: FetchHandlers ) { // Correlate the options with the request that they're associated with const requestOptionEntries: Array<[ @@ -53,9 +53,7 @@ export function callClient( // then an error would have been thrown above const { searching, abort } = searchStrategy!.search({ searchRequests: requests, - es, - config, - esShardTimeout, + ...fetchHandlers, }); requests.forEach((request, i) => { diff --git a/src/legacy/core_plugins/data/public/search/fetch/fetch_soon.ts b/src/legacy/core_plugins/data/public/search/fetch/fetch_soon.ts index 75de85e02a1a2b..4830464047ad66 100644 --- a/src/legacy/core_plugins/data/public/search/fetch/fetch_soon.ts +++ b/src/legacy/core_plugins/data/public/search/fetch/fetch_soon.ts @@ -28,10 +28,10 @@ import { SearchRequest, SearchResponse } from '../types'; export async function fetchSoon( request: SearchRequest, options: FetchOptions, - { es, config, esShardTimeout }: FetchHandlers + fetchHandlers: FetchHandlers ) { - const msToDelay = config.get('courier:batchSearches') ? 50 : 0; - return delayedFetch(request, options, { es, config, esShardTimeout }, msToDelay); + const msToDelay = fetchHandlers.config.get('courier:batchSearches') ? 50 : 0; + return delayedFetch(request, options, fetchHandlers, msToDelay); } /** @@ -64,7 +64,7 @@ let fetchInProgress: Promise | null = null; async function delayedFetch( request: SearchRequest, options: FetchOptions, - { es, config, esShardTimeout }: FetchHandlers, + fetchHandlers: FetchHandlers, ms: number ) { const i = requestsToFetch.length; @@ -73,7 +73,7 @@ async function delayedFetch( const responses = await (fetchInProgress = fetchInProgress || delay(() => { - const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); + const response = callClient(requestsToFetch, requestOptions, fetchHandlers); requestsToFetch = []; requestOptions = []; fetchInProgress = null; diff --git a/src/legacy/core_plugins/data/public/search/fetch/types.ts b/src/legacy/core_plugins/data/public/search/fetch/types.ts index 0887a1f84c7c8c..fba14119d83c37 100644 --- a/src/legacy/core_plugins/data/public/search/fetch/types.ts +++ b/src/legacy/core_plugins/data/public/search/fetch/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { ISearchStart } from 'src/plugins/data/public'; import { IUiSettingsClient } from '../../../../../../core/public'; import { SearchRequest, SearchResponse } from '../types'; @@ -35,7 +36,7 @@ export interface FetchOptions { } export interface FetchHandlers { - es: ApiCaller; + searchService: ISearchStart; config: IUiSettingsClient; esShardTimeout: number; } diff --git a/src/legacy/core_plugins/data/public/search/search_source/search_source.test.ts b/src/legacy/core_plugins/data/public/search/search_source/search_source.test.ts index 28f8dba9a75de3..ebeee60b67c8ad 100644 --- a/src/legacy/core_plugins/data/public/search/search_source/search_source.test.ts +++ b/src/legacy/core_plugins/data/public/search/search_source/search_source.test.ts @@ -19,19 +19,34 @@ import { SearchSource } from '../search_source'; import { IndexPattern } from '../../../../../../plugins/data/public'; - -jest.mock('ui/new_platform'); +import { + setSearchService, + setUiSettings, + setInjectedMetadata, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/data/public/services'; + +import { + injectedMetadataServiceMock, + uiSettingsServiceMock, +} from '../../../../../../core/public/mocks'; + +setUiSettings(uiSettingsServiceMock.createStartContract()); +setInjectedMetadata(injectedMetadataServiceMock.createSetupContract()); +setSearchService({ + search: jest.fn(), + __LEGACY: { + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, +}); jest.mock('../fetch', () => ({ fetchSoon: jest.fn().mockResolvedValue({}), })); -jest.mock('ui/chrome', () => ({ - dangerouslyGetActiveInjector: () => ({ - get: jest.fn(), - }), -})); - const getComputedFields = () => ({ storedFields: [], scriptFields: [], diff --git a/src/legacy/core_plugins/data/public/search/search_source/search_source.ts b/src/legacy/core_plugins/data/public/search/search_source/search_source.ts index 6efcae4d4b88dc..e977db713ebaa8 100644 --- a/src/legacy/core_plugins/data/public/search/search_source/search_source.ts +++ b/src/legacy/core_plugins/data/public/search/search_source/search_source.ts @@ -70,8 +70,6 @@ */ import _ from 'lodash'; -import { npSetup } from 'ui/new_platform'; -import chrome from 'ui/chrome'; import { normalizeSortRequest } from './normalize_sort_request'; import { fetchSoon } from '../fetch'; import { fieldWildcardFilter } from '../../../../../../plugins/kibana_utils/public'; @@ -79,10 +77,14 @@ import { getHighlightRequest, esFilters, esQuery } from '../../../../../../plugi import { RequestFailure } from '../fetch/errors'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { SearchSourceOptions, SearchSourceFields, SearchRequest } from './types'; -import { FetchOptions, ApiCaller } from '../fetch/types'; +import { FetchOptions } from '../fetch/types'; -const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout') as number; -const config = npSetup.core.uiSettings; +import { + getSearchService, + getUiSettings, + getInjectedMetadata, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/data/public/services'; export type ISearchSource = Pick; @@ -192,21 +194,23 @@ export class SearchSource { * @async */ async fetch(options: FetchOptions = {}) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const es = $injector.get('es') as ApiCaller; - await this.requestIsStarting(options); const searchRequest = await this.flatten(); this.history = [searchRequest]; + const esShardTimeout = getInjectedMetadata().getInjectedVar('esShardTimeout') as number; const response = await fetchSoon( searchRequest, { ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), ...options, }, - { es, config, esShardTimeout } + { + searchService: getSearchService(), + config: getUiSettings(), + esShardTimeout, + } ); if (response.error) { @@ -313,7 +317,11 @@ export class SearchSource { case 'source': return addToBody('_source', val); case 'sort': - const sort = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); + const sort = normalizeSortRequest( + val, + this.getField('index'), + getUiSettings().get('sort:options') + ); return addToBody(key, sort); default: return addToBody(key, val); @@ -359,7 +367,7 @@ export class SearchSource { if (body._source) { // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(body._source.excludes, config.get('metaFields')); + const filter = fieldWildcardFilter(body._source.excludes, getUiSettings().get('metaFields')); body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => filter(docvalueField.field) ); @@ -377,11 +385,11 @@ export class SearchSource { _.set(body, '_source.includes', remainingFields); } - const esQueryConfigs = esQuery.getEsQueryConfig(config); + const esQueryConfigs = esQuery.getEsQueryConfig(getUiSettings()); body.query = esQuery.buildEsQuery(index, query, filters, esQueryConfigs); if (highlightAll && body.query) { - body.highlight = getHighlightRequest(body.query, config.get('doc_table:highlight')); + body.highlight = getHighlightRequest(body.query, getUiSettings().get('doc_table:highlight')); delete searchRequest.highlightAll; } diff --git a/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.test.ts b/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.test.ts index 0ec6a6c2e143e7..8caf20c50cd3aa 100644 --- a/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.test.ts +++ b/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.test.ts @@ -37,9 +37,16 @@ const searchMockResponse: any = Promise.resolve([]); searchMockResponse.abort = jest.fn(); const searchMock = jest.fn().mockReturnValue(searchMockResponse); +const newSearchMockResponse: any = Promise.resolve([]); +newSearchMockResponse.abort = jest.fn(); +const newSearchMock = jest.fn().mockReturnValue({ + toPromise: () => searchMockResponse, +}); + describe('defaultSearchStrategy', function() { describe('search', function() { let searchArgs: MockedKeys>; + let es: any; beforeEach(() => { msearchMockResponse.abort.mockClear(); @@ -55,17 +62,24 @@ describe('defaultSearchStrategy', function() { }, ], esShardTimeout: 0, - es: { - msearch: msearchMock, - search: searchMock, + searchService: { + search: newSearchMock, + __LEGACY: { + esClient: { + search: searchMock, + msearch: msearchMock, + }, + }, }, }; + + es = searchArgs.searchService.__LEGACY.esClient; }); test('does not send max_concurrent_shard_requests by default', async () => { const config = getConfigStub({ 'courier:batchSearches': true }); await search({ ...searchArgs, config }); - expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); + expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); }); test('allows configuration of max_concurrent_shard_requests', async () => { @@ -74,13 +88,13 @@ describe('defaultSearchStrategy', function() { 'courier:maxConcurrentShardRequests': 42, }); await search({ ...searchArgs, config }); - expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); + expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); }); test('should set rest_total_hits_as_int to true on a request', async () => { const config = getConfigStub({ 'courier:batchSearches': true }); await search({ ...searchArgs, config }); - expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); + expect(es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); }); test('should set ignore_throttled=false when including frozen indices', async () => { @@ -89,7 +103,7 @@ describe('defaultSearchStrategy', function() { 'search:includeFrozen': true, }); await search({ ...searchArgs, config }); - expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); + expect(es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); }); test('should properly call abort with msearch', () => { @@ -100,12 +114,18 @@ describe('defaultSearchStrategy', function() { expect(msearchMockResponse.abort).toHaveBeenCalled(); }); - test('should properly abort with search', async () => { - const config = getConfigStub({ - 'courier:batchSearches': false, - }); + test('should call new search service', () => { + const config = getConfigStub(); + search({ ...searchArgs, config }); + expect(searchMock).toHaveBeenCalled(); + expect(newSearchMock).toHaveBeenCalledTimes(0); + }); + + test('should properly abort with new search service', async () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const config = getConfigStub({}); search({ ...searchArgs, config }).abort(); - expect(searchMockResponse.abort).toHaveBeenCalled(); + expect(abortSpy).toHaveBeenCalled(); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.ts b/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.ts index 9bfa1df71aa81a..39789504de0a74 100644 --- a/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.ts +++ b/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.ts @@ -38,7 +38,14 @@ export const defaultSearchStrategy: SearchStrategyProvider = { }, }; -function msearch({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { +// @deprecated +function msearch({ + searchRequests, + searchService, + config, + esShardTimeout, +}: SearchStrategySearchParams) { + const es = searchService.__LEGACY.esClient; const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { const inlineHeader = { index: index.title || index, @@ -57,19 +64,39 @@ function msearch({ searchRequests, es, config, esShardTimeout }: SearchStrategyS ...getMSearchParams(config), body: `${inlineRequests.join('\n')}\n`, }); + return { - searching: searching.then(({ responses }) => responses), + searching: searching.then(({ responses }: any) => responses), abort: searching.abort, }; } -function search({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { +function search({ + searchRequests, + searchService, + config, + esShardTimeout, +}: SearchStrategySearchParams) { const abortController = new AbortController(); const searchParams = getSearchParams(config, esShardTimeout); + const es = searchService.__LEGACY.esClient; const promises = searchRequests.map(({ index, body }) => { const searching = es.search({ index: index.title || index, body, ...searchParams }); abortController.signal.addEventListener('abort', searching.abort); - return searching.catch(({ response }) => JSON.parse(response)); + return searching.catch(({ response }: any) => JSON.parse(response)); + /* + * Once #44302 is resolved, replace the old implementation with this one - + * const params = { + * index: index.title || index, + * body, + * ...searchParams, + * }; + * const { signal } = abortController; + * return searchService + * .search({ params }, { signal }) + * .toPromise() + * .then(({ rawResponse }) => rawResponse); + */ }); return { searching: Promise.all(promises), diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts index 212fd870a5aeb8..2bbeea9d675c7e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts @@ -25,6 +25,7 @@ export function getPainlessError(error: Error) { error, 'resp.error.root_cause' ); + const message: string = get(error, 'message'); if (!rootCause) { return; @@ -43,6 +44,6 @@ export function getPainlessError(error: Error) { defaultMessage: "Error with Painless scripted field '{script}'.", values: { script }, }), - error: error.message, + error: message, }; } diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts index b750557c24b94a..fd40c831ce0efa 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -18,7 +18,6 @@ */ export { PersistedState } from '../../../ui/public/persisted_state'; -export { SearchError } from '../../../ui/public/courier/search_strategy/search_error'; export { AggConfig } from '../../../ui/public/agg_types/agg_config'; export { AggConfigs } from '../../../ui/public/agg_types/agg_configs'; export { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_requesterror.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_requesterror.tsx index 8b9fded919f135..1af9aa3c3e6021 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_requesterror.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_requesterror.tsx @@ -19,7 +19,7 @@ import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; -import { SearchError } from '../../../legacy_imports'; +import { SearchError } from '../../../../../data/public/search/search_strategy'; interface VisualizationRequestErrorProps { onInit?: () => void; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 2e9d055858a483..29ff812b95473f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -52,3 +52,5 @@ export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/bui // @ts-ignore export { updateOldState } from './legacy/vis_update_state'; export { calculateObjectHash } from './legacy/calculate_object_hash'; +// @ts-ignore +export { createFiltersFromEvent } from './filters/vis_filters'; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index d3f74a540b9604..dfd26bc4be0397 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -236,6 +236,14 @@ export const npStart = { history: sinon.fake(), }, }, + search: { + __LEGACY: { + esClient: { + search: sinon.fake(), + msearch: sinon.fake(), + }, + }, + }, fieldFormats: getFieldFormatsRegistry(mockCore), }, share: { diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 08a7a3ef11537b..f44d40f533eed3 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -69,13 +69,28 @@ const createStartContract = (): Start => { const startContract = { autocomplete: autocompleteMock, getSuggestions: jest.fn(), - search: { search: jest.fn() }, + search: { + search: jest.fn(), + + __LEGACY: { + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, + }, fieldFormats: fieldFormatsMock as FieldFormatsStart, query: queryStartMock, ui: { IndexPatternSelect: jest.fn(), SearchBar: jest.fn(), }, + __LEGACY: { + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, indexPatterns: {} as IndexPatternsContract, }; return startContract; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 78abcbcfec467f..ce8ca0317bd7d8 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -17,7 +17,13 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + PackageInfo, +} from 'src/core/public'; import { Storage, IStorageWrapper } from '../../kibana_utils/public'; import { DataPublicPluginSetup, @@ -31,7 +37,13 @@ import { FieldFormatsService } from './field_formats_provider'; import { QueryService } from './query'; import { createIndexPatternSelect } from './ui/index_pattern_select'; import { IndexPatterns } from './index_patterns'; -import { setNotifications, setFieldFormats, setOverlays, setIndexPatterns } from './services'; +import { + setNotifications, + setFieldFormats, + setOverlays, + setIndexPatterns, + setHttp, +} from './services'; import { createFilterAction, GLOBAL_APPLY_FILTER_ACTION } from './actions'; import { APPLY_FILTER_TRIGGER } from '../../embeddable/public'; import { createSearchBar } from './ui/search_bar/create_search_bar'; @@ -42,12 +54,14 @@ export class DataPublicPlugin implements Plugin { it('Returns search fn when there are no strategies', () => { - const context = createAppMountSearchContext({}); + const context = createAppMountSearchContext({}, new BehaviorSubject(0)); expect(context.search).toBeDefined(); }); it(`Search throws an error when the strategy doesn't exist`, () => { - const context = createAppMountSearchContext({}); + const context = createAppMountSearchContext({}, new BehaviorSubject(0)); expect(() => context.search({}, {}, 'noexist').toPromise()).toThrowErrorMatchingInlineSnapshot( `"Strategy with name noexist does not exist"` ); }); it(`Search fn is called on appropriate strategy name`, done => { - const context = createAppMountSearchContext({ - mysearch: search => - Promise.resolve({ - search: () => from(Promise.resolve({ percentComplete: 98 })), - }), - anothersearch: search => - Promise.resolve({ - search: () => from(Promise.resolve({ percentComplete: 0 })), - }), - }); + const context = createAppMountSearchContext( + { + mysearch: search => + Promise.resolve({ + search: () => from(Promise.resolve({ percentComplete: 98 })), + }), + anothersearch: search => + Promise.resolve({ + search: () => from(Promise.resolve({ percentComplete: 0 })), + }), + }, + new BehaviorSubject(0) + ); context.search({}, {}, 'mysearch').subscribe(response => { expect(response).toEqual({ percentComplete: 98 }); @@ -52,16 +55,19 @@ describe('Create app mount search context', () => { }); it(`Search fn is called with the passed in request object`, done => { - const context = createAppMountSearchContext({ - mysearch: search => { - return Promise.resolve({ - search: request => { - expect(request).toEqual({ greeting: 'hi' }); - return from(Promise.resolve({})); - }, - }); + const context = createAppMountSearchContext( + { + mysearch: search => { + return Promise.resolve({ + search: request => { + expect(request).toEqual({ greeting: 'hi' }); + return from(Promise.resolve({})); + }, + }); + }, }, - }); + new BehaviorSubject(0) + ); context.search({ greeting: 'hi' } as any, {}, 'mysearch').subscribe( response => {}, () => {}, diff --git a/src/plugins/data/public/search/create_app_mount_context_search.ts b/src/plugins/data/public/search/create_app_mount_context_search.ts index 5659a9c863dc18..f480b8f3e042e6 100644 --- a/src/plugins/data/public/search/create_app_mount_context_search.ts +++ b/src/plugins/data/public/search/create_app_mount_context_search.ts @@ -17,8 +17,8 @@ * under the License. */ -import { mergeMap } from 'rxjs/operators'; -import { from } from 'rxjs'; +import { mergeMap, tap } from 'rxjs/operators'; +import { from, BehaviorSubject } from 'rxjs'; import { ISearchAppMountContext } from './i_search_app_mount_context'; import { ISearchGeneric } from './i_search'; import { @@ -30,7 +30,8 @@ import { TStrategyTypes } from './strategy_types'; import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; export const createAppMountSearchContext = ( - searchStrategies: TSearchStrategiesMap + searchStrategies: TSearchStrategiesMap, + loadingCount$: BehaviorSubject ): ISearchAppMountContext => { const getSearchStrategy = ( strategyName?: K @@ -48,7 +49,13 @@ export const createAppMountSearchContext = ( const strategyPromise = getSearchStrategy(strategyName); return from(strategyPromise).pipe( mergeMap(strategy => { - return strategy.search(request, options); + loadingCount$.next(loadingCount$.getValue() + 1); + return strategy.search(request, options).pipe( + tap( + error => loadingCount$.next(loadingCount$.getValue() - 1), + complete => loadingCount$.next(loadingCount$.getValue() - 1) + ) + ); }) ); }; diff --git a/src/plugins/data/public/search/es_client/get_es_client.ts b/src/plugins/data/public/search/es_client/get_es_client.ts new file mode 100644 index 00000000000000..6c271643ba0124 --- /dev/null +++ b/src/plugins/data/public/search/es_client/get_es_client.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { default as es } from 'elasticsearch-browser/elasticsearch'; +import { CoreStart, PackageInfo } from 'kibana/public'; +import { BehaviorSubject } from 'rxjs'; + +export function getEsClient( + injectedMetadata: CoreStart['injectedMetadata'], + http: CoreStart['http'], + packageInfo: PackageInfo, + loadingCount$: BehaviorSubject +) { + const esRequestTimeout = injectedMetadata.getInjectedVar('esRequestTimeout') as number; + const esApiVersion = injectedMetadata.getInjectedVar('esApiVersion') as string; + + // Use legacy es client for msearch. + const client = es.Client({ + host: getEsUrl(http, packageInfo), + log: 'info', + requestTimeout: esRequestTimeout, + apiVersion: esApiVersion, + }); + + return { + search: wrapEsClientMethod(client, 'search', loadingCount$), + msearch: wrapEsClientMethod(client, 'msearch', loadingCount$), + create: wrapEsClientMethod(client, 'create', loadingCount$), + }; +} + +function wrapEsClientMethod(esClient: any, method: string, loadingCount$: BehaviorSubject) { + return (args: any) => { + // esClient returns a promise, with an additional abort handler + // To tap into the abort handling, we have to override that abort handler. + const customPromiseThingy = esClient[method](args); + const { abort } = customPromiseThingy; + let resolved = false; + + // Start LoadingIndicator + loadingCount$.next(loadingCount$.getValue() + 1); + + // Stop LoadingIndicator when user aborts + customPromiseThingy.abort = () => { + abort(); + if (!resolved) { + resolved = true; + loadingCount$.next(loadingCount$.getValue() - 1); + } + }; + + // Stop LoadingIndicator when promise finishes + customPromiseThingy.finally(() => { + resolved = true; + loadingCount$.next(loadingCount$.getValue() - 1); + }); + + return customPromiseThingy; + }; +} + +function getEsUrl(http: CoreStart['http'], packageInfo: PackageInfo) { + const a = document.createElement('a'); + a.href = http.basePath.prepend('/elasticsearch'); + const protocolPort = /https/.test(a.protocol) ? 443 : 80; + const port = a.port || protocolPort; + return { + host: a.hostname, + port, + protocol: a.protocol, + pathname: a.pathname, + headers: { + 'kbn-version': packageInfo.version, + }, + }; +} diff --git a/src/plugins/data/public/search/es_client/index.ts b/src/plugins/data/public/search/es_client/index.ts new file mode 100644 index 00000000000000..bf1a3f5d6e7c4f --- /dev/null +++ b/src/plugins/data/public/search/es_client/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getEsClient } from './get_es_client'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 6030884c9f6b1d..6f3e228939d6dd 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { BehaviorSubject } from 'rxjs'; import { Plugin, CoreSetup, @@ -23,6 +24,7 @@ import { CoreStart, IContextContainer, PluginOpaqueId, + PackageInfo, } from '../../../../core/public'; import { ISearchAppMountContext } from './i_search_app_mount_context'; @@ -37,6 +39,7 @@ import { import { TStrategyTypes } from './strategy_types'; import { esSearchService } from './es_search'; import { ISearchGeneric } from './i_search'; +import { getEsClient } from './es_client'; /** * Extends the AppMountContext so other plugins have access @@ -50,6 +53,9 @@ declare module 'kibana/public' { export interface ISearchStart { search: ISearchGeneric; + __LEGACY: { + esClient: any; + }; } /** @@ -74,11 +80,16 @@ export class SearchService implements Plugin { private contextContainer?: IContextContainer>; private search?: ISearchGeneric; + private readonly loadingCount$ = new BehaviorSubject(0); constructor(private initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup): ISearchSetup { - const search = (this.search = createAppMountSearchContext(this.searchStrategies).search); + core.http.addLoadingCountSource(this.loadingCount$); + const search = (this.search = createAppMountSearchContext( + this.searchStrategies, + this.loadingCount$ + ).search); core.application.registerMountContext<'search'>('search', () => { return { search }; }); @@ -115,11 +126,16 @@ export class SearchService implements Plugin { return api; } - public start(core: CoreStart) { + public start(core: CoreStart, packageInfo: PackageInfo) { if (!this.search) { throw new Error('Search should always be defined'); } - return { search: this.search }; + return { + search: this.search, + __LEGACY: { + esClient: getEsClient(core.injectedMetadata, core.http, packageInfo, this.loadingCount$), + }, + }; } public stop() {} diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index 76b3283220f673..6a15893f573d83 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -18,7 +18,7 @@ */ import { NotificationsStart } from 'src/core/public'; -import { CoreStart } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; import { FieldFormatsStart } from '.'; import { createGetterSetter } from '../../kibana_utils/public'; import { IndexPatternsContract } from './index_patterns'; @@ -28,6 +28,12 @@ export const [getNotifications, setNotifications] = createGetterSetter( + 'UiSettings' +); + +export const [getHttp, setHttp] = createGetterSetter('Http'); + export const [getFieldFormats, setFieldFormats] = createGetterSetter( 'FieldFormats' ); @@ -41,3 +47,11 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('Query'); + +export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< + CoreSetup['injectedMetadata'] +>('InjectedMetadata'); + +export const [getSearchService, setSearchService] = createGetterSetter< + DataPublicPluginStart['search'] +>('Search'); diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index d8db53d4c6020b..f38adff8920995 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -150,6 +150,12 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { "getQuerySuggestions": [MockFunction], "getValueSuggestions": [MockFunction], @@ -205,6 +211,12 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -780,6 +792,12 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { "getQuerySuggestions": [MockFunction], "getValueSuggestions": [MockFunction], @@ -835,6 +853,12 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -1392,6 +1416,12 @@ exports[`QueryStringInput Should pass the query language to the language switche "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { "getQuerySuggestions": [MockFunction], "getValueSuggestions": [MockFunction], @@ -1447,6 +1477,12 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -2019,6 +2055,12 @@ exports[`QueryStringInput Should pass the query language to the language switche "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { "getQuerySuggestions": [MockFunction], "getValueSuggestions": [MockFunction], @@ -2074,6 +2116,12 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -2631,6 +2679,12 @@ exports[`QueryStringInput Should render the given query 1`] = ` "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { "getQuerySuggestions": [MockFunction], "getValueSuggestions": [MockFunction], @@ -2686,6 +2740,12 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -3258,6 +3318,12 @@ exports[`QueryStringInput Should render the given query 1`] = ` "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { "getQuerySuggestions": [MockFunction], "getValueSuggestions": [MockFunction], @@ -3313,6 +3379,12 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index a2394d88f39314..6ea0799f790fcf 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -65,8 +65,13 @@ describe('Search service', () => { expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' }); }); - it('handler throws internal error if the search throws an error', async () => { - const mockSearch = jest.fn().mockRejectedValue('oh no'); + it('handler throws an error if the search throws an error', async () => { + const mockSearch = jest.fn().mockRejectedValue({ + message: 'oh no', + body: { + error: 'oops', + }, + }); const mockContext = { core: { elasticsearch: { @@ -93,7 +98,9 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); - expect(mockResponse.internalError).toBeCalled(); - expect(mockResponse.internalError.mock.calls[0][0]).toEqual({ body: 'oh no' }); + expect(mockResponse.customError).toBeCalled(); + const error: any = mockResponse.customError.mock.calls[0][0]; + expect(error.body.message).toBe('oh no'); + expect(error.body.attributes.error).toBe('oops'); }); }); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index eaa72548e08ee7..6f726771c41b2f 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -39,7 +39,15 @@ export function registerSearchRoute(router: IRouter): void { const response = await context.search!.search(searchRequest, {}, strategy); return res.ok({ body: response }); } catch (err) { - return res.internalError({ body: err }); + return res.customError({ + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + error: err.body.error, + }, + }, + }); } } ); diff --git a/test/functional/apps/discover/_errors.js b/test/functional/apps/discover/_errors.js index 53dcd8cc9e5c14..7dbb93c884f46d 100644 --- a/test/functional/apps/discover/_errors.js +++ b/test/functional/apps/discover/_errors.js @@ -22,10 +22,11 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common']); + const PageObjects = getPageObjects(['common', 'discover']); describe('errors', function describeIndexTests() { before(async function() { + await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('invalid_scripted_field'); await PageObjects.common.navigateToApp('discover'); }); diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 75971d5dfe2a84..eec97dc5c71e9c 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -367,7 +367,9 @@ app.controller( if (prevIndexPatternIds !== nextIndexPatternIds) { return; } - $scope.indexPatterns = indexPatterns; + $scope.$evalAsync(() => { + $scope.indexPatterns = indexPatterns; + }); } $scope.isFullScreen = false; From 5aa85dc7516450a8bf5fe5c3a79f51742fb0f5d4 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 21 Jan 2020 12:35:02 -0500 Subject: [PATCH 18/19] Update Monitoring plugin's Elasticsearch configuration (#55119) * Fix Monitoring plugin Elasticsearch SSL config Plugin now allows "keystore" and "truststore" values in its config schema as the documentation currently states. Plugin also now reads PEM and PKCS12 files off of the filesystem before attempting to create an Elasticsearch client. * Add missing Elasticsearch config deprecations Several Elasticsearch config deprecations were overlooked for monitoring-specific Elasticsearch settings. --- .../monitoring/__tests__/deprecations.js | 50 +++++ x-pack/legacy/plugins/monitoring/config.js | 8 + .../legacy/plugins/monitoring/deprecations.js | 26 +++ .../es_client/__tests__/instantiate_client.js | 15 +- .../server/es_client/instantiate_client.js | 13 +- .../parse_elasticsearch_config.test.mocks.ts | 15 ++ .../parse_elasticsearch_config.test.ts | 181 ++++++++++++++++++ .../es_client/parse_elasticsearch_config.ts | 111 +++++++++++ .../plugins/monitoring/server/plugin.js | 9 +- 9 files changed, 408 insertions(+), 20 deletions(-) create mode 100644 x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.mocks.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.ts diff --git a/x-pack/legacy/plugins/monitoring/__tests__/deprecations.js b/x-pack/legacy/plugins/monitoring/__tests__/deprecations.js index aaaf3d6ad40cf1..3df93bdb24f328 100644 --- a/x-pack/legacy/plugins/monitoring/__tests__/deprecations.js +++ b/x-pack/legacy/plugins/monitoring/__tests__/deprecations.js @@ -80,4 +80,54 @@ describe('monitoring plugin deprecations', function() { expect(log.called).to.be(true); }); }); + + describe('elasticsearch.username', function() { + it('logs a warning if elasticsearch.username is set to "elastic"', () => { + const settings = { elasticsearch: { username: 'elastic' } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(true); + }); + + it('does not log a warning if elasticsearch.username is set to something besides "elastic"', () => { + const settings = { elasticsearch: { username: 'otheruser' } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + + it('does not log a warning if elasticsearch.username is unset', () => { + const settings = { elasticsearch: { username: undefined } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + + it('logs a warning if ssl.key is set and ssl.certificate is not', () => { + const settings = { elasticsearch: { ssl: { key: '' } } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(true); + }); + + it('logs a warning if ssl.certificate is set and ssl.key is not', () => { + const settings = { elasticsearch: { ssl: { certificate: '' } } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(true); + }); + + it('does not log a warning if both ssl.key and ssl.certificate are set', () => { + const settings = { elasticsearch: { ssl: { key: '', certificate: '' } } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + }); }); diff --git a/x-pack/legacy/plugins/monitoring/config.js b/x-pack/legacy/plugins/monitoring/config.js index c33b0b28e830ad..91c1ee99a0b2e4 100644 --- a/x-pack/legacy/plugins/monitoring/config.js +++ b/x-pack/legacy/plugins/monitoring/config.js @@ -83,6 +83,14 @@ export const config = Joi => { certificate: Joi.string(), key: Joi.string(), keyPassphrase: Joi.string(), + keystore: Joi.object({ + path: Joi.string(), + password: Joi.string(), + }).default(), + truststore: Joi.object({ + path: Joi.string(), + password: Joi.string(), + }).default(), alwaysPresentCertificate: Joi.boolean().default(false), }).default(), apiVersion: Joi.string().default('master'), diff --git a/x-pack/legacy/plugins/monitoring/deprecations.js b/x-pack/legacy/plugins/monitoring/deprecations.js index 13a6a58fa8752f..c3b2b70690f33b 100644 --- a/x-pack/legacy/plugins/monitoring/deprecations.js +++ b/x-pack/legacy/plugins/monitoring/deprecations.js @@ -27,5 +27,31 @@ export const deprecations = () => { ); } }, + (settings, log) => { + const fromPath = 'xpack.monitoring.elasticsearch'; + const es = get(settings, 'elasticsearch'); + if (es) { + if (es.username === 'elastic') { + log( + `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana" user instead.` + ); + } + } + }, + (settings, log) => { + const fromPath = 'xpack.monitoring.elasticsearch.ssl'; + const ssl = get(settings, 'elasticsearch.ssl'); + if (ssl) { + if (ssl.key !== undefined && ssl.certificate === undefined) { + log( + `Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` + ); + } else if (ssl.certificate !== undefined && ssl.key === undefined) { + log( + `Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` + ); + } + } + }, ]; }; diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/__tests__/instantiate_client.js b/x-pack/legacy/plugins/monitoring/server/es_client/__tests__/instantiate_client.js index 8797f92e489bb1..6844bd5febf8ee 100644 --- a/x-pack/legacy/plugins/monitoring/server/es_client/__tests__/instantiate_client.js +++ b/x-pack/legacy/plugins/monitoring/server/es_client/__tests__/instantiate_client.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { get, noop } from 'lodash'; +import { noop } from 'lodash'; import { exposeClient, hasMonitoringCluster } from '../instantiate_client'; function getMockServerFromConnectionUrl(monitoringClusterUrl) { @@ -26,15 +26,8 @@ function getMockServerFromConnectionUrl(monitoringClusterUrl) { }, }; - const config = { - get: path => { - return get(server, path); - }, - set: noop, - }; - return { - config, + elasticsearchConfig: server.xpack.monitoring.elasticsearch, elasticsearchPlugin: { getCluster: sinon .stub() @@ -141,12 +134,12 @@ describe('Instantiate Client', () => { describe('hasMonitoringCluster', () => { it('returns true if monitoring is configured', () => { const server = getMockServerFromConnectionUrl('http://monitoring-cluster.test:9200'); // pass null for URL to create the client using prod config - expect(hasMonitoringCluster(server.config)).to.be(true); + expect(hasMonitoringCluster(server.elasticsearchConfig)).to.be(true); }); it('returns false if monitoring is not configured', () => { const server = getMockServerFromConnectionUrl(null); - expect(hasMonitoringCluster(server.config)).to.be(false); + expect(hasMonitoringCluster(server.elasticsearchConfig)).to.be(false); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/instantiate_client.js b/x-pack/legacy/plugins/monitoring/server/es_client/instantiate_client.js index 87a2e5349cf1b7..9aed1ac1456174 100644 --- a/x-pack/legacy/plugins/monitoring/server/es_client/instantiate_client.js +++ b/x-pack/legacy/plugins/monitoring/server/es_client/instantiate_client.js @@ -14,24 +14,21 @@ import { LOGGING_TAG } from '../../common/constants'; * Kibana itself is connected to a production cluster. */ -export function exposeClient({ config, events, log, elasticsearchPlugin }) { - const elasticsearchConfig = hasMonitoringCluster(config) - ? config.get('xpack.monitoring.elasticsearch') - : {}; +export function exposeClient({ elasticsearchConfig, events, log, elasticsearchPlugin }) { + const isMonitoringCluster = hasMonitoringCluster(elasticsearchConfig); const cluster = elasticsearchPlugin.createCluster('monitoring', { - ...elasticsearchConfig, + ...(isMonitoringCluster ? elasticsearchConfig : {}), plugins: [monitoringBulk], logQueries: Boolean(elasticsearchConfig.logQueries), }); events.on('stop', bindKey(cluster, 'close')); - const configSource = hasMonitoringCluster(config) ? 'monitoring' : 'production'; + const configSource = isMonitoringCluster ? 'monitoring' : 'production'; log([LOGGING_TAG, 'es-client'], `config sourced from: ${configSource} cluster`); } export function hasMonitoringCluster(config) { - const hosts = config.get('xpack.monitoring.elasticsearch.hosts'); - return Boolean(hosts && hosts.length); + return Boolean(config.hosts && config.hosts.length); } export const instantiateClient = once(exposeClient); diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.mocks.ts b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.mocks.ts new file mode 100644 index 00000000000000..42141313ceea2a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.mocks.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockReadFileSync = jest.fn(); +jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); + +export const mockReadPkcs12Keystore = jest.fn(); +export const mockReadPkcs12Truststore = jest.fn(); +jest.mock('../../../../../../src/core/utils', () => ({ + readPkcs12Keystore: mockReadPkcs12Keystore, + readPkcs12Truststore: mockReadPkcs12Truststore, +})); diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.ts b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.ts new file mode 100644 index 00000000000000..c6f4e0fa685045 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.ts @@ -0,0 +1,181 @@ +/* + * 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 { + mockReadFileSync, + mockReadPkcs12Keystore, + mockReadPkcs12Truststore, +} from './parse_elasticsearch_config.test.mocks'; + +import { parseElasticsearchConfig } from './parse_elasticsearch_config'; + +const parse = (config: any) => { + return parseElasticsearchConfig({ + get: () => config, + }); +}; + +describe('reads files', () => { + beforeEach(() => { + mockReadFileSync.mockReset(); + mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); + mockReadPkcs12Keystore.mockReset(); + mockReadPkcs12Keystore.mockImplementation((path: string) => ({ + key: `content-of-${path}.key`, + cert: `content-of-${path}.cert`, + ca: [`content-of-${path}.ca`], + })); + mockReadPkcs12Truststore.mockReset(); + mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]); + }); + + it('reads certificate authorities when ssl.keystore.path is specified', () => { + const configValue = parse({ ssl: { keystore: { path: 'some-path' } } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path.ca']); + }); + + it('reads certificate authorities when ssl.truststore.path is specified', () => { + const configValue = parse({ ssl: { truststore: { path: 'some-path' } } }); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + }); + + it('reads certificate authorities when ssl.certificateAuthorities is specified', () => { + let configValue = parse({ ssl: { certificateAuthorities: 'some-path' } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = parse({ ssl: { certificateAuthorities: ['some-path'] } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = parse({ ssl: { certificateAuthorities: ['some-path', 'another-path'] } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.ssl.certificateAuthorities).toEqual([ + 'content-of-some-path', + 'content-of-another-path', + ]); + }); + + it('reads certificate authorities when ssl.keystore.path, ssl.truststore.path, and ssl.certificateAuthorities are specified', () => { + const configValue = parse({ + ssl: { + keystore: { path: 'some-path' }, + truststore: { path: 'another-path' }, + certificateAuthorities: 'yet-another-path', + }, + }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual([ + 'content-of-some-path.ca', + 'content-of-another-path', + 'content-of-yet-another-path', + ]); + }); + + it('reads a private key and certificate when ssl.keystore.path is specified', () => { + const configValue = parse({ ssl: { keystore: { path: 'some-path' } } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.key).toEqual('content-of-some-path.key'); + expect(configValue.ssl.certificate).toEqual('content-of-some-path.cert'); + }); + + it('reads a private key when ssl.key is specified', () => { + const configValue = parse({ ssl: { key: 'some-path' } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.key).toEqual('content-of-some-path'); + }); + + it('reads a certificate when ssl.certificate is specified', () => { + const configValue = parse({ ssl: { certificate: 'some-path' } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificate).toEqual('content-of-some-path'); + }); +}); + +describe('throws when config is invalid', () => { + beforeAll(() => { + const realFs = jest.requireActual('fs'); + mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + const utils = jest.requireActual('../../../../../../src/core/utils'); + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Keystore(path, password) + ); + mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Truststore(path, password) + ); + }); + + it('throws if key is invalid', () => { + const value = { ssl: { key: '/invalid/key' } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/key'"` + ); + }); + + it('throws if certificate is invalid', () => { + const value = { ssl: { certificate: '/invalid/cert' } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/cert'"` + ); + }); + + it('throws if certificateAuthorities is invalid', () => { + const value = { ssl: { certificateAuthorities: '/invalid/ca' } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/ca'"` + ); + }); + + it('throws if keystore path is invalid', () => { + const value = { ssl: { keystore: { path: '/invalid/keystore' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/keystore'"` + ); + }); + + it('throws if keystore does not contain a key', () => { + mockReadPkcs12Keystore.mockReturnValueOnce({}); + const value = { ssl: { keystore: { path: 'some-path' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"Did not find key in Elasticsearch keystore."` + ); + }); + + it('throws if keystore does not contain a certificate', () => { + mockReadPkcs12Keystore.mockReturnValueOnce({ key: 'foo' }); + const value = { ssl: { keystore: { path: 'some-path' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"Did not find certificate in Elasticsearch keystore."` + ); + }); + + it('throws if truststore path is invalid', () => { + const value = { ssl: { keystore: { path: '/invalid/truststore' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/truststore'"` + ); + }); + + it('throws if key and keystore.path are both specified', () => { + const value = { ssl: { key: 'foo', keystore: { path: 'bar' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"[config validation of [xpack.monitoring.elasticsearch].ssl]: cannot use [key] when [keystore.path] is specified"` + ); + }); + + it('throws if certificate and keystore.path are both specified', () => { + const value = { ssl: { certificate: 'foo', keystore: { path: 'bar' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"[config validation of [xpack.monitoring.elasticsearch].ssl]: cannot use [certificate] when [keystore.path] is specified"` + ); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.ts b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.ts new file mode 100644 index 00000000000000..70e6235602b5b9 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.ts @@ -0,0 +1,111 @@ +/* + * 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 { readFileSync } from 'fs'; +import { readPkcs12Truststore, readPkcs12Keystore } from '../../../../../../src/core/utils'; + +const KEY = 'xpack.monitoring.elasticsearch'; + +/* + * Parse a config object's Elasticsearch configuration, reading any + * certificates/keys from the filesystem + * + * TODO: this code can be removed when this plugin is migrated to the Kibana Platform, + * at that point the ElasticsearchClient and ElasticsearchConfig should be used instead + */ +export const parseElasticsearchConfig = (config: any) => { + const es = config.get(KEY); + + const errorPrefix = `[config validation of [${KEY}].ssl]`; + if (es.ssl?.key && es.ssl?.keystore?.path) { + throw new Error(`${errorPrefix}: cannot use [key] when [keystore.path] is specified`); + } + if (es.ssl?.certificate && es.ssl?.keystore?.path) { + throw new Error(`${errorPrefix}: cannot use [certificate] when [keystore.path] is specified`); + } + + const { alwaysPresentCertificate, verificationMode } = es.ssl; + const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(es); + + return { + ...es, + ssl: { + alwaysPresentCertificate, + key, + keyPassphrase, + certificate, + certificateAuthorities, + verificationMode, + }, + }; +}; + +const readKeyAndCerts = (rawConfig: any) => { + let key: string | undefined; + let keyPassphrase: string | undefined; + let certificate: string | undefined; + let certificateAuthorities: string[] | undefined; + + const addCAs = (ca: string[] | undefined) => { + if (ca && ca.length) { + certificateAuthorities = [...(certificateAuthorities || []), ...ca]; + } + }; + + if (rawConfig.ssl.keystore?.path) { + const keystore = readPkcs12Keystore( + rawConfig.ssl.keystore.path, + rawConfig.ssl.keystore.password + ); + if (!keystore.key) { + throw new Error(`Did not find key in Elasticsearch keystore.`); + } else if (!keystore.cert) { + throw new Error(`Did not find certificate in Elasticsearch keystore.`); + } + key = keystore.key; + certificate = keystore.cert; + addCAs(keystore.ca); + } else { + if (rawConfig.ssl.key) { + key = readFile(rawConfig.ssl.key); + keyPassphrase = rawConfig.ssl.keyPassphrase; + } + if (rawConfig.ssl.certificate) { + certificate = readFile(rawConfig.ssl.certificate); + } + } + + if (rawConfig.ssl.truststore?.path) { + const ca = readPkcs12Truststore( + rawConfig.ssl.truststore.path, + rawConfig.ssl.truststore.password + ); + addCAs(ca); + } + + const ca = rawConfig.ssl.certificateAuthorities; + if (ca) { + const parsed: string[] = []; + const paths = Array.isArray(ca) ? ca : [ca]; + if (paths.length > 0) { + for (const path of paths) { + parsed.push(readFile(path)); + } + addCAs(parsed); + } + } + + return { + key, + keyPassphrase, + certificate, + certificateAuthorities, + }; +}; + +const readFile = (file: string) => { + return readFileSync(file, 'utf8'); +}; diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index e26dd96dde1bf7..163bc43945be1c 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -11,6 +11,7 @@ import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; +import { parseElasticsearchConfig } from './es_client/parse_elasticsearch_config'; export class Plugin { setup(core, plugins) { @@ -36,6 +37,12 @@ export class Plugin { * fetch methods and uploads to the ES monitoring bulk endpoint */ const xpackMainPlugin = plugins.xpack_main; + + /* + * Parse the Elasticsearch config and read any certificates/keys if necessary + */ + const elasticsearchConfig = parseElasticsearchConfig(config); + xpackMainPlugin.status.once('green', async () => { // first time xpack_main turns green /* @@ -47,7 +54,7 @@ export class Plugin { await instantiateClient({ log: core.log, events: core.events, - config, + elasticsearchConfig, elasticsearchPlugin: plugins.elasticsearch, }); // Instantiate the dedicated ES client await initMonitoringXpackInfo({ From 01fe8afb987fc80125e84d965c5534bdcdc6e409 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 21 Jan 2020 17:35:47 +0000 Subject: [PATCH 19/19] [ML] Fixing categorization wizard example results (#54924) * [ML] Fixing categorization wizard example results * moving validation results to class * cleaning up category analyzer types * small tweaks * removing commented out code * fixing string ids * small refactor * improving validation messages * fixing types * updating message text * fixing typo * adding privileges error * updating privilege message * changes based on review * removing old warning message * fixing translations * renaming enum --- .../plugins/ml/common/constants/new_job.ts | 7 + .../plugins/ml/common/types/categories.ts | 29 ++ .../ml/common/util/string_utils.test.ts | 24 +- .../plugins/ml/common/util/string_utils.ts | 5 + .../job_creator/categorization_job_creator.ts | 53 +-- .../common/job_validator/job_validator.ts | 4 +- .../categorization_examples_loader.ts | 25 +- .../new_job/common/results_loader/index.ts | 2 +- .../examples_valid_callout.tsx | 64 ++-- .../categorization_view/field_examples.tsx | 4 +- .../categorization_view/metric_selection.tsx | 34 +- .../services/ml_api_service/index.d.ts | 15 +- .../application/services/ml_server_info.ts | 8 +- .../ml/server/models/job_service/index.js | 7 +- .../job_service/new_job/categorization.ts | 314 ------------------ .../new_job/categorization/examples.ts | 206 ++++++++++++ .../new_job/categorization/index.ts | 8 + .../new_job/categorization/top_categories.ts | 164 +++++++++ .../categorization/validation_results.ts | 208 ++++++++++++ .../models/job_service/new_job/index.ts | 2 +- 20 files changed, 763 insertions(+), 420 deletions(-) delete mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization.ts create mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts create mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts create mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts create mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts diff --git a/x-pack/legacy/plugins/ml/common/constants/new_job.ts b/x-pack/legacy/plugins/ml/common/constants/new_job.ts index 3c98b372afdf73..862fa72d11fdb7 100644 --- a/x-pack/legacy/plugins/ml/common/constants/new_job.ts +++ b/x-pack/legacy/plugins/ml/common/constants/new_job.ts @@ -26,7 +26,14 @@ export const DEFAULT_QUERY_DELAY = '60s'; export const SHARED_RESULTS_INDEX_NAME = 'shared'; +// Categorization export const NUMBER_OF_CATEGORY_EXAMPLES = 5; export const CATEGORY_EXAMPLES_SAMPLE_SIZE = 1000; export const CATEGORY_EXAMPLES_WARNING_LIMIT = 0.75; export const CATEGORY_EXAMPLES_ERROR_LIMIT = 0.02; + +export enum CATEGORY_EXAMPLES_VALIDATION_STATUS { + VALID = 'valid', + PARTIALLY_VALID = 'partially_valid', + INVALID = 'invalid', +} diff --git a/x-pack/legacy/plugins/ml/common/types/categories.ts b/x-pack/legacy/plugins/ml/common/types/categories.ts index 6ccd13ed9a39ec..765053ced52012 100644 --- a/x-pack/legacy/plugins/ml/common/types/categories.ts +++ b/x-pack/legacy/plugins/ml/common/types/categories.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../constants/new_job'; + export type CategoryId = number; export interface Category { @@ -23,3 +25,30 @@ export interface Token { type: string; position: number; } + +export interface CategorizationAnalyzer { + char_filter?: any[]; + tokenizer?: string; + filter?: any[]; + analyzer?: string; +} + +export interface CategoryFieldExample { + text: string; + tokens: Token[]; +} + +export enum VALIDATION_RESULT { + TOKEN_COUNT, + MEDIAN_LINE_LENGTH, + NULL_VALUES, + TOO_MANY_TOKENS, + FAILED_TO_TOKENIZE, + INSUFFICIENT_PRIVILEGES, +} + +export interface FieldExampleCheck { + id: VALIDATION_RESULT; + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS; + message: string; +} diff --git a/x-pack/legacy/plugins/ml/common/util/string_utils.test.ts b/x-pack/legacy/plugins/ml/common/util/string_utils.test.ts index aba2dbd230ada3..026c8e6110c993 100644 --- a/x-pack/legacy/plugins/ml/common/util/string_utils.test.ts +++ b/x-pack/legacy/plugins/ml/common/util/string_utils.test.ts @@ -4,7 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderTemplate } from './string_utils'; +import { renderTemplate, getMedianStringLength } from './string_utils'; + +const strings: string[] = [ + 'foo', + 'foofoofoofoofoo', + 'foofoofoo', + 'f', + 'f', + 'foofoofoofoofoofoofoo', +]; +const noStrings: string[] = []; describe('ML - string utils', () => { describe('renderTemplate', () => { @@ -24,4 +34,16 @@ describe('ML - string utils', () => { expect(result).toBe('string with 1 replacement, and a 2nd one.'); }); }); + + describe('getMedianStringLength', () => { + test('test median for string array', () => { + const result = getMedianStringLength(strings); + expect(result).toBe(9); + }); + + test('test median for no strings', () => { + const result = getMedianStringLength(noStrings); + expect(result).toBe(0); + }); + }); }); diff --git a/x-pack/legacy/plugins/ml/common/util/string_utils.ts b/x-pack/legacy/plugins/ml/common/util/string_utils.ts index 432baabe773cc1..9dd2ce3d74cd5d 100644 --- a/x-pack/legacy/plugins/ml/common/util/string_utils.ts +++ b/x-pack/legacy/plugins/ml/common/util/string_utils.ts @@ -17,3 +17,8 @@ export function renderTemplate(str: string, data?: Record): stri return str; } + +export function getMedianStringLength(strings: string[]) { + const sortedStringLengths = strings.map(s => s.length).sort((a, b) => a - b); + return sortedStringLengths[Math.floor(sortedStringLengths.length / 2)] || 0; +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts index 71619311c4361d..0ff0ffb6f3bb39 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts @@ -16,25 +16,31 @@ import { CREATED_BY_LABEL, DEFAULT_BUCKET_SPAN, DEFAULT_RARE_BUCKET_SPAN, + CATEGORY_EXAMPLES_VALIDATION_STATUS, } from '../../../../../../common/constants/new_job'; import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; +import { + CategorizationAnalyzer, + CategoryFieldExample, + FieldExampleCheck, +} from '../../../../../../common/types/categories'; import { getRichDetectors } from './util/general'; -import { CategorizationExamplesLoader, CategoryExample } from '../results_loader'; -import { CategorizationAnalyzer, getNewJobDefaults } from '../../../../services/ml_server_info'; - -type CategorizationAnalyzerType = CategorizationAnalyzer | null; +import { CategorizationExamplesLoader } from '../results_loader'; +import { getNewJobDefaults } from '../../../../services/ml_server_info'; export class CategorizationJobCreator extends JobCreator { protected _type: JOB_TYPE = JOB_TYPE.CATEGORIZATION; private _createCountDetector: () => void = () => {}; private _createRareDetector: () => void = () => {}; private _examplesLoader: CategorizationExamplesLoader; - private _categoryFieldExamples: CategoryExample[] = []; - private _categoryFieldValid: number = 0; + private _categoryFieldExamples: CategoryFieldExample[] = []; + private _validationChecks: FieldExampleCheck[] = []; + private _overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS = + CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID; private _detectorType: ML_JOB_AGGREGATION.COUNT | ML_JOB_AGGREGATION.RARE = ML_JOB_AGGREGATION.COUNT; - private _categorizationAnalyzer: CategorizationAnalyzerType = null; - private _defaultCategorizationAnalyzer: CategorizationAnalyzerType; + private _categorizationAnalyzer: CategorizationAnalyzer = {}; + private _defaultCategorizationAnalyzer: CategorizationAnalyzer; constructor( indexPattern: IndexPattern, @@ -46,7 +52,7 @@ export class CategorizationJobCreator extends JobCreator { this._examplesLoader = new CategorizationExamplesLoader(this, indexPattern, query); const { anomaly_detectors: anomalyDetectors } = getNewJobDefaults(); - this._defaultCategorizationAnalyzer = anomalyDetectors.categorization_analyzer || null; + this._defaultCategorizationAnalyzer = anomalyDetectors.categorization_analyzer || {}; } public setDefaultDetectorProperties( @@ -93,7 +99,7 @@ export class CategorizationJobCreator extends JobCreator { } else { delete this._job_config.analysis_config.categorization_field_name; this._categoryFieldExamples = []; - this._categoryFieldValid = 0; + this._validationChecks = []; } } @@ -102,31 +108,38 @@ export class CategorizationJobCreator extends JobCreator { } public async loadCategorizationFieldExamples() { - const { valid, examples, sampleSize } = await this._examplesLoader.loadExamples(); + const { + examples, + sampleSize, + overallValidStatus, + validationChecks, + } = await this._examplesLoader.loadExamples(); this._categoryFieldExamples = examples; - this._categoryFieldValid = valid; - return { valid, examples, sampleSize }; + this._validationChecks = validationChecks; + this._overallValidStatus = overallValidStatus; + return { examples, sampleSize, overallValidStatus, validationChecks }; } public get categoryFieldExamples() { return this._categoryFieldExamples; } - public get categoryFieldValid() { - return this._categoryFieldValid; + public get validationChecks() { + return this._validationChecks; + } + + public get overallValidStatus() { + return this._overallValidStatus; } public get selectedDetectorType() { return this._detectorType; } - public set categorizationAnalyzer(analyzer: CategorizationAnalyzerType) { + public set categorizationAnalyzer(analyzer: CategorizationAnalyzer) { this._categorizationAnalyzer = analyzer; - if ( - analyzer === null || - isEqual(this._categorizationAnalyzer, this._defaultCategorizationAnalyzer) - ) { + if (isEqual(this._categorizationAnalyzer, this._defaultCategorizationAnalyzer)) { delete this._job_config.analysis_config.categorization_analyzer; } else { this._job_config.analysis_config.categorization_analyzer = analyzer; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 976e94b377ae8b..8f6b16c407fb66 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -16,7 +16,7 @@ import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_c import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; import { cardinalityValidator, CardinalityValidatorResult } from './validators'; -import { CATEGORY_EXAMPLES_ERROR_LIMIT } from '../../../../../../common/constants/new_job'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../common/constants/new_job'; // delay start of validation to allow the user to make changes // e.g. if they are typing in a new value, try not to validate @@ -207,7 +207,7 @@ export class JobValidator { private _runAdvancedValidation() { if (isCategorizationJobCreator(this._jobCreator)) { this._advancedValidations.categorizationFieldValid.valid = - this._jobCreator.categoryFieldValid > CATEGORY_EXAMPLES_ERROR_LIMIT; + this._jobCreator.overallValidStatus !== CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID; } } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts index ce1ea0bdaf181c..62a4d070fec328 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts @@ -6,15 +6,12 @@ import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { IndexPatternTitle } from '../../../../../../common/types/kibana'; -import { Token } from '../../../../../../common/types/categories'; import { CategorizationJobCreator } from '../job_creator'; import { ml } from '../../../../services/ml_api_service'; -import { NUMBER_OF_CATEGORY_EXAMPLES } from '../../../../../../common/constants/new_job'; - -export interface CategoryExample { - text: string; - tokens: Token[]; -} +import { + NUMBER_OF_CATEGORY_EXAMPLES, + CATEGORY_EXAMPLES_VALIDATION_STATUS, +} from '../../../../../../common/constants/new_job'; export class CategorizationExamplesLoader { private _jobCreator: CategorizationJobCreator; @@ -36,20 +33,22 @@ export class CategorizationExamplesLoader { const analyzer = this._jobCreator.categorizationAnalyzer; const categorizationFieldName = this._jobCreator.categorizationFieldName; if (categorizationFieldName === null) { - return { valid: 0, examples: [], sampleSize: 0 }; + return { + examples: [], + sampleSize: 0, + overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID, + validationChecks: [], + }; } - const start = Math.floor( - this._jobCreator.start + (this._jobCreator.end - this._jobCreator.start) / 2 - ); const resp = await ml.jobs.categorizationFieldExamples( this._indexPatternTitle, this._query, NUMBER_OF_CATEGORY_EXAMPLES, categorizationFieldName, this._timeFieldName, - start, - 0, + this._jobCreator.start, + this._jobCreator.end, analyzer ); return resp; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts index 724c62f22e469f..e15d859f8e6c31 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts @@ -5,4 +5,4 @@ */ export { ResultsLoader, Results, ModelItem, Anomaly } from './results_loader'; -export { CategorizationExamplesLoader, CategoryExample } from './categorization_examples_loader'; +export { CategorizationExamplesLoader } from './categorization_examples_loader'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx index 270ba99d938cdc..ac886a3aea61a7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx @@ -9,27 +9,24 @@ import { EuiCallOut, EuiSpacer, EuiCallOutProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CategorizationAnalyzer } from '../../../../../../../services/ml_server_info'; -import { EditCategorizationAnalyzerFlyout } from '../../../common/edit_categorization_analyzer_flyout'; import { - CATEGORY_EXAMPLES_ERROR_LIMIT, - CATEGORY_EXAMPLES_WARNING_LIMIT, -} from '../../../../../../../../../common/constants/new_job'; - -type CategorizationAnalyzerType = CategorizationAnalyzer | null; + CategorizationAnalyzer, + FieldExampleCheck, +} from '../../../../../../../../../common/types/categories'; +import { EditCategorizationAnalyzerFlyout } from '../../../common/edit_categorization_analyzer_flyout'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../../../../common/constants/new_job'; interface Props { - examplesValid: number; - sampleSize: number; - categorizationAnalyzer: CategorizationAnalyzerType; + validationChecks: FieldExampleCheck[]; + overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS; + categorizationAnalyzer: CategorizationAnalyzer; } export const ExamplesValidCallout: FC = ({ - examplesValid, + overallValidStatus, + validationChecks, categorizationAnalyzer, - sampleSize, }) => { - const percentageText = ; const analyzerUsed = ; let color: EuiCallOutProps['color'] = 'success'; @@ -40,7 +37,7 @@ export const ExamplesValidCallout: FC = ({ } ); - if (examplesValid < CATEGORY_EXAMPLES_ERROR_LIMIT) { + if (overallValidStatus === CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID) { color = 'danger'; title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldCalloutTitle.invalid', @@ -48,7 +45,7 @@ export const ExamplesValidCallout: FC = ({ defaultMessage: 'Selected category field is invalid', } ); - } else if (examplesValid < CATEGORY_EXAMPLES_WARNING_LIMIT) { + } else if (overallValidStatus === CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID) { color = 'warning'; title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldCalloutTitle.possiblyInvalid', @@ -60,45 +57,24 @@ export const ExamplesValidCallout: FC = ({ return ( - {percentageText} + {validationChecks.map((v, i) => ( +
{v.message}
+ ))} {analyzerUsed}
); }; -const PercentageText: FC<{ examplesValid: number; sampleSize: number }> = ({ - examplesValid, - sampleSize, -}) => ( -
- -
-); - -const AnalyzerUsed: FC<{ categorizationAnalyzer: CategorizationAnalyzerType }> = ({ +const AnalyzerUsed: FC<{ categorizationAnalyzer: CategorizationAnalyzer }> = ({ categorizationAnalyzer, }) => { let analyzer = ''; - if (typeof categorizationAnalyzer === null) { - return null; - } - if (typeof categorizationAnalyzer === 'string') { - analyzer = categorizationAnalyzer; - } else { - if (categorizationAnalyzer?.tokenizer !== undefined) { - analyzer = categorizationAnalyzer?.tokenizer!; - } else if (categorizationAnalyzer?.analyzer !== undefined) { - analyzer = categorizationAnalyzer?.analyzer!; - } + if (categorizationAnalyzer?.tokenizer !== undefined) { + analyzer = categorizationAnalyzer.tokenizer; + } else if (categorizationAnalyzer?.analyzer !== undefined) { + analyzer = categorizationAnalyzer.analyzer; } return ( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx index 7f9b2e43b90050..51cea179a6c0d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx @@ -7,10 +7,10 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiText } from '@elastic/eui'; -import { CategoryExample } from '../../../../../common/results_loader'; +import { CategoryFieldExample } from '../../../../../../../../../common/types/categories'; interface Props { - fieldExamples: CategoryExample[] | null; + fieldExamples: CategoryFieldExample[] | null; } const TOKEN_HIGHLIGHT_COLOR = '#b0ccf7'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx index 52b5c61e70fe5e..411f6e898bd486 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx @@ -14,7 +14,11 @@ import { CategorizationField } from '../categorization_field'; import { CategorizationDetector } from '../categorization_detector'; import { FieldExamples } from './field_examples'; import { ExamplesValidCallout } from './examples_valid_callout'; -import { CategoryExample } from '../../../../../common/results_loader'; +import { + CategoryFieldExample, + FieldExampleCheck, +} from '../../../../../../../../../common/types/categories'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../../../../common/constants/new_job'; import { LoadingWrapper } from '../../../charts/loading_wrapper'; interface Props { @@ -31,9 +35,11 @@ export const CategorizationDetectors: FC = ({ setIsValid }) => { const [categorizationAnalyzerString, setCategorizationAnalyzerString] = useState( JSON.stringify(jobCreator.categorizationAnalyzer) ); - const [fieldExamples, setFieldExamples] = useState(null); - const [examplesValid, setExamplesValid] = useState(0); - const [sampleSize, setSampleSize] = useState(0); + const [fieldExamples, setFieldExamples] = useState(null); + const [overallValidStatus, setOverallValidStatus] = useState( + CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID + ); + const [validationChecks, setValidationChecks] = useState([]); const [categorizationFieldName, setCategorizationFieldName] = useState( jobCreator.categorizationFieldName @@ -73,28 +79,32 @@ export const CategorizationDetectors: FC = ({ setIsValid }) => { setLoadingData(true); try { const { - valid, examples, - sampleSize: tempSampleSize, + overallValidStatus: tempOverallValidStatus, + validationChecks: tempValidationChecks, } = await jobCreator.loadCategorizationFieldExamples(); setFieldExamples(examples); - setExamplesValid(valid); + setOverallValidStatus(tempOverallValidStatus); + setValidationChecks(tempValidationChecks); setLoadingData(false); - setSampleSize(tempSampleSize); } catch (error) { setLoadingData(false); + setFieldExamples(null); + setValidationChecks([]); + setOverallValidStatus(CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID); mlMessageBarService.notify.error(error); } } else { setFieldExamples(null); - setExamplesValid(0); + setValidationChecks([]); + setOverallValidStatus(CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID); } setIsValid(categorizationFieldName !== null); } useEffect(() => { jobCreatorUpdate(); - }, [examplesValid]); + }, [overallValidStatus]); return ( <> @@ -109,8 +119,8 @@ export const CategorizationDetectors: FC = ({ setIsValid }) => { {fieldExamples !== null && loadingData === false && ( <> diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index db9d158c0ead99..6420b60e4c8380 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -22,6 +22,12 @@ import { PartitionFieldsDefinition } from '../results_service/result_service_rx' import { annotations } from './annotations'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; import { CombinedJob, JobId } from '../../jobs/new_job/common/job_creator/configs'; +import { + CategorizationAnalyzer, + CategoryFieldExample, + FieldExampleCheck, +} from '../../../../common/types/categories'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/new_job'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -184,8 +190,13 @@ declare interface Ml { timeField: string | undefined, start: number, end: number, - analyzer: any - ): Promise<{ valid: number; examples: any[]; sampleSize: number }>; + analyzer: CategorizationAnalyzer + ): Promise<{ + examples: CategoryFieldExample[]; + sampleSize: number; + overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS; + validationChecks: FieldExampleCheck[]; + }>; topCategories( jobId: string, count: number diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts index 6bf5a7b0c97433..304778281c2f2f 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts @@ -5,6 +5,7 @@ */ import { ml } from './ml_api_service'; +import { CategorizationAnalyzer } from '../../../common/types/categories'; export interface MlServerDefaults { anomaly_detectors: { @@ -16,13 +17,6 @@ export interface MlServerDefaults { datafeeds: { scroll_size?: number }; } -export interface CategorizationAnalyzer { - char_filter?: any[]; - tokenizer?: string; - filter?: any[]; - analyzer?: string; -} - export interface MlServerLimits { max_model_memory_limit?: string; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/legacy/plugins/ml/server/models/job_service/index.js index 186bcbae84546f..5c0eff3112a53e 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/index.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/index.js @@ -8,7 +8,11 @@ import { datafeedsProvider } from './datafeeds'; import { jobsProvider } from './jobs'; import { groupsProvider } from './groups'; import { newJobCapsProvider } from './new_job_caps'; -import { newJobChartsProvider, categorizationExamplesProvider } from './new_job'; +import { + newJobChartsProvider, + categorizationExamplesProvider, + topCategoriesProvider, +} from './new_job'; export function jobServiceProvider(callWithRequest, request) { return { @@ -18,5 +22,6 @@ export function jobServiceProvider(callWithRequest, request) { ...newJobCapsProvider(callWithRequest, request), ...newJobChartsProvider(callWithRequest, request), ...categorizationExamplesProvider(callWithRequest, request), + ...topCategoriesProvider(callWithRequest, request), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization.ts deleted file mode 100644 index b3c70bf589cd04..00000000000000 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization.ts +++ /dev/null @@ -1,314 +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 { chunk } from 'lodash'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; -import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../common/constants/new_job'; -import { CategoryId, Category, Token } from '../../../../common/types/categories'; -import { callWithRequestType } from '../../../../common/types/kibana'; - -const VALID_TOKEN_COUNT = 3; -const CHUNK_SIZE = 100; - -export function categorizationExamplesProvider(callWithRequest: callWithRequestType) { - async function categorizationExamples( - indexPatternTitle: string, - query: any, - size: number, - categorizationFieldName: string, - timeField: string | undefined, - start: number, - end: number, - analyzer?: any - ) { - if (timeField !== undefined) { - const range = { - range: { - [timeField]: { - gte: start, - format: 'epoch_millis', - }, - }, - }; - - if (query.bool === undefined) { - query.bool = {}; - } - if (query.bool.filter === undefined) { - query.bool.filter = range; - } else { - if (Array.isArray(query.bool.filter)) { - query.bool.filter.push(range); - } else { - query.bool.filter.range = range; - } - } - } - - const results = await callWithRequest('search', { - index: indexPatternTitle, - size, - body: { - _source: categorizationFieldName, - query, - }, - }); - const examples: string[] = results.hits?.hits - ?.map((doc: any) => doc._source[categorizationFieldName]) - .filter((example: string | null | undefined) => example !== undefined && example !== null); - - async function loadTokens(chunkSize: number) { - const exampleChunks = chunk(examples, chunkSize); - const tokensPerChunks = await Promise.all(exampleChunks.map(c => getTokens(c, analyzer))); - const tokensPerExample = tokensPerChunks.flat(); - return examples.map((e, i) => ({ text: e, tokens: tokensPerExample[i] })); - } - try { - return loadTokens(CHUNK_SIZE); - } catch (error) { - // if an error is thrown when loading the tokens, lower the chunk size by half and try again - // the error may have been caused by too many tokens being found. - // the _analyze endpoint has a maximum of 10000 tokens. - return loadTokens(CHUNK_SIZE / 2); - } - } - - async function getTokens(examples: string[], analyzer?: any) { - const { tokens }: { tokens: Token[] } = await callWithRequest('indices.analyze', { - body: { - ...getAnalyzer(analyzer), - text: examples, - }, - }); - - const lengths = examples.map(e => e.length); - const sumLengths = lengths.map((s => (a: number) => (s += a))(0)); - - const tokensPerExample: Token[][] = examples.map(e => []); - - tokens.forEach((t, i) => { - for (let g = 0; g < sumLengths.length; g++) { - if (t.start_offset <= sumLengths[g] + g) { - const offset = g > 0 ? sumLengths[g - 1] + g : 0; - tokensPerExample[g].push({ - ...t, - start_offset: t.start_offset - offset, - end_offset: t.end_offset - offset, - }); - break; - } - } - }); - return tokensPerExample; - } - - function getAnalyzer(analyzer: any) { - if (typeof analyzer === 'object' && analyzer.tokenizer !== undefined) { - return analyzer; - } else { - return { analyzer: 'standard' }; - } - } - - async function validateCategoryExamples( - indexPatternTitle: string, - query: any, - size: number, - categorizationFieldName: string, - timeField: string | undefined, - start: number, - end: number, - analyzer?: any - ) { - const resp = await categorizationExamples( - indexPatternTitle, - query, - CATEGORY_EXAMPLES_SAMPLE_SIZE, - categorizationFieldName, - timeField, - start, - end, - analyzer - ); - - const sortedExamples = resp - .map((e, i) => ({ ...e, origIndex: i })) - .sort((a, b) => b.tokens.length - a.tokens.length); - const validExamples = sortedExamples.filter(e => e.tokens.length >= VALID_TOKEN_COUNT); - const sampleSize = sortedExamples.length; - - const multiple = Math.floor(sampleSize / size) || sampleSize; - const filteredExamples = []; - let i = 0; - while (filteredExamples.length < size && i < sortedExamples.length) { - filteredExamples.push(sortedExamples[i]); - i += multiple; - } - const examples = filteredExamples - .sort((a, b) => a.origIndex - b.origIndex) - .map(e => ({ text: e.text, tokens: e.tokens })); - - return { - sampleSize, - valid: sortedExamples.length === 0 ? 0 : validExamples.length / sortedExamples.length, - examples, - }; - } - - async function getTotalCategories(jobId: string): Promise<{ total: number }> { - const totalResp = await callWithRequest('search', { - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [ - { - term: { - job_id: jobId, - }, - }, - { - exists: { - field: 'category_id', - }, - }, - ], - }, - }, - }, - }); - return totalResp?.hits?.total?.value ?? 0; - } - - async function getTopCategoryCounts(jobId: string, numberOfCategories: number) { - const top = await callWithRequest('search', { - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [ - { - term: { - job_id: jobId, - }, - }, - { - term: { - result_type: 'model_plot', - }, - }, - { - term: { - by_field_name: 'mlcategory', - }, - }, - ], - }, - }, - aggs: { - cat_count: { - terms: { - field: 'by_field_value', - size: numberOfCategories, - }, - }, - }, - }, - }); - - const catCounts: Array<{ - id: CategoryId; - count: number; - }> = top.aggregations?.cat_count?.buckets.map((c: any) => ({ - id: c.key, - count: c.doc_count, - })); - return catCounts || []; - } - - async function getCategories( - jobId: string, - catIds: CategoryId[], - size: number - ): Promise { - const categoryFilter = catIds.length - ? { - terms: { - category_id: catIds, - }, - } - : { - exists: { - field: 'category_id', - }, - }; - const result = await callWithRequest('search', { - index: ML_RESULTS_INDEX_PATTERN, - size, - body: { - query: { - bool: { - filter: [ - { - term: { - job_id: jobId, - }, - }, - categoryFilter, - ], - }, - }, - }, - }); - - return result.hits.hits?.map((c: { _source: Category }) => c._source) || []; - } - - async function topCategories(jobId: string, numberOfCategories: number) { - const catCounts = await getTopCategoryCounts(jobId, numberOfCategories); - const categories = await getCategories( - jobId, - catCounts.map(c => c.id), - catCounts.length || numberOfCategories - ); - - const catsById = categories.reduce((p, c) => { - p[c.category_id] = c; - return p; - }, {} as { [id: number]: Category }); - - const total = await getTotalCategories(jobId); - - if (catCounts.length) { - return { - total, - categories: catCounts.map(({ id, count }) => { - return { - count, - category: catsById[id] ?? null, - }; - }), - }; - } else { - return { - total, - categories: categories.map(category => { - return { - category, - }; - }), - }; - } - } - - return { - categorizationExamples, - validateCategoryExamples, - topCategories, - }; -} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts new file mode 100644 index 00000000000000..76473bd55db7fb --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -0,0 +1,206 @@ +/* + * 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 { chunk } from 'lodash'; +import { SearchResponse } from 'elasticsearch'; +import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../common/constants/new_job'; +import { + Token, + CategorizationAnalyzer, + CategoryFieldExample, +} from '../../../../../common/types/categories'; +import { callWithRequestType } from '../../../../../common/types/kibana'; +import { ValidationResults } from './validation_results'; + +const CHUNK_SIZE = 100; + +export function categorizationExamplesProvider(callWithRequest: callWithRequestType) { + const validationResults = new ValidationResults(); + + async function categorizationExamples( + indexPatternTitle: string, + query: any, + size: number, + categorizationFieldName: string, + timeField: string | undefined, + start: number, + end: number, + analyzer: CategorizationAnalyzer + ): Promise<{ examples: CategoryFieldExample[]; error?: any }> { + if (timeField !== undefined) { + const range = { + range: { + [timeField]: { + gte: start, + lt: end, + format: 'epoch_millis', + }, + }, + }; + if (query.bool === undefined) { + query.bool = {}; + } + if (query.bool.filter === undefined) { + query.bool.filter = range; + } else { + if (Array.isArray(query.bool.filter)) { + query.bool.filter.push(range); + } else { + query.bool.filter.range = range; + } + } + } + + const results: SearchResponse<{ [id: string]: string }> = await callWithRequest('search', { + index: indexPatternTitle, + size, + body: { + _source: categorizationFieldName, + query, + sort: ['_doc'], + }, + }); + + const tempExamples = results.hits.hits.map(({ _source }) => _source[categorizationFieldName]); + + validationResults.createNullValueResult(tempExamples); + + const allExamples = tempExamples.filter( + (example: string | null | undefined) => example !== undefined && example !== null + ); + + validationResults.createMedianMessageLengthResult(allExamples); + + try { + const examplesWithTokens = await getTokens(CHUNK_SIZE, allExamples, analyzer); + return { examples: examplesWithTokens }; + } catch (err) { + // console.log('dropping to 50 chunk size'); + // if an error is thrown when loading the tokens, lower the chunk size by half and try again + // the error may have been caused by too many tokens being found. + // the _analyze endpoint has a maximum of 10000 tokens. + const halfExamples = allExamples.splice(0, Math.ceil(allExamples.length / 2)); + const halfChunkSize = CHUNK_SIZE / 2; + try { + const examplesWithTokens = await getTokens(halfChunkSize, halfExamples, analyzer); + return { examples: examplesWithTokens }; + } catch (error) { + validationResults.createTooManyTokensResult(error, halfChunkSize); + return { examples: halfExamples.map(e => ({ text: e, tokens: [] })) }; + } + } + } + + async function getTokens( + chunkSize: number, + examples: string[], + analyzer: CategorizationAnalyzer + ): Promise { + const exampleChunks = chunk(examples, chunkSize); + const tokensPerExampleChunks: Token[][][] = []; + for (const c of exampleChunks) { + tokensPerExampleChunks.push(await loadTokens(c, analyzer)); + } + const tokensPerExample = tokensPerExampleChunks.flat(); + return examples.map((e, i) => ({ text: e, tokens: tokensPerExample[i] })); + } + + async function loadTokens(examples: string[], analyzer: CategorizationAnalyzer) { + const { tokens }: { tokens: Token[] } = await callWithRequest('indices.analyze', { + body: { + ...getAnalyzer(analyzer), + text: examples, + }, + }); + + const lengths = examples.map(e => e.length); + const sumLengths = lengths.map((s => (a: number) => (s += a))(0)); + + const tokensPerExample: Token[][] = examples.map(e => []); + + tokens.forEach((t, i) => { + for (let g = 0; g < sumLengths.length; g++) { + if (t.start_offset <= sumLengths[g] + g) { + const offset = g > 0 ? sumLengths[g - 1] + g : 0; + tokensPerExample[g].push({ + ...t, + start_offset: t.start_offset - offset, + end_offset: t.end_offset - offset, + }); + break; + } + } + }); + return tokensPerExample; + } + + function getAnalyzer(analyzer: CategorizationAnalyzer) { + if (typeof analyzer === 'object' && analyzer.tokenizer !== undefined) { + return analyzer; + } else { + return { analyzer: 'standard' }; + } + } + + async function validateCategoryExamples( + indexPatternTitle: string, + query: any, + size: number, + categorizationFieldName: string, + timeField: string | undefined, + start: number, + end: number, + analyzer: CategorizationAnalyzer + ) { + const resp = await categorizationExamples( + indexPatternTitle, + query, + CATEGORY_EXAMPLES_SAMPLE_SIZE, + categorizationFieldName, + timeField, + start, + end, + analyzer + ); + + const { examples } = resp; + const sampleSize = examples.length; + validationResults.createTokenCountResult(examples, sampleSize); + + // sort examples by number of tokens, keeping track of their original order + // with an origIndex property + const sortedExamples = examples + .map((e, i) => ({ ...e, origIndex: i })) + .sort((a, b) => b.tokens.length - a.tokens.length); + + // we only want 'size' (e.g. 5) number of examples, + // so loop through the sorted examples, taking 5 at evenly + // spread intervals + const multiple = Math.floor(sampleSize / size) || sampleSize; + const filteredExamples = []; + let i = 0; + while (filteredExamples.length < size && i < sampleSize) { + filteredExamples.push(sortedExamples[i]); + i += multiple; + } + + // sort back into original order and remove origIndex property + const processedExamples = filteredExamples + .sort((a, b) => a.origIndex - b.origIndex) + .map(e => ({ text: e.text, tokens: e.tokens })); + + return { + overallValidStatus: validationResults.overallResult, + validationChecks: validationResults.results, + sampleSize, + examples: processedExamples, + }; + } + + return { + validateCategoryExamples, + }; +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts new file mode 100644 index 00000000000000..be32b99b5e527e --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { categorizationExamplesProvider } from './examples'; +export { topCategoriesProvider } from './top_categories'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts new file mode 100644 index 00000000000000..3361cc454e2b7b --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -0,0 +1,164 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; +import { CategoryId, Category } from '../../../../../common/types/categories'; +import { callWithRequestType } from '../../../../../common/types/kibana'; + +export function topCategoriesProvider(callWithRequest: callWithRequestType) { + async function getTotalCategories(jobId: string): Promise<{ total: number }> { + const totalResp = await callWithRequest('search', { + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + term: { + job_id: jobId, + }, + }, + { + exists: { + field: 'category_id', + }, + }, + ], + }, + }, + }, + }); + return totalResp?.hits?.total?.value ?? 0; + } + + async function getTopCategoryCounts(jobId: string, numberOfCategories: number) { + const top: SearchResponse = await callWithRequest('search', { + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + term: { + job_id: jobId, + }, + }, + { + term: { + result_type: 'model_plot', + }, + }, + { + term: { + by_field_name: 'mlcategory', + }, + }, + ], + }, + }, + aggs: { + cat_count: { + terms: { + field: 'by_field_value', + size: numberOfCategories, + }, + }, + }, + }, + }); + + const catCounts: Array<{ + id: CategoryId; + count: number; + }> = top.aggregations?.cat_count?.buckets.map((c: any) => ({ + id: c.key, + count: c.doc_count, + })); + return catCounts || []; + } + + async function getCategories( + jobId: string, + catIds: CategoryId[], + size: number + ): Promise { + const categoryFilter = catIds.length + ? { + terms: { + category_id: catIds, + }, + } + : { + exists: { + field: 'category_id', + }, + }; + const result: SearchResponse = await callWithRequest('search', { + index: ML_RESULTS_INDEX_PATTERN, + size, + body: { + query: { + bool: { + filter: [ + { + term: { + job_id: jobId, + }, + }, + categoryFilter, + ], + }, + }, + }, + }); + + return result.hits.hits?.map((c: { _source: Category }) => c._source) || []; + } + + async function topCategories(jobId: string, numberOfCategories: number) { + const catCounts = await getTopCategoryCounts(jobId, numberOfCategories); + const categories = await getCategories( + jobId, + catCounts.map(c => c.id), + catCounts.length || numberOfCategories + ); + + const catsById = categories.reduce((p, c) => { + p[c.category_id] = c; + return p; + }, {} as { [id: number]: Category }); + + const total = await getTotalCategories(jobId); + + if (catCounts.length) { + return { + total, + categories: catCounts.map(({ id, count }) => { + return { + count, + category: catsById[id] ?? null, + }; + }), + }; + } else { + return { + total, + categories: categories.map(category => { + return { + category, + }; + }), + }; + } + } + + return { + topCategories, + }; +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts new file mode 100644 index 00000000000000..e173b893dfbfa1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { + CATEGORY_EXAMPLES_VALIDATION_STATUS, + CATEGORY_EXAMPLES_ERROR_LIMIT, + CATEGORY_EXAMPLES_WARNING_LIMIT, +} from '../../../../../common/constants/new_job'; +import { + FieldExampleCheck, + CategoryFieldExample, + VALIDATION_RESULT, +} from '../../../../../common/types/categories'; +import { getMedianStringLength } from '../../../../../common/util/string_utils'; + +const VALID_TOKEN_COUNT = 3; +const MEDIAN_LINE_LENGTH_LIMIT = 400; +const NULL_COUNT_PERCENT_LIMIT = 0.75; + +export class ValidationResults { + private _results: FieldExampleCheck[] = []; + + public get results() { + return this._results; + } + + public get overallResult() { + if (this._results.some(c => c.valid === CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID)) { + return CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID; + } + if (this._results.some(c => c.valid === CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID)) { + return CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID; + } + return CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID; + } + + private _resultExists(id: VALIDATION_RESULT) { + return this._results.some(r => r.id === id); + } + + public createTokenCountResult(examples: CategoryFieldExample[], sampleSize: number) { + if (examples.length === 0) { + this.createNoExamplesResult(); + return; + } + + if (this._resultExists(VALIDATION_RESULT.INSUFFICIENT_PRIVILEGES) === true) { + // if tokenizing has failed due to insufficient privileges, don't show + // the message about token count + return; + } + + const validExamplesSize = examples.filter(e => e.tokens.length >= VALID_TOKEN_COUNT).length; + const percentValid = sampleSize === 0 ? 0 : validExamplesSize / sampleSize; + + let valid = CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID; + if (percentValid < CATEGORY_EXAMPLES_ERROR_LIMIT) { + valid = CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID; + } else if (percentValid < CATEGORY_EXAMPLES_WARNING_LIMIT) { + valid = CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID; + } + + const message = i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.tokenLengthValidation', + { + defaultMessage: + '{number} field {number, plural, zero {value} one {value} other {values}} analyzed, {percentage}% contain {validTokenCount} or more tokens.', + values: { + number: sampleSize, + percentage: Math.floor(percentValid * 100), + validTokenCount: VALID_TOKEN_COUNT, + }, + } + ); + + if ( + this._resultExists(VALIDATION_RESULT.TOO_MANY_TOKENS) === false && + this._resultExists(VALIDATION_RESULT.FAILED_TO_TOKENIZE) === false + ) { + this._results.unshift({ + id: VALIDATION_RESULT.TOKEN_COUNT, + valid, + message, + }); + } + } + + public createMedianMessageLengthResult(examples: string[]) { + const median = getMedianStringLength(examples); + + if (median > MEDIAN_LINE_LENGTH_LIMIT) { + this._results.push({ + id: VALIDATION_RESULT.MEDIAN_LINE_LENGTH, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message: i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.medianLineLength', + { + defaultMessage: + 'The median length for the field values analyzed is over {medianLimit} characters.', + values: { medianLimit: MEDIAN_LINE_LENGTH_LIMIT }, + } + ), + }); + } + } + + public createNoExamplesResult() { + this._results.push({ + id: VALIDATION_RESULT.NULL_VALUES, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message: i18n.translate('xpack.ml.models.jobService.categorization.messages.noDataFound', { + defaultMessage: + 'No examples for this field could be found. Please ensure the selected date range contains data.', + }), + }); + } + + public createNullValueResult(examples: Array) { + const nullCount = examples.filter(e => e === null).length; + + if (nullCount / examples.length >= NULL_COUNT_PERCENT_LIMIT) { + this._results.push({ + id: VALIDATION_RESULT.NULL_VALUES, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message: i18n.translate('xpack.ml.models.jobService.categorization.messages.nullValues', { + defaultMessage: 'More than {percent}% of field values are null.', + values: { percent: NULL_COUNT_PERCENT_LIMIT * 100 }, + }), + }); + } + } + + public createTooManyTokensResult(error: any, sampleSize: number) { + // expecting error message: + // The number of tokens produced by calling _analyze has exceeded the allowed maximum of [10000]. + // This limit can be set by changing the [index.analyze.max_token_count] index level setting. + + if (error.statusCode === 403) { + this.createPrivilegesErrorResult(error); + return; + } + const message: string = error.message; + if (message) { + const rxp = /exceeded the allowed maximum of \[(\d+?)\]/; + const match = rxp.exec(message); + if (match?.length === 2) { + const tokenLimit = match[1]; + this._results.push({ + id: VALIDATION_RESULT.TOO_MANY_TOKENS, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID, + message: i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.tooManyTokens', + { + defaultMessage: + 'Tokenization of field value examples has failed due to more than {tokenLimit} tokens being found in a sample of {sampleSize} values.', + values: { sampleSize, tokenLimit }, + } + ), + }); + return; + } + return; + } + this.createFailureToTokenize(message); + } + + public createPrivilegesErrorResult(error: any) { + const message: string = error.message; + if (message) { + this._results.push({ + id: VALIDATION_RESULT.INSUFFICIENT_PRIVILEGES, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message: i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.insufficientPrivileges', + { + defaultMessage: + 'Tokenization of field value examples could not be performed due to insufficient privileges. Field values cannot therefore be checked to see if they are appropriate for use in a categorization job.', + } + ), + }); + this._results.push({ + id: VALIDATION_RESULT.INSUFFICIENT_PRIVILEGES, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message, + }); + return; + } + } + + public createFailureToTokenize(message: string | undefined) { + this._results.push({ + id: VALIDATION_RESULT.FAILED_TO_TOKENIZE, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID, + message: i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.failureToGetTokens', + { + defaultMessage: + 'It was not possible to tokenize a sample of example field values. {message}', + values: { message: message || '' }, + } + ), + }); + } +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts index da23efa67d0b5c..da60a90f4bfbc1 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts @@ -5,4 +5,4 @@ */ export { newJobChartsProvider } from './charts'; -export { categorizationExamplesProvider } from './categorization'; +export { categorizationExamplesProvider, topCategoriesProvider } from './categorization';