diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 7e7c8953fd527..c2306b80734d8 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -458,7 +458,7 @@ of buckets to try to represent. [horizontal] [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation. +Enables legacy charts library for area, line and bar charts in visualize. [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** diff --git a/docs/management/managing-tags.asciidoc b/docs/management/managing-tags.asciidoc index 3da98b2281fdc..88fdef66a7418 100644 --- a/docs/management/managing-tags.asciidoc +++ b/docs/management/managing-tags.asciidoc @@ -37,8 +37,7 @@ Create a tag to assign to your saved objects. image::images/tags/create-tag.png[Tag creation popin] . Enter a name and select a color for the new tag. + -The name can include alphanumeric characters (English letters and digits), `:`, `-`, `_` and the space character, -and cannot be longer than 50 characters. +The name cannot be longer than 50 characters. . Click *Create tag*. [float] diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 74d097164c4a7..7436536d22781 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -1,11 +1,13 @@ [[upgrade-migrations]] === Upgrade migrations -Every time {kib} is upgraded it checks to see if all saved objects, such as dashboards, visualizations, and index patterns, are compatible with the new version. If any saved objects need to be updated, then the automatic saved object migration process is kicked off. +Every time {kib} is upgraded it will perform an upgrade migration to ensure that all <> are compatible with the new version. NOTE: 6.7 includes an https://www.elastic.co/guide/en/kibana/6.7/upgrade-assistant.html[Upgrade Assistant] to help you prepare for your upgrade to 7.0. To access the assistant, go to *Management > 7.0 Upgrade Assistant*. +WARNING: {kib} 7.12.0 and later uses a new migration process and index naming scheme. Be sure to read the documentation for your version of {kib} before proceeding. + WARNING: The following instructions assumes {kib} is using the default index names. If the `kibana.index` or `xpack.tasks.index` configuration settings were changed these instructions will have to be adapted accordingly. [float] @@ -14,19 +16,35 @@ WARNING: The following instructions assumes {kib} is using the default index nam Saved objects are stored in two indices: -* `.kibana_N`, or if set, the `kibana.index` configuration setting -* `.kibana_task_manager_N`, or if set, the `xpack.tasks.index` configuration setting +* `.kibana_{kibana_version}_001`, or if the `kibana.index` configuration setting is set `.{kibana.index}_{kibana_version}_001`. E.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. +* `.kibana_task_manager_{kibana_version}_001`, or if the `xpack.tasks.index` configuration setting is set `.{xpack.tasks.index}_{kibana_version}_001` E.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. -For each of these indices, `N` is a number that increments every time {kib} runs an upgrade migration on that index. The index aliases `.kibana` and `.kibana_task_manager` point to the most up-to-date index. +The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date version indices. + +The first time a newer {kib} starts, it will first perform an upgrade migration before starting plugins or serving HTTP traffic. To prevent losing acknowledged writes old nodes should be shutdown before starting the upgrade. To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later will add a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}. + +.Saved object indices and aliases per {kib} version +[options="header"] +[cols="a,a,a"] +|======================= +|Upgrading from version | Outdated index (alias) | Upgraded index (alias) +| 6.0.0 through 6.4.x | `.kibana` 1.3+^.^| `.kibana_7.12.0_001` +(`.kibana` alias) + +`.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias) +| 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias) +| 7.4.0 through 7.11.x +| `.kibana_N` (`.kibana` alias) -While {kib} is starting up and before serving any HTTP traffic, it checks to see if any internal mapping changes or data transformations for existing saved objects are required. +`.kibana_task_manager_N` (`.kibana_task_manager` alias) +|======================= -When changes are necessary, a new migration is started. To ensure that only one {kib} instance performs the migration, each instance will attempt to obtain a migration lock by creating a new `.kibana_N+1` index. The instance that succeeds in creating the index will then read batches of documents from the existing index, migrate them, and write them to the new index. Once the objects are migrated, the lock is released by pointing the `.kibana` index alias the new upgraded `.kibana_N+1` index. +==== Upgrading multiple {kib} instances +When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. -Instances that failed to acquire a lock will log `Another Kibana instance appears to be migrating the index. Waiting for that migration to complete`. The instance will then wait until `.kibana` points to an upgraded index before starting up and serving HTTP traffic. +Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel. -NOTE: Prior to 6.5.0, saved objects were stored directly in an index named `.kibana`. After upgrading to version 6.5+, {kib} will migrate this index into `.kibana_N` and set `.kibana` up as an index alias. + -Prior to 7.4.0, task manager tasks were stored directly in an index name `.kibana_task_manager`. After upgrading to version 7.4+, {kib} will migrate this index into `.kibana_task_manager_N` and set `.kibana_task_manager` up as an index alias. +For large deployments with more than 10 {kib} instances and more than 10 000 saved objects, the upgrade downtime can be reduced by bringing up a single {kib} instance and waiting for it to complete the upgrade migration before bringing up the remaining instances. [float] [[preventing-migration-failures]] @@ -54,50 +72,31 @@ Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensu * a "green" cluster status [float] -===== Running different versions of {kib} connected to the same {es} index -Kibana does not support rolling upgrades. Stop all {kib} instances before starting a newer version to prevent upgrade failures and data loss. +===== Different versions of {kib} connected to the same {es} index +When different versions of {kib} are attempting an upgrade migration in parallel this can lead to migration failures. Ensure that all {kib} instances are running the same version, configuration and plugins. [float] ===== Incompatible `xpack.tasks.index` configuration setting -For {kib} < 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. +For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. [float] [[resolve-migrations-failures]] ==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index, manual intervention is required before {kib} will attempt to perform the migration again. Follow the advice in (preventing migration failures)[preventing-migration-failures] before retrying a migration upgrade. - -As mentioned above, {kib} will create a migration lock for each index that requires a migration by creating a new `.kibana_N+1` index. For example: if the `.kibana_task_manager` alias is pointing to `.kibana_task_manager_5` then the first {kib} that succeeds in creating `.kibana_task_manager_6` will obtain the lock to start migrations. - -However, if the instance that obtained the lock fails to migrate the index, all other {kib} instances will be blocked from performing this migration. This includes the instance that originally obtained the lock, it will be blocked from retrying the migration even when restarted. - -[float] -===== Retry a migration by restoring a backup snapshot: - -1. Before proceeding ensure that you have a recent and successful backup snapshot of all `.kibana*` indices. -2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -3. Delete all saved object indices with `DELETE /.kibana*` -4. Restore the `.kibana* indices and their aliases from the backup snapshot. See {es} {ref}/modules-snapshots.html[snapshots] -5. Start up all {kib} instances to retry the upgrade migration. - -[float] -===== (Not recommended) Retry a migration without a backup snapshot: +If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to perform the migration again once the process has restarted. Do not delete any saved objects indices to attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and later does not require deleting any indices to release a failed migration lock. -1. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -2. Identify any migration locks by comparing the output of `GET /_cat/aliases` and `GET /_cat/indices`. If e.g. `.kibana` is pointing to `.kibana_4` and there is a `.kibana_5` index, the `.kibana_5` index will act like a migration lock blocking further attempts. Be sure to check both the `.kibana` and `.kibana_task_manager` aliases and their indices. -3. Remove any migration locks e.g. `DELETE /.kibana_5`. -4. Start up all {kib} instances. +If upgrade migrations fail repeatedly, follow the advice in (preventing migration failures)[preventing-migration-failures]. Once the root cause for the migration failure has been addressed, {kib} will automatically retry the migration without any further intervention. If you're unable to resolve a failed migration following these steps, please contact support. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify the root cause. +If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify and fix the root cause. WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. + Any changes made after an upgrade will be lost when rolling back to a previous version. -In order to rollback after a failed upgrade migration, the saved object indices might also have to be rolled back to be compatible with the previous {kibana} version. +In order to rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kibana} version. [float] ===== Rollback by restoring a backup snapshot: @@ -111,17 +110,15 @@ In order to rollback after a failed upgrade migration, the saved object indices [float] ===== (Not recommended) Rollback without a backup snapshot: -WARNING: {kib} does not run a migration for every saved object index on every upgrade. A {kib} version upgrade can cause no migrations, migrate only the `.kibana` or the `.kibana_task_manager` index or both. Carefully read the logs to ensure that you're only deleting indices created by a later version of {kib} to avoid data loss. - 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. 2. Create a backup snapshot of the `.kibana*` indices. -3. Use the logs from the upgraded instances to identify which indices {kib} attempted to upgrade. The server logs will contain an entry like `[savedobjects-service] Creating index .kibana_4.` and/or `[savedobjects-service] Creating index .kibana_task_manager_2.` If no indices were created after upgrading {kib} then no further action is required to perform a rollback, skip ahead to step (5). If you're running multiple {kib} instances, be sure to inspect all instances' logs. -4. Delete each of the indices identified in step (2). e.g. `DELETE /.kibana_task_manager_2` -5. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` 6. Start up {kib} on the older version you wish to rollback to. [float] [[upgrade-migrations-old-indices]] ==== Handling old `.kibana_N` indices -After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, etc). {kib} only uses the index that the `.kibana` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file +After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). {kib} only uses the index that the `.kibana` and `.kibana_task_manager` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file diff --git a/docs/setup/upgrade/upgrade-standard.asciidoc b/docs/setup/upgrade/upgrade-standard.asciidoc index b27bb8867e624..b43da6aef9765 100644 --- a/docs/setup/upgrade/upgrade-standard.asciidoc +++ b/docs/setup/upgrade/upgrade-standard.asciidoc @@ -15,17 +15,17 @@ necessary remediation steps as per those instructions. [float] ==== Upgrading multiple {kib} instances -WARNING: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. +NOTE: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. -Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause upgrade migration failures and data loss. This is because acknowledged writes from the older instances could be written into the _old_ index while the migration is in progress. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. - -The first instance that triggers saved object migrations will run the entire process. Any other instances started up while a migration is running will log a message and then wait until saved object migrations has completed before they start serving HTTP traffic. +Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause data loss. This is because older instances will continue to write saved objects in a different format than the newer instances. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. [float] ==== Upgrade using a `deb` or `rpm` package . Stop the existing {kib} process using the appropriate command for your - system. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. + system. If you have multiple {kib} instances connecting to the same {es} + cluster ensure that all instances are stopped before proceeding to the next + step to avoid data loss. . Use `rpm` or `dpkg` to install the new package. All files should be placed in their proper locations and config files should not be overwritten. + @@ -65,5 +65,7 @@ and becomes a new instance in the monitoring data. . Install the appropriate versions of all your plugins for your new installation using the `kibana-plugin` script. Check out the <> documentation for more information. -. Stop the old {kib} process. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. +. Stop the old {kib} process. If you have multiple {kib} instances connecting + to the same {es} cluster ensure that all instances are stopped before + proceeding to the next step to avoid data loss. . Start the new {kib} process. diff --git a/package.json b/package.json index 87e0f84695235..24297011ccc63 100644 --- a/package.json +++ b/package.json @@ -287,7 +287,7 @@ "react-resizable": "^1.7.5", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "react-use": "^13.27.0", + "react-use": "^15.3.4", "recompose": "^0.26.0", "redux": "^4.0.5", "redux-actions": "^2.6.5", @@ -828,7 +828,7 @@ "url-loader": "^2.2.0", "use-resize-observer": "^6.0.0", "val-loader": "^1.1.1", - "vega": "^5.18.0", + "vega": "^5.19.1", "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.25.0", diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts index 20865bea2f897..7c28db333cc83 100644 --- a/src/plugins/charts/public/static/utils/transform_click_event.ts +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -30,6 +30,9 @@ export interface BrushTriggerEvent { type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>; +// TODO: replace when exported from elastic/charts +const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; + /** * returns accessor value from string or function accessor * @param datum @@ -82,6 +85,29 @@ const getAllSplitAccessors = ( value, ]); +/** + * Gets value from small multiple accessors + * + * Only handles single small multiple accessor + */ +function getSplitChartValue({ + smHorizontalAccessorValue, + smVerticalAccessorValue, +}: Pick): + | string + | number + | undefined { + if (smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + return smHorizontalAccessorValue; + } + + if (smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + return smVerticalAccessorValue; + } + + return; +} + /** * Reduces matching column indexes * @@ -92,7 +118,8 @@ const getAllSplitAccessors = ( const columnReducer = ( xAccessor: Accessor | AccessorFn | null, yAccessor: Accessor | AccessorFn | null, - splitAccessors: AllSeriesAccessors + splitAccessors: AllSeriesAccessors, + splitChartAccessor?: Accessor | AccessorFn ) => ( acc: Array<[index: number, id: string]>, { id }: Datatable['columns'][number], @@ -101,6 +128,7 @@ const columnReducer = ( if ( (xAccessor !== null && validateAccessorId(id, xAccessor)) || (yAccessor !== null && validateAccessorId(id, yAccessor)) || + (splitChartAccessor !== undefined && validateAccessorId(id, splitChartAccessor)) || splitAccessors.some(([accessor]) => validateAccessorId(id, accessor)) ) { acc.push([index, id]); @@ -121,13 +149,18 @@ const rowFindPredicate = ( geometry: GeometryValue | null, xAccessor: Accessor | AccessorFn | null, yAccessor: Accessor | AccessorFn | null, - splitAccessors: AllSeriesAccessors + splitAccessors: AllSeriesAccessors, + splitChartAccessor?: Accessor | AccessorFn, + splitChartValue?: string | number ) => (row: Datatable['rows'][number]): boolean => (geometry === null || (xAccessor !== null && getAccessorValue(row, xAccessor) === geometry.x && yAccessor !== null && - getAccessorValue(row, yAccessor) === geometry.y)) && + getAccessorValue(row, yAccessor) === geometry.y && + (splitChartAccessor === undefined || + (splitChartValue !== undefined && + getAccessorValue(row, splitChartAccessor) === splitChartValue)))) && [...splitAccessors].every(([accessor, value]) => getAccessorValue(row, accessor) === value); /** @@ -142,19 +175,28 @@ export const getFilterFromChartClickEventFn = ( table: Datatable, xAccessor: Accessor | AccessorFn, splitSeriesAccessorFnMap?: Map, + splitChartAccessor?: Accessor | AccessorFn, negate: boolean = false ) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ClickTriggerEvent => { const data: ValueClickContext['data']['data'] = []; points.forEach((point) => { const [geometry, { yAccessor, splitAccessors }] = point; + const splitChartValue = getSplitChartValue(point[1]); const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); const columns = table.columns.reduce>( - columnReducer(xAccessor, yAccessor, allSplitAccessors), + columnReducer(xAccessor, yAccessor, allSplitAccessors, splitChartAccessor), [] ); const row = table.rows.findIndex( - rowFindPredicate(geometry, xAccessor, yAccessor, allSplitAccessors) + rowFindPredicate( + geometry, + xAccessor, + yAccessor, + allSplitAccessors, + splitChartAccessor, + splitChartValue + ) ); const newData = columns.map(([column, id]) => ({ table, @@ -179,16 +221,20 @@ export const getFilterFromChartClickEventFn = ( * Helper function to get filter action event from series */ export const getFilterFromSeriesFn = (table: Datatable) => ( - { splitAccessors }: XYChartSeriesIdentifier, + { splitAccessors, ...rest }: XYChartSeriesIdentifier, splitSeriesAccessorFnMap?: Map, + splitChartAccessor?: Accessor | AccessorFn, negate = false ): ClickTriggerEvent => { + const splitChartValue = getSplitChartValue(rest); const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); const columns = table.columns.reduce>( - columnReducer(null, null, allSplitAccessors), + columnReducer(null, null, allSplitAccessors, splitChartAccessor), [] ); - const row = table.rows.findIndex(rowFindPredicate(null, null, null, allSplitAccessors)); + const row = table.rows.findIndex( + rowFindPredicate(null, null, null, allSplitAccessors, splitChartAccessor, splitChartValue) + ); const data: ValueClickContext['data']['data'] = columns.map(([column, id]) => ({ table, column, diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.test.ts b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts new file mode 100644 index 0000000000000..56db5346b7c6c --- /dev/null +++ b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { dataPluginMock } from '../../../../data/public/mocks'; +import { createSessionRestorationDataProvider } from './session_restoration'; +import { getAppStateDefaults } from './get_app_state_defaults'; +import { getSavedDashboardMock } from '../test_helpers'; +import { SavedObjectTagDecoratorTypeGuard } from '../../../../saved_objects_tagging_oss/public'; + +describe('createSessionRestorationDataProvider', () => { + const mockDataPlugin = dataPluginMock.createStartContract(); + const searchSessionInfoProvider = createSessionRestorationDataProvider({ + data: mockDataPlugin, + getAppState: () => + getAppStateDefaults( + getSavedDashboardMock(), + false, + ((() => false) as unknown) as SavedObjectTagDecoratorTypeGuard + ), + getDashboardTitle: () => 'Dashboard', + getDashboardId: () => 'Id', + }); + + describe('session state', () => { + test('restoreState has sessionId and initialState has not', async () => { + const searchSessionId = 'id'; + (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation( + () => searchSessionId + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.searchSessionId).toBeUndefined(); + expect(restoreState.searchSessionId).toBe(searchSessionId); + }); + + test('restoreState has absoluteTimeRange', async () => { + const relativeTime = 'relativeTime'; + const absoluteTime = 'absoluteTime'; + (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation( + () => relativeTime + ); + (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation( + () => absoluteTime + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.timeRange).toBe(relativeTime); + expect(restoreState.timeRange).toBe(absoluteTime); + }); + + test('restoreState has refreshInterval paused', async () => { + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.refreshInterval).toBeUndefined(); + expect(restoreState.refreshInterval?.pause).toBe(true); + }); + }); +}); diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts index 60a0c56a63218..fb57f8caa5ce4 100644 --- a/src/plugins/dashboard/public/application/lib/session_restoration.ts +++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts @@ -21,8 +21,8 @@ export function createSessionRestorationDataProvider(deps: { getUrlGeneratorData: async () => { return { urlGeneratorId: DASHBOARD_APP_URL_GENERATOR, - initialState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), - restoreState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + initialState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: false }), + restoreState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: true }), }; }, }; @@ -32,20 +32,17 @@ function getUrlGeneratorState({ data, getAppState, getDashboardId, - forceAbsoluteTime, + shouldRestoreSearchSession, }: { data: DataPublicPluginStart; getAppState: () => DashboardAppState; getDashboardId: () => string; - /** - * Can force time range from time filter to convert from relative to absolute time range - */ - forceAbsoluteTime: boolean; + shouldRestoreSearchSession: boolean; }): DashboardUrlGeneratorState { const appState = getAppState(); return { dashboardId: getDashboardId(), - timeRange: forceAbsoluteTime + timeRange: shouldRestoreSearchSession ? data.query.timefilter.timefilter.getAbsoluteTime() : data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -55,6 +52,12 @@ function getUrlGeneratorState({ preserveSavedFilters: false, viewMode: appState.viewMode, panels: getDashboardId() ? undefined : appState.panels, - searchSessionId: data.search.session.getSessionId(), + searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, + refreshInterval: shouldRestoreSearchSession + ? { + pause: true, // force pause refresh interval when restoring a session + value: 0, + } + : undefined, }; } diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 75fed9f809aa6..946baa7f4ecb1 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -175,7 +175,7 @@ app.directive('discoverApp', function () { }; }); -function discoverController($element, $route, $scope, $timeout, Promise) { +function discoverController($route, $scope, Promise) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); const refetch$ = new Subject(); @@ -725,20 +725,20 @@ function discoverController($element, $route, $scope, $timeout, Promise) { $route.reload(); }; - $scope.onSkipBottomButtonClick = function () { + $scope.onSkipBottomButtonClick = async () => { // show all the Rows $scope.minimumVisibleRows = $scope.hits; // delay scrolling to after the rows have been rendered - const bottomMarker = $element.find('#discoverBottomMarker'); - $timeout(() => { - bottomMarker.focus(); - // The anchor tag is not technically empty (it's a hack to make Safari scroll) - // so the browser will show a highlight: remove the focus once scrolled - $timeout(() => { - bottomMarker.blur(); - }, 0); - }, 0); + const bottomMarker = document.getElementById('discoverBottomMarker'); + const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + while ($scope.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { + await wait(50); + } + bottomMarker.focus(); + await wait(50); + bottomMarker.blur(); }; $scope.newQuery = function () { diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts index 45e5e252e8361..809664de5f073 100644 --- a/src/plugins/discover/public/application/angular/discover_state.test.ts +++ b/src/plugins/discover/public/application/angular/discover_state.test.ts @@ -101,8 +101,9 @@ describe('Test discover state with legacy migration', () => { describe('createSearchSessionRestorationDataProvider', () => { let mockSavedSearch: SavedSearch = ({} as unknown) as SavedSearch; + const mockDataPlugin = dataPluginMock.createStartContract(); const searchSessionInfoProvider = createSearchSessionRestorationDataProvider({ - data: dataPluginMock.createStartContract(), + data: mockDataPlugin, appStateContainer: getState({ history: createBrowserHistory(), }).appStateContainer, @@ -124,4 +125,30 @@ describe('createSearchSessionRestorationDataProvider', () => { expect(await searchSessionInfoProvider.getName()).toBe('Discover'); }); }); + + describe('session state', () => { + test('restoreState has sessionId and initialState has not', async () => { + const searchSessionId = 'id'; + (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation( + () => searchSessionId + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.searchSessionId).toBeUndefined(); + expect(restoreState.searchSessionId).toBe(searchSessionId); + }); + + test('restoreState has absoluteTimeRange', async () => { + const relativeTime = 'relativeTime'; + const absoluteTime = 'absoluteTime'; + (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation( + () => relativeTime + ); + (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation( + () => absoluteTime + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.timeRange).toBe(relativeTime); + expect(restoreState.timeRange).toBe(absoluteTime); + }); + }); }); diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index fe05fceb858e5..c769e263655ab 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -275,12 +275,12 @@ export function createSearchSessionRestorationDataProvider(deps: { initialState: createUrlGeneratorState({ ...deps, getSavedSearchId, - forceAbsoluteTime: false, + shouldRestoreSearchSession: false, }), restoreState: createUrlGeneratorState({ ...deps, getSavedSearchId, - forceAbsoluteTime: true, + shouldRestoreSearchSession: true, }), }; }, @@ -291,15 +291,12 @@ function createUrlGeneratorState({ appStateContainer, data, getSavedSearchId, - forceAbsoluteTime, + shouldRestoreSearchSession, }: { appStateContainer: StateContainer; data: DataPublicPluginStart; getSavedSearchId: () => string | undefined; - /** - * Can force time range from time filter to convert from relative to absolute time range - */ - forceAbsoluteTime: boolean; + shouldRestoreSearchSession: boolean; }): DiscoverUrlGeneratorState { const appState = appStateContainer.get(); return { @@ -307,10 +304,10 @@ function createUrlGeneratorState({ indexPatternId: appState.index, query: appState.query, savedSearchId: getSavedSearchId(), - timeRange: forceAbsoluteTime + timeRange: shouldRestoreSearchSession ? data.query.timefilter.timefilter.getAbsoluteTime() : data.query.timefilter.timefilter.getTime(), - searchSessionId: data.search.session.getSessionId(), + searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, columns: appState.columns, sort: appState.sort, savedQuery: appState.savedQuery, diff --git a/src/plugins/saved_objects_management/public/lib/create_field_list.ts b/src/plugins/saved_objects_management/public/lib/create_field_list.ts index cd30a02bd0ef3..4497fb04ffa2c 100644 --- a/src/plugins/saved_objects_management/public/lib/create_field_list.ts +++ b/src/plugins/saved_objects_management/public/lib/create_field_list.ts @@ -11,11 +11,12 @@ import { SimpleSavedObject } from '../../../../core/public'; import { castEsToKbnFieldTypeName } from '../../../data/public'; import { ObjectField } from '../management_section/types'; import { SavedObjectLoader } from '../../../saved_objects/public'; +import { SavedObjectWithMetadata } from '../types'; const maxRecursiveIterations = 20; export function createFieldList( - object: SimpleSavedObject, + object: SimpleSavedObject | SavedObjectWithMetadata, service?: SavedObjectLoader ): ObjectField[] { let fields = Object.entries(object.attributes as Record).reduce( diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx index 96a4a24f6591e..e048b92b9566c 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx @@ -19,14 +19,15 @@ import { set } from '@elastic/safer-lodash-set'; import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SimpleSavedObject, SavedObjectsClientContract } from '../../../../../../core/public'; +import { SavedObjectsClientContract } from '../../../../../../core/public'; import { SavedObjectLoader } from '../../../../../saved_objects/public'; import { Field } from './field'; import { ObjectField, FieldState, SubmittedFormData } from '../../types'; import { createFieldList } from '../../../lib'; +import { SavedObjectWithMetadata } from '../../../types'; interface FormProps { - object: SimpleSavedObject; + object: SavedObjectWithMetadata; service: SavedObjectLoader; savedObjectsClient: SavedObjectsClientContract; editionEnabled: boolean; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx index 31c0a76e16f58..3343e0a63f54c 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx @@ -14,16 +14,18 @@ import { SavedObjectsClientContract, OverlayStart, NotificationsStart, - SimpleSavedObject, ScopedHistory, + HttpSetup, } from '../../../../../core/public'; import { ISavedObjectsManagementServiceRegistry } from '../../services'; import { Header, NotFoundErrors, Intro, Form } from './components'; -import { canViewInApp } from '../../lib'; +import { canViewInApp, findObject } from '../../lib'; import { SubmittedFormData } from '../types'; +import { SavedObjectWithMetadata } from '../../types'; interface SavedObjectEditionProps { id: string; + http: HttpSetup; serviceName: string; serviceRegistry: ISavedObjectsManagementServiceRegistry; capabilities: Capabilities; @@ -36,7 +38,7 @@ interface SavedObjectEditionProps { interface SavedObjectEditionState { type: string; - object?: SimpleSavedObject; + object?: SavedObjectWithMetadata; } export class SavedObjectEdition extends Component< @@ -56,9 +58,9 @@ export class SavedObjectEdition extends Component< } componentDidMount() { - const { id, savedObjectsClient } = this.props; + const { http, id } = this.props; const { type } = this.state; - savedObjectsClient.get(type, id).then((object) => { + findObject(http, type, id).then((object) => { this.setState({ object, }); @@ -70,7 +72,7 @@ export class SavedObjectEdition extends Component< capabilities, notFoundType, serviceRegistry, - id, + http, serviceName, savedObjectsClient, } = this.props; @@ -80,7 +82,7 @@ export class SavedObjectEdition extends Component< string, boolean >; - const canView = canViewInApp(capabilities, type); + const canView = canViewInApp(capabilities, type) && Boolean(object?.meta.inAppUrl?.path); const service = serviceRegistry.get(serviceName)!.service; return ( @@ -91,7 +93,7 @@ export class SavedObjectEdition extends Component< canViewInApp={canView} type={type} onDeleteClick={() => this.delete()} - viewUrl={service.urlFor(id)} + viewUrl={http.basePath.prepend(object?.meta.inAppUrl?.path || '')} /> {notFoundType && ( <> diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx index 758789aa0f47e..2af7c22488c51 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx @@ -11,6 +11,7 @@ import { useParams, useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; import { CoreStart, ChromeBreadcrumb, ScopedHistory } from 'src/core/public'; +import { RedirectAppLinks } from '../../../kibana_react/public'; import { ISavedObjectsManagementServiceRegistry } from '../services'; import { SavedObjectEdition } from './object_view'; @@ -50,17 +51,20 @@ const SavedObjectsEditionPage = ({ }, [setBreadcrumbs, service]); return ( - + + + ); }; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 50a08d96de951..27d9b5ce83203 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -5247,6 +5247,36 @@ } } }, + "vis_type_table": { + "properties": { + "total": { + "type": "long" + }, + "total_split": { + "type": "long" + }, + "split_columns": { + "properties": { + "total": { + "type": "long" + }, + "enabled": { + "type": "long" + } + } + }, + "split_rows": { + "properties": { + "total": { + "type": "long" + }, + "enabled": { + "type": "long" + } + } + } + } + }, "vis_type_vega": { "properties": { "vega_lib_specs_total": { diff --git a/src/plugins/vis_type_table/common/index.ts b/src/plugins/vis_type_table/common/index.ts new file mode 100644 index 0000000000000..cc54db82d37e7 --- /dev/null +++ b/src/plugins/vis_type_table/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export * from './types'; diff --git a/src/plugins/vis_type_table/common/types.ts b/src/plugins/vis_type_table/common/types.ts new file mode 100644 index 0000000000000..3380e730770c3 --- /dev/null +++ b/src/plugins/vis_type_table/common/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export const VIS_TYPE_TABLE = 'table'; + +export enum AggTypes { + SUM = 'sum', + AVG = 'avg', + MIN = 'min', + MAX = 'max', + COUNT = 'count', +} + +export interface TableVisParams { + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; + showToolbar: boolean; + showTotal: boolean; + totalFunc: AggTypes; + percentageCol: string; + row?: boolean; +} diff --git a/src/plugins/vis_type_table/jest.config.js b/src/plugins/vis_type_table/jest.config.js index 4e5ddbcf8d7c5..3a7906f6ec543 100644 --- a/src/plugins/vis_type_table/jest.config.js +++ b/src/plugins/vis_type_table/jest.config.js @@ -11,4 +11,5 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/vis_type_table'], testRunner: 'jasmine2', + collectCoverageFrom: ['/src/plugins/vis_type_table/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx index eb76659a601d6..a70ecb43f1be7 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -19,7 +19,7 @@ import { NumberInputOption, VisOptionsProps, } from '../../../vis_default_editor/public'; -import { TableVisParams } from '../types'; +import { TableVisParams } from '../../common'; import { totalAggregations } from './utils'; const { tabifyGetColumns } = search; diff --git a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx index fb0044a986f5e..716b77e9c91d2 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx @@ -9,7 +9,7 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { TableVisParams } from '../types'; +import { TableVisParams } from '../../common'; const TableOptionsComponent = lazy(() => import('./table_vis_options')); diff --git a/src/plugins/vis_type_table/public/components/utils.ts b/src/plugins/vis_type_table/public/components/utils.ts index f11d7bc4b7f33..8f30788c76468 100644 --- a/src/plugins/vis_type_table/public/components/utils.ts +++ b/src/plugins/vis_type_table/public/components/utils.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { AggTypes } from '../types'; +import { AggTypes } from '../../common'; const totalAggregations = [ { diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts index cec16eefb360c..db0b92154d2dd 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, Datatable, Render } from 'src/plugins/expressions/public'; import { tableVisLegacyResponseHandler, TableContext } from './table_vis_legacy_response_handler'; import { TableVisConfig } from '../types'; +import { VIS_TYPE_TABLE } from '../../common'; export type Input = Datatable; @@ -19,7 +20,7 @@ interface Arguments { export interface TableVisRenderValue { visData: TableContext; - visType: 'table'; + visType: typeof VIS_TYPE_TABLE; visConfig: TableVisConfig; } @@ -53,7 +54,7 @@ export const createTableVisLegacyFn = (): TableExpressionFunctionDefinition => ( as: 'table_vis', value: { visData: convertedData, - visType: 'table', + visType: VIS_TYPE_TABLE, visConfig, }, }; diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts index a1ceee8c741d4..3e1140275593d 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts @@ -12,11 +12,11 @@ import { BaseVisTypeOptions } from '../../../visualizations/public'; import { TableOptions } from '../components/table_vis_options_lazy'; import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; +import { TableVisParams, VIS_TYPE_TABLE } from '../../common'; import { toExpressionAst } from '../to_ast'; -import { TableVisParams } from '../types'; export const tableVisLegacyTypeDefinition: BaseVisTypeOptions = { - name: 'table', + name: VIS_TYPE_TABLE, title: i18n.translate('visTypeTable.tableVisTitle', { defaultMessage: 'Data table', }), diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index a45f1e828fc47..99fee424b8bea 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; import { TableVisConfig } from './types'; +import { VIS_TYPE_TABLE } from '../common'; export type Input = Datatable; @@ -19,7 +20,7 @@ interface Arguments { export interface TableVisRenderValue { visData: TableContext; - visType: 'table'; + visType: typeof VIS_TYPE_TABLE; visConfig: TableVisConfig; } @@ -56,7 +57,7 @@ export const createTableVisFn = (): TableExpressionFunctionDefinition => ({ as: 'table_vis', value: { visData: convertedData, - visType: 'table', + visType: VIS_TYPE_TABLE, visConfig, }, }; diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index 8cd45b54c6ced..ef6d85db103b3 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -10,13 +10,13 @@ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; +import { TableVisParams, VIS_TYPE_TABLE } from '../common'; import { TableOptions } from './components/table_vis_options_lazy'; import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; -import { TableVisParams } from './types'; export const tableVisTypeDefinition: BaseVisTypeOptions = { - name: 'table', + name: VIS_TYPE_TABLE, title: i18n.translate('visTypeTable.tableVisTitle', { defaultMessage: 'Data table', }), diff --git a/src/plugins/vis_type_table/public/to_ast.test.ts b/src/plugins/vis_type_table/public/to_ast.test.ts index 1ca62475b7af0..f0aed7199a2f2 100644 --- a/src/plugins/vis_type_table/public/to_ast.test.ts +++ b/src/plugins/vis_type_table/public/to_ast.test.ts @@ -8,7 +8,7 @@ import { Vis } from 'src/plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; -import { AggTypes, TableVisParams } from './types'; +import { AggTypes, TableVisParams } from '../common'; const mockSchemas = { metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }], diff --git a/src/plugins/vis_type_table/public/to_ast.ts b/src/plugins/vis_type_table/public/to_ast.ts index 9d9f23d31d802..1cbe9832e4c98 100644 --- a/src/plugins/vis_type_table/public/to_ast.ts +++ b/src/plugins/vis_type_table/public/to_ast.ts @@ -12,8 +12,9 @@ import { } from '../../data/public'; import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public'; +import { TableVisParams } from '../common'; import { TableExpressionFunctionDefinition } from './table_vis_fn'; -import { TableVisConfig, TableVisParams } from './types'; +import { TableVisConfig } from './types'; const buildTableVisConfig = ( schemas: ReturnType, diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index 75d48f4f53ac7..03cf8bb3395d6 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -8,14 +8,7 @@ import { IFieldFormat } from 'src/plugins/data/public'; import { SchemaConfig } from 'src/plugins/visualizations/public'; - -export enum AggTypes { - SUM = 'sum', - AVG = 'avg', - MIN = 'min', - MAX = 'max', - COUNT = 'count', -} +import { TableVisParams } from '../common'; export interface Dimensions { buckets: SchemaConfig[]; @@ -44,16 +37,6 @@ export interface TableVisUseUiStateProps { setColumnsWidth: (column: ColumnWidthData) => void; } -export interface TableVisParams { - perPage: number | ''; - showPartialRows: boolean; - showMetricsAtAllLevels: boolean; - showToolbar: boolean; - showTotal: boolean; - totalFunc: AggTypes; - percentageCol: string; -} - export interface TableVisConfig extends TableVisParams { title: string; dimensions: Dimensions; diff --git a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts index 5398aa908f6eb..3a733e7a9a4dc 100644 --- a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts +++ b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts @@ -9,8 +9,9 @@ import { useMemo } from 'react'; import { chain, findIndex } from 'lodash'; +import { AggTypes } from '../../../common'; import { Table } from '../../table_vis_response_handler'; -import { FormattedColumn, TableVisConfig, AggTypes } from '../../types'; +import { FormattedColumn, TableVisConfig } from '../../types'; import { getFormatService } from '../../services'; import { addPercentageColumn } from '../add_percentage_column'; diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts index 1573a3c6b7b88..7e55e63f9249c 100644 --- a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts +++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts @@ -7,7 +7,7 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { TableVisParams } from '../../types'; +import { TableVisParams } from '../../../common'; export const usePagination = (visParams: TableVisParams, rowCount: number) => { const [pagination, setPagination] = useState({ diff --git a/src/plugins/vis_type_table/server/index.ts b/src/plugins/vis_type_table/server/index.ts index 75068c646f501..39618d687168e 100644 --- a/src/plugins/vis_type_table/server/index.ts +++ b/src/plugins/vis_type_table/server/index.ts @@ -6,9 +6,11 @@ * Public License, v 1. */ -import { PluginConfigDescriptor } from 'kibana/server'; +import { CoreSetup, PluginConfigDescriptor } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { configSchema, ConfigSchema } from '../config'; +import { registerVisTypeTableUsageCollector } from './usage_collector'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -21,6 +23,10 @@ export const config: PluginConfigDescriptor = { }; export const plugin = () => ({ - setup() {}, + setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { + if (plugins.usageCollection) { + registerVisTypeTableUsageCollector(plugins.usageCollection); + } + }, start() {}, }); diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts new file mode 100644 index 0000000000000..55daa5c64349a --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { getStats } from './get_stats'; + +const mockVisualizations = { + saved_objects: [ + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": true }], "params": { "row": true }}', + }, + }, + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": false }], "params": { "row": true }}', + }, + }, + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "split", "enabled": true }], "params": { "row": false }}', + }, + }, + { + attributes: { + visState: '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }]}', + }, + }, + { + attributes: { visState: '{"type": "histogram"}' }, + }, + ], +}; + +describe('vis_type_table getStats', () => { + const mockSoClient = ({ + find: jest.fn().mockResolvedValue(mockVisualizations), + } as unknown) as SavedObjectsClientContract; + + test('Returns stats from saved objects for table vis only', async () => { + const result = await getStats(mockSoClient); + expect(mockSoClient.find).toHaveBeenCalledWith({ + type: 'visualization', + perPage: 10000, + }); + expect(result).toEqual({ + total: 4, + total_split: 3, + split_columns: { + total: 1, + enabled: 1, + }, + split_rows: { + total: 2, + enabled: 1, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts new file mode 100644 index 0000000000000..bd3e1d2f089e2 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server'; +import { + SavedVisState, + VisualizationSavedObjectAttributes, +} from 'src/plugins/visualizations/common'; +import { TableVisParams, VIS_TYPE_TABLE } from '../../common'; + +export interface VisTypeTableUsage { + /** + * Total number of table type visualizations + */ + total: number; + /** + * Total number of table visualizations, using "Split table" agg + */ + total_split: number; + /** + * Split table by columns stats + */ + split_columns: { + total: number; + enabled: number; + }; + /** + * Split table by rows stats + */ + split_rows: { + total: number; + enabled: number; + }; +} + +/* + * Parse the response data into telemetry payload + */ +export async function getStats( + soClient: SavedObjectsClientContract | ISavedObjectsRepository +): Promise { + const visualizations = await soClient.find({ + type: 'visualization', + perPage: 10000, + }); + + const tableVisualizations = visualizations.saved_objects + .map>(({ attributes }) => JSON.parse(attributes.visState)) + .filter(({ type }) => type === VIS_TYPE_TABLE); + + const defaultStats = { + total: tableVisualizations.length, + total_split: 0, + split_columns: { + total: 0, + enabled: 0, + }, + split_rows: { + total: 0, + enabled: 0, + }, + }; + + return tableVisualizations.reduce((acc, { aggs, params }) => { + const hasSplitAgg = aggs.find((agg) => agg.schema === 'split'); + + if (hasSplitAgg) { + acc.total_split += 1; + + const isSplitRow = params.row; + const isSplitEnabled = hasSplitAgg.enabled; + + const container = isSplitRow ? acc.split_rows : acc.split_columns; + container.total += 1; + container.enabled = isSplitEnabled ? container.enabled + 1 : container.enabled; + } + + return acc; + }, defaultStats); +} diff --git a/src/plugins/vis_type_table/server/usage_collector/index.ts b/src/plugins/vis_type_table/server/usage_collector/index.ts new file mode 100644 index 0000000000000..090ed3077b27c --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export { registerVisTypeTableUsageCollector } from './register_usage_collector'; diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts new file mode 100644 index 0000000000000..cbf39a4d937a7 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +jest.mock('./get_stats', () => ({ + getStats: jest.fn().mockResolvedValue({ somestat: 1 }), +})); + +import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; + +import { registerVisTypeTableUsageCollector } from './register_usage_collector'; +import { getStats } from './get_stats'; + +describe('registerVisTypeTableUsageCollector', () => { + it('Usage collector configs fit the shape', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisTypeTableUsageCollector(mockCollectorSet); + expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); + expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); + expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ + type: 'vis_type_table', + isReady: expect.any(Function), + fetch: expect.any(Function), + schema: { + total: { type: 'long' }, + total_split: { type: 'long' }, + split_columns: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + split_rows: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + }, + }); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('Usage collector config.fetch calls getStats', async () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisTypeTableUsageCollector(mockCollectorSet); + const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; + const mockCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollector.fetch(mockCollectorFetchContext); + expect(getStats).toBeCalledTimes(1); + expect(getStats).toBeCalledWith(mockCollectorFetchContext.soClient); + expect(fetchResult).toEqual({ somestat: 1 }); + }); +}); diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts new file mode 100644 index 0000000000000..2ac4ce22a47e4 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + +import { getStats, VisTypeTableUsage } from './get_stats'; + +export function registerVisTypeTableUsageCollector(collectorSet: UsageCollectionSetup) { + const collector = collectorSet.makeUsageCollector({ + type: 'vis_type_table', + isReady: () => true, + schema: { + total: { type: 'long' }, + total_split: { type: 'long' }, + split_columns: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + split_rows: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + }, + fetch: ({ soClient }) => getStats(soClient), + }); + collectorSet.registerCollector(collector); +} diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json index bda86d06c0ff7..ccff3c349cf21 100644 --- a/src/plugins/vis_type_table/tsconfig.json +++ b/src/plugins/vis_type_table/tsconfig.json @@ -8,6 +8,7 @@ "declarationMap": true }, "include": [ + "common/**/*", "public/**/*", "server/**/*", "*.ts" diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js index 157523cdf09f4..ee9bed141fe4b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js @@ -7,7 +7,7 @@ */ import d3 from 'd3'; -import _ from 'lodash'; +import { isNumber, reduce, times } from 'lodash'; import moment from 'moment'; import { InvalidLogScaleValues } from '../../errors'; @@ -62,7 +62,7 @@ export class AxisScale { return d3[extent]( opts.reduce(function (opts, v) { - if (!_.isNumber(v)) v = +v; + if (!isNumber(v)) v = +v; if (!isNaN(v)) opts.push(v); return opts; }, []) @@ -90,7 +90,7 @@ export class AxisScale { const y = moment(x); const method = n > 0 ? 'add' : 'subtract'; - _.times(Math.abs(n), function () { + times(Math.abs(n), function () { y[method](interval); }); @@ -100,7 +100,7 @@ export class AxisScale { getAllPoints() { const config = this.axisConfig; const data = this.visConfig.data.chartData(); - const chartPoints = _.reduce( + const chartPoints = reduce( data, (chartPoints, chart, chartIndex) => { const points = chart.series.reduce((points, seri, seriIndex) => { @@ -254,6 +254,6 @@ export class AxisScale { } validateScale(scale) { - if (!scale || _.isNaN(scale)) throw new Error('scale is ' + scale); + if (!scale || Number.isNaN(scale)) throw new Error('scale is ' + scale); } } diff --git a/src/plugins/vis_type_xy/public/chart_splitter.tsx b/src/plugins/vis_type_xy/public/chart_splitter.tsx new file mode 100644 index 0000000000000..bf63ac1896bd1 --- /dev/null +++ b/src/plugins/vis_type_xy/public/chart_splitter.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { Accessor, AccessorFn, GroupBy, GroupBySort, SmallMultiples } from '@elastic/charts'; + +interface ChartSplitterProps { + splitColumnAccessor?: Accessor | AccessorFn; + splitRowAccessor?: Accessor | AccessorFn; + sort?: GroupBySort; +} + +const CHART_SPLITTER_ID = '__chart_splitter__'; + +export const ChartSplitter = ({ + splitColumnAccessor, + splitRowAccessor, + sort, +}: ChartSplitterProps) => + splitColumnAccessor || splitRowAccessor ? ( + <> + { + const splitTypeAccessor = splitColumnAccessor || splitRowAccessor; + if (splitTypeAccessor) { + return typeof splitTypeAccessor === 'function' + ? splitTypeAccessor(datum) + : datum[splitTypeAccessor]; + } + return spec.id; + }} + sort={sort || 'dataIndex'} + /> + + + ) : null; diff --git a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx index 49b2ab483bc55..02c7157d32c27 100644 --- a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx +++ b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx @@ -16,19 +16,20 @@ import { XYChartSeriesIdentifier, } from '@elastic/charts'; -import { BUCKET_TYPES } from '../../../data/public'; - import { Aspects } from '../types'; import './_detailed_tooltip.scss'; import { fillEmptyValue } from '../utils/get_series_name_fn'; -import { COMPLEX_SPLIT_ACCESSOR } from '../utils/accessors'; +import { COMPLEX_SPLIT_ACCESSOR, isRangeAggType } from '../utils/accessors'; interface TooltipData { label: string; value: string; } +// TODO: replace when exported from elastic/charts +const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; + const getTooltipData = ( aspects: Aspects, header: TooltipValue | null, @@ -37,10 +38,7 @@ const getTooltipData = ( const data: TooltipData[] = []; if (header) { - const xFormatter = - aspects.x.aggType === BUCKET_TYPES.DATE_RANGE || aspects.x.aggType === BUCKET_TYPES.RANGE - ? null - : aspects.x.formatter; + const xFormatter = isRangeAggType(aspects.x.aggType) ? null : aspects.x.formatter; data.push({ label: aspects.x.title, value: xFormatter ? xFormatter(header.value) : `${header.value}`, @@ -80,6 +78,28 @@ const getTooltipData = ( } }); + if ( + aspects.splitColumn && + valueSeries.smHorizontalAccessorValue !== undefined && + valueSeries.smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE + ) { + data.push({ + label: aspects.splitColumn.title, + value: `${valueSeries.smHorizontalAccessorValue}`, + }); + } + + if ( + aspects.splitRow && + valueSeries.smVerticalAccessorValue !== undefined && + valueSeries.smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE + ) { + data.push({ + label: aspects.splitRow.title, + value: `${valueSeries.smVerticalAccessorValue}`, + }); + } + return data; }; diff --git a/src/plugins/vis_type_xy/public/components/index.ts b/src/plugins/vis_type_xy/public/components/index.ts index 260c08e0fc4a9..9b2559bafd18e 100644 --- a/src/plugins/vis_type_xy/public/components/index.ts +++ b/src/plugins/vis_type_xy/public/components/index.ts @@ -11,4 +11,3 @@ export { XYEndzones } from './xy_endzones'; export { XYCurrentTime } from './xy_current_time'; export { XYSettings } from './xy_settings'; export { XYThresholdLine } from './xy_threshold_line'; -export { SplitChartWarning } from './split_chart_warning'; diff --git a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx b/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx deleted file mode 100644 index b708590e04479..0000000000000 --- a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import React, { FC } from 'react'; - -import { EuiLink, EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { getDocLinks } from '../services'; - -export const SplitChartWarning: FC = () => { - const advancedSettingsLink = getDocLinks().links.management.visualizationSettings; - - return ( - - - - - ), - }} - /> - - ); -}; diff --git a/src/plugins/vis_type_xy/public/config/get_aspects.ts b/src/plugins/vis_type_xy/public/config/get_aspects.ts index b8da4386806d4..c031d3fa1fb9b 100644 --- a/src/plugins/vis_type_xy/public/config/get_aspects.ts +++ b/src/plugins/vis_type_xy/public/config/get_aspects.ts @@ -29,7 +29,10 @@ export function getEmptyAspect(): Aspect { }, }; } -export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dimensions): Aspects { +export function getAspects( + columns: DatatableColumn[], + { x, y, z, series, splitColumn, splitRow }: Dimensions +): Aspects { const seriesDimensions = Array.isArray(series) || series === undefined ? series : [series]; return { @@ -37,6 +40,8 @@ export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dime y: getAspectsFromDimension(columns, y) ?? [], z: z && z?.length > 0 ? getAspectsFromDimension(columns, z[0]) : undefined, series: getAspectsFromDimension(columns, seriesDimensions), + splitColumn: splitColumn?.length ? getAspectsFromDimension(columns, splitColumn[0]) : undefined, + splitRow: splitRow?.length ? getAspectsFromDimension(columns, splitRow[0]) : undefined, }; } diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts index af3d840739f17..9d4660afa1634 100644 --- a/src/plugins/vis_type_xy/public/types/config.ts +++ b/src/plugins/vis_type_xy/public/types/config.ts @@ -43,6 +43,8 @@ export interface Aspects { y: Aspect[]; z?: Aspect; series?: Aspect[]; + splitColumn?: Aspect; + splitRow?: Aspect; } export interface AxisGrid { diff --git a/src/plugins/vis_type_xy/public/utils/accessors.tsx b/src/plugins/vis_type_xy/public/utils/accessors.tsx index d1337251d36aa..e40248ae92e12 100644 --- a/src/plugins/vis_type_xy/public/utils/accessors.tsx +++ b/src/plugins/vis_type_xy/public/utils/accessors.tsx @@ -26,11 +26,15 @@ const getFieldName = (fieldName: string, index?: number) => { return `${fieldName}${indexStr}`; }; +export const isRangeAggType = (type: string | null) => + type === BUCKET_TYPES.DATE_RANGE || type === BUCKET_TYPES.RANGE; + /** * Returns accessor function for complex accessor types * @param aspect + * @param isComplex - forces to be functional/complex accessor */ -export const getComplexAccessor = (fieldName: string) => ( +export const getComplexAccessor = (fieldName: string, isComplex: boolean = false) => ( aspect: Aspect, index?: number ): Accessor | AccessorFn | undefined => { @@ -38,12 +42,7 @@ export const getComplexAccessor = (fieldName: string) => ( return; } - if ( - !( - (aspect.aggType === BUCKET_TYPES.DATE_RANGE || aspect.aggType === BUCKET_TYPES.RANGE) && - aspect.formatter - ) - ) { + if (!((isComplex || isRangeAggType(aspect.aggType)) && aspect.formatter)) { return aspect.accessor; } @@ -51,7 +50,7 @@ export const getComplexAccessor = (fieldName: string) => ( const accessor = aspect.accessor; const fn: AccessorFn = (d) => { const v = d[accessor]; - if (!v) { + if (v === undefined) { return; } const f = formatter(v); diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts new file mode 100644 index 0000000000000..393a6ee06cf58 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts @@ -0,0 +1,386 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { VisConfig } from '../types'; + +export const getVisConfig = (): VisConfig => { + return { + markSizeRatio: 5.3999999999999995, + fittingFunction: 'linear', + detailedTooltip: true, + isTimeChart: true, + showCurrentTime: false, + showValueLabel: false, + enableHistogramMode: true, + tooltip: { + type: 'vertical', + }, + aspects: { + x: { + accessor: 'col-0-2', + column: 0, + title: 'order_date per minute', + format: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + aggType: 'date_histogram', + aggId: '2', + params: { + date: true, + intervalESUnit: 'm', + intervalESValue: 1, + interval: 60000, + format: 'HH:mm', + }, + }, + y: [ + { + accessor: 'col-1-3', + column: 1, + title: 'Average products.base_price', + format: { + id: 'number', + }, + aggType: 'avg', + aggId: '3', + params: {}, + }, + ], + }, + xAxis: { + id: 'CategoryAxis-1', + position: 'bottom', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'CategoryAxis-1', + title: 'order_date per minute', + ticks: { + show: true, + showOverlappingLabels: false, + showDuplicates: false, + }, + grid: { + show: false, + }, + scale: { + type: 'time', + }, + integersOnly: false, + }, + yAxes: [ + { + id: 'ValueAxis-1', + position: 'left', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'ValueAxis-1', + title: 'Percentiles of products.base_price', + ticks: { + show: true, + rotation: 0, + showOverlappingLabels: true, + showDuplicates: true, + }, + grid: { + show: false, + }, + scale: { + mode: 'normal', + type: 'linear', + }, + domain: {}, + integersOnly: false, + }, + ], + legend: { + show: true, + position: 'right', + }, + rotation: 0, + thresholdLine: { + color: '#E7664C', + show: false, + value: 10, + width: 1, + groupId: 'ValueAxis-1', + }, + }; +}; + +export const getVisConfigPercentiles = (): VisConfig => { + return { + markSizeRatio: 5.3999999999999995, + fittingFunction: 'linear', + detailedTooltip: true, + isTimeChart: true, + showCurrentTime: false, + showValueLabel: false, + enableHistogramMode: true, + tooltip: { + type: 'vertical', + }, + aspects: { + x: { + accessor: 'col-0-2', + column: 0, + title: 'order_date per minute', + format: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + aggType: 'date_histogram', + aggId: '2', + params: { + date: true, + intervalESUnit: 'm', + intervalESValue: 1, + interval: 60000, + format: 'HH:mm', + }, + }, + y: [ + { + accessor: 'col-1-3.1', + column: 1, + title: '1st percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.1', + params: {}, + }, + { + accessor: 'col-2-3.5', + column: 2, + title: '5th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.5', + params: {}, + }, + { + accessor: 'col-3-3.25', + column: 3, + title: '25th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.25', + params: {}, + }, + { + accessor: 'col-4-3.50', + column: 4, + title: '50th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.50', + params: {}, + }, + { + accessor: 'col-5-3.75', + column: 5, + title: '75th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.75', + params: {}, + }, + { + accessor: 'col-6-3.95', + column: 6, + title: '95th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.95', + params: {}, + }, + { + accessor: 'col-7-3.99', + column: 7, + title: '99th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.99', + params: {}, + }, + ], + }, + xAxis: { + id: 'CategoryAxis-1', + position: 'bottom', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'CategoryAxis-1', + title: 'order_date per minute', + ticks: { + show: true, + showOverlappingLabels: false, + showDuplicates: false, + }, + grid: { + show: false, + }, + scale: { + type: 'time', + }, + integersOnly: false, + }, + yAxes: [ + { + id: 'ValueAxis-1', + position: 'left', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'ValueAxis-1', + title: 'Percentiles of products.base_price', + ticks: { + show: true, + rotation: 0, + showOverlappingLabels: true, + showDuplicates: true, + }, + grid: { + show: false, + }, + scale: { + mode: 'normal', + type: 'linear', + }, + domain: {}, + integersOnly: false, + }, + ], + legend: { + show: true, + position: 'right', + }, + rotation: 0, + thresholdLine: { + color: '#E7664C', + show: false, + value: 10, + width: 1, + groupId: 'ValueAxis-1', + }, + }; +}; + +export const getPercentilesData = () => { + return [ + { + 'col-0-2': 1610961900000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 38.49609375, + 'col-5-3.75': 65, + 'col-6-3.95': 65, + 'col-7-3.99': 65, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3.1': 28.984375000000004, + 'col-2-3.5': 28.984375, + 'col-3-3.25': 28.984375, + 'col-4-3.50': 30.9921875, + 'col-5-3.75': 41.5, + 'col-6-3.95': 50, + 'col-7-3.99': 50, + }, + { + 'col-0-2': 1610963280000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 12.9921875, + 'col-5-3.75': 13.9921875, + 'col-6-3.95': 13.9921875, + 'col-7-3.99': 13.9921875, + }, + { + 'col-0-2': 1610964180000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 14.9921875, + 'col-4-3.50': 15.98828125, + 'col-5-3.75': 24.984375, + 'col-6-3.95': 85, + 'col-7-3.99': 85, + }, + { + 'col-0-2': 1610964420000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 23.99609375, + 'col-5-3.75': 42, + 'col-6-3.95': 42, + 'col-7-3.99': 42, + }, + { + 'col-0-2': 1610964600000, + 'col-1-3.1': 10.9921875, + 'col-2-3.5': 10.992187500000002, + 'col-3-3.25': 10.9921875, + 'col-4-3.50': 12.4921875, + 'col-5-3.75': 13.9921875, + 'col-6-3.95': 13.9921875, + 'col-7-3.99': 13.9921875, + }, + ]; +}; diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx new file mode 100644 index 0000000000000..d76ea49a2f110 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { AreaSeries, BarSeries, CurveType } from '@elastic/charts'; +import { DatatableRow } from '../../../expressions/public'; +import { renderAllSeries } from './render_all_series'; +import { + getVisConfig, + getVisConfigPercentiles, + getPercentilesData, +} from './render_all_series.test.mocks'; +import { SeriesParam, VisConfig } from '../types'; + +const defaultSeriesParams = [ + { + data: { + id: '3', + label: 'Label', + }, + drawLinesBetweenPoints: true, + interpolate: 'linear', + lineWidth: 2, + mode: 'stacked', + show: true, + showCircles: true, + type: 'area', + valueAxis: 'ValueAxis-1', + }, +] as SeriesParam[]; + +const defaultData = [ + { + 'col-0-2': 1610960220000, + 'col-1-3': 26.984375, + }, + { + 'col-0-2': 1610961300000, + 'col-1-3': 30.99609375, + }, + { + 'col-0-2': 1610961900000, + 'col-1-3': 38.49609375, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3': 35.2421875, + }, +]; + +describe('renderAllSeries', function () { + const getAllSeries = (visConfig: VisConfig, params: SeriesParam[], data: DatatableRow[]) => { + return renderAllSeries( + visConfig, + params, + data, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + }; + + it('renders an area Series and not a bar series if type is area', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).length).toBe(1); + expect(wrapper.find(BarSeries).length).toBe(0); + }); + + it('renders a bar Series in case of histogram', () => { + const barSeriesParams = [{ ...defaultSeriesParams[0], type: 'histogram' }]; + + const renderBarSeries = renderAllSeries( + getVisConfig(), + barSeriesParams as SeriesParam[], + defaultData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderBarSeries}
); + expect(wrapper.find(AreaSeries).length).toBe(0); + expect(wrapper.find(BarSeries).length).toBe(1); + }); + + it('renders the correct yAccessors for not percentile aggs', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).prop('yAccessors')).toEqual(['col-1-3']); + }); + + it('renders the correct yAccessors for percentile aggs', () => { + const percentilesConfig = getVisConfigPercentiles(); + const percentilesData = getPercentilesData(); + const renderPercentileSeries = renderAllSeries( + percentilesConfig, + defaultSeriesParams as SeriesParam[], + percentilesData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderPercentileSeries}
); + expect(wrapper.find(AreaSeries).prop('yAccessors')).toEqual([ + 'col-1-3.1', + 'col-2-3.5', + 'col-3-3.25', + 'col-4-3.50', + 'col-5-3.75', + 'col-6-3.95', + 'col-7-3.99', + ]); + }); + + it('defaults the CurveType to linear', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).prop('curve')).toEqual(CurveType.LINEAR); + }); +}); diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx index 264fa539c1980..fb884bb235971 100644 --- a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx @@ -71,13 +71,15 @@ export const renderAllSeries = ( interpolate, type, }) => { - const yAspect = aspects.y.find(({ aggId }) => aggId === paramId); - - if (!show || !yAspect || yAspect.accessor === null) { + const yAspects = aspects.y.filter( + ({ aggId, accessor }) => aggId?.includes(paramId) && accessor !== null + ); + if (!show || !yAspects.length) { return null; } + const yAccessors = yAspects.map((aspect) => aspect.accessor) as string[]; - const id = `${type}-${yAspect.accessor}`; + const id = `${type}-${yAccessors[0]}`; const yAxisScale = yAxes.find(({ groupId: axisGroupId }) => axisGroupId === groupId)?.scale; const isStacked = mode === 'stacked' || yAxisScale?.mode === 'percentage'; const stackMode = yAxisScale?.mode === 'normal' ? undefined : yAxisScale?.mode; @@ -94,13 +96,13 @@ export const renderAllSeries = ( id={id} name={getSeriesName} color={getSeriesColor} - tickFormat={yAspect.formatter} + tickFormat={yAspects[0].formatter} groupId={pseudoGroupId} useDefaultGroupDomain={useDefaultGroupDomain} xScaleType={xAxis.scale.type} yScaleType={yAxisScale?.type} xAccessor={xAccessor} - yAccessors={[yAspect.accessor]} + yAccessors={yAccessors} splitSeriesAccessors={splitSeriesAccessors} data={data} timeZone={timeZone} @@ -125,7 +127,7 @@ export const renderAllSeries = ( id={id} fit={fittingFunction} color={getSeriesColor} - tickFormat={yAspect.formatter} + tickFormat={yAspects[0].formatter} name={getSeriesName} curve={getCurveType(interpolate)} groupId={pseudoGroupId} @@ -133,7 +135,7 @@ export const renderAllSeries = ( xScaleType={xAxis.scale.type} yScaleType={yAxisScale?.type} xAccessor={xAccessor} - yAccessors={[yAspect.accessor]} + yAccessors={yAccessors} markSizeAccessor={markSizeAccessor} markFormat={aspects.z?.formatter} splitSeriesAccessors={splitSeriesAccessors} diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index 0cdabd2fa409e..871fb408d4da0 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -65,6 +65,7 @@ import { getComplexAccessor, getSplitSeriesAccessorFnMap, } from './utils/accessors'; +import { ChartSplitter } from './chart_splitter'; export interface VisComponentProps { visParams: VisParams; @@ -117,7 +118,8 @@ const VisComponent = (props: VisComponentProps) => { ( visData: Datatable, xAccessor: Accessor | AccessorFn, - splitSeriesAccessors: Array + splitSeriesAccessors: Array, + splitChartAccessor?: Accessor | AccessorFn ): ElementClickListener => { const splitSeriesAccessorFnMap = getSplitSeriesAccessorFnMap(splitSeriesAccessors); return (elements) => { @@ -125,7 +127,8 @@ const VisComponent = (props: VisComponentProps) => { const event = getFilterFromChartClickEventFn( visData, xAccessor, - splitSeriesAccessorFnMap + splitSeriesAccessorFnMap, + splitChartAccessor )(elements as XYChartElementEvent[]); props.fireEvent(event); } @@ -154,12 +157,17 @@ const VisComponent = (props: VisComponentProps) => { ( visData: Datatable, xAccessor: Accessor | AccessorFn, - splitSeriesAccessors: Array + splitSeriesAccessors: Array, + splitChartAccessor?: Accessor | AccessorFn ) => { const splitSeriesAccessorFnMap = getSplitSeriesAccessorFnMap(splitSeriesAccessors); return (series: XYChartSeriesIdentifier): ClickTriggerEvent | null => { if (xAccessor !== null) { - return getFilterFromSeriesFn(visData)(series, splitSeriesAccessorFnMap); + return getFilterFromSeriesFn(visData)( + series, + splitSeriesAccessorFnMap, + splitChartAccessor + ); } return null; @@ -296,10 +304,44 @@ const VisComponent = (props: VisComponentProps) => { ] ); const xAccessor = getXAccessor(config.aspects.x); - const splitSeriesAccessors = config.aspects.series - ? compact(config.aspects.series.map(getComplexAccessor(COMPLEX_SPLIT_ACCESSOR))) - : []; + const splitSeriesAccessors = useMemo( + () => + config.aspects.series + ? compact(config.aspects.series.map(getComplexAccessor(COMPLEX_SPLIT_ACCESSOR))) + : [], + [config.aspects.series] + ); + const splitChartColumnAccessor = config.aspects.splitColumn + ? getComplexAccessor(COMPLEX_SPLIT_ACCESSOR, true)(config.aspects.splitColumn) + : undefined; + const splitChartRowAccessor = config.aspects.splitRow + ? getComplexAccessor(COMPLEX_SPLIT_ACCESSOR, true)(config.aspects.splitRow) + : undefined; + + const renderSeries = useMemo( + () => + renderAllSeries( + config, + visParams.seriesParams, + visData.rows, + getSeriesName, + getSeriesColor, + timeZone, + xAccessor, + splitSeriesAccessors + ), + [ + config, + getSeriesColor, + getSeriesName, + splitSeriesAccessors, + timeZone, + visData.rows, + visParams.seriesParams, + xAccessor, + ] + ); return (
{ legendPosition={legendPosition} /> + { xDomain={xDomain} adjustedXDomain={adjustedXDomain} legendColorPicker={useColorPicker(legendPosition, setColor, getSeriesName)} - onElementClick={handleFilterClick(visData, xAccessor, splitSeriesAccessors)} + onElementClick={handleFilterClick( + visData, + xAccessor, + splitSeriesAccessors, + splitChartColumnAccessor ?? splitChartRowAccessor + )} onBrushEnd={handleBrush(visData, xAccessor, 'interval' in config.aspects.x.params)} onRenderChange={onRenderChange} legendAction={ config.aspects.series && (config.aspects.series?.length ?? 0) > 0 ? getLegendActions( canFilter, - getFilterEventData(visData, xAccessor, splitSeriesAccessors), + getFilterEventData( + visData, + xAccessor, + splitSeriesAccessors, + splitChartColumnAccessor ?? splitChartRowAccessor + ), handleFilterAction, getSeriesName ) @@ -343,16 +399,7 @@ const VisComponent = (props: VisComponentProps) => { {config.yAxes.map((axisProps) => ( ))} - {renderAllSeries( - config, - visParams.seriesParams, - visData.rows, - getSeriesName, - getSeriesColor, - timeZone, - xAccessor, - splitSeriesAccessors - )} + {renderSeries}
); diff --git a/src/plugins/vis_type_xy/public/vis_renderer.tsx b/src/plugins/vis_type_xy/public/vis_renderer.tsx index 612388939d26b..1a47742b3d004 100644 --- a/src/plugins/vis_type_xy/public/vis_renderer.tsx +++ b/src/plugins/vis_type_xy/public/vis_renderer.tsx @@ -16,7 +16,6 @@ import { VisualizationContainer } from '../../visualizations/public'; import type { PersistedState } from '../../visualizations/public'; import { XyVisType } from '../common'; -import { SplitChartWarning } from './components/split_chart_warning'; import { VisComponentType } from './vis_component'; import { RenderValue, visName } from './xy_vis_fn'; @@ -36,24 +35,20 @@ export const xyVisRenderer: ExpressionRenderDefinition = { reuseDomNode: true, render: async (domNode, { visData, visConfig, visType, syncColors }, handlers) => { const showNoResult = shouldShowNoResultsMessage(visData, visType); - const isSplitChart = Boolean(visConfig.dimensions.splitRow); handlers.onDestroy(() => unmountComponentAtNode(domNode)); render( - <> - {isSplitChart && } - - - - + + + , domNode ); diff --git a/src/plugins/vis_type_xy/public/vis_types/area.tsx b/src/plugins/vis_type_xy/public/vis_types/area.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/area.tsx rename to src/plugins/vis_type_xy/public/vis_types/area.ts index 50721c349d6e9..09007a01ca8bc 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/area.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; -import { SplitTooltip } from './split_tooltip'; export const getAreaVisTypeDefinition = ( showElasticChartsOptions = false @@ -181,12 +178,6 @@ export const getAreaVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx b/src/plugins/vis_type_xy/public/vis_types/histogram.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/histogram.tsx rename to src/plugins/vis_type_xy/public/vis_types/histogram.ts index 4fc8dbbb80e7b..daae5f5e48e61 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; -import { SplitTooltip } from './split_tooltip'; export const getHistogramVisTypeDefinition = ( showElasticChartsOptions = false @@ -184,12 +181,6 @@ export const getHistogramVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx rename to src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts index b53bb7bc9dd40..9e026fa0d7474 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; -import { SplitTooltip } from './split_tooltip'; export const getHorizontalBarVisTypeDefinition = ( showElasticChartsOptions = false @@ -183,12 +180,6 @@ export const getHorizontalBarVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/line.tsx b/src/plugins/vis_type_xy/public/vis_types/line.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/line.tsx rename to src/plugins/vis_type_xy/public/vis_types/line.ts index e9b0533b957f5..3f3087207fa19 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/line.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; -import { SplitTooltip } from './split_tooltip'; export const getLineVisTypeDefinition = ( showElasticChartsOptions = false @@ -175,12 +172,6 @@ export const getLineVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx b/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx deleted file mode 100644 index ca22136599341..0000000000000 --- a/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx +++ /dev/null @@ -1,20 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import React from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export function SplitTooltip() { - return ( - - ); -} diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts index bd7957164fd1a..fa3dddfeca02a 100644 --- a/src/plugins/vis_type_xy/server/plugin.ts +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -24,8 +24,7 @@ export const uiSettingsConfig: Record> = { description: i18n.translate( 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', { - defaultMessage: - 'Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation.', + defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.', } ), category: ['visualization'], diff --git a/src/plugins/visualizations/common/index.ts b/src/plugins/visualizations/common/index.ts new file mode 100644 index 0000000000000..d4133eb9b7163 --- /dev/null +++ b/src/plugins/visualizations/common/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** @public types */ +export * from './types'; diff --git a/src/plugins/visualizations/common/types.ts b/src/plugins/visualizations/common/types.ts new file mode 100644 index 0000000000000..4881b82a0e8d3 --- /dev/null +++ b/src/plugins/visualizations/common/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObjectAttributes } from 'kibana/server'; +import { AggConfigOptions } from 'src/plugins/data/common'; + +export interface VisParams { + [key: string]: any; +} + +export interface SavedVisState { + title: string; + type: string; + params: TVisParams; + aggs: AggConfigOptions[]; +} + +export interface VisualizationSavedObjectAttributes extends SavedObjectAttributes { + description: string; + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; + title: string; + version: number; + visState: string; + uiStateJSON: string; +} diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index d1976cc84acec..0bf8aa6e5c418 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -34,7 +34,7 @@ export type { Schema, ISchemas, } from './vis_types'; -export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; +export { SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; export { VisualizeInput } from './embeddable'; @@ -46,12 +46,13 @@ export { PersistedState } from './persisted_state'; export { VisualizationControllerConstructor, VisualizationController, - SavedVisState, ISavedVis, VisSavedObject, VisResponseValue, VisToExpressionAst, + VisParams, } from './types'; export { ExprVisAPIEvents } from './expressions/vis'; export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry'; export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; +export { SavedVisState } from '../common'; diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.d.ts b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts index f3643ad6adcbe..0d871b3b1dea4 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.d.ts +++ b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { SavedVisState } from '../types'; +import { SavedVisState } from '../../common'; declare function updateOldState(oldState: unknown): SavedVisState; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts index c858306968ad8..a85a1d453a939 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts @@ -7,7 +7,8 @@ */ import { extractReferences, injectReferences } from './saved_visualization_references'; -import { VisSavedObject, SavedVisState } from '../types'; +import { VisSavedObject } from '../types'; +import { SavedVisState } from '../../common'; describe('extractReferences', () => { test('extracts nothing if savedSearchId is empty', () => { diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index 2e57cd00486f7..dc9ca49840561 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -7,15 +7,12 @@ */ import { SavedObject } from '../../../plugins/saved_objects/public'; -import { - AggConfigOptions, - SearchSourceFields, - TimefilterContract, -} from '../../../plugins/data/public'; +import { SearchSourceFields, TimefilterContract } from '../../../plugins/data/public'; import { ExpressionAstExpression } from '../../expressions/public'; -import { SerializedVis, Vis, VisParams } from './vis'; +import { SerializedVis, Vis } from './vis'; import { ExprVis } from './expressions/vis'; +import { SavedVisState, VisParams } from '../common/types'; export { Vis, SerializedVis, VisParams }; @@ -30,13 +27,6 @@ export type VisualizationControllerConstructor = new ( vis: ExprVis ) => VisualizationController; -export interface SavedVisState { - title: string; - type: string; - params: VisParams; - aggs: AggConfigOptions[]; -} - export interface ISavedVis { id?: string; title: string; diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 58bcdb9ea49c6..56a151fb82ed3 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -30,6 +30,7 @@ import { AggConfigOptions, SearchSourceFields, } from '../../../plugins/data/public'; +import { VisParams } from '../common/types'; export interface SerializedVisData { expression?: string; @@ -56,10 +57,6 @@ export interface VisData { savedSearchId?: string; } -export interface VisParams { - [key: string]: any; -} - const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => { const searchSource = inputSearchSource.createCopy(); if (savedSearchId) { diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/_inspector.ts index 07b5dfb8a769d..9d4623feef74a 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/_inspector.ts @@ -6,12 +6,15 @@ * Public License, v 1. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('inspector', function describeIndexTests() { @@ -23,6 +26,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + describe('advanced input JSON', () => { + it('should have "missing" property with value 10', async () => { + log.debug('Add Max Metric on memory field'); + await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); + await PageObjects.visEditor.selectAggregation('Max', 'metrics'); + await PageObjects.visEditor.selectField('memory', 'metrics'); + + log.debug('Add value to advanced JSON input'); + await PageObjects.visEditor.toggleAdvancedParams('2'); + await testSubjects.setValue('codeEditorContainer', '{ "missing": 10 }'); + await PageObjects.visEditor.clickGo(); + + await inspector.open(); + await inspector.openInspectorRequestsView(); + const requestTab = await inspector.getOpenRequestDetailRequestButton(); + await requestTab.click(); + const requestJSON = JSON.parse(await inspector.getCodeEditorValue()); + + expect(requestJSON.aggs['2'].max).property('missing', 10); + }); + + after(async () => { + await inspector.close(); + await PageObjects.visEditor.removeDimension(2); + await PageObjects.visEditor.clickGo(); + }); + }); + describe('inspector table', function indexPatternCreation() { it('should update table header when columns change', async function () { await inspector.open(); diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/_line_chart_split_chart.ts index aeb80a58c9655..3e74bf0b7c0ec 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/_line_chart_split_chart.ts @@ -176,8 +176,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = 2; - const maxLabel = 5000; + const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); + const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000); const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -188,8 +188,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = 2; - const maxLabel = 5000; + const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); + const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000); const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -201,7 +201,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -210,7 +213,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['2,000', '4,000', '6,000', '8,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['2,000', '4,000', '6,000', '8,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -220,7 +226,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); log.debug(labels); - const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -228,7 +237,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['2,000', '4,000', '6,000', '8,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['2,000', '4,000', '6,000', '8,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); }); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index dddcd82f1d3f8..8dd2854419693 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -52,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // Test replaced vislib chart types loadTestFile(require.resolve('./_area_chart')); loadTestFile(require.resolve('./_line_chart_split_series')); + loadTestFile(require.resolve('./_line_chart_split_chart')); loadTestFile(require.resolve('./_point_series_options')); loadTestFile(require.resolve('./_vertical_bar_chart')); loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 7326d2efb8565..8a4cf1d8566a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -183,7 +183,7 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [http.basePath, state.start, state.end, logStreamQuery] ); - const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); + const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogFeatureAvailable = useMemo(() => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 331b6bfa882da..94e81e296b5a9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -115,7 +115,10 @@ async function deleteAssets( try { await Promise.all(deletePromises); } catch (err) { - logger.error(err); + // in the rollback case, partial installs are likely, so missing assets are not an error + if (!savedObjectsClient.errors.isNotFoundError(err)) { + logger.error(err); + } } } diff --git a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts index 3719f4d97779c..42f6eb1f8f9c8 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts @@ -5,7 +5,12 @@ */ import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { Job } from '../../../common/types/anomaly_detection_jobs'; +import { + Job, + JobStats, + Datafeed, + DatafeedStats, +} from '../../../common/types/anomaly_detection_jobs'; import { GetGuards } from '../shared_services'; export interface AnomalyDetectorsProvider { @@ -14,6 +19,9 @@ export interface AnomalyDetectorsProvider { savedObjectsClient: SavedObjectsClientContract ): { jobs(jobId?: string): Promise<{ count: number; jobs: Job[] }>; + jobStats(jobId?: string): Promise<{ count: number; jobs: JobStats[] }>; + datafeeds(datafeedId?: string): Promise<{ count: number; datafeeds: Datafeed[] }>; + datafeedStats(datafeedId?: string): Promise<{ count: number; datafeeds: DatafeedStats[] }>; }; } @@ -36,6 +44,42 @@ export function getAnomalyDetectorsProvider(getGuards: GetGuards): AnomalyDetect return body; }); }, + async jobStats(jobId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getJobStats<{ + count: number; + jobs: JobStats[]; + }>(jobId !== undefined ? { job_id: jobId } : undefined); + return body; + }); + }, + async datafeeds(datafeedId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetDatafeeds']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getDatafeeds<{ + count: number; + datafeeds: Datafeed[]; + }>(datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined); + return body; + }); + }, + async datafeedStats(datafeedId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetDatafeeds']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getDatafeedStats<{ + count: number; + datafeeds: DatafeedStats[]; + }>(datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined); + return body; + }); + }, }; }, }; diff --git a/x-pack/plugins/saved_objects_tagging/common/validation.test.ts b/x-pack/plugins/saved_objects_tagging/common/validation.test.ts index 232387e964cbf..a601a96a49e75 100644 --- a/x-pack/plugins/saved_objects_tagging/common/validation.test.ts +++ b/x-pack/plugins/saved_objects_tagging/common/validation.test.ts @@ -20,10 +20,8 @@ describe('Tag attributes validation', () => { ); }); - it('returns an error message if the name contains invalid characters', () => { - expect(validateTagName('t^ag+name&')).toMatchInlineSnapshot( - `"Tag name can only include a-z, 0-9, _, -,:."` - ); + it('does not return an error message if the name contains special characters', () => { + expect(validateTagName('t^ag+name&')).toBeUndefined(); }); }); diff --git a/x-pack/plugins/saved_objects_tagging/common/validation.ts b/x-pack/plugins/saved_objects_tagging/common/validation.ts index 12149d7bdbe79..5cb9e068516fe 100644 --- a/x-pack/plugins/saved_objects_tagging/common/validation.ts +++ b/x-pack/plugins/saved_objects_tagging/common/validation.ts @@ -12,7 +12,6 @@ export const tagNameMaxLength = 50; export const tagDescriptionMaxLength = 100; const hexColorRegexp = /^#[0-9A-F]{6}$/i; -const nameValidCharsRegexp = /^[0-9A-Z:\-_\s]+$/i; export interface TagValidation { valid: boolean; @@ -49,11 +48,6 @@ export const validateTagName = (name: string): string | undefined => { }, }); } - if (!nameValidCharsRegexp.test(name)) { - return i18n.translate('xpack.savedObjectsTagging.validation.name.errorInvalidCharacters', { - defaultMessage: 'Tag name can only include a-z, 0-9, _, -,:.', - }); - } }; export const validateTagDescription = (description: string): string | undefined => { diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index f53b5ca6d56ca..c235c296bcbae 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -22,3 +22,16 @@ export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint'; export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider'; export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg'; export const NEXT_URL_QUERY_STRING_PARAMETER = 'next'; + +/** + * Matches valid usernames and role names. + * + * - Must contain only letters, numbers, spaces, punctuation and printable symbols. + * - Must not contain leading or trailing spaces. + */ +export const NAME_REGEX = /^(?! )[a-zA-Z0-9 !"#$%&'()*+,\-./\\:;<=>?@\[\]^_`{|}~]+(?) { * @param {role} the Role as returned by roles API */ export function isRoleDeprecated(role: Partial) { - return role.metadata?._deprecated ?? false; + return (role.metadata?._deprecated as boolean) ?? false; +} + +/** + * Returns whether given role is a system role or not. + * + * @param {role} the Role as returned by roles API + */ +export function isRoleSystem(role: Partial) { + return (isRoleReserved(role) && role.name?.endsWith('_system')) ?? false; +} + +/** + * Returns whether given role is an admin role or not. + * + * @param {role} the Role as returned by roles API + */ +export function isRoleAdmin(role: Partial) { + return ( + (isRoleReserved(role) && (role.name?.endsWith('_admin') || role.name === 'superuser')) ?? false + ); } /** diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx new file mode 100644 index 0000000000000..7246e37b33da9 --- /dev/null +++ b/x-pack/plugins/security/public/components/breadcrumb.tsx @@ -0,0 +1,137 @@ +/* + * 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, { createContext, useEffect, useRef, useContext, FunctionComponent } from 'react'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; + +interface BreadcrumbsContext { + parents: BreadcrumbProps[]; + onMount(breadcrumbs: BreadcrumbProps[]): void; + onUnmount(breadcrumbs: BreadcrumbProps[]): void; +} + +const BreadcrumbsContext = createContext(undefined); + +export interface BreadcrumbProps extends EuiBreadcrumb { + text: string; +} + +/** + * Component that automatically sets breadcrumbs and document title based on the render tree. + * + * @example + * // Breadcrumbs will be set to: "Users > Create" + * // Document title will be set to: "Create - Users" + * + * ```typescript + * + * + * {showForm && ( + * + *
+ *
+ * )} + * + * ``` + */ +export const Breadcrumb: FunctionComponent = ({ children, ...breadcrumb }) => { + const context = useContext(BreadcrumbsContext); + const component = {children}; + + if (context) { + return component; + } + + return {component}; +}; + +export interface BreadcrumbsProviderProps { + onChange?: BreadcrumbsChangeHandler; +} + +export type BreadcrumbsChangeHandler = (breadcrumbs: BreadcrumbProps[]) => void; + +/** + * Component that can be used to define any side effects that should occur when breadcrumbs change. + * + * By default the breadcrumbs in application chrome are set and the document title is updated. + * + * @example + * ```typescript + * setBreadcrumbs(breadcrumbs)}> + * + * + * ``` + */ +export const BreadcrumbsProvider: FunctionComponent = ({ + children, + onChange, +}) => { + const { services } = useKibana(); + const breadcrumbsRef = useRef([]); + + const handleChange = (breadcrumbs: BreadcrumbProps[]) => { + if (onChange) { + onChange(breadcrumbs); + } else if (services.chrome) { + services.chrome.setBreadcrumbs(breadcrumbs); + services.chrome.docTitle.change(getDocTitle(breadcrumbs)); + } + }; + + return ( + { + if (breadcrumbs.length > breadcrumbsRef.current.length) { + breadcrumbsRef.current = breadcrumbs; + handleChange(breadcrumbs); + } + }, + onUnmount: (breadcrumbs) => { + if (breadcrumbs.length < breadcrumbsRef.current.length) { + breadcrumbsRef.current = breadcrumbs; + handleChange(breadcrumbs); + } + }, + }} + > + {children} + + ); +}; + +export interface InnerBreadcrumbProps { + breadcrumb: BreadcrumbProps; +} + +export const InnerBreadcrumb: FunctionComponent = ({ + breadcrumb, + children, +}) => { + const { parents, onMount, onUnmount } = useContext(BreadcrumbsContext)!; + const nextParents = [...parents, breadcrumb]; + + useEffect(() => { + onMount(nextParents); + return () => onUnmount(parents); + }, [breadcrumb.text, breadcrumb.href]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + {children} + + ); +}; + +export function getDocTitle(breadcrumbs: BreadcrumbProps[], maxBreadcrumbs = 2) { + return breadcrumbs + .slice(0, maxBreadcrumbs) + .reverse() + .map(({ text }) => text); +} diff --git a/x-pack/plugins/security/public/components/confirm_modal.tsx b/x-pack/plugins/security/public/components/confirm_modal.tsx new file mode 100644 index 0000000000000..8dfbf9e3f0649 --- /dev/null +++ b/x-pack/plugins/security/public/components/confirm_modal.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, { FunctionComponent } from 'react'; +import { + EuiButton, + EuiButtonProps, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalProps, + EuiOverlayMask, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ConfirmModalProps extends Omit { + confirmButtonText: string; + confirmButtonColor?: EuiButtonProps['color']; + isLoading?: EuiButtonProps['isLoading']; + isDisabled?: EuiButtonProps['isDisabled']; + onCancel(): void; + onConfirm(): void; + ownFocus?: boolean; +} + +/** + * Component that renders a confirmation modal similar to `EuiConfirmModal`, except that + * it adds `isLoading` prop, which renders a loading spinner and disables action buttons, + * and `ownFocus` prop to render overlay mask. + */ +export const ConfirmModal: FunctionComponent = ({ + children, + confirmButtonColor: buttonColor, + confirmButtonText, + isLoading, + isDisabled, + onCancel, + onConfirm, + ownFocus = true, + title, + ...rest +}) => { + const modal = ( + + + {title} + + {children} + + + + + + + + + + {confirmButtonText} + + + + + + ); + + return ownFocus ? ( + {modal} + ) : ( + modal + ); +}; diff --git a/x-pack/plugins/security/public/components/doc_link.tsx b/x-pack/plugins/security/public/components/doc_link.tsx new file mode 100644 index 0000000000000..50a93b8ee5090 --- /dev/null +++ b/x-pack/plugins/security/public/components/doc_link.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, FunctionComponent } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { CoreStart } from '../../../../../src/core/public'; + +export type DocLinks = CoreStart['docLinks']['links']; +export type GetDocLinkFunction = (app: string, doc: string) => string; + +/** + * Creates links to the documentation. + * + * @see {@link DocLink} for a component that creates a link to the docs. + * + * @example + * ```typescript + * + * Learn what privileges individual roles grant. + * + * ``` + * + * @example + * ```typescript + * const [docs] = useDocLinks(); + * + * + * Learn how to get started with dashboards. + * + * ``` + */ +export function useDocLinks(): [DocLinks, GetDocLinkFunction] { + const { services } = useKibana(); + const { links, ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = services.docLinks!; + const getDocLink = useCallback( + (app, doc) => { + return `${ELASTIC_WEBSITE_URL}guide/en/${app}/reference/${DOC_LINK_VERSION}/${doc}`; + }, + [ELASTIC_WEBSITE_URL, DOC_LINK_VERSION] + ); + return [links, getDocLink]; +} + +export interface DocLinkProps { + app: string; + doc: string; +} + +export const DocLink: FunctionComponent = ({ app, doc, children }) => { + const [, getDocLink] = useDocLinks(); + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/security/public/components/form_flyout.tsx b/x-pack/plugins/security/public/components/form_flyout.tsx new file mode 100644 index 0000000000000..a0d397f81751e --- /dev/null +++ b/x-pack/plugins/security/public/components/form_flyout.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, FunctionComponent, RefObject } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiFlyout, + EuiFlyoutProps, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonProps, + EuiButtonEmpty, + EuiPortal, +} from '@elastic/eui'; +import { useHtmlId } from './use_html_id'; + +export interface FormFlyoutProps extends Omit { + title: string; + initialFocus?: RefObject; + onCancel(): void; + onSubmit(): void; + submitButtonText: string; + submitButtonColor?: EuiButtonProps['color']; + isLoading?: EuiButtonProps['isLoading']; + isDisabled?: EuiButtonProps['isDisabled']; +} + +export const FormFlyout: FunctionComponent = ({ + title, + submitButtonText, + submitButtonColor, + onCancel, + onSubmit, + isLoading, + isDisabled, + children, + initialFocus, + ...rest +}) => { + useEffect(() => { + if (initialFocus && initialFocus.current) { + initialFocus.current.focus(); + } + }, [initialFocus]); + + const titleId = useHtmlId('formFlyout', 'title'); + + return ( + + + + +

{title}

+
+
+ {children} + + + + + + + + + + {submitButtonText} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/components/use_current_user.ts b/x-pack/plugins/security/public/components/use_current_user.ts new file mode 100644 index 0000000000000..b686e0ae9d778 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_current_user.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import useAsync from 'react-use/lib/useAsync'; +import constate from 'constate'; +import { AuthenticationServiceSetup } from '../authentication'; + +export interface AuthenticationProviderProps { + authc: AuthenticationServiceSetup; +} + +const [AuthenticationProvider, useAuthentication] = constate( + ({ authc }: AuthenticationProviderProps) => authc +); + +export { AuthenticationProvider, useAuthentication }; + +export function useCurrentUser() { + const authc = useAuthentication(); + return useAsync(authc.getCurrentUser, [authc]); +} diff --git a/x-pack/plugins/security/public/components/use_form.ts b/x-pack/plugins/security/public/components/use_form.ts new file mode 100644 index 0000000000000..33c7e184ec171 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_form.ts @@ -0,0 +1,205 @@ +/* + * 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 { ChangeEventHandler, FocusEventHandler, ReactEventHandler, useState } from 'react'; +import { get, set, cloneDeep, cloneDeepWith } from 'lodash'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +export type FormReturnTuple = [FormState, FormProps]; + +export interface FormProps { + onSubmit: ReactEventHandler; + onChange: ChangeEventHandler; + onBlur: FocusEventHandler; +} + +export interface FormOptions { + onSubmit: SubmitCallback; + validate: ValidateCallback; + defaultValues: Values; +} + +/** + * Returns state and {@link HTMLFormElement} event handlers useful for creating + * forms with inline validation. + * + * @see {@link useFormState} if you don't want to use {@link HTMLFormElement}. + * + * @example + * ```typescript + * const [form, eventHandlers] = useForm({ + * onSubmit: (values) => apiClient.create(values), + * validate: (values) => !values.email ? { email: 'Required' } : {} + * }); + * + * + * + * Submit + * + * ``` + */ +export function useForm( + options: FormOptions +): FormReturnTuple { + const form = useFormState(options); + + const eventHandlers: FormProps = { + onSubmit: (event) => { + event.preventDefault(); + form.submit(); + }, + onChange: (event) => { + const { name, type, checked, value } = event.target; + if (name) { + form.setValue(name, type === 'checkbox' ? checked : value); + } + }, + onBlur: (event) => { + const { name } = event.target; + if (name) { + form.setTouched(event.target.name); + } + }, + }; + + return [form, eventHandlers]; +} + +export type FormValues = Record; +export type SubmitCallback = (values: Values) => Promise; +export type ValidateCallback = ( + values: Values +) => ValidationErrors | Promise>; +export type ValidationErrors = DeepMap; +export type TouchedFields = DeepMap; + +export interface FormState { + setValue(name: string, value: any): Promise; + setError(name: string, message: string): void; + setTouched(name: string): Promise; + reset(values: Values): void; + submit(): Promise; + values: Values; + errors: ValidationErrors; + touched: TouchedFields; + isValidating: boolean; + isSubmitting: boolean; + submitError: Error | undefined; + isInvalid: boolean; + isSubmitted: boolean; +} + +/** + * Returns state useful for creating forms with inline validation. + * + * @example + * ```typescript + * const form = useFormState({ + * onSubmit: (values) => apiClient.create(values), + * validate: (values) => !values.toggle ? { toggle: 'Required' } : {} + * }); + * + * form.setValue('toggle', e.target.checked)} + * onBlur={() => form.setTouched('toggle')} + * isInvalid={!!form.errors.toggle} + * /> + * + * Submit + * + * ``` + */ +export function useFormState({ + onSubmit, + validate, + defaultValues, +}: FormOptions): FormState { + const [values, setValues] = useState(defaultValues); + const [errors, setErrors] = useState>({}); + const [touched, setTouched] = useState>({}); + const [submitCount, setSubmitCount] = useState(0); + + const [validationState, validateForm] = useAsyncFn( + async (formValues: Values) => { + const nextErrors = await validate(formValues); + setErrors(nextErrors); + if (Object.keys(nextErrors).length === 0) { + setSubmitCount(0); + } + return nextErrors; + }, + [validate] + ); + + const [submitState, submitForm] = useAsyncFn( + async (formValues: Values) => { + const nextErrors = await validateForm(formValues); + setTouched(mapDeep(formValues, true)); + setSubmitCount(submitCount + 1); + if (Object.keys(nextErrors).length === 0) { + return onSubmit(formValues); + } + }, + [validateForm, onSubmit] + ); + + return { + setValue: async (name, value) => { + const nextValues = setDeep(values, name, value); + setValues(nextValues); + await validateForm(nextValues); + }, + setTouched: async (name, value = true) => { + setTouched(setDeep(touched, name, value)); + await validateForm(values); + }, + setError: (name, message) => { + setErrors(setDeep(errors, name, message)); + setTouched(setDeep(touched, name, true)); + }, + reset: (nextValues) => { + setValues(nextValues); + setErrors({}); + setTouched({}); + setSubmitCount(0); + }, + submit: () => submitForm(values), + values, + errors, + touched, + isValidating: validationState.loading, + isSubmitting: submitState.loading, + submitError: submitState.error, + isInvalid: Object.keys(errors).length > 0, + isSubmitted: submitCount > 0, + }; +} + +type DeepMap = { + [K in keyof T]?: T[K] extends any[] + ? T[K][number] extends object + ? Array> + : TValue + : T[K] extends object + ? DeepMap + : TValue; +}; + +function mapDeep(values: T, value: V): DeepMap { + return cloneDeepWith(values, (v) => { + if (typeof v !== 'object' && v !== null) { + return value; + } + }); +} + +function setDeep(values: T, name: string, value: V): T { + if (get(values, name) !== value) { + return set(cloneDeep(values), name, value); + } + return values; +} diff --git a/x-pack/plugins/security/public/components/use_html_id.ts b/x-pack/plugins/security/public/components/use_html_id.ts new file mode 100644 index 0000000000000..23666e83cbf23 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_html_id.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 { useMemo } from 'react'; +import { htmlIdGenerator } from '@elastic/eui'; + +/** + * Generates an ID that can be used for HTML elements. + * + * @param prefix Prefix of the id to be generated + * @param suffix Suffix of the id to be generated + * + * @example + * ```typescript + * const titleId = useHtmlId('changePasswordForm', 'title'); + * + * + *

Change password

+ *
+ * ``` + */ +export function useHtmlId(prefix?: string, suffix?: string) { + return useMemo(() => htmlIdGenerator(prefix)(suffix), [prefix, suffix]); +} diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx index c5582d3526242..b7808ffb30e74 100644 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx @@ -5,141 +5,153 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; - +import { shallowWithIntl } from '@kbn/test/jest'; import { RoleComboBox } from '.'; -import { EuiComboBox } from '@elastic/eui'; -import { findTestSubject } from '@kbn/test/jest'; describe('RoleComboBox', () => { - it('renders the provided list of roles via EuiComboBox options', () => { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - { - name: 'role-2', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - ]; - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(` - Array [ - Object { - "color": "default", - "data-test-subj": "roleOption-role-1", - "label": "role-1", - "value": Object { - "isDeprecated": false, + it('renders roles grouped by custom, user, admin, system and deprecated roles with correct color', () => { + const wrapper = shallowWithIntl( + { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: { _deprecated: true }, - }, - ]; - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(` - Array [ - Object { - "color": "warning", - "data-test-subj": "roleOption-role-1", - "label": "role-1", - "value": Object { - "isDeprecated": true, + { + name: 'some_admin', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: false }, }, - }, - ] - `); - }); - - it('renders the selected role names in the expanded list, coded according to deprecated status', () => { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - { - name: 'role-2', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - ]; - const wrapper = mountWithIntl( -
- -
+ { + name: 'some_system', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: false }, + }, + { + name: 'deprecated_role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: true }, + }, + ]} + selectedRoleNames={[]} + onChange={jest.fn()} + /> ); - findTestSubject(wrapper, 'comboBoxToggleListButton').simulate('click'); - - wrapper.find(EuiComboBox).setState({ isListOpen: true }); - - expect(findTestSubject(wrapper, 'rolesDropdown-renderOption')).toMatchInlineSnapshot(` - Array [ -
- -
- role-1 - -
-
-
, -
- -
- role-2 - -
-
-
, - ] + expect(wrapper).toMatchInlineSnapshot(` + `); }); }); diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx index 5b24b296b299f..91d953c4aa29a 100644 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx @@ -6,11 +6,28 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox } from '@elastic/eui'; -import { Role, isRoleDeprecated } from '../../../common/model'; -import { RoleComboBoxOption } from './role_combo_box_option'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiBadge, + EuiComboBox, + EuiComboBoxProps, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { + Role, + isRoleSystem, + isRoleAdmin, + isRoleReserved, + isRoleDeprecated, +} from '../../../common/model'; -interface Props { +interface Props + extends Omit< + EuiComboBoxProps, + 'onChange' | 'options' | 'selectedOptions' | 'renderOption' + > { availableRoles: Role[]; selectedRoleNames: readonly string[]; onChange: (selectedRoleNames: string[]) => void; @@ -19,43 +36,132 @@ interface Props { isDisabled?: boolean; } +type Option = EuiComboBoxOptionOption<{ + isReserved: boolean; + isDeprecated: boolean; + isSystem: boolean; + isAdmin: boolean; + deprecatedReason?: string; +}>; + export const RoleComboBox = (props: Props) => { const onRolesChange = (selectedItems: Array<{ label: string }>) => { props.onChange(selectedItems.map((item) => item.label)); }; - const roleNameToOption = (roleName: string) => { + const roleNameToOption = (roleName: string): Option => { const roleDefinition = props.availableRoles.find((role) => role.name === roleName); + const isReserved: boolean = (roleDefinition && isRoleReserved(roleDefinition)) ?? false; const isDeprecated: boolean = (roleDefinition && isRoleDeprecated(roleDefinition)) ?? false; + const isSystem: boolean = (roleDefinition && isRoleSystem(roleDefinition)) ?? false; + const isAdmin: boolean = (roleDefinition && isRoleAdmin(roleDefinition)) ?? false; return { - color: isDeprecated ? 'warning' : 'default', + color: isDeprecated ? 'warning' : isReserved ? 'primary' : undefined, 'data-test-subj': `roleOption-${roleName}`, label: roleName, value: { + isReserved, isDeprecated, + isSystem, + isAdmin, + deprecatedReason: roleDefinition?.metadata?._deprecated_reason, }, }; }; const options = props.availableRoles.map((role) => roleNameToOption(role.name)); - const selectedOptions = props.selectedRoleNames.map(roleNameToOption); + const groupedOptions = options.reduce>((acc, option) => { + const type = option.value?.isDeprecated + ? 'deprecated' + : option.value?.isSystem + ? 'system' + : option.value?.isAdmin + ? 'admin' + : option.value?.isReserved + ? 'user' + : 'custom'; + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(option); + return acc; + }, {}); return ( } + renderOption={renderOption} /> ); }; + +function renderOption(option: Option) { + return ( + + {option.label} + {option.value?.isDeprecated ? ( + + + + + + ) : option.value?.isReserved ? ( + + + + + + ) : undefined} + + ); +} diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx deleted file mode 100644 index b24a48145b461..0000000000000 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx +++ /dev/null @@ -1,57 +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 { shallowWithIntl } from '@kbn/test/jest'; -import { RoleComboBoxOption } from './role_combo_box_option'; - -describe('RoleComboBoxOption', () => { - it('renders a regular role correctly', () => { - const wrapper = shallowWithIntl( - - ); - - expect(wrapper).toMatchInlineSnapshot(` - - role-1 - - - `); - }); - - it('renders a deprecated role correctly', () => { - const wrapper = shallowWithIntl( - - ); - - expect(wrapper).toMatchInlineSnapshot(` - - role-1 - - (deprecated) - - `); - }); -}); diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx deleted file mode 100644 index ae9b79c796275..0000000000000 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiComboBoxOptionOption, EuiText } from '@elastic/eui'; - -interface Props { - option: EuiComboBoxOptionOption<{ isDeprecated: boolean }>; -} - -export const RoleComboBoxOption = ({ option }: Props) => { - const isDeprecated = option.value?.isDeprecated ?? false; - const deprecatedLabel = i18n.translate( - 'xpack.security.management.users.editUser.deprecatedRoleText', - { - defaultMessage: '(deprecated)', - } - ); - - return ( - - {option.label} {isDeprecated ? deprecatedLabel : ''} - - ); -}; 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 index c750ec373b9f7..df5e5c8be9025 100644 --- 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 @@ -407,7 +407,7 @@ export const EditRolePage: FunctionComponent = ({ const onNameChange = (e: ChangeEvent) => setRole({ ...role, - name: e.target.value.replace(/\s/g, '_'), + name: e.target.value, }); const getElasticsearchPrivileges = () => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts index 868674aec6f86..e6b9b19022f31 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts @@ -40,7 +40,7 @@ describe('validateRoleName', () => { expect(validator.validateRoleName(role)).toEqual({ isInvalid: true, - error: `Please provide a role name`, + error: `Please provide a role name.`, }); }); @@ -57,13 +57,30 @@ describe('validateRoleName', () => { expect(validator.validateRoleName(role)).toEqual({ isInvalid: true, - error: `Name must not exceed 1024 characters`, + error: `Name must not exceed 1024 characters.`, + }); + }); + + test('it cannot start with whitespace character', () => { + const role = { + name: ' role-name', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + expect(validator.validateRoleName(role)).toEqual({ + isInvalid: true, + error: `Name must not contain leading or trailing spaces.`, }); }); const charList = `!#%^&*()+=[]{}\|';:"/,<>?`.split(''); charList.forEach((element) => { - test(`it cannot support the "${element}" character`, () => { + test(`it allows the "${element}" character`, () => { const role = { name: `role-${element}`, elasticsearch: { @@ -74,10 +91,7 @@ describe('validateRoleName', () => { kibana: [], }; - expect(validator.validateRoleName(role)).toEqual({ - isInvalid: true, - error: `Name must begin with a letter or underscore and contain only letters, underscores, and numbers.`, - }); + expect(validator.validateRoleName(role)).toEqual({ isInvalid: false }); }); }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts index 89b16b1467776..e0459bbd3dd0d 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { Role, RoleIndexPrivilege } from '../../../../common/model'; +import { NAME_REGEX, MAX_NAME_LENGTH } from '../../../../common/constants'; interface RoleValidatorOptions { shouldValidate?: boolean; @@ -41,25 +42,36 @@ export class RoleValidator { i18n.translate( 'xpack.security.management.editRole.validateRole.provideRoleNameWarningMessage', { - defaultMessage: 'Please provide a role name', + defaultMessage: 'Please provide a role name.', } ) ); } - if (role.name.length > 1024) { + if (role.name.length > MAX_NAME_LENGTH) { return invalid( i18n.translate('xpack.security.management.editRole.validateRole.nameLengthWarningMessage', { - defaultMessage: 'Name must not exceed 1024 characters', + defaultMessage: 'Name must not exceed {maxLength} characters.', + values: { maxLength: MAX_NAME_LENGTH }, }) ); } - if (!role.name.match(/^[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*$/)) { + if (role.name.trim() !== role.name) { + return invalid( + i18n.translate( + 'xpack.security.management.editRole.validateRole.nameWhitespaceWarningMessage', + { + defaultMessage: `Name must not contain leading or trailing spaces.`, + } + ) + ); + } + if (!role.name.match(NAME_REGEX)) { return invalid( i18n.translate( 'xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage', { defaultMessage: - 'Name must begin with a letter or underscore and contain only letters, underscores, and numbers.', + 'Name must contain only letters, numbers, spaces, punctuation and printable symbols.', } ) ); diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx new file mode 100644 index 0000000000000..2586b7c24bf4c --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx @@ -0,0 +1,286 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { + EuiCallOut, + EuiFieldPassword, + EuiFlexGroup, + EuiForm, + EuiFormRow, + EuiIcon, + EuiLoadingContent, + EuiSpacer, + EuiText, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useForm, ValidationErrors } from '../../../components/use_form'; +import { useCurrentUser } from '../../../components/use_current_user'; +import { FormFlyout } from '../../../components/form_flyout'; +import { UserAPIClient } from '..'; + +export interface ChangePasswordFormValues { + current_password?: string; + password: string; + confirm_password: string; +} + +export interface ChangePasswordFlyoutProps { + username: string; + defaultValues?: ChangePasswordFormValues; + onCancel(): void; + onSuccess?(): void; +} + +export const ChangePasswordFlyout: FunctionComponent = ({ + username, + defaultValues = { + current_password: '', + password: '', + confirm_password: '', + }, + onSuccess, + onCancel, +}) => { + const { services } = useKibana(); + const { value: currentUser, loading: isLoading } = useCurrentUser(); + const isCurrentUser = currentUser?.username === username; + const isSystemUser = username === 'kibana' || username === 'kibana_system'; + + const [form, eventHandlers] = useForm({ + onSubmit: async (values) => { + try { + await new UserAPIClient(services.http!).changePassword( + username, + values.password, + values.current_password + ); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.changePasswordFlyout.successMessage', { + defaultMessage: "Password changed for '{username}'.", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + if ((error as any).body?.message === 'security_exception') { + form.setError( + 'current_password', + i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.currentPasswordInvalidError', + { + defaultMessage: 'Invalid password.', + } + ) + ); + } else { + services.notifications!.toasts.addDanger({ + title: i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.errorMessage', + { + defaultMessage: 'Could not change password', + } + ), + text: (error as any).body?.message || error.message, + }); + throw error; + } + } + }, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (isCurrentUser) { + if (!values.current_password) { + errors.current_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError', + { + defaultMessage: 'Enter your current password.', + } + ); + } + } + + if (!values.password) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordRequiredError', + { + defaultMessage: 'Enter a new password.', + } + ); + } else if (values.password.length < 6) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordInvalidError', + { + defaultMessage: 'Password must be at least 6 characters.', + } + ); + } else if (!values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.confirmPasswordRequiredError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } else if (values.password !== values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } + + return errors; + }, + defaultValues, + }); + + return ( + + {isLoading ? ( + + ) : ( + + {isSystemUser ? ( + <> + +

+ +

+

+ +

+
+ + + ) : undefined} + + + + + + + + + {username} + + + + + + {isCurrentUser ? ( + + + + ) : null} + + + + + + + + + {/* Hidden submit button is required for enter key to trigger form submission */} + +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx new file mode 100644 index 0000000000000..18be46ebefed0 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx @@ -0,0 +1,96 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmDeleteUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmDeleteUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + + const [state, deleteUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).deleteUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmDeleteUsers.successMessage', { + defaultMessage: "Deleted user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.confirmDeleteUsers.errorMessage', { + defaultMessage: "Could not delete user '{username}'", + values: { username }, + }), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx new file mode 100644 index 0000000000000..b0a9e875c2089 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx @@ -0,0 +1,119 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmDisableUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmDisableUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + const isSystemUser = usernames[0] === 'kibana' || usernames[0] === 'kibana_system'; + + const [state, disableUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).disableUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmDisableUsers.successMessage', { + defaultMessage: "Deactivated user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate( + 'xpack.security.management.users.confirmDisableUsers.errorMessage', + { + defaultMessage: "Could not deactivate user '{username}'", + values: { username }, + } + ), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + {isSystemUser ? ( + +

+ +

+

+ +

+
+ ) : ( + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx new file mode 100644 index 0000000000000..c9589cfa17da2 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmEnableUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmEnableUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + + const [state, enableUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).enableUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmEnableUsers.successMessage', { + defaultMessage: "Activated user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.confirmEnableUsers.errorMessage', { + defaultMessage: "Could not activate user '{username}'", + values: { username }, + }), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx new file mode 100644 index 0000000000000..e7e3e1164ae14 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx @@ -0,0 +1,108 @@ +/* + * 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, fireEvent, waitFor, within } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { securityMock } from '../../../mocks'; +import { Providers } from '../users_management_app'; +import { CreateUserPage } from './create_user_page'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('CreateUserPage', () => { + it('creates user when submitting form and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/create'] }); + const authc = securityMock.createSetup().authc; + coreStart.http.post.mockResolvedValue({}); + + const { findByRole, findByLabelText } = render( + + + + ); + + fireEvent.change(await findByLabelText('Username'), { target: { value: 'jdoe' } }); + fireEvent.change(await findByLabelText('Password'), { target: { value: 'changeme' } }); + fireEvent.change(await findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await findByRole('button', { name: 'Create user' })); + + await waitFor(() => { + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + password: 'changeme', + username: 'jdoe', + full_name: '', + email: '', + roles: [], + }), + }); + expect(history.location.pathname).toBe('/'); + }); + }); + + it('validates form', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/create'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.get.mockResolvedValueOnce([ + { + username: 'existing_username', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], + }, + ]); + + const { findAllByText, findByRole, findByLabelText } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Create user' })); + + const alert = await findByRole('alert'); + within(alert).getByText(/Enter a username/i); + within(alert).getByText(/Enter a password/i); + + fireEvent.change(await findByLabelText('Username'), { target: { value: 'existing_username' } }); + + await findAllByText(/User 'existing_username' already exists/i); + + fireEvent.change(await findByLabelText('Username'), { + target: { value: ' username_with_leading_space' }, + }); + + await findAllByText(/Username must not contain leading or trailing spaces/i); + + fireEvent.change(await findByLabelText('Username'), { + target: { value: '€' }, + }); + + await findAllByText( + /Username must contain only letters, numbers, spaces, punctuation, and symbols/i + ); + + fireEvent.change(await findByLabelText('Password'), { target: { value: '111' } }); + + await findAllByText(/Password must be at least 6 characters/i); + + fireEvent.change(await findByLabelText('Password'), { target: { value: '123456' } }); + fireEvent.change(await findByLabelText('Confirm password'), { target: { value: '111' } }); + + await findAllByText(/Passwords do not match/i); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx new file mode 100644 index 0000000000000..6842ddb774bda --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx @@ -0,0 +1,44 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { + EuiHorizontalRule, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; +import { UserForm } from './user_form'; + +export const CreateUserPage: FunctionComponent = () => { + const history = useHistory(); + const backToUsers = () => history.push('/'); + + return ( + + + + +

+ +

+
+
+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss deleted file mode 100644 index 727fac4782752..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss +++ /dev/null @@ -1,6 +0,0 @@ -.secUsersEditPage__content { - max-width: 460px; - margin-left: auto; - margin-right: auto; - flex-grow: 0; -} diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 5e8c9f2d14a4c..f065c45d7080c 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -4,228 +4,440 @@ * you may not use this file except in compliance with the Elastic License. */ -import { act } from '@testing-library/react'; -import { mountWithIntl, nextTick } from '@kbn/test/jest'; -import { EditUserPage } from './edit_user_page'; import React from 'react'; -import { User, Role } from '../../../../common/model'; -import { ReactWrapper } from 'enzyme'; -import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; +import { + fireEvent, + render, + waitFor, + waitForElementToBeRemoved, + within, +} from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +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'; -import { findTestSubject } from '@kbn/test/jest'; - -const createUser = (username: string, roles = ['idk', 'something']) => { - const user: User = { - username, - full_name: 'my full name', - email: 'foo@bar.com', - roles, - enabled: true, - }; - - if (username === 'reserved_user') { - user.metadata = { - _reserved: true, - }; - } - - if (username === 'deprecated_user') { - user.metadata = { - _reserved: true, - _deprecated: true, - _deprecated_reason: 'beacuse I said so.', - }; - } - - return user; +import { Providers } from '../users_management_app'; +import { EditUserPage } from './edit_user_page'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +const userMock = { + username: 'jdoe', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], }; -const buildClients = (user: User) => { - const apiClient = userAPIClientMock.create(); - apiClient.getUser.mockResolvedValue(user); +describe('EditUserPage', () => { + it('warns when viewing deactivated user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + enabled: false, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByText } = render( + + + + ); - const rolesAPIClient = rolesAPIClientMock.create(); - rolesAPIClient.getRoles.mockImplementation(() => { - return Promise.resolve([ - { - name: 'role 1', - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: [], - }, - kibana: [], - }, - { - name: 'role 2', - elasticsearch: { - cluster: [], - indices: [], - run_as: ['bar'], - }, - kibana: [], + await findByText(/User has been deactivated/i); + }); + + it('warns when viewing deprecated user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + metadata: { + _reserved: true, + _deprecated: true, + _deprecated_reason: 'Use [new_user] instead.', }, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByRole, findByText } = render( + + + + ); + + await findByText(/User is deprecated/i); + await findByText(/Use .new_user. instead/i); + + fireEvent.click(await findByRole('button', { name: 'Back to users' })); + + await waitFor(() => expect(history.location.pathname).toBe('/')); + }); + + it('warns when viewing built-in user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + metadata: { _reserved: true, _deprecated: false }, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByRole, findByText } = render( + + + + ); + + await findByText(/User is built in/i); + + fireEvent.click(await findByRole('button', { name: 'Back to users' })); + + await waitFor(() => expect(history.location.pathname).toBe('/')); + }); + + it('warns when selecting deprecated role', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + enabled: false, + roles: ['deprecated_role'], + }); + coreStart.http.get.mockResolvedValueOnce([ { - name: 'deprecated-role', - elasticsearch: { - cluster: [], - indices: [], - run_as: ['bar'], - }, - kibana: [], + name: 'deprecated_role', metadata: { + _reserved: true, _deprecated: true, + _deprecated_reason: 'Use [new_role] instead.', }, }, - ] as Role[]); + ]); + + const { findByText } = render( + + + + ); + + await findByText(/Role .deprecated_role. is deprecated. Use .new_role. instead/i); }); - return { apiClient, rolesAPIClient }; -}; + it('updates user when submitting form and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; -function buildSecuritySetup() { - const securitySetupMock = securityMock.createSetup(); - securitySetupMock.authc.getCurrentUser.mockResolvedValue( - mockAuthenticatedUser(createUser('current_user')) - ); - return securitySetupMock; -} + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); -function expectSaveButton(wrapper: ReactWrapper) { - expect(wrapper.find('EuiButton[data-test-subj="userFormSaveButton"]')).toHaveLength(1); -} + const { findByRole, findByLabelText } = render( + + + + ); -function expectMissingSaveButton(wrapper: ReactWrapper) { - expect(wrapper.find('EuiButton[data-test-subj="userFormSaveButton"]')).toHaveLength(0); -} + fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); + fireEvent.change(await findByLabelText('Email address'), { + target: { value: 'jdoe@elastic.co' }, + }); + fireEvent.click(await findByRole('button', { name: 'Update user' })); + + await waitFor(() => { + expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); + expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + ...userMock, + full_name: 'John Doe', + email: 'jdoe@elastic.co', + }), + }); + expect(history.location.pathname).toBe('/'); + }); + }); -describe('EditUserPage', () => { - const history = scopedHistoryMock.create(); - - it('allows reserved users to be viewed', async () => { - const user = createUser('reserved_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('warns when user form submission fails', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); + + const { findByRole, findByLabelText } = render( + + + ); - await waitForRender(wrapper); + fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); + fireEvent.change(await findByLabelText('Email address'), { + target: { value: 'jdoe@elastic.co' }, + }); + fireEvent.click(await findByRole('button', { name: 'Update user' })); + + await waitFor(() => { + expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); + expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + ...userMock, + full_name: 'John Doe', + email: 'jdoe@elastic.co', + }), + }); + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'Error message', + title: "Could not update user 'jdoe'", + }); + expect(history.location.pathname).toBe('/edit/jdoe'); + }); + }); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + it('changes password of other user when submitting form and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce( + mockAuthenticatedUser({ ...userMock, username: 'elastic' }) + ); + coreStart.http.post.mockResolvedValueOnce({}); - expectMissingSaveButton(wrapper); + const { getByRole, findByRole } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = getByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(within(dialog).getByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(within(dialog).getByRole('button', { name: 'Change password' })); + + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { + body: JSON.stringify({ + newPassword: 'changeme', + }), + }); }); - it('allows new users to be created', async () => { - const user = createUser(''); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('changes password of current user when submitting form and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + ); - await waitForRender(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('Current password'), { + target: { value: '123456' }, + }); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); + + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { + body: JSON.stringify({ + newPassword: 'changeme', + password: '123456', + }), + }); + }); + + it('warns when change password form submission fails', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce( + mockAuthenticatedUser({ ...userMock, username: 'elastic' }) + ); + coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); - expect(apiClient.getUser).toBeCalledTimes(0); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(0); + const { findByRole } = render( + + + + ); - expectSaveButton(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); + + await waitFor(() => { + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'Error message', + title: 'Could not change password', + }); + }); }); - it('allows existing users to be edited', async () => { - const user = createUser('existing_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('validates change password form', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); + coreStart.http.post.mockResolvedValueOnce({}); + + const { findByRole } = render( + + + ); - await waitForRender(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + await within(dialog).findByText(/Enter your current password/i); + await within(dialog).findByText(/Enter a new password/i); - expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(0); - expectSaveButton(wrapper); + fireEvent.change(await within(dialog).findByLabelText('Current password'), { + target: { value: 'changeme' }, + }); + + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: '111' }, + }); + + await within(dialog).findAllByText(/Password must be at least 6 characters/i); + + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: '123456' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: '111' }, + }); + + await within(dialog).findAllByText(/Passwords do not match/i); }); - it('warns when viewing a depreciated user', async () => { - const user = createUser('deprecated_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - - const wrapper = mountWithIntl( - + it('deactivates user when confirming and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + ); - await waitForRender(wrapper); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + fireEvent.click(await findByRole('button', { name: 'Deactivate user' })); + + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Deactivate user' })); - expect(findTestSubject(wrapper, 'deprecatedUserWarning')).toHaveLength(1); + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_disable'); }); - it('warns when user is assigned a deprecated role', async () => { - const user = createUser('existing_user', ['deprecated-role']); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - - const wrapper = mountWithIntl( - + it('activates user when confirming and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false }); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findAllByRole } = render( + + + ); - await waitForRender(wrapper); + const [enableButton] = await findAllByRole('button', { name: 'Activate user' }); + fireEvent.click(enableButton); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Activate user' })); - expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(1); + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable'); }); -}); -async function waitForRender(wrapper: ReactWrapper) { - await act(async () => { - await nextTick(); - wrapper.update(); + it('deletes user when confirming and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.delete.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Delete user' })); + + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Delete user' })); + + expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); + await waitFor(() => { + expect(history.location.pathname).toBe('/'); + }); }); -} +}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index dc0c3336cb85f..68c01bf509b0d 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -4,615 +4,315 @@ * you may not use this file except in compliance with the Elastic License. */ -import './edit_user_page.scss'; - -import { get } from 'lodash'; -import React, { Component, Fragment, ChangeEvent } from 'react'; +import React, { FunctionComponent, useState, useEffect } from 'react'; import { + EuiAvatar, EuiButton, EuiCallOut, - EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiLink, - EuiTitle, - EuiForm, - EuiFormRow, - EuiIcon, - EuiText, - EuiFieldText, + EuiHorizontalRule, EuiPageContent, + EuiPageContentBody, EuiPageContentHeader, EuiPageContentHeaderSection, - EuiPageContentBody, - EuiHorizontalRule, + EuiPanel, EuiSpacer, + EuiText, + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { NotificationsStart, ScopedHistory } from 'src/core/public'; -import { User, EditUser, Role, isRoleDeprecated } from '../../../../common/model'; -import { AuthenticationServiceSetup } from '../../../authentication'; -import { RolesAPIClient } from '../../roles'; -import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; -import { UserValidator, UserValidationResult } from './validate_user'; -import { RoleComboBox } from '../../role_combo_box'; -import { isUserDeprecated, getExtendedUserDeprecationNotice, isUserReserved } from '../user_utils'; +import { useHistory } from 'react-router-dom'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { getUserDisplayName } from '../../../../common/model'; +import { isUserDeprecated, isUserReserved } from '../user_utils'; +import { UserForm } from './user_form'; +import { ChangePasswordFlyout } from './change_password_flyout'; +import { ConfirmDisableUsers } from './confirm_disable_users'; +import { ConfirmEnableUsers } from './confirm_enable_users'; +import { ConfirmDeleteUsers } from './confirm_delete_users'; import { UserAPIClient } from '..'; -interface Props { - username?: string; - userAPIClient: PublicMethodsOf; - rolesAPIClient: PublicMethodsOf; - authc: AuthenticationServiceSetup; - notifications: NotificationsStart; - history: ScopedHistory; -} - -interface State { - isLoaded: boolean; - isNewUser: boolean; - currentUser: User | null; - showChangePasswordForm: boolean; - showDeleteConfirmation: boolean; - user: EditUser; - roles: Role[]; - selectedRoles: readonly string[]; - formError: UserValidationResult | null; +export interface EditUserPageProps { + username: string; } -export class EditUserPage extends Component { - private validator: UserValidator; - - constructor(props: Props) { - super(props); - this.validator = new UserValidator({ shouldValidate: false }); - this.state = { - isLoaded: false, - isNewUser: true, - currentUser: null, - showChangePasswordForm: false, - showDeleteConfirmation: false, - user: { - email: '', - username: '', - full_name: '', - roles: [], - enabled: true, - password: '', - confirmPassword: '', - }, - roles: [], - selectedRoles: [], - formError: null, - }; - } - - public async componentDidMount() { - await this.setCurrentUser(); - } - - public async componentDidUpdate(prevProps: Props) { - if (prevProps.username !== this.props.username) { - await this.setCurrentUser(); +export type EditUserPageAction = + | 'changePassword' + | 'disableUser' + | 'enableUser' + | 'deleteUser' + | 'none'; + +export const EditUserPage: FunctionComponent = ({ username }) => { + const { services } = useKibana(); + const history = useHistory(); + const [{ value: user, error }, getUser] = useAsyncFn( + () => new UserAPIClient(services.http!).getUser(username), + [services.http] + ); + const [action, setAction] = useState('none'); + + const backToUsers = () => history.push('/'); + + useEffect(() => { + getUser(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (error) { + backToUsers(); } - } + }, [error]); // eslint-disable-line react-hooks/exhaustive-deps - private backToUserList() { - this.props.history.push('/'); + if (!user) { + return null; } - private async setCurrentUser() { - const { username, userAPIClient, rolesAPIClient, notifications, authc } = this.props; - let { user, currentUser } = this.state; - if (username) { - try { - user = { - ...(await userAPIClient.getUser(username)), - password: '', - confirmPassword: '', - }; - currentUser = await authc.getCurrentUser(); - } catch (err) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.security.management.users.editUser.errorLoadingUserTitle', { - defaultMessage: 'Error loading user', - }), - text: get(err, 'body.message') || err.message, - }); - return this.backToUserList(); - } - } - - let roles: Role[] = []; - try { - roles = await rolesAPIClient.getRoles(); - } catch (err) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.security.management.users.editUser.errorLoadingRolesTitle', { - defaultMessage: 'Error loading roles', - }), - text: get(err, 'body.message') || err.message, - }); - } - - this.setState({ - isLoaded: true, - isNewUser: !username, - currentUser, - user, - roles, - selectedRoles: user.roles || [], - }); - } - - private handleDelete = (usernames: string[], errors: string[]) => { - if (errors.length === 0) { - this.backToUserList(); - } - }; - - private saveUser = async () => { - this.validator.enableValidation(); - - const result = this.validator.validateForSave(this.state.user, this.state.isNewUser); - if (result.isInvalid) { - this.setState({ - formError: result, - }); - } else { - this.setState({ - formError: null, - }); - const { userAPIClient } = this.props; - const { user, isNewUser, selectedRoles } = this.state; - const userToSave: EditUser = { ...user }; - if (!isNewUser) { - delete userToSave.password; - } - delete userToSave.confirmPassword; - userToSave.roles = [...selectedRoles]; - try { - await userAPIClient.saveUser(userToSave); - this.props.notifications.toasts.addSuccess( - i18n.translate( - 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', - { - defaultMessage: 'Saved user {message}', - values: { message: user.username }, - } - ) - ); - - this.backToUserList(); - } catch (e) { - 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') }, - }) - ); - } - } - }; - - private passwordFields = () => { - return ( - - - - - - - - - ); - }; - - private changePasswordForm = () => { - const { showChangePasswordForm, user, currentUser } = this.state; - - const userIsLoggedInUser = Boolean( - currentUser && user.username && user.username === currentUser.username - ); - - if (!showChangePasswordForm) { - return null; - } - return ( - + const isReservedUser = isUserReserved(user); + const isDeprecatedUser = isUserDeprecated(user); + const displayName = getUserDisplayName(user); + + return ( + + + + + + + + + +

{displayName}

+
+ {user.email} +
+
+
+
+ - {user.username === 'kibana' || user.username === 'kibana_system' ? ( - + {isDeprecatedUser ? ( + <> + } + iconType="alert" color="warning" - iconType="help" > -

+ {user.metadata?._deprecated_reason?.replace(/\[(.+)\]/, "'$1'")} + + + + ) : isReservedUser ? ( + <> + + } + iconType="lock" + /> + + + ) : user.enabled === false ? ( + <> + -

+ } + > + setAction('enableUser')} size="s"> + +
-
- ) : null} - + ) : undefined} + + -
- ); - }; - - private toggleChangePasswordForm = () => { - const { showChangePasswordForm } = this.state; - this.setState({ showChangePasswordForm: !showChangePasswordForm }); - }; - - private onUsernameChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - username: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - private onEmailChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - email: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onFullNameChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - full_name: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onPasswordChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - password: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onConfirmPasswordChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - confirmPassword: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onRolesChange = (selectedRoles: string[]) => { - this.setState({ - selectedRoles, - }); - }; - - private cannotSaveUser = () => { - const { user, isNewUser } = this.state; - const result = this.validator.validateForSave(user, isNewUser); - return result.isInvalid; - }; - - private onCancelDelete = () => { - this.setState({ showDeleteConfirmation: false }); - }; - - public render() { - const { - user, - selectedRoles, - roles, - showChangePasswordForm, - isNewUser, - showDeleteConfirmation, - } = this.state; - const reserved = isUserReserved(user); - if (!user || !roles) { - return null; - } - - if (!this.state.isLoaded) { - return null; - } - - const hasAnyDeprecatedRolesAssigned = selectedRoles.some((selected) => { - const role = roles.find((r) => r.name === selected); - return role && isRoleDeprecated(role); - }); + {action === 'changePassword' ? ( + setAction('none')} + onSuccess={() => setAction('none')} + /> + ) : action === 'disableUser' ? ( + setAction('none')} + onSuccess={() => { + setAction('none'); + getUser(); + }} + /> + ) : action === 'enableUser' ? ( + setAction('none')} + onSuccess={() => { + setAction('none'); + getUser(); + }} + /> + ) : action === 'deleteUser' ? ( + setAction('none')} + onSuccess={backToUsers} + /> + ) : undefined} - const roleHelpText = hasAnyDeprecatedRolesAssigned ? ( - - - - ) : undefined; + + - return ( -
- - - - -

- {isNewUser ? ( + + + + + + + + + + + + + + setAction('changePassword')} size="s"> + + + + + + + + {user.enabled === false ? ( + + + + + - ) : ( + + - )} -

-
-
- {reserved && ( - - - - )} -
- - {reserved && ( - - -

+ + + + + setAction('enableUser')} size="s"> + + + + + + ) : ( + + + + + -

-
- -
- )} - - {isUserDeprecated(user) && ( - - - - - )} - - {showDeleteConfirmation ? ( - - ) : null} - - - - - - {isNewUser ? this.passwordFields() : null} - {reserved ? null : ( - - - - - - - - - )} - - - - - {isNewUser || showChangePasswordForm ? null : ( - - + + - - - )} - {this.changePasswordForm()} - - - - {reserved && ( - this.backToUserList()}> + + + + + setAction('disableUser')} size="s"> - )} - {reserved ? null : ( - - - this.saveUser()} - > - {isNewUser ? ( - - ) : ( - - )} - - - - this.backToUserList()} - > + + + + )} + + {!isReservedUser && ( + <> + + + + + + - - - - {isNewUser || reserved ? null : ( - - { - this.setState({ showDeleteConfirmation: true }); - }} - data-test-subj="userFormDeleteButton" - color="danger" - > - - - - )} - - )} - -
-
-
- ); - } -} + + + + + + + + setAction('deleteUser')} size="s" color="danger"> + + + + + + + )} + + + ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/index.ts b/x-pack/plugins/security/public/management/users/edit_user/index.ts index 92eb17b9ebd36..30069d8e97c31 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/index.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/index.ts @@ -5,3 +5,4 @@ */ export { EditUserPage } from './edit_user_page'; +export { CreateUserPage } from './create_user_page'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx new file mode 100644 index 0000000000000..daa488d674fbb --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx @@ -0,0 +1,466 @@ +/* + * 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, { FunctionComponent, useEffect, useCallback } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiDescribedFormGroup, + EuiFieldPassword, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { throttle } from 'lodash'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { User, Role, isRoleDeprecated } from '../../../../common/model'; +import { NAME_REGEX, MAX_NAME_LENGTH } from '../../../../common/constants'; +import { useForm, ValidationErrors } from '../../../components/use_form'; +import { DocLink } from '../../../components/doc_link'; +import { RolesAPIClient } from '../../roles'; +import { RoleComboBox } from '../../role_combo_box'; +import { UserAPIClient } from '..'; + +export const THROTTLE_USERS_WAIT = 10000; + +export interface UserFormValues { + username?: string; + full_name: string; + email: string; + password?: string; + confirm_password?: string; + roles: readonly string[]; +} + +export interface UserFormProps { + isNewUser?: boolean; + isReservedUser?: boolean; + isCurrentUser?: boolean; + defaultValues?: UserFormValues; + onCancel(): void; + onSuccess?(): void; +} + +const defaultDefaultValues: UserFormValues = { + username: '', + password: '', + confirm_password: '', + full_name: '', + email: '', + roles: [], +}; + +export const UserForm: FunctionComponent = ({ + isNewUser = false, + isReservedUser = false, + defaultValues = defaultDefaultValues, + onSuccess, + onCancel, +}) => { + const { services } = useKibana(); + + const [rolesState, getRoles] = useAsyncFn(() => new RolesAPIClient(services.http!).getRoles(), [ + services.http, + ]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const getUsersThrottled = useCallback( + throttle(() => new UserAPIClient(services.http!).getUsers(), THROTTLE_USERS_WAIT), + [services.http] + ); + + const [form, eventHandlers] = useForm({ + onSubmit: async (values) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { password, confirm_password, ...rest } = values; + const user = isNewUser ? { password, ...rest } : rest; + try { + await new UserAPIClient(services.http!).saveUser(user as User); + services.notifications!.toasts.addSuccess( + isNewUser + ? i18n.translate('xpack.security.management.users.userForm.createSuccessMessage', { + defaultMessage: "Created user '{username}'", + values: { username: user.username }, + }) + : i18n.translate('xpack.security.management.users.userForm.updateSuccessMessage', { + defaultMessage: "Updated user '{username}'", + values: { username: user.username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: isNewUser + ? i18n.translate('xpack.security.management.users.userForm.createErrorMessage', { + defaultMessage: "Could not create user '{username}'", + values: { username: user.username }, + }) + : i18n.translate('xpack.security.management.users.userForm.updateErrorMessage', { + defaultMessage: "Could not update user '{username}'", + values: { username: user.username }, + }), + text: (error as any).body?.message || error.message, + }); + throw error; + } + }, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (isNewUser) { + if (!values.username) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameRequiredError', + { + defaultMessage: 'Enter a username.', + } + ); + } else if (values.username.length > MAX_NAME_LENGTH) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameMaxLengthError', + { + defaultMessage: 'Username must not exceed {maxLength} characters.', + values: { maxLength: MAX_NAME_LENGTH }, + } + ); + } else if (values.username.trim() !== values.username) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameWhitespaceError', + { + defaultMessage: `Username must not contain leading or trailing spaces.`, + } + ); + } else if (!values.username.match(NAME_REGEX)) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameInvalidError', + { + defaultMessage: + 'Username must contain only letters, numbers, spaces, punctuation, and symbols.', + } + ); + } else { + try { + const users = await getUsersThrottled(); + if (users.some((user) => user.username === values.username)) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameTakenError', + { + defaultMessage: "User '{username}' already exists.", + values: { username: values.username }, + } + ); + } + } catch (error) {} // eslint-disable-line no-empty + } + + if (!values.password) { + errors.password = i18n.translate( + 'xpack.security.management.users.userForm.passwordRequiredError', + { + defaultMessage: 'Enter a password.', + } + ); + } else if (values.password.length < 6) { + errors.password = i18n.translate( + 'xpack.security.management.users.userForm.passwordInvalidError', + { + defaultMessage: 'Password must be at least 6 characters.', + } + ); + } else if (!values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.userForm.confirmPasswordRequiredError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } else if (values.password !== values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.userForm.confirmPasswordInvalidError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } + } + + return errors; + }, + defaultValues, + }); + + useEffect(() => { + form.reset(defaultValues); + }, [defaultValues]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + getRoles(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const availableRoles = rolesState.value ?? []; + const selectedRoleNames = form.values.roles ?? []; + const deprecatedRoles = selectedRoleNames.reduce((roles, name) => { + const role = availableRoles.find((r) => r.name === name); + if (role && isRoleDeprecated(role)) { + roles.push(role); + } + return roles; + }, []); + + return ( + + + + + } + description={i18n.translate('xpack.security.management.users.userForm.profileDescription', { + defaultMessage: 'Provide personal details.', + })} + > + + + + + {!isReservedUser ? ( + <> + + + + + + + + ) : undefined} + + + {isNewUser ? ( + + + + } + description={i18n.translate( + 'xpack.security.management.users.userForm.passwordDescription', + { + defaultMessage: 'Protect your data with a strong password.', + } + )} + > + + + + + + + + ) : null} + + + + + } + description={i18n.translate( + 'xpack.security.management.users.userForm.privilegesDescription', + { + defaultMessage: 'Assign roles to manage access and permissions.', + } + )} + > + 0 ? ( + + {deprecatedRoles.map((role) => ( +

+ +

+ ))} +
+ ) : ( + + + + ) + } + > + form.setValue('roles', value)} + isLoading={rolesState.loading} + isDisabled={isReservedUser} + /> +
+ + + {isReservedUser ? ( + + + + + + + + ) : ( + + + + {isNewUser ? ( + + ) : ( + + )} + + + + + + + + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts deleted file mode 100644 index 6050e1868a759..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UserValidator, UserValidationResult } from './validate_user'; -import { User, EditUser } from '../../../../common/model'; - -function expectValid(result: UserValidationResult) { - expect(result.isInvalid).toBe(false); -} - -function expectInvalid(result: UserValidationResult) { - expect(result.isInvalid).toBe(true); -} - -describe('UserValidator', () => { - describe('#validateUsername', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateUsername({} as User)); - }); - - it(`returns 'invalid' if username is missing`, () => { - expectInvalid(new UserValidator({ shouldValidate: true }).validateUsername({} as User)); - }); - - it(`returns 'invalid' if username contains invalid characters`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateUsername({ - username: '!@#$%^&*()', - } as User) - ); - }); - - it(`returns 'valid' for correct usernames`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateUsername({ - username: 'my_user', - } as User) - ); - }); - }); - - describe('#validateEmail', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateEmail({} as EditUser)); - }); - - it(`returns 'valid' if email is missing`, () => { - expectValid(new UserValidator({ shouldValidate: true }).validateEmail({} as EditUser)); - }); - - it(`returns 'invalid' for invalid emails`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateEmail({ - email: 'asf', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct emails`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateEmail({ - email: 'foo@bar.co', - } as EditUser) - ); - }); - }); - - describe('#validatePassword', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validatePassword({} as EditUser)); - }); - - it(`returns 'invalid' if password is missing`, () => { - expectInvalid(new UserValidator({ shouldValidate: true }).validatePassword({} as EditUser)); - }); - - it(`returns 'invalid' for invalid password`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validatePassword({ - password: 'short', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct passwords`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validatePassword({ - password: 'changeme', - } as EditUser) - ); - }); - }); - - describe('#validateConfirmPassword', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateConfirmPassword({} as EditUser)); - }); - - it(`returns 'invalid' if confirm password is missing`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - } as EditUser) - ); - }); - - it(`returns 'invalid' for mismatched passwords`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - confirmPassword: 'changeyou', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct passwords`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - confirmPassword: 'changeme', - } as EditUser) - ); - }); - }); -}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts deleted file mode 100644 index 5edd96c68bf0d..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts +++ /dev/null @@ -1,142 +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 { User, EditUser } from '../../../../common/model'; - -interface UserValidatorOptions { - shouldValidate?: boolean; -} - -export interface UserValidationResult { - isInvalid: boolean; - error?: string; -} - -const validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -const validUsernameRegex = /[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*/; - -export class UserValidator { - private shouldValidate?: boolean; - - constructor(options: UserValidatorOptions = {}) { - this.shouldValidate = options.shouldValidate; - } - - public enableValidation() { - this.shouldValidate = true; - } - - public disableValidation() { - this.shouldValidate = false; - } - - public validateUsername(user: User): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { username } = user; - if (!username) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.requiredUsernameErrorMessage', { - defaultMessage: 'Username is required', - }) - ); - } else if (username && !username.match(validUsernameRegex)) { - return invalid( - i18n.translate( - 'xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage', - { - defaultMessage: - 'Username must begin with a letter or underscore and contain only letters, underscores, and numbers', - } - ) - ); - } - - return valid(); - } - - public validateEmail(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { email } = user; - if (email && !email.match(validEmailRegex)) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.validEmailRequiredErrorMessage', { - defaultMessage: 'Email address is invalid', - }) - ); - } - return valid(); - } - - public validatePassword(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { password } = user; - if (!password || password.length < 6) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.passwordLengthErrorMessage', { - defaultMessage: 'Password must be at least 6 characters', - }) - ); - } - return valid(); - } - - public validateConfirmPassword(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { password, confirmPassword } = user; - if (password && confirmPassword !== null && password !== confirmPassword) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage', { - defaultMessage: 'Passwords do not match', - }) - ); - } - return valid(); - } - - public validateForSave(user: EditUser, isNewUser: boolean): UserValidationResult { - const { isInvalid: isUsernameInvalid } = this.validateUsername(user); - const { isInvalid: isEmailInvalid } = this.validateEmail(user); - let isPasswordInvalid = false; - let isConfirmPasswordInvalid = false; - - if (isNewUser) { - isPasswordInvalid = this.validatePassword(user).isInvalid; - isConfirmPasswordInvalid = this.validateConfirmPassword(user).isInvalid; - } - - if (isUsernameInvalid || isEmailInvalid || isPasswordInvalid || isConfirmPasswordInvalid) { - return invalid(); - } - - return valid(); - } -} - -function invalid(error?: string): UserValidationResult { - return { - isInvalid: true, - error, - }; -} - -function valid(): UserValidationResult { - return { - isInvalid: false, - }; -} 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 index 7223f78d57fdc..54c7ae8f4ae3b 100644 --- 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 @@ -9,6 +9,8 @@ export const userAPIClientMock = { getUsers: jest.fn(), getUser: jest.fn(), deleteUser: jest.fn(), + enableUser: jest.fn(), + disableUser: 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 index 61dd09d2c5e3d..b96596ba7c653 100644 --- a/x-pack/plugins/security/public/management/users/user_api_client.ts +++ b/x-pack/plugins/security/public/management/users/user_api_client.ts @@ -30,7 +30,7 @@ export class UserAPIClient { }); } - public async changePassword(username: string, password: string, currentPassword: string) { + public async changePassword(username: string, password: string, currentPassword?: string) { const data: Record = { newPassword: password, }; @@ -42,4 +42,12 @@ export class UserAPIClient { body: JSON.stringify(data), }); } + + public async disableUser(username: string) { + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/_disable`); + } + + public async enableUser(username: string) { + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/_enable`); + } } diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index 37747f9a1ccfa..3b1705d2bc46b 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -237,7 +237,7 @@ export class UsersGridPage extends Component { ({ - 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, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; import { securityMock } from '../../mocks'; +import { usersManagementApp } from './users_management_app'; -async function mountApp(basePath: string, pathname: string) { - const container = document.createElement('div'); - const setBreadcrumbs = jest.fn(); +const element = document.body.appendChild(document.createElement('div')); - const unmount = await usersManagementApp - .create({ - authc: securityMock.createSetup().authc, - getStartServices: coreMock.createSetup().getStartServices as any, - }) - .mount({ - basePath, - element: container, +describe('usersManagementApp', () => { + it('renders application and sets breadcrumbs', async () => { + const { getStartServices } = coreMock.createSetup(); + const coreStartMock = coreMock.createStart(); + getStartServices.mockResolvedValue([coreStartMock, {}, {}]); + const { authc } = securityMock.createSetup(); + + const setBreadcrumbs = jest.fn(); + const history = scopedHistoryMock.create({ pathname: '/create' }); + + const unmount = await usersManagementApp.create({ authc, getStartServices }).mount({ + basePath: '/', + element, setBreadcrumbs, - history: scopedHistoryMock.create({ pathname }), + history, }); - 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 { setBreadcrumbs, container, unmount } = await mountApp('/', '/'); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Users' }]); - expect(container).toMatchInlineSnapshot(` -
- Users Page: {"notifications":{"toasts":{}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}} -
- `); - - unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - it('mount() works for the `create user` page', async () => { - const { setBreadcrumbs, container, unmount } = await mountApp('/', '/edit'); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Users' }, { text: 'Create' }]); - expect(container).toMatchInlineSnapshot(` -
- User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} -
- `); - - unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - it('mount() works for the `edit user` page', async () => { - const userName = 'foo@bar.com'; - - const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${userName}`); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Users' }, - { href: `/edit/${encodeURIComponent(userName)}`, text: userName }, + expect(setBreadcrumbs).toHaveBeenLastCalledWith([ + { href: '/', text: 'Users' }, + { href: '/create', text: 'Create' }, ]); - expect(container).toMatchInlineSnapshot(` -
- User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"username":"foo@bar.com","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/foo@bar.com","search":"","hash":""}}} -
- `); unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - const usernames = ['foo@bar.com', 'foo&bar.com', 'some 安全性 user']; - usernames.forEach((username) => { - it( - 'mount() properly encodes user name in `edit user` page link in breadcrumbs for user ' + - username, - async () => { - const { setBreadcrumbs } = await mountApp('/', `/edit/${username}`); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Users' }, - { - href: `/edit/${encodeURIComponent(username)}`, - 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 index 2f16f85d5fcae..cbb303d1a128d 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -4,14 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Router, Route, Switch, useParams } from 'react-router-dom'; +import { Router, Route, Switch, Redirect, RouteComponentProps } from 'react-router-dom'; +import { History } from 'history'; import { i18n } from '@kbn/i18n'; -import { StartServicesAccessor } from 'src/core/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { StartServicesAccessor, CoreStart } from '../../../../../../src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { AuthenticationServiceSetup } from '../../authentication'; import { PluginStartDependencies } from '../../plugin'; +import { + BreadcrumbsProvider, + BreadcrumbsChangeHandler, + Breadcrumb, + getDocTitle, +} from '../../components/breadcrumb'; +import { AuthenticationProvider } from '../../components/use_current_user'; import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { @@ -19,6 +29,10 @@ interface CreateParams { getStartServices: StartServicesAccessor; } +interface EditUserParams { + username: string; +} + export const usersManagementApp = Object.freeze({ id: 'users', create({ authc, getStartServices }: CreateParams) { @@ -27,18 +41,10 @@ export const usersManagementApp = Object.freeze({ order: 10, title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), async mount({ element, setBreadcrumbs, history }) { - const [coreStart] = await getStartServices(); - const usersBreadcrumbs = [ - { - text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), - href: `/`, - }, - ]; - const [ - [{ http, notifications, i18n: i18nStart }], + [coreStart], { UsersGridPage }, - { EditUserPage }, + { CreateUserPage, EditUserPage }, { UserAPIClient }, { RolesAPIClient }, ] = await Promise.all([ @@ -49,64 +55,61 @@ export const usersManagementApp = Object.freeze({ import('../roles'), ]); - const userAPIClient = new UserAPIClient(http); - const rolesAPIClient = new RolesAPIClient(http); - const UsersGridPageWithBreadcrumbs = () => { - setBreadcrumbs(usersBreadcrumbs); - return ( - - ); - }; - - const EditUserPageWithBreadcrumbs = () => { - const { username } = useParams<{ username?: string }>(); - - // Additional decoding is a workaround for a bug in react-router's version of the `history` module. - // See https://github.com/elastic/kibana/issues/82440 - const decodedUsername = username ? tryDecodeURIComponent(username) : undefined; - - setBreadcrumbs([ - ...usersBreadcrumbs, - username - ? { text: decodedUsername, href: `/edit/${encodeURIComponent(username)}` } - : { - text: i18n.translate('xpack.security.users.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]); - - return ( - - ); - }; - render( - - + { + setBreadcrumbs(breadcrumbs); + coreStart.chrome.docTitle.change(getDocTitle(breadcrumbs)); + }} + > + - + - - + + + + + + ) => { + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const username = tryDecodeURIComponent(props.match.params.username); + return ( + + + + ); + }} + /> + + - - , + + , element ); @@ -117,3 +120,28 @@ export const usersManagementApp = Object.freeze({ } as RegisterManagementAppArgs; }, }); + +export interface ProvidersProps { + services: CoreStart; + history: History; + authc: AuthenticationServiceSetup; + onChange?: BreadcrumbsChangeHandler; +} + +export const Providers: FunctionComponent = ({ + services, + history, + authc, + onChange, + children, +}) => ( + + + + + {children} + + + + +); diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts index a98848a583500..bdb6e89719037 100644 --- a/x-pack/plugins/security/server/routes/users/create_or_update.ts +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -30,12 +30,7 @@ export function defineCreateOrUpdateUserRoutes({ router }: RouteDefinitionParams try { await context.core.elasticsearch.client.asCurrentUser.security.putUser({ username: request.params.username, - // Omit `username`, `enabled` and all fields with `null` value. - body: Object.fromEntries( - Object.entries(request.body).filter( - ([key, value]) => value !== null && key !== 'enabled' && key !== 'username' - ) - ), + body: request.body, }); return response.ok({ body: request.body }); diff --git a/x-pack/plugins/security/server/routes/users/disable.ts b/x-pack/plugins/security/server/routes/users/disable.ts new file mode 100644 index 0000000000000..45e1f63149e1a --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/disable.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineDisableUserRoutes({ router }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}/_disable', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await context.core.elasticsearch.client.asCurrentUser.security.disableUser({ + username: request.params.username, + }); + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/enable.ts b/x-pack/plugins/security/server/routes/users/enable.ts new file mode 100644 index 0000000000000..0f4e15c953a42 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/enable.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineEnableUserRoutes({ router }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}/_enable', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await context.core.elasticsearch.client.asCurrentUser.security.enableUser({ + username: request.params.username, + }); + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/index.ts b/x-pack/plugins/security/server/routes/users/index.ts index 931af0734b416..473b3459ad4e1 100644 --- a/x-pack/plugins/security/server/routes/users/index.ts +++ b/x-pack/plugins/security/server/routes/users/index.ts @@ -9,6 +9,8 @@ import { defineGetUserRoutes } from './get'; import { defineGetAllUsersRoutes } from './get_all'; import { defineCreateOrUpdateUserRoutes } from './create_or_update'; import { defineDeleteUserRoutes } from './delete'; +import { defineDisableUserRoutes } from './disable'; +import { defineEnableUserRoutes } from './enable'; import { defineChangeUserPasswordRoutes } from './change_password'; export function defineUsersRoutes(params: RouteDefinitionParams) { @@ -16,5 +18,7 @@ export function defineUsersRoutes(params: RouteDefinitionParams) { defineGetAllUsersRoutes(params); defineCreateOrUpdateUserRoutes(params); defineDeleteUserRoutes(params); + defineDisableUserRoutes(params); + defineEnableUserRoutes(params); defineChangeUserPasswordRoutes(params); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1bbf4b8033755..ef2149c4931fa 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16633,7 +16633,6 @@ "xpack.savedObjectsTagging.uiApi.table.columnTagsName": "タグ", "xpack.savedObjectsTagging.validation.color.errorInvalid": "タグ色は有効な 16 進数値色でなければなりません", "xpack.savedObjectsTagging.validation.description.errorTooLong": "タグ説明は {length} 文字以下で入力してください", - "xpack.savedObjectsTagging.validation.name.errorInvalidCharacters": "タグ名には、a-z、0-9、-、: のみを使用できます。", "xpack.savedObjectsTagging.validation.name.errorTooLong": "タグ名は {length} 文字以下で入力してください", "xpack.savedObjectsTagging.validation.name.errorTooShort": "タグ名は {length} 文字以上で入力してください", "xpack.searchProfiler.advanceTimeDescription": "イテレーターを次のドキュメントに進めるためにかかった時間。", @@ -16894,7 +16893,7 @@ "xpack.security.management.editRole.updateRoleText": "ロールを更新", "xpack.security.management.editRole.validateRole.indicesTypeErrorMessage": "{elasticIndices} は数列でなければなりません", "xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage": "名前は文字またはアンダーラインで始まり、文字、アンダーライン、数字のみ使用できます。", - "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名前は 1024 文字以内でなければなりません", + "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名前は {maxLength} 文字以内でなければなりません", "xpack.security.management.editRole.validateRole.onePrivilegeRequiredWarningMessage": "権限が最低 1 つ必要です", "xpack.security.management.editRole.validateRole.oneSpaceRequiredWarningMessage": "スペースが最低 1 つ必要です", "xpack.security.management.editRole.validateRole.privilegeRequiredWarningMessage": "権限が必要です", @@ -17073,37 +17072,6 @@ "xpack.security.management.users.createNewUserButtonLabel": "ユーザーを作成", "xpack.security.management.users.deleteUsersButtonLabel": "{numSelected} 人のユーザー{numSelected, plural, one { } other {s}} 削除", "xpack.security.management.users.deniedPermissionTitle": "ユーザーを管理するにはパーミッションが必要です", - "xpack.security.management.users.editUser.addRolesPlaceholder": "ロールを追加", - "xpack.security.management.users.editUser.cancelButtonLabel": "キャンセル", - "xpack.security.management.users.editUser.changePasswordButtonLabel": "パスワードを変更", - "xpack.security.management.users.editUser.changePasswordExtraStepTitle": "追加ステップが必要です", - "xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle": "{username}ユーザーのパスワードを変更後、{kibana}ファイルを更新し、Kibanaを再起動する必要があります。", - "xpack.security.management.users.editUser.changingUserNameAfterCreationDescription": "ユーザー名は作成後変更できません。", - "xpack.security.management.users.editUser.confirmPasswordFormRowLabel": "パスワードの確認", - "xpack.security.management.users.editUser.createUserButtonLabel": "ユーザーを作成", - "xpack.security.management.users.editUser.deleteUserButtonLabel": "ユーザーを削除", - "xpack.security.management.users.editUser.deprecatedRolesAssignedWarning": "このユーザーには非推奨ロールが割り当てられています。サポートされているロールに移行してください。", - "xpack.security.management.users.editUser.deprecatedRoleText": "(非推奨)", - "xpack.security.management.users.editUser.editUserTitle": "ユーザー {userName} の編集", - "xpack.security.management.users.editUser.emailAddressFormRowLabel": "メールアドレス", - "xpack.security.management.users.editUser.errorLoadingRolesTitle": "ロールの読み込み中にエラーが発生", - "xpack.security.management.users.editUser.errorLoadingUserTitle": "ユーザーの読み込み中にエラーが発生", - "xpack.security.management.users.editUser.fullNameFormRowLabel": "フルネーム", - "xpack.security.management.users.editUser.modifyingReservedUsersDescription": "リザーブされたユーザーはビルトインのため削除または変更できません。パスワードのみ変更できます。", - "xpack.security.management.users.editUser.newUserTitle": "新規ユーザー", - "xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage": "パスワードが一致しません", - "xpack.security.management.users.editUser.passwordFormRowLabel": "パスワード", - "xpack.security.management.users.editUser.passwordLengthErrorMessage": "パスワードは最低 6 文字必要です", - "xpack.security.management.users.editUser.requiredUsernameErrorMessage": "ユーザー名が必要です", - "xpack.security.management.users.editUser.returnToUserListButtonLabel": "ユーザーリストに戻る", - "xpack.security.management.users.editUser.rolesFormRowLabel": "ロール", - "xpack.security.management.users.editUser.savingUserErrorMessage": "ユーザーの保存中にエラーが発生しました: {message}", - "xpack.security.management.users.editUser.settingPasswordErrorMessage": "パスワードの設定中にエラーが発生しました: {message}", - "xpack.security.management.users.editUser.updateUserButtonLabel": "ユーザーを更新", - "xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage": "ユーザー名は文字またはアンダーラインで始まり、文字、アンダーライン、数字のみ使用できます", - "xpack.security.management.users.editUser.usernameFormRowLabel": "ユーザー名", - "xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage": "保存されたユーザー {message}", - "xpack.security.management.users.editUser.validEmailRequiredErrorMessage": "メールアドレスが無効です", "xpack.security.management.users.emailAddressColumnName": "メールアドレス", "xpack.security.management.users.extendedUserDeprecationNotice": "{username}ユーザーは推奨されません。{reason}", "xpack.security.management.users.fetchingUsersErrorMessage": "ユーザーの取得中にエラーが発生: {message}", @@ -17140,7 +17108,6 @@ "xpack.security.roles.breadcrumb": "ロール", "xpack.security.roles.createBreadcrumb": "作成", "xpack.security.users.breadcrumb": "ユーザー", - "xpack.security.users.createBreadcrumb": "作成", "xpack.securitySolution.accessibility.tooltipWithKeyboardShortcut.pressTooltipLabel": "プレス", "xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction": "値でフィルター", "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "値を除外", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 51205a3420be5..08d064ce8a05c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16675,7 +16675,6 @@ "xpack.savedObjectsTagging.uiApi.table.columnTagsName": "标签", "xpack.savedObjectsTagging.validation.color.errorInvalid": "标签颜色必须为有效的十六进制颜色", "xpack.savedObjectsTagging.validation.description.errorTooLong": "标签描述不能超过 {length} 个字符。", - "xpack.savedObjectsTagging.validation.name.errorInvalidCharacters": "标签名称只能包含 a-z、0-9、_、-、:。", "xpack.savedObjectsTagging.validation.name.errorTooLong": "标签名称不能超过 {length} 个字符", "xpack.savedObjectsTagging.validation.name.errorTooShort": "标签名称必须至少有 {length} 个字符", "xpack.searchProfiler.advanceTimeDescription": "将迭代器推进至下一文档所用时间。", @@ -16938,7 +16937,7 @@ "xpack.security.management.editRole.updateRoleText": "更新角色", "xpack.security.management.editRole.validateRole.indicesTypeErrorMessage": "{elasticIndices} 应为数组", "xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage": "名称必须以字母或下划线开头,且只能包含字母、下划线和数字。", - "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名称不能超过 1024 个字符", + "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名称不能超过 {maxLength} 个字符", "xpack.security.management.editRole.validateRole.onePrivilegeRequiredWarningMessage": "至少需要一个权限", "xpack.security.management.editRole.validateRole.oneSpaceRequiredWarningMessage": "至少需要一个工作区", "xpack.security.management.editRole.validateRole.privilegeRequiredWarningMessage": "“权限”必填", @@ -17117,37 +17116,6 @@ "xpack.security.management.users.createNewUserButtonLabel": "创建用户", "xpack.security.management.users.deleteUsersButtonLabel": "删除 {numSelected} 个用户{numSelected, plural, one { } other { 个用户}}", "xpack.security.management.users.deniedPermissionTitle": "您需要用于管理用户的权限", - "xpack.security.management.users.editUser.addRolesPlaceholder": "添加角色", - "xpack.security.management.users.editUser.cancelButtonLabel": "取消", - "xpack.security.management.users.editUser.changePasswordButtonLabel": "更改密码", - "xpack.security.management.users.editUser.changePasswordExtraStepTitle": "需要额外的步骤", - "xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle": "更改 {username} 用户的密码后,必须更新 {kibana} 文件并重新启动 Kibana。", - "xpack.security.management.users.editUser.changingUserNameAfterCreationDescription": "用户名一经创建,将无法更改。", - "xpack.security.management.users.editUser.confirmPasswordFormRowLabel": "确认密码", - "xpack.security.management.users.editUser.createUserButtonLabel": "创建用户", - "xpack.security.management.users.editUser.deleteUserButtonLabel": "删除用户", - "xpack.security.management.users.editUser.deprecatedRolesAssignedWarning": "为此用户分配了过时的角色。请迁移到支持的角色。", - "xpack.security.management.users.editUser.deprecatedRoleText": "(已过时)", - "xpack.security.management.users.editUser.editUserTitle": "编辑 {userName} 用户", - "xpack.security.management.users.editUser.emailAddressFormRowLabel": "电子邮件地址", - "xpack.security.management.users.editUser.errorLoadingRolesTitle": "加载角色时出错", - "xpack.security.management.users.editUser.errorLoadingUserTitle": "加载用户时出错", - "xpack.security.management.users.editUser.fullNameFormRowLabel": "全名", - "xpack.security.management.users.editUser.modifyingReservedUsersDescription": "保留用户是内置用户,无法删除或修改。只能更改密码。", - "xpack.security.management.users.editUser.newUserTitle": "新建用户", - "xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage": "密码不匹配", - "xpack.security.management.users.editUser.passwordFormRowLabel": "密码", - "xpack.security.management.users.editUser.passwordLengthErrorMessage": "密码长度必须至少为 6 个字符", - "xpack.security.management.users.editUser.requiredUsernameErrorMessage": "“用户名”必填", - "xpack.security.management.users.editUser.returnToUserListButtonLabel": "返回到用户列表", - "xpack.security.management.users.editUser.rolesFormRowLabel": "角色", - "xpack.security.management.users.editUser.savingUserErrorMessage": "保存用户时出错:{message}", - "xpack.security.management.users.editUser.settingPasswordErrorMessage": "设置密码时出错:{message}", - "xpack.security.management.users.editUser.updateUserButtonLabel": "更新用户", - "xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage": "用户名必须以字母或下划线开头,并只能包含字母、下划线和数字", - "xpack.security.management.users.editUser.usernameFormRowLabel": "用户名", - "xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage": "已保存用户{message}", - "xpack.security.management.users.editUser.validEmailRequiredErrorMessage": "电子邮件地址无效", "xpack.security.management.users.emailAddressColumnName": "电子邮件地址", "xpack.security.management.users.extendedUserDeprecationNotice": "用户 {username} 已过时。{reason}", "xpack.security.management.users.fetchingUsersErrorMessage": "提取用户时出错:{message}", @@ -17184,7 +17152,6 @@ "xpack.security.roles.breadcrumb": "角色", "xpack.security.roles.createBreadcrumb": "创建", "xpack.security.users.breadcrumb": "用户", - "xpack.security.users.createBreadcrumb": "创建", "xpack.securitySolution.accessibility.tooltipWithKeyboardShortcut.pressTooltipLabel": "按", "xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction": "筛留值", "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "筛除值", diff --git a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx index a8e4c90f2d29a..886496a7f6e2f 100644 --- a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx +++ b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx @@ -5,8 +5,7 @@ */ import React, { FC } from 'react'; -import { EuiLink } from '@elastic/eui'; -import { Link } from 'react-router-dom'; +import { ReactRouterEuiButton } from './react_router_helpers'; interface StepDetailLinkProps { /** @@ -23,10 +22,14 @@ export const StepDetailLink: FC = ({ children, checkGroupId const to = `/journey/${checkGroupId}/step/${stepIndex}`; return ( - - - {children} - - + + {children} + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx index 01a599f8e8a60..934427643757d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx @@ -13,6 +13,7 @@ import { StepScreenshotDisplay } from './step_screenshot_display'; import { StatusBadge } from './status_badge'; import { Ping } from '../../../../common/runtime_types'; import { StepDetailLink } from '../../common/step_detail_link'; +import { VIEW_PERFORMANCE } from './translations'; const CODE_BLOCK_OVERFLOW_HEIGHT = 360; @@ -26,24 +27,9 @@ export const ExecutedStep: FC = ({ step, index, checkGroup }) return ( <>
-
- {step.synthetics?.step?.index && checkGroup ? ( - - - - - - - - ) : ( - + + + = ({ step, index, checkGroup }) /> - )} -
- -
- -
+ +
+ +
+ +
@@ -73,6 +59,14 @@ export const ExecutedStep: FC = ({ step, index, checkGroup }) /> + {step.synthetics?.step?.index && ( + + + {VIEW_PERFORMANCE} + + + + )} { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ - blocked: '#b9a888', + blocked: '#dcd4c4', connect: '#da8b45', dns: '#54b399', font: '#aa6556', @@ -173,10 +173,10 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 0.854ms", }, }, @@ -264,10 +264,10 @@ describe('getSeriesAndDomain', () => { }, Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 84.546ms", }, }, @@ -330,10 +330,10 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 0.854ms", }, }, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 5e59026fd65f8..3cc0497bda8ec 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -197,7 +197,7 @@ const buildTimingPalette = (): TimingColourPalette => { const palette = Object.values(Timings).reduce>((acc, value) => { switch (value) { case Timings.Blocked: - acc[value] = SAFE_PALETTE[6]; + acc[value] = SAFE_PALETTE[16]; break; case Timings.Dns: acc[value] = SAFE_PALETTE[0]; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/translations.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/translations.ts new file mode 100644 index 0000000000000..85a9db1527d57 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const VIEW_PERFORMANCE = i18n.translate( + 'xpack.uptime.pingList.synthetics.performanceBreakDown', + { + defaultMessage: 'View performance breakdown', + } +); diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index a7cacd0ad1cbb..71673d49c0c08 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -7,15 +7,12 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'home', 'settings', 'lens']); + const PageObjects = getPageObjects(['common', 'visualize', 'timePicker', 'home', 'lens']); const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); - // FLAKY: https://github.com/elastic/kibana/issues/88926 - // FLAKY: https://github.com/elastic/kibana/issues/88927 - // FLAKY: https://github.com/elastic/kibana/issues/88929 - describe.skip('Lens', () => { + describe('Lens', () => { const lensChartName = 'MyLensChart'; before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { @@ -35,12 +32,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('lens', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await a11y.testAppSnapshot(); }); it('lens XY chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', @@ -75,6 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('dimension configuration panel', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await PageObjects.lens.openDimensionEditor('lnsXY_xDimensionPanel > lns-empty-dimension'); await a11y.testAppSnapshot(); diff --git a/x-pack/test/accessibility/apps/users.ts b/x-pack/test/accessibility/apps/users.ts index efdcf4f3f022f..ede120ca43de7 100644 --- a/x-pack/test/accessibility/apps/users.ts +++ b/x-pack/test/accessibility/apps/users.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); + const find = getService('find'); const retry = getService('retry'); describe('Kibana users page a11y tests', () => { @@ -52,24 +53,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('a11y test for roles drop down', async () => { - await testSubjects.setValue('userFormUserNameInput', 'a11y'); - await testSubjects.setValue('passwordInput', 'password'); - await testSubjects.setValue('passwordConfirmationInput', 'password'); - await testSubjects.setValue('userFormFullNameInput', 'a11y user'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); + await PageObjects.security.clickElasticsearchUsers(); + await PageObjects.security.clickCreateNewUser(); + await PageObjects.security.fillUserForm({ + username: 'a11y', + password: 'password', + confirm_password: 'password', + full_name: 'a11y user', + email: 'example@example.com', + roles: ['apm_user'], + }); await testSubjects.click('rolesDropdown'); await a11y.testAppSnapshot(); }); - it('a11y test for display of delete button on users page ', async () => { - await testSubjects.setValue('userFormUserNameInput', 'deleteA11y'); - await testSubjects.setValue('passwordInput', 'password'); - await testSubjects.setValue('passwordConfirmationInput', 'password'); - await testSubjects.setValue('userFormFullNameInput', 'DeleteA11y user'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await testSubjects.click('rolesDropdown'); - await testSubjects.setValue('rolesDropdown', 'roleOption-apm_user'); - await testSubjects.click('userFormSaveButton'); + it('a11y test for display of delete button on users page', async () => { + await PageObjects.security.createUser({ + username: 'deleteA11y', + password: 'password', + confirm_password: 'password', + full_name: 'DeleteA11y user', + email: 'example@example.com', + roles: ['apm_user'], + }); await testSubjects.click('checkboxSelectRow-deleteA11y'); await a11y.testAppSnapshot(); }); @@ -77,17 +83,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('a11y test for delete user panel ', async () => { await testSubjects.click('deleteUserButton'); await a11y.testAppSnapshot(); + await testSubjects.click('confirmModalCancelButton'); }); it('a11y test for edit user panel', async () => { - await testSubjects.click('confirmModalCancelButton'); await PageObjects.settings.clickLinkText('deleteA11y'); await a11y.testAppSnapshot(); }); - it('a11y test for Change password screen', async () => { + it('a11y test for change password screen', async () => { + await PageObjects.settings.clickLinkText('deleteA11y'); + await find.clickByButtonText('Change password'); + await a11y.testAppSnapshot(); + await testSubjects.click('formFlyoutCancelButton'); + }); + + it('a11y test for deactivate user screen', async () => { await PageObjects.settings.clickLinkText('deleteA11y'); - await testSubjects.click('changePassword'); + await find.clickByButtonText('Deactivate user'); await a11y.testAppSnapshot(); }); }); diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index 2d112215f4fc1..9084e635f8109 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -19,6 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./change_password')); loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./users')); loadTestFile(require.resolve('./privileges')); }); } diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index 191523e969717..6872f423fe630 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -19,6 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./change_password')); loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./users')); loadTestFile(require.resolve('./privileges_basic')); }); } diff --git a/x-pack/test/api_integration/apis/security/users.ts b/x-pack/test/api_integration/apis/security/users.ts new file mode 100644 index 0000000000000..e177cf998beee --- /dev/null +++ b/x-pack/test/api_integration/apis/security/users.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const security = getService('security'); + const es = getService('es'); + + const mockUserName = 'test-user'; + const mockUserPassword = 'test-password'; + + describe('Users', () => { + beforeEach(async () => { + await security.user.create(mockUserName, { password: mockUserPassword, roles: [] }); + }); + + afterEach(async () => { + await security.user.delete(mockUserName); + }); + + it('should disable user', async () => { + await supertest + .post(`/internal/security/users/${mockUserName}/_disable`) + .set('kbn-xsrf', 'xxx') + .expect(204); + + const { body } = await es.security.getUser({ username: mockUserName }); + expect(body[mockUserName].enabled).to.be(false); + }); + + it('should enable user', async () => { + await supertest + .post(`/internal/security/users/${mockUserName}/_enable`) + .set('kbn-xsrf', 'xxx') + .expect(204); + + const { body } = await es.security.getUser({ username: mockUserName }); + expect(body[mockUserName].enabled).to.be(true); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 00183113a4d59..b2ddf7d47b1f1 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -68,46 +68,36 @@ export default function ({ getService, getPageObjects }) { before('Create dashboard only mode user', async () => { await PageObjects.settings.navigateTo(); - await PageObjects.security.clickUsersSection(); - await PageObjects.security.clickCreateNewUser(); - await testSubjects.setValue('userFormUserNameInput', 'dashuser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'dashuser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('logstash-data'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'dashuser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'dashuser', + roles: ['kibana_dashboard_only_user', 'logstash-data'], + }); }); before('Create user with mixes roles', async () => { - await PageObjects.security.clickCreateNewUser(); - - await testSubjects.setValue('userFormUserNameInput', 'mixeduser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('kibana_admin'); - await PageObjects.security.assignRoleToUser('logstash-data'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'mixeduser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'mixeduser', + roles: ['kibana_dashboard_only_user', 'kibana_admin', 'logstash-data'], + }); }); before('Create user with dashboard and superuser role', async () => { - await PageObjects.security.clickCreateNewUser(); - - await testSubjects.setValue('userFormUserNameInput', 'mysuperuser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('superuser'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'mysuperuser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'mixeduser', + roles: ['kibana_dashboard_only_user', 'superuser'], + }); }); after(async () => { diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index e0130bc394271..57e5990a74012 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -11,73 +11,108 @@ export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); describe('lens drag and drop tests', () => { - it('should construct the basic split xy chart', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickVisType('lens'); - await PageObjects.lens.goToTimeRange(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + describe('basic drag and drop', () => { + it('should construct the basic split xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('@timestamp'); - expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( - '@timestamp' - ); - }); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + '@timestamp' + ); + }); - it('should allow dropping fields to existing and empty dimension triggers', async () => { - await PageObjects.lens.switchToVisualization('lnsDatatable'); + it('should allow dropping fields to existing and empty dimension triggers', async () => { + await PageObjects.lens.switchToVisualization('lnsDatatable'); - await PageObjects.lens.dragFieldToDimensionTrigger( - 'clientip', - 'lnsDatatable_column > lns-dimensionTrigger' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql( - 'Top values of clientip' - ); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'clientip', + 'lnsDatatable_column > lns-dimensionTrigger' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql( + 'Top values of clientip' + ); - await PageObjects.lens.dragFieldToDimensionTrigger( - 'bytes', - 'lnsDatatable_column > lns-empty-dimension' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql( - 'bytes' - ); - await PageObjects.lens.dragFieldToDimensionTrigger( - '@message.raw', - 'lnsDatatable_column > lns-empty-dimension' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql( - 'Top values of @message.raw' - ); - }); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'bytes', + 'lnsDatatable_column > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql( + 'bytes' + ); + await PageObjects.lens.dragFieldToDimensionTrigger( + '@message.raw', + 'lnsDatatable_column > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql( + 'Top values of @message.raw' + ); + }); - it('should reorder the elements for the table', async () => { - await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0); - await PageObjects.header.waitUntilLoadingHasFinished(); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ - 'Top values of @message.raw', - 'Top values of clientip', - 'bytes', - ]); - }); + it('should reorder the elements for the table', async () => { + await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ + 'Top values of @message.raw', + 'Top values of clientip', + 'bytes', + ]); + }); - it('should move the column to compatible dimension group', async () => { - await PageObjects.lens.switchToVisualization('bar'); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ - 'Top values of @message.raw', - ]); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')).to.eql([ - 'Top values of clientip', - ]); + it('should move the column to compatible dimension group', async () => { + await PageObjects.lens.switchToVisualization('bar'); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ + 'Top values of @message.raw', + ]); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of clientip']); + + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_xDimensionPanel > lns-dimensionTrigger', + 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' + ); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql( + [] + ); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of @message.raw']); + }); + }); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_xDimensionPanel > lns-dimensionTrigger', - 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' - ); + describe('workspace drop', () => { + it('should always nest time dimension in categorical dimension', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('clientip'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of clientip']); + await PageObjects.lens.openDimensionEditor( + 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' + ); + expect(await PageObjects.lens.isTopLevelAggregation()).to.be(true); + await PageObjects.lens.closeDimensionEditor(); + }); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([]); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')).to.eql([ - 'Top values of @message.raw', - ]); + it('overwrite existing time dimension if one exists already', async () => { + await PageObjects.lens.dragFieldToWorkspace('utc_time'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('clientip'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ + 'utc_time', + ]); + }); }); }); } diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 0595322ad2d21..a76475fbbbd8c 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -51,14 +51,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user userEAST ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'userEast', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'dls EAST', + confirm_password: 'changeme', + full_name: 'dls EAST', email: 'dlstest@elastic.com', - save: true, roles: ['kibana_admin', 'myroleEast'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 3f3984dd05a94..a4e2680c394ee 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -71,14 +71,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user customer1 ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'customer1', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'customer one', + confirm_password: 'changeme', + full_name: 'customer one', email: 'flstest@elastic.com', - save: true, roles: ['kibana_admin', 'a_viewssnrole'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); @@ -87,14 +85,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user customer2 ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'customer2', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'customer two', + confirm_password: 'changeme', + full_name: 'customer two', email: 'flstest@elastic.com', - save: true, roles: ['kibana_admin', 'a_view_no_ssn_role'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index 58c72eaa3072e..de4515c501187 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -58,13 +58,12 @@ export default function ({ getService, getPageObjects }) { ], }, }); - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'kibanauser', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'kibanafirst kibanalast', + confirm_password: 'changeme', + full_name: 'kibanafirst kibanalast', email: 'kibanauser@myEmail.com', save: true, roles: ['rbac_all'], @@ -76,13 +75,12 @@ export default function ({ getService, getPageObjects }) { expect(users.kibanauser.roles).to.eql(['rbac_all']); expect(users.kibanauser.fullname).to.eql('kibanafirst kibanalast'); expect(users.kibanauser.reserved).to.be(false); - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'kibanareadonly', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'kibanareadonlyFirst kibanareadonlyLast', + confirm_password: 'changeme', + full_name: 'kibanareadonlyFirst kibanareadonlyLast', email: 'kibanareadonly@myEmail.com', save: true, roles: ['rbac_read'], diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 96f16aebd11b9..6f76367801536 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -9,7 +9,7 @@ import { parse } from 'url'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['common', 'roleMappings']); + const pageObjects = getPageObjects(['common', 'security', 'roleMappings']); const security = getService('security'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); @@ -32,8 +32,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('allows a role mapping to be created', async () => { await testSubjects.click('createRoleMappingButton'); await testSubjects.setValue('roleMappingFormNameInput', 'new_role_mapping'); - await testSubjects.setValue('rolesDropdown', 'superuser'); - await browser.pressKeys(browser.keys.ENTER); + await pageObjects.security.selectRole('superuser'); await testSubjects.click('roleMappingsAddRuleButton'); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index c547657bf880a..830d8384f1e3d 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -51,15 +51,13 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'Rashmi', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'RashmiFirst RashmiLast', + confirm_password: 'changeme', + full_name: 'RashmiFirst RashmiLast', email: 'rashmi@myEmail.com', - save: true, roles: ['logstash_reader', 'kibana_admin'], }); log.debug('After Add user: , userObj.userName'); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index a2a2b705172d7..c05220b6a59f3 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -19,13 +19,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'newuser', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'newuserFirst newuserLast', + confirm_password: 'changeme', + full_name: 'newuserFirst newuserLast', email: 'newuser@myEmail.com', - save: true, roles: ['kibana_admin', 'superuser'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 4fd4384a93c59..7f2b0cfd96ca2 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -41,13 +41,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'Lee', password: 'LeePwd', - confirmPassword: 'LeePwd', - fullname: 'LeeFirst LeeLast', + confirm_password: 'LeePwd', + full_name: 'LeeFirst LeeLast', email: 'lee@myEmail.com', - save: true, roles: ['kibana_admin'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); @@ -59,11 +58,10 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user with optional fields left empty', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'OptionalUser', password: 'OptionalUserPwd', - confirmPassword: 'OptionalUserPwd', - save: true, + confirm_password: 'OptionalUserPwd', roles: [], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/es_archives/lens/basic/data.json b/x-pack/test/functional/es_archives/lens/basic/data.json new file mode 100644 index 0000000000000..a985de882929d --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/basic/data.json @@ -0,0 +1,577 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [], + "space": { + "_reserved": true, + "description": "This is the default space!", + "disabledFeatures": [], + "name": "Default" + }, + "type": "space" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:logstash-*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:log*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "log*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + + +{ + "type": "doc", + "value": { + "id": "custom_space:index-pattern:logstash-*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "namespace": "custom_space", + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:i-exist", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.3.1" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "custom_space:visualization:i-exist", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.3.1" + }, + "namespace": "custom_space", + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "query:okjpgs", + "index": ".kibana_1", + "source": { + "query": { + "description": "Ok responses for jpg files", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": null, + "disabled": false, + "index": "b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b", + "key": "extension.raw", + "negate": false, + "params": { + "query": "jpg" + }, + "type": "phrase", + "value": "jpg" + }, + "query": { + "match": { + "extension.raw": { + "query": "jpg", + "type": "phrase" + } + } + } + } + ], + "query": { + "language": "kuery", + "query": "response:200" + }, + "title": "OKJpgs" + }, + "references": [], + "type": "query", + "updated_at": "2019-07-17T17:54:26.378Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "accessibility:disableAnimations": true, + "buildNum": 9007199254740991, + "dateFormat:tz": "UTC", + "defaultIndex": "logstash-*" + }, + "references": [], + "type": "config", + "updated_at": "2019-09-04T18:47:24.761Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:76fc4200-cf44-11e9-b933-fd84270f3ac1", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"language\\\":\\\"kuery\\\",\\\"query\\\":\\\"\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"c61a8afb-a185-4fae-a064-fb3846f6c451\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs={lens_auto_date aggConfigs=\"[{\\\"id\\\":\\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"max\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\"}}]\"} | lens_rename_columns idMap=\"{\\\"col-0-2cd09808-3915-49f4-b3b0-82767eba23f7\\\":{\\\"dataType\\\":\\\"number\\\",\\\"isBucketed\\\":false,\\\"label\\\":\\\"Maximum of bytes\\\",\\\"operationType\\\":\\\"max\\\",\\\"scale\\\":\\\"ratio\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"id\\\":\\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\\"}}\"}\n| lens_metric_chart title=\"Maximum of bytes\" accessor=\"2cd09808-3915-49f4-b3b0-82767eba23f7\" mode=\"full\"", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "c61a8afb-a185-4fae-a064-fb3846f6c451": { + "columnOrder": [ + "2cd09808-3915-49f4-b3b0-82767eba23f7" + ], + "columns": { + "2cd09808-3915-49f4-b3b0-82767eba23f7": { + "dataType": "number", + "isBucketed": false, + "label": "Maximum of bytes", + "operationType": "max", + "scale": "ratio", + "sourceField": "bytes" + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "accessor": "2cd09808-3915-49f4-b3b0-82767eba23f7", + "isHorizontal": false, + "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451", + "layers": [ + { + "accessors": [ + "d3e62a7a-c259-4fff-a2fc-eebf20b7008a", + "26ef70a9-c837-444c-886e-6bd905ee7335" + ], + "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451", + "seriesType": "area", + "splitAccessor": "54cd64ed-2a44-4591-af84-b2624504569a", + "xAccessor": "d6e40cea-6299-43b4-9c9d-b4ee305a2ce8" + } + ], + "legend": { + "isVisible": true, + "position": "right" + }, + "preferredSeriesType": "area" + } + }, + "title": "Artistpreviouslyknownaslens", + "visualizationType": "lnsMetric" + }, + "references": [], + "type": "lens", + "updated_at": "2019-10-16T00:28:08.979Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:9536bed0-d57e-11ea-b169-e3a222a76b9c", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"language\\\":\\\"kuery\\\",\\\"query\\\":\\\"\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs=\"[{\\\"id\\\":\\\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"geo.dest\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":7,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"geo.src\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":3,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"avg\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\",\\\"missing\\\":0}}]\" | lens_rename_columns idMap=\"{\\\"col-0-bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\":{\\\"dataType\\\":\\\"string\\\",\\\"isBucketed\\\":true,\\\"label\\\":\\\"Top values of geo.dest\\\",\\\"operationType\\\":\\\"terms\\\",\\\"params\\\":{\\\"orderBy\\\":{\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"type\\\":\\\"column\\\"},\\\"orderDirection\\\":\\\"desc\\\",\\\"size\\\":7},\\\"scale\\\":\\\"ordinal\\\",\\\"sourceField\\\":\\\"geo.dest\\\",\\\"id\\\":\\\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\"},\\\"col-2-c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\":{\\\"label\\\":\\\"Top values of geo.src\\\",\\\"dataType\\\":\\\"string\\\",\\\"operationType\\\":\\\"terms\\\",\\\"scale\\\":\\\"ordinal\\\",\\\"sourceField\\\":\\\"geo.src\\\",\\\"isBucketed\\\":true,\\\"params\\\":{\\\"size\\\":3,\\\"orderBy\\\":{\\\"type\\\":\\\"column\\\",\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"},\\\"orderDirection\\\":\\\"desc\\\"},\\\"id\\\":\\\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\"},\\\"col-3-3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":{\\\"dataType\\\":\\\"number\\\",\\\"isBucketed\\\":false,\\\"label\\\":\\\"Average of bytes\\\",\\\"operationType\\\":\\\"avg\\\",\\\"scale\\\":\\\"ratio\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"}}\"}\n| lens_pie shape=\"pie\" hideLabels=false groups=\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\"\n groups=\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\" metric=\"3dc0bd55-2087-4e60-aea2-f9910714f7db\" numberDisplay=\"percent\" categoryDisplay=\"default\" legendDisplay=\"default\" legendPosition=\"right\" percentDecimals=3 nestedLegend=false", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "4ba1a1be-6e67-434b-b3a0-f30db8ea5395": { + "columnOrder": [ + "bafe3009-1776-4227-a0fe-b0d6ccbb4961", + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8", + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "columns": { + "3dc0bd55-2087-4e60-aea2-f9910714f7db": { + "dataType": "number", + "isBucketed": false, + "label": "Average of bytes", + "operationType": "avg", + "scale": "ratio", + "sourceField": "bytes" + }, + "5bd1c078-e1dd-465b-8d25-7a6404befa88": { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": { + "interval": "auto" + }, + "scale": "interval", + "sourceField": "@timestamp" + }, + "65340cf3-8402-4494-96f2-293701c59571": { + "dataType": "number", + "isBucketed": true, + "label": "Top values of bytes", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "bytes" + }, + "87554e1d-3dbf-4c1c-a358-4c9d40424cfa": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of type", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "type" + }, + "bafe3009-1776-4227-a0fe-b0d6ccbb4961": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.dest", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 7 + }, + "scale": "ordinal", + "sourceField": "geo.dest" + }, + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.src", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "geo.src" + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "layers": [ + { + "categoryDisplay": "default", + "groups": [ + "bafe3009-1776-4227-a0fe-b0d6ccbb4961", + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8" + ], + "layerId": "4ba1a1be-6e67-434b-b3a0-f30db8ea5395", + "legendDisplay": "default", + "metric": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "nestedLegend": false, + "numberDisplay": "percent" + } + ], + "shape": "pie" + } + }, + "title": "lnsPieVis", + "visualizationType": "lnsPie" + }, + "references": [], + "type": "lens", + "updated_at": "2020-08-03T11:43:43.421Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:76fc4200-cf44-11e9-b933-fd84270f3ac2", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"query\\\":\\\"\\\",\\\"language\\\":\\\"kuery\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs={lens_auto_date aggConfigs=\"[{\\\"id\\\":\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"ip\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":3,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"3cf18f28-3495-4d45-a55f-d97f88022099\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"date_histogram\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"@timestamp\\\",\\\"useNormalizedEsInterval\\\":true,\\\"interval\\\":\\\"auto\\\",\\\"drop_partials\\\":false,\\\"min_doc_count\\\":0,\\\"extended_bounds\\\":{}}},{\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"avg\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\",\\\"missing\\\":0}}]\"} | lens_rename_columns idMap=\"{\\\"col-0-7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\":{\\\"label\\\":\\\"Top values of ip\\\",\\\"dataType\\\":\\\"ip\\\",\\\"operationType\\\":\\\"terms\\\",\\\"scale\\\":\\\"ordinal\\\",\\\"suggestedPriority\\\":0,\\\"sourceField\\\":\\\"ip\\\",\\\"isBucketed\\\":true,\\\"params\\\":{\\\"size\\\":3,\\\"orderBy\\\":{\\\"type\\\":\\\"column\\\",\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"},\\\"orderDirection\\\":\\\"desc\\\"},\\\"id\\\":\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\"},\\\"col-1-3cf18f28-3495-4d45-a55f-d97f88022099\\\":{\\\"label\\\":\\\"@timestamp\\\",\\\"dataType\\\":\\\"date\\\",\\\"operationType\\\":\\\"date_histogram\\\",\\\"suggestedPriority\\\":1,\\\"sourceField\\\":\\\"@timestamp\\\",\\\"isBucketed\\\":true,\\\"scale\\\":\\\"interval\\\",\\\"params\\\":{\\\"interval\\\":\\\"auto\\\"},\\\"id\\\":\\\"3cf18f28-3495-4d45-a55f-d97f88022099\\\"},\\\"col-2-3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":{\\\"label\\\":\\\"Average of bytes\\\",\\\"dataType\\\":\\\"number\\\",\\\"operationType\\\":\\\"avg\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"isBucketed\\\":false,\\\"scale\\\":\\\"ratio\\\",\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"}}\"}\n| lens_xy_chart xTitle=\"@timestamp\" yTitle=\"Average of bytes\" legend={lens_xy_legendConfig isVisible=true position=\"right\"} \n layers={lens_xy_layer layerId=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" hide=false xAccessor=\"3cf18f28-3495-4d45-a55f-d97f88022099\" yScaleType=\"linear\" xScaleType=\"time\" isHistogram=true splitAccessor=\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\" seriesType=\"bar_stacked\" accessors=\"3dc0bd55-2087-4e60-aea2-f9910714f7db\" columnToLabel=\"{\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":\\\"Average of bytes\\\",\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\":\\\"Top values of ip\\\"}\"}", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "4ba1a1be-6e67-434b-b3a0-f30db8ea5395": { + "columnOrder": [ + "7a5d833b-ca6f-4e48-a924-d2a28d365dc3", + "3cf18f28-3495-4d45-a55f-d97f88022099", + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "columns": { + "3cf18f28-3495-4d45-a55f-d97f88022099": { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": { + "interval": "auto" + }, + "scale": "interval", + "sourceField": "@timestamp", + "suggestedPriority": 1 + }, + "3dc0bd55-2087-4e60-aea2-f9910714f7db": { + "dataType": "number", + "isBucketed": false, + "label": "Average of bytes", + "operationType": "avg", + "scale": "ratio", + "sourceField": "bytes" + }, + "7a5d833b-ca6f-4e48-a924-d2a28d365dc3": { + "dataType": "ip", + "isBucketed": true, + "label": "Top values of ip", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "ip", + "suggestedPriority": 0 + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "layers": [ + { + "accessors": [ + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "layerId": "4ba1a1be-6e67-434b-b3a0-f30db8ea5395", + "seriesType": "bar_stacked", + "splitAccessor": "7a5d833b-ca6f-4e48-a924-d2a28d365dc3", + "xAccessor": "3cf18f28-3495-4d45-a55f-d97f88022099" + } + ], + "legend": { + "isVisible": true, + "position": "right" + }, + "preferredSeriesType": "bar_stacked" + } + }, + "title": "lnsXYvis", + "visualizationType": "lnsXY" + }, + "references": [], + "type": "lens", + "updated_at": "2019-10-16T00:28:08.979Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:DashboardPanelVersionInUrl:8.0.0", + "index": ".kibana_1", + "source": { + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2019-10-16T00:28:24.399Z" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/page_objects/graph_page.ts b/x-pack/test/functional/page_objects/graph_page.ts index 9ce1f87b5bf3d..8dda82e4e08bb 100644 --- a/x-pack/test/functional/page_objects/graph_page.ts +++ b/x-pack/test/functional/page_objects/graph_page.ts @@ -196,7 +196,7 @@ export function GraphPageProvider({ getService, getPageObjects }: FtrProviderCon await testSubjects.click('confirmSaveSavedObjectButton'); // Confirm that the Graph has been saved. - return await testSubjects.exists('saveGraphSuccess'); + return await testSubjects.exists('saveGraphSuccess', { timeout: 10000 }); } async getSearchFilter() { diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 04c660847bcee..31a4d6e29fc35 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -241,6 +241,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); }, + async isTopLevelAggregation() { + return await testSubjects.isEuiSwitchChecked('indexPattern-nesting-switch'); + }, /** * Removes the dimension matching a specific test subject */ diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index cad5e29528e9c..868d8115e7f0f 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -7,6 +7,7 @@ import { adminTestUser } from '@kbn/test'; import { FtrProviderContext } from '../ftr_provider_context'; import { AuthenticatedUser, Role } from '../../../plugins/security/common/model'; +import type { UserFormValues } from '../../../plugins/security/public/management/users/edit_user/user_form'; export function SecurityPageProvider({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); @@ -275,7 +276,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider } async clickCancelEditUser() { - await testSubjects.click('userFormCancelButton'); + await find.clickByButtonText('Cancel'); } async clickCancelEditRole() { @@ -283,7 +284,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider } async clickSaveEditUser() { - await testSubjects.click('userFormSaveButton'); + await find.clickByButtonText('Update user'); await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -380,53 +381,58 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider return roles; } + /** + * @deprecated Use `PageObjects.security.clickCreateNewUser` instead + */ async clickNewUser() { return await testSubjects.click('createUserButton'); } + /** + * @deprecated Use `PageObjects.security.clickCreateNewUser` instead + */ async clickNewRole() { return await testSubjects.click('createRoleButton'); } - async addUser(userObj: { - username: string; - password: string; - confirmPassword: string; - email: string; - fullname: string; - roles: string[]; - save?: boolean; - }) { - const self = this; - await this.clickNewUser(); - log.debug('username = ' + userObj.username); - await testSubjects.setValue('userFormUserNameInput', userObj.username); - await testSubjects.setValue('passwordInput', userObj.password); - await testSubjects.setValue('passwordConfirmationInput', userObj.confirmPassword); - if (userObj.fullname) { - await testSubjects.setValue('userFormFullNameInput', userObj.fullname); + async fillUserForm(user: UserFormValues) { + if (user.username) { + await find.setValue('[name=username]', user.username); + } + if (user.password) { + await find.setValue('[name=password]', user.password); + } + if (user.confirm_password) { + await find.setValue('[name=confirm_password]', user.confirm_password); } - if (userObj.email) { - await testSubjects.setValue('userFormEmailInput', userObj.email); + if (user.full_name) { + await find.setValue('[name=full_name]', user.full_name); + } + if (user.email) { + await find.setValue('[name=email]', user.email); } - log.debug('Add roles: ', userObj.roles); - const rolesToAdd = userObj.roles || []; + const rolesToAdd = user.roles || []; for (let i = 0; i < rolesToAdd.length; i++) { - await self.selectRole(rolesToAdd[i]); - } - log.debug('After Add role: , userObj.roleName'); - if (userObj.save === true) { - await testSubjects.click('userFormSaveButton'); - } else { - await testSubjects.click('userFormCancelButton'); + await this.selectRole(rolesToAdd[i]); } } + async submitCreateUserForm() { + await find.clickByButtonText('Create user'); + } + + async createUser(user: UserFormValues) { + await this.clickElasticsearchUsers(); + await this.clickCreateNewUser(); + await this.fillUserForm(user); + await this.submitCreateUserForm(); + } + async addRole(roleName: string, roleObj: Role) { const self = this; - await this.clickNewRole(); + await this.clickCreateNewRole(); // We have to use non-test-subject selectors because this markup is generated by ui-select. log.debug('roleObj.indices[0].names = ' + roleObj.elasticsearch.indices[0].names); @@ -498,37 +504,23 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider const dropdown = await testSubjects.find('rolesDropdown'); const input = await dropdown.findByCssSelector('input'); await input.type(role); - await testSubjects.click(`roleOption-${role}`); + await find.clickByCssSelector(`[role=option][title="${role}"]`); await testSubjects.click('comboBoxToggleListButton'); - await testSubjects.find(`roleOption-${role}`); } - deleteUser(username: string) { - let alertText: string; + async deleteUser(username: string) { log.debug('Delete user ' + username); - return find - .clickByDisplayedLinkText(username) - .then(() => { - return PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - }) - .then(() => { - log.debug('Find delete button and click'); - return testSubjects.click('userFormDeleteButton'); - }) - .then(() => { - return PageObjects.common.sleep(2000); - }) - .then(() => { - return testSubjects.getVisibleText('confirmModalBodyText'); - }) - .then((alert) => { - alertText = alert; - log.debug('Delete user alert text = ' + alertText); - return testSubjects.click('confirmModalConfirmButton'); - }) - .then(() => { - return alertText; - }); + await find.clickByDisplayedLinkText(username); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + + log.debug('Find delete button and click'); + await find.clickByButtonText('Delete user'); + await PageObjects.common.sleep(2000); + + const confirmText = await testSubjects.getVisibleText('confirmModalBodyText'); + log.debug('Delete user alert text = ' + confirmText); + await testSubjects.click('confirmModalConfirmButton'); + return confirmText; } } return new SecurityPage(); diff --git a/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json b/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json index 69220756639dc..8379290f5d9bb 100644 --- a/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json +++ b/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json @@ -88,6 +88,24 @@ } } +{ + "type": "doc", + "value": { + "id": "tag:tag-special-chars", + "index": ".kibana", + "source": { + "tag": { + "name": "my%tag", + "description": "Special chars", + "color": "#AA0077" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + { "type": "doc", "value": { @@ -356,3 +374,41 @@ } } } + +{ + "type": "doc", + "value": { + "id": "dashboard:ref-to-tag-special-chars", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 4 (tag-special-chars)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "tag-special-chars", + "name": "tag-special-ref", + "type": "tag" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + + diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts index f0c70ee8f718d..6f84440fc27e6 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - describe('GlobalSearchBar', function () { + describe('TOTO GlobalSearchBar', function () { const { common, navigationalSearch } = getPageObjects(['common', 'navigationalSearch']); const esArchiver = getService('esArchiver'); const browser = getService('browser'); @@ -61,6 +61,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); it('shows a suggestion when searching for a term matching a tag name', async () => { @@ -94,6 +95,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); @@ -111,6 +113,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); @@ -181,6 +184,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(results.map((result) => result.label)).to.eql(['My awesome vis (tag-4)']); }); + it('allows to filter by tags containing special characters', async () => { + await navigationalSearch.searchFor('tag:"my%tag"'); + + const results = await navigationalSearch.getDisplayedResults(); + + expect(results.map((result) => result.label)).to.eql(['dashboard 4 (tag-special-chars)']); + }); + it('returns no results when searching for an unknown tag', async () => { await navigationalSearch.searchFor('tag:unknown'); diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts index bd7fa7538703c..30008e635b628 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts @@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .post(`/api/saved_objects_tagging/tags/create`) .send({ - name: 'Inv%li& t@g n*me', + name: 'a', description: 'some desc', color: 'this is not a valid color', }) @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { valid: false, warnings: [], errors: { - name: 'Tag name can only include a-z, 0-9, _, -,:.', + name: 'Tag name must be at least 2 characters', color: 'Tag color must be a valid hex color', }, }, diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts index 7b4298607c666..ddf39ccf90b34 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts @@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .post(`/api/saved_objects_tagging/tags/tag-1`) .send({ - name: 'Inv%li& t@g n*me', + name: 'a', description: 'some desc', color: 'this is not a valid color', }) @@ -92,7 +92,7 @@ export default function ({ getService }: FtrProviderContext) { valid: false, warnings: [], errors: { - name: 'Tag name can only include a-z, 0-9, _, -,:.', + name: 'Tag name must be at least 2 characters', color: 'Tag color must be a valid hex color', }, }, diff --git a/x-pack/test/saved_object_tagging/functional/tests/create.ts b/x-pack/test/saved_object_tagging/functional/tests/create.ts index b62e9a70b43e8..2f2db856c0657 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/create.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/create.ts @@ -54,7 +54,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openCreate(); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'The name will fails validation', color: '#FF00CC', }, @@ -73,7 +73,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openCreate(); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'The name will fails validation', color: '#FF00CC', }, diff --git a/x-pack/test/saved_object_tagging/functional/tests/edit.ts b/x-pack/test/saved_object_tagging/functional/tests/edit.ts index 1883d3f23dc9d..1de101433179d 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/edit.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/edit.ts @@ -71,7 +71,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openEdit('tag-2'); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', }, { submit: true } ); @@ -88,7 +88,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openEdit('tag-2'); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'edited description', color: '#FF00CC', }, diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index e92ba226f3959..51f4bf8883521 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -151,7 +151,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('editing', () => { + // FLAKY: https://github.com/elastic/kibana/issues/88639 + describe.skip('editing', () => { beforeEach(async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.waitUntilTableIsLoaded(); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts index f06e8eba0bf68..e3797550984aa 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); - describe('Search search sessions Management UI', () => { + // FLAKY: https://github.com/elastic/kibana/issues/89069 + describe.skip('Search search sessions Management UI', () => { describe('New search sessions', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); diff --git a/yarn.lock b/yarn.lock index cc32349b10860..e7870415b0dda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5137,10 +5137,10 @@ dependencies: "@types/sizzle" "*" -"@types/js-cookie@2.2.5": - version "2.2.5" - resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" - integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg== +"@types/js-cookie@2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" + integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== "@types/js-search@^1.4.0": version "1.4.0" @@ -6374,10 +6374,10 @@ dependencies: tslib "^1.9.3" -"@xobotyi/scrollbar-width@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.4.tgz#a7dce20b7465bcad29cd6bbb557695e4ea7863cb" - integrity sha512-o12FCQt/X5n3pgKEWGpt0f/7Eg4mfv3uRwPUrctiOT8ZuxbH3cNLGWfH/8y6KxVJg4L2885ucuXQ6XECZzUiJA== +"@xobotyi/scrollbar-width@1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" + integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -13368,7 +13368,7 @@ fancy-log@^1.3.2: color-support "^1.1.3" time-stamp "^1.0.0" -fast-deep-equal@^3.1.1, fast-deep-equal@~3.1.3: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.3: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== @@ -23597,24 +23597,30 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-use@^13.27.0: - version "13.27.0" - resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.27.0.tgz#53a619dc9213e2cbe65d6262e8b0e76641ade4aa" - integrity sha512-2lyTyqJWyvnaP/woVtDcFS4B5pUYz0FQWI9pVHk/6TBWom2x3/ziJthkEn/LbCA9Twv39xSQU7Dn0zdIWfsNTQ== +react-universal-interface@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" + integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== + +react-use@^15.3.4: + version "15.3.4" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-15.3.4.tgz#f853d310bd71f75b38900a8caa3db93f6dc6e872" + integrity sha512-cHq1dELW6122oi1+xX7lwNyE/ugZs5L902BuO8eFJCfn2api1KeuPVG1M/GJouVARoUf54S2dYFMKo5nQXdTag== dependencies: - "@types/js-cookie" "2.2.5" - "@xobotyi/scrollbar-width" "1.9.4" + "@types/js-cookie" "2.2.6" + "@xobotyi/scrollbar-width" "1.9.5" copy-to-clipboard "^3.2.0" - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" fast-shallow-equal "^1.0.0" js-cookie "^2.2.1" nano-css "^5.2.1" + react-universal-interface "^0.6.2" resize-observer-polyfill "^1.5.1" screenfull "^5.0.0" set-harmonic-interval "^1.0.1" throttle-debounce "^2.1.0" ts-easing "^0.2.0" - tslib "^1.10.0" + tslib "^2.0.0" react-virtualized-auto-sizer@^1.0.2: version "1.0.2" @@ -28645,10 +28651,10 @@ vega-functions@^5.10.0: vega-time "^2.0.4" vega-util "^1.16.0" -vega-functions@~5.11.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.11.0.tgz#a590d016f93c81730bdbc336b377231d7ae48569" - integrity sha512-/p0QIDiA3RaUZ7drxHuClpDQCrIScSHJlY0oo0+GFYGfp+lvb29Ox1T4a+wtqeCp6NRaTWry+EwDxojnshTZIQ== +vega-functions@^5.12.0, vega-functions@~5.12.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668" + integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ== dependencies: d3-array "^2.7.1" d3-color "^2.0.0" @@ -28656,8 +28662,8 @@ vega-functions@~5.11.0: vega-dataflow "^5.7.3" vega-expression "^4.0.1" vega-scale "^7.1.1" - vega-scenegraph "^4.9.2" - vega-selections "^5.2.0" + vega-scenegraph "^4.9.3" + vega-selections "^5.3.0" vega-statistics "^1.7.9" vega-time "^2.0.4" vega-util "^1.16.0" @@ -28724,16 +28730,16 @@ vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: vega-format "^1.0.4" vega-util "^1.16.0" -vega-parser@~6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.2.tgz#7f25751177e38c3239560a9c427ded8d2ba617bb" - integrity sha512-aGyZrNzPrBruEb/WhemKDuDjQsIkMDGIgnSJci0b+9ZVxjyAzMl7UfGbiYorPiJlnIercjUJbMoFD6fCIf4gqQ== +vega-parser@~6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.3.tgz#df72785e4b086eceb90ee6219a399210933b507b" + integrity sha512-8oiVhhW26GQ4GZBvolId8FVFvhn3s1KGgPlD7Z+4P2wkV+xe5Nqu0TEJ20F/cn3b88fd0Vj48X3BH3dlSeKNFg== dependencies: vega-dataflow "^5.7.3" vega-event-selector "^2.0.6" - vega-functions "^5.10.0" + vega-functions "^5.12.0" vega-scale "^7.1.1" - vega-util "^1.15.2" + vega-util "^1.16.0" vega-projection@^1.4.5, vega-projection@~1.4.5: version "1.4.5" @@ -28772,7 +28778,7 @@ vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: vega-time "^2.0.4" vega-util "^1.15.2" -vega-scenegraph@^4.9.2, vega-scenegraph@~4.9.2: +vega-scenegraph@^4.9.2: version "4.9.2" resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.2.tgz#83b1dbc34a9ab5595c74d547d6d95849d74451ed" integrity sha512-epm1CxcB8AucXQlSDeFnmzy0FCj+HV2k9R6ch2lfLRln5lPLEfgJWgFcFhVf5jyheY0FSeHH52Q5zQn1vYI1Ow== @@ -28784,6 +28790,18 @@ vega-scenegraph@^4.9.2, vega-scenegraph@~4.9.2: vega-scale "^7.1.1" vega-util "^1.15.2" +vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.3.tgz#c4720550ea7ff5c8d9d0690f47fe2640547cfc6b" + integrity sha512-lBvqLbXqrqRCTGJmSgzZC/tLR/o+TXfakbdhDzNdpgTavTaQ65S/67Gpj5hPpi77DvsfZUIY9lCEeO37aJhy0Q== + dependencies: + d3-path "^2.0.0" + d3-shape "^2.0.0" + vega-canvas "^1.2.5" + vega-loader "^4.3.3" + vega-scale "^7.1.1" + vega-util "^1.15.2" + vega-schema-url-parser@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" @@ -28797,10 +28815,10 @@ vega-selections@^5.1.5: vega-expression "^4.0.0" vega-util "^1.15.2" -vega-selections@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.2.0.tgz#d85968d1bccc175fd92661c91d88151ffd5ade83" - integrity sha512-Xf3nTTJHRGw4tQMbt+0sBI/7WkEIzPG9E4HXkZk5Y9Q2HsGRVLmrAEXHSfpENrBLWTBZk/uvmP9rKDG7cbcTrg== +vega-selections@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7" + integrity sha512-vC4NPsuN+IffruFXfH0L3i2A51RgG4PqpLv85TvrEAIYnSkyKDE4bf+wVraR3aPdnLLkc3+tYuMi6le5FmThIA== dependencies: vega-expression "^4.0.1" vega-util "^1.16.0" @@ -28899,10 +28917,10 @@ vega-wordcloud@~4.1.3: vega-statistics "^1.7.9" vega-util "^1.15.2" -vega@^5.18.0: - version "5.18.0" - resolved "https://registry.yarnpkg.com/vega/-/vega-5.18.0.tgz#98645e5d3bd5267d66ea3e701d99dcff63cfff8a" - integrity sha512-ysqouhboWNXSuQNN7W5IGOXsnEJNFVX5duCi0tTwRsFLc61FshpqVh4+4VoXg5pH0ZCxwpqbOwd2ULZWjJTx6g== +vega@^5.19.1: + version "5.19.1" + resolved "https://registry.yarnpkg.com/vega/-/vega-5.19.1.tgz#64c8350740fe1a11d56cc6617ab3a76811fd704c" + integrity sha512-UE6/c9q9kzuz4HULFuU9HscBASoZa+zcXqGKdbQP545Nwmhd078QpcH+wZsq9lYfiTxmFtzLK/a0OH0zhkghvA== dependencies: vega-crossfilter "~4.0.5" vega-dataflow "~5.7.3" @@ -28911,17 +28929,17 @@ vega@^5.18.0: vega-expression "~4.0.1" vega-force "~4.0.7" vega-format "~1.0.4" - vega-functions "~5.11.0" + vega-functions "~5.12.0" vega-geo "~4.3.8" vega-hierarchy "~4.0.9" vega-label "~1.0.0" vega-loader "~4.4.0" - vega-parser "~6.1.2" + vega-parser "~6.1.3" vega-projection "~1.4.5" vega-regression "~1.0.9" vega-runtime "~6.1.3" vega-scale "~7.1.1" - vega-scenegraph "~4.9.2" + vega-scenegraph "~4.9.3" vega-statistics "~1.7.9" vega-time "~2.0.4" vega-transforms "~4.9.3"