diff --git a/.buildkite/scripts/steps/cloud/purge.js b/.buildkite/scripts/steps/cloud/purge.js index 0eccb55cef83..b14a3be8f8da 100644 --- a/.buildkite/scripts/steps/cloud/purge.js +++ b/.buildkite/scripts/steps/cloud/purge.js @@ -50,6 +50,9 @@ for (const deployment of deploymentsToPurge) { console.log(`Scheduling deployment for deletion: ${deployment.name} / ${deployment.id}`); try { execSync(`ecctl deployment shutdown --force '${deployment.id}'`, { stdio: 'inherit' }); + execSync(`vault delete secret/kibana-issues/dev/cloud-deploy/${deployment.name}`, { + stdio: 'inherit', + }); } catch (ex) { console.error(ex.toString()); } diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc deleted file mode 100644 index ccd3f41b9d88..000000000000 --- a/docs/management/upgrade-assistant/index.asciidoc +++ /dev/null @@ -1,26 +0,0 @@ -[role="xpack"] -[[upgrade-assistant]] -== Upgrade Assistant - -The Upgrade Assistant helps you prepare for your upgrade -to the next major version of the Elastic Stack. -To access the assistant, open the main menu and go to *Stack Management > Upgrade Assistant*. - -The assistant identifies deprecated settings in your configuration, -enables you to see if you are using deprecated features, -and guides you through the process of resolving issues. - -If you have indices that were created prior to 7.0, -you can use the assistant to reindex them so they can be accessed from 8.0+. - -IMPORTANT: To see the most up-to-date deprecation information before -upgrading to 8.0, upgrade to the latest {prev-major-last} release. - -For more information about upgrading, -refer to {stack-ref}/upgrading-elastic-stack.html[Upgrading to Elastic {version}.] - -[discrete] -=== Required permissions - -The `manage` cluster privilege is required to access the *Upgrade assistant*. -Additional privileges may be needed to perform certain actions. \ No newline at end of file diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index ff6ccbd6fab3..0ca518c3a878 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -386,12 +386,17 @@ This content has moved. Refer to <>. This content has moved. Refer to <>. -[role="exclude" logging-configuration-changes] -== Logging configuration changes +[role="exclude",id="logging-configuration-changes"] +== Logging configuration changes This content has moved. Refer to <>. -[role="exclude" upgrade-migrations] +[role="exclude",id="upgrade-migrations"] == Upgrade migrations This content has moved. Refer to <>. + +[role="exclude",id="upgrade-assistant"] +== Upgrade Assistant + +This content has moved. Refer to {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant]. diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index fc921f9118bd..7136011a4f8f 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -15,11 +15,11 @@ WARNING: The following instructions assumes {kib} is using the default index nam [[upgrade-migrations-process]] ==== Background -Saved objects are stored in two indices: +Saved objects are stored in two indices: * `.kibana_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. * `.kibana_task_manager_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. - + The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date saved object indices. @@ -29,18 +29,18 @@ The first time a newer {kib} starts, it will first perform an upgrade migration [options="header"] |======================= |Upgrading from version | Outdated index (alias) -| 6.0.0 through 6.4.x | `.kibana` +| 6.0.0 through 6.4.x | `.kibana` `.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) +| 7.4.0 through 7.11.x +| `.kibana_N` (`.kibana` alias) `.kibana_task_manager_N` (`.kibana_task_manager` alias) |======================= ==== 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. +When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. 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. @@ -64,13 +64,15 @@ Error: Unable to complete saved object migrations for the [.kibana] index. Pleas Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] -------------------------------------------- -See https://github.com/elastic/kibana/issues/95321 for instructions to work around this issue. - +Instructions to work around this issue are in https://github.com/elastic/kibana/issues/95321[this GitHub issue]. + [float] ===== Corrupt saved objects We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. -Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. +Saved objects that were corrupted through manual editing or integrations will cause migration +failures with a log message like `Unable to migrate the corrupt Saved Object document ...`. +Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. For example, given the following error message: @@ -101,7 +103,7 @@ DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab -------------------------------------------- . Restart {kib}. - ++ In this example, the Dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **will no longer be available**. Be sure you have a snapshot before you delete the corrupt document. If restoring from a snapshot is not an option, it is recommended to also delete the `temp` and `target` indices the migration created before restarting {kib} and retrying. @@ -112,15 +114,16 @@ Matching index templates which specify `settings.refresh_interval` or `mappings` Prevention: narrow down the index patterns of any user-defined index templates to ensure that these won't apply to new `.kibana*` indices. -Note: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` and index pattern `.kibana`. This index template will not interfere and does not need to be changed or removed. +NOTE: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` +and uses an index pattern of `.kibana`. This index template will not interfere and does not need to be changed or removed. [float] ===== An unhealthy {es} cluster Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensure that your cluster has: - * enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices - * sufficient heap size - * a "green" cluster status + * Enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices + * Sufficient heap size + * A "green" cluster status [float] ===== Different versions of {kib} connected to the same {es} index @@ -134,20 +137,32 @@ For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` [[resolve-migrations-failures]] ==== Resolving migration failures -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. +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. -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. +If upgrade migrations fail repeatedly, follow the advice in +<>. +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 and fix the root cause. +If you've followed the advice in <> +and <> 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}. + +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 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 {kib} version. [float] ===== Rollback by restoring a backup snapshot: @@ -164,8 +179,11 @@ In order to rollback after a failed upgrade migration, the saved object indices 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. 2. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. Snapshots include this feature state by default. -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`. +3. Delete the version specific indices created by the failed upgrade migration. For example, 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. +For example. 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. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index e682f7372f81..6c309d56f229 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -167,10 +167,6 @@ set the timespan for notification messages, and much more. the full list of features that are included in your license, see the https://www.elastic.co/subscriptions[subscription page]. -| <> -| Identify the issues that you need to address before upgrading to the -next major version of {es}, and then reindex, if needed. - |=== @@ -197,6 +193,4 @@ include::{kib-repo-dir}/spaces/index.asciidoc[] include::{kib-repo-dir}/management/managing-tags.asciidoc[] -include::{kib-repo-dir}/management/upgrade-assistant/index.asciidoc[] - include::{kib-repo-dir}/management/watcher-ui/index.asciidoc[] diff --git a/src/dev/build/tasks/download_cloud_dependencies.ts b/src/dev/build/tasks/download_cloud_dependencies.ts index 1207594304e6..6ecc09c21ddc 100644 --- a/src/dev/build/tasks/download_cloud_dependencies.ts +++ b/src/dev/build/tasks/download_cloud_dependencies.ts @@ -36,12 +36,19 @@ export const DownloadCloudDependencies: Task = { let buildId = ''; if (!config.isRelease) { - const manifest = await Axios.get( - `https://artifacts-api.elastic.co/v1/versions/${config.getBuildVersion()}/builds/latest` - ); - buildId = manifest.data.build.build_id; + const manifestUrl = `https://artifacts-api.elastic.co/v1/versions/${config.getBuildVersion()}/builds/latest`; + try { + const manifest = await Axios.get(manifestUrl); + buildId = manifest.data.build.build_id; + } catch (e) { + log.error( + `Unable to find Elastic artifacts for ${config.getBuildVersion()} at ${manifestUrl}.` + ); + throw e; + } } await del([config.resolveFromRepo('.beats')]); + await downloadBeat('metricbeat', buildId); await downloadBeat('filebeat', buildId); }, diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 683a1a551f81..0130d4a5f811 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -8,6 +8,7 @@ "version": "kibana", "requiredPlugins": [ "data", + "dataViews", "embeddable", "controls", "inspector", diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index ae16527b6444..05d663bdac26 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -110,7 +110,7 @@ export async function mountApp({ uiSettings: coreStart.uiSettings, scopedHistory: () => scopedHistory, screenshotModeService: screenshotMode, - indexPatterns: dataStart.indexPatterns, + dataViews: dataStart.dataViews, savedQueryService: dataStart.query.savedQueries, savedObjectsClient: coreStart.savedObjects.client, savedDashboards: dashboardStart.getSavedDashboardLoader(), @@ -212,7 +212,7 @@ export async function mountApp({ .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, false) ); if (!hasEmbeddableIncoming) { - dataStart.indexPatterns.clearCache(); + dataStart.dataViews.clearCache(); } // dispatch synthetic hash change event to update hash history objects diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index 0ef21fca26f2..039a600d153b 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -24,13 +24,15 @@ import { EmbeddableFactory, ViewMode } from '../../services/embeddable'; import { dashboardStateStore, setDescription, setViewMode } from '../state'; import { DashboardContainerServices } from '../embeddable/dashboard_container'; import { createKbnUrlStateStorage, defer } from '../../../../kibana_utils/public'; -import { Filter, IIndexPattern, IndexPatternsContract } from '../../services/data'; import { useDashboardAppState, UseDashboardStateProps } from './use_dashboard_app_state'; import { getSampleDashboardInput, getSavedDashboardMock, makeDefaultServices, } from '../test_helpers'; +import { DataViewsContract } from '../../services/data'; +import { DataView } from '../../services/data_views'; +import type { Filter } from '@kbn/es-query'; interface SetupEmbeddableFactoryReturn { finalizeEmbeddableCreation: () => void; @@ -56,12 +58,10 @@ const createDashboardAppStateProps = (): UseDashboardStateProps => ({ const createDashboardAppStateServices = () => { const defaults = makeDefaultServices(); - const indexPatterns = {} as IndexPatternsContract; - const defaultIndexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; - indexPatterns.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); - indexPatterns.getDefault = jest - .fn() - .mockImplementation(() => Promise.resolve(defaultIndexPattern)); + const dataViews = {} as DataViewsContract; + const defaultDataView = { id: 'foo', fields: [{ name: 'bar' }] } as DataView; + dataViews.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); + dataViews.getDefault = jest.fn().mockImplementation(() => Promise.resolve(defaultDataView)); const data = dataPluginMock.createStartContract(); data.query.filterManager.getUpdates$ = jest.fn().mockImplementation(() => of(void 0)); @@ -71,7 +71,7 @@ const createDashboardAppStateServices = () => { .fn() .mockImplementation(() => of(void 0)); - return { ...defaults, indexPatterns, data }; + return { ...defaults, dataViews, data }; }; const setupEmbeddableFactory = ( diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 8c58eab0ded8..2ce1c87252d3 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -15,6 +15,7 @@ import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; import { DashboardConstants } from '../..'; import { ViewMode } from '../../services/embeddable'; import { useKibana } from '../../services/kibana_react'; +import { DataView } from '../../services/data_views'; import { getNewDashboardTitle } from '../../dashboard_strings'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; import { setDashboardState, useDashboardDispatch, useDashboardSelector } from '../state'; @@ -30,7 +31,7 @@ import { tryDestroyDashboardContainer, syncDashboardContainerInput, savedObjectToDashboardState, - syncDashboardIndexPatterns, + syncDashboardDataViews, syncDashboardFilterState, loadSavedDashboardState, buildDashboardContainer, @@ -81,7 +82,7 @@ export const useDashboardAppState = ({ core, chrome, embeddable, - indexPatterns, + dataViews, usageCollection, savedDashboards, initializerContext, @@ -121,7 +122,7 @@ export const useDashboardAppState = ({ search, history, embeddable, - indexPatterns, + dataViews, notifications, kibanaVersion, savedDashboards, @@ -234,11 +235,11 @@ export const useDashboardAppState = ({ /** * Start syncing index patterns between the Query Service and the Dashboard Container. */ - const indexPatternsSubscription = syncDashboardIndexPatterns({ + const dataViewsSubscription = syncDashboardDataViews({ dashboardContainer, - indexPatterns: dashboardBuildContext.indexPatterns, - onUpdateIndexPatterns: (newIndexPatterns) => - setDashboardAppState((s) => ({ ...s, indexPatterns: newIndexPatterns })), + dataViews: dashboardBuildContext.dataViews, + onUpdateDataViews: (newDataViews: DataView[]) => + setDashboardAppState((s) => ({ ...s, dataViews: newDataViews })), }); /** @@ -339,7 +340,7 @@ export const useDashboardAppState = ({ stopWatchingAppStateInUrl(); stopSyncingDashboardFilterState(); lastSavedSubscription.unsubscribe(); - indexPatternsSubscription.unsubscribe(); + dataViewsSubscription.unsubscribe(); tryDestroyDashboardContainer(dashboardContainer); setDashboardAppState((state) => ({ ...state, @@ -368,7 +369,7 @@ export const useDashboardAppState = ({ usageCollection, scopedHistory, notifications, - indexPatterns, + dataViews, kibanaVersion, embeddable, docTitle, diff --git a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts index 1dd39cc3e5ba..5752a6445d2a 100644 --- a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts @@ -31,7 +31,7 @@ import { } from '../../services/embeddable'; type BuildDashboardContainerProps = DashboardBuildContext & { - data: DashboardAppServices['data']; // the whole data service is required here because it is required by getUrlGeneratorState + data: DashboardAppServices['data']; // the whole data service is required here because it is required by getLocatorParams savedDashboard: DashboardSavedObject; initialDashboardState: DashboardState; incomingEmbeddable?: EmbeddablePackageState; diff --git a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts index 90d5a67c3da4..0d1eb3537377 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts @@ -8,7 +8,7 @@ import { Subscription } from 'rxjs'; import deepEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators'; import { DashboardContainer } from '..'; diff --git a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts index 264c8fcb1de2..729b0d06f4ab 100644 --- a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts @@ -8,7 +8,7 @@ import { xor, omit, isEmpty } from 'lodash'; import fastIsEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter, isFilterPinned } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter, isFilterPinned } from '@kbn/es-query'; import { DashboardContainerInput } from '../..'; import { controlGroupInputIsEqual } from './dashboard_control_group'; diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index 58f962591b67..eab3604ff841 100644 --- a/src/plugins/dashboard/public/application/lib/index.ts +++ b/src/plugins/dashboard/public/application/lib/index.ts @@ -18,7 +18,7 @@ export { DashboardSessionStorage } from './dashboard_session_storage'; export { loadSavedDashboardState } from './load_saved_dashboard_state'; export { attemptLoadDashboardByTitle } from './load_dashboard_by_title'; export { syncDashboardFilterState } from './sync_dashboard_filter_state'; -export { syncDashboardIndexPatterns } from './sync_dashboard_index_patterns'; +export { syncDashboardDataViews } from './sync_dashboard_data_views'; export { syncDashboardContainerInput } from './sync_dashboard_container_input'; export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state'; export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container'; diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts index 03a03842c0e6..45eda98dcc49 100644 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts @@ -28,7 +28,7 @@ export const loadSavedDashboardState = async ({ query, history, notifications, - indexPatterns, + dataViews, savedDashboards, usageCollection, savedDashboardId, @@ -51,7 +51,7 @@ export const loadSavedDashboardState = async ({ notifications.toasts.addWarning(getDashboard60Warning()); return; } - await indexPatterns.ensureDefaultDataView(); + await dataViews.ensureDefaultDataView(); try { const savedDashboard = (await savedDashboards.get({ id: savedDashboardId, diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts index 5a699eb11640..0be2211d4c2f 100644 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/save_dashboard.ts @@ -8,6 +8,7 @@ import _ from 'lodash'; +import { isFilterPinned } from '@kbn/es-query'; import { convertTimeToUTCString } from '.'; import { NotificationsStart } from '../../services/core'; import { DashboardSavedObject } from '../../saved_dashboards'; @@ -16,7 +17,7 @@ import { SavedObjectSaveOpts } from '../../services/saved_objects'; import { dashboardSaveToastStrings } from '../../dashboard_strings'; import { getHasTaggingCapabilitiesGuard } from './dashboard_tagging'; import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss'; -import { RefreshInterval, TimefilterContract, esFilters } from '../../services/data'; +import { RefreshInterval, TimefilterContract } from '../../services/data'; import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters'; import { DashboardSessionStorage } from './dashboard_session_storage'; import { serializeControlGroupToDashboardSavedObject } from './dashboard_control_group'; @@ -81,9 +82,7 @@ export const saveDashboard = async ({ savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; // only save unpinned filters - const unpinnedFilters = savedDashboard - .getFilters() - .filter((filter) => !esFilters.isFilterPinned(filter)); + const unpinnedFilters = savedDashboard.getFilters().filter((filter) => !isFilterPinned(filter)); savedDashboard.searchSource.setField('filter', unpinnedFilters); try { diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts index 0fa7487390cd..d3930cb5c062 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts @@ -10,8 +10,9 @@ import _ from 'lodash'; import { Subscription } from 'rxjs'; import { debounceTime, tap } from 'rxjs/operators'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { DashboardContainer } from '../embeddable'; -import { esFilters, Filter, Query } from '../../services/data'; +import { Query } from '../../services/data'; import { DashboardConstants, DashboardSavedObject } from '../..'; import { setControlGroupState, @@ -96,13 +97,7 @@ export const applyContainerChangesToState = ({ return; } const { filterManager } = query; - if ( - !esFilters.compareFilters( - input.filters, - filterManager.getFilters(), - esFilters.COMPARE_ALL_OPTIONS - ) - ) { + if (!compareFilters(input.filters, filterManager.getFilters(), COMPARE_ALL_OPTIONS)) { // Add filters modifies the object passed to it, hence the clone deep. filterManager.addFilters(_.cloneDeep(input.filters)); applyFilters(latestState.query, input.filters); diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts similarity index 56% rename from src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts rename to src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts index 5460ef7b0003..63cecaa76fb2 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts @@ -13,48 +13,51 @@ import { distinctUntilChanged, switchMap, filter, mapTo, map } from 'rxjs/operat import { DashboardContainer } from '..'; import { isErrorEmbeddable } from '../../services/embeddable'; -import { IndexPattern, IndexPatternsContract } from '../../services/data'; +import { DataViewsContract } from '../../services/data'; +import { DataView } from '../../services/data_views'; -interface SyncDashboardIndexPatternsProps { +interface SyncDashboardDataViewsProps { dashboardContainer: DashboardContainer; - indexPatterns: IndexPatternsContract; - onUpdateIndexPatterns: (newIndexPatterns: IndexPattern[]) => void; + dataViews: DataViewsContract; + onUpdateDataViews: (newDataViews: DataView[]) => void; } -export const syncDashboardIndexPatterns = ({ +export const syncDashboardDataViews = ({ dashboardContainer, - indexPatterns, - onUpdateIndexPatterns, -}: SyncDashboardIndexPatternsProps) => { - const updateIndexPatternsOperator = pipe( + dataViews, + onUpdateDataViews, +}: SyncDashboardDataViewsProps) => { + const updateDataViewsOperator = pipe( filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)), - map((container: DashboardContainer): IndexPattern[] | undefined => { - let panelIndexPatterns: IndexPattern[] = []; + map((container: DashboardContainer): DataView[] | undefined => { + let panelDataViews: DataView[] = []; Object.values(container.getChildIds()).forEach((id) => { const embeddableInstance = container.getChild(id); if (isErrorEmbeddable(embeddableInstance)) return; - const embeddableIndexPatterns = (embeddableInstance.getOutput() as any).indexPatterns; - if (!embeddableIndexPatterns) return; - panelIndexPatterns.push(...embeddableIndexPatterns); + const embeddableDataViews = ( + embeddableInstance.getOutput() as { indexPatterns: DataView[] } + ).indexPatterns; + if (!embeddableDataViews) return; + panelDataViews.push(...embeddableDataViews); }); if (container.controlGroup) { - panelIndexPatterns.push(...(container.controlGroup.getOutput().dataViews ?? [])); + panelDataViews.push(...(container.controlGroup.getOutput().dataViews ?? [])); } - panelIndexPatterns = uniqBy(panelIndexPatterns, 'id'); + panelDataViews = uniqBy(panelDataViews, 'id'); /** * If no index patterns have been returned yet, and there is at least one embeddable which * hasn't yet loaded, defer the loading of the default index pattern by returning undefined. */ if ( - panelIndexPatterns.length === 0 && + panelDataViews.length === 0 && Object.keys(container.getOutput().embeddableLoaded).length > 0 && Object.values(container.getOutput().embeddableLoaded).some((value) => value === false) ) { return; } - return panelIndexPatterns; + return panelDataViews; }), distinctUntilChanged((a, b) => deepEqual( @@ -63,17 +66,17 @@ export const syncDashboardIndexPatterns = ({ ) ), // using switchMap for previous task cancellation - switchMap((panelIndexPatterns?: IndexPattern[]) => { + switchMap((panelDataViews?: DataView[]) => { return new Observable((observer) => { - if (!panelIndexPatterns) return; - if (panelIndexPatterns.length > 0) { + if (!panelDataViews) return; + if (panelDataViews.length > 0) { if (observer.closed) return; - onUpdateIndexPatterns(panelIndexPatterns); + onUpdateDataViews(panelDataViews); observer.complete(); } else { - indexPatterns.getDefault().then((defaultIndexPattern) => { + dataViews.getDefault().then((defaultDataView) => { if (observer.closed) return; - onUpdateIndexPatterns([defaultIndexPattern as IndexPattern]); + onUpdateDataViews([defaultDataView as DataView]); observer.complete(); }); } @@ -81,11 +84,11 @@ export const syncDashboardIndexPatterns = ({ }) ); - const indexPatternSources = [dashboardContainer.getOutput$()]; + const dataViewSources = [dashboardContainer.getOutput$()]; if (dashboardContainer.controlGroup) - indexPatternSources.push(dashboardContainer.controlGroup.getOutput$()); + dataViewSources.push(dashboardContainer.controlGroup.getOutput$()); - return combineLatest(indexPatternSources) - .pipe(mapTo(dashboardContainer), updateIndexPatternsOperator) + return combineLatest(dataViewSources) + .pipe(mapTo(dashboardContainer), updateDataViewsOperator) .subscribe(); }; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts index 36b8b57cfdbd..ce9535e54944 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts @@ -8,10 +8,10 @@ import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { ApplicationStart } from 'kibana/public'; -import { esFilters } from '../../../../data/public'; import { createHashHistory } from 'history'; +import { FilterStateStore } from '@kbn/es-query'; import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../dashboard_constants'; const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647'; @@ -118,7 +118,7 @@ describe('when global filters change', () => { }, query: { query: 'q1' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ]; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts index 2f19924d4598..8af3f2a10666 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts @@ -9,8 +9,11 @@ import { ApplicationStart } from 'kibana/public'; import { QueryState } from '../../../../data/public'; import { setStateToKbnUrl } from '../../../../kibana_utils/public'; -import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { + DashboardConstants, + createDashboardEditUrl, + GLOBAL_STATE_STORAGE_KEY, +} from '../../dashboard_constants'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; export const getDashboardListItemLink = ( diff --git a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts index 616fe56102df..656f5672e38c 100644 --- a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts +++ b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts @@ -13,7 +13,7 @@ import { UrlForwardingStart } from '../../../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../services/navigation'; import { DashboardAppServices, DashboardAppCapabilities } from '../../types'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; -import { IndexPatternsContract, SavedQueryService } from '../../services/data'; +import { DataViewsContract, SavedQueryService } from '../../services/data'; import { savedObjectsPluginMock } from '../../../../saved_objects/public/mocks'; import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; import { visualizationsPluginMock } from '../../../../visualizations/public/mocks'; @@ -83,7 +83,7 @@ export function makeDefaultServices(): DashboardAppServices { savedObjectsClient: core.savedObjects.client, dashboardCapabilities: defaultCapabilities, data: dataPluginMock.createStartContract(), - indexPatterns: {} as IndexPatternsContract, + dataViews: {} as DataViewsContract, savedQueryService: {} as SavedQueryService, scopedHistory: () => ({} as ScopedHistory), setHeaderActionMenu: (mountPoint) => {}, diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 005d40a90f38..eb251ad41f62 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -525,7 +525,7 @@ export function DashboardTopNav({ showDatePicker, showFilterBar, setMenuMountPoint: embedSettings ? undefined : setHeaderActionMenu, - indexPatterns: dashboardAppState.indexPatterns, + indexPatterns: dashboardAppState.dataViews, showSaveQuery: dashboardCapabilities.saveQuery, useDefaultBehaviors: true, savedQuery: state.savedQuery, diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 9063b279c25f..88fbc3b30392 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -9,6 +9,7 @@ import type { ControlStyle } from '../../controls/public'; export const DASHBOARD_STATE_STORAGE_KEY = '_a'; +export const GLOBAL_STATE_STORAGE_KEY = '_g'; export const DashboardConstants = { LANDING_PAGE_PATH: '/list', diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index f25a92275d72..bff2d4d79108 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -16,15 +16,7 @@ export { } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -export type { - DashboardSetup, - DashboardStart, - DashboardUrlGenerator, - DashboardFeatureFlagConfig, -} from './plugin'; - -export type { DashboardUrlGeneratorState } from './url_generator'; -export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator } from './url_generator'; +export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin'; export type { DashboardAppLocator, DashboardAppLocatorParams } from './locator'; export type { DashboardSavedObject } from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts index f3f5aec9f478..11ec16908b81 100644 --- a/src/plugins/dashboard/public/locator.test.ts +++ b/src/plugins/dashboard/public/locator.test.ts @@ -9,7 +9,7 @@ import { DashboardAppLocatorDefinition } from './locator'; import { hashedItemStore } from '../../kibana_utils/public'; import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters } from '../../data/public'; +import { FilterStateStore } from '@kbn/es-query'; describe('dashboard locator', () => { beforeEach(() => { @@ -79,7 +79,7 @@ describe('dashboard locator', () => { }, query: { query: 'hi' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ], diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index b6655e246de3..42efb521cf6e 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -8,11 +8,11 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { flow } from 'lodash'; -import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import { type Filter } from '@kbn/es-query'; +import type { TimeRange, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import type { SavedDashboardPanel } from '../common/types'; import type { RawDashboardState } from './types'; -import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { ViewMode } from '../../embeddable/public'; import { DashboardConstants } from './dashboard_constants'; @@ -152,12 +152,14 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition( '_g', cleanEmptyKeys({ time: params.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), + filters: filters?.filter((f) => isFilterPinned(f)), refreshInterval: params.refreshInterval, }), { useHash }, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 7f784d43c0cb..2f63062ccf60 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -33,8 +33,8 @@ import { UiActionsSetup, UiActionsStart } from './services/ui_actions'; import { PresentationUtilPluginStart } from './services/presentation_util'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home'; import { NavigationPublicPluginStart as NavigationStart } from './services/navigation'; -import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from './services/data'; -import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from './services/share'; +import { DataPublicPluginSetup, DataPublicPluginStart } from './services/data'; +import { SharePluginSetup, SharePluginStart } from './services/share'; import type { SavedObjectTaggingOssPluginStart } from './services/saved_objects_tagging_oss'; import type { ScreenshotModePluginSetup, @@ -70,29 +70,15 @@ import { CopyToDashboardAction, DashboardCapabilities, } from './application'; -import { - createDashboardUrlGenerator, - DASHBOARD_APP_URL_GENERATOR, - DashboardUrlGeneratorState, -} from './url_generator'; import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator'; import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; -import { UrlGeneratorState } from '../../share/public'; import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; import { replaceUrlHashQuery } from '../../kibana_utils/public'; import { SpacesPluginStart } from './services/spaces'; -declare module '../../share/public' { - export interface UrlGeneratorStateMapping { - [DASHBOARD_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - -export type DashboardUrlGenerator = UrlGeneratorContract; - export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; } @@ -134,15 +120,6 @@ export interface DashboardStart { getDashboardContainerByValueRenderer: () => ReturnType< typeof createDashboardContainerByValueRenderer >; - /** - * @deprecated Use dashboard locator instead. Dashboard locator is available - * under `.locator` key. This dashboard URL generator will be removed soon. - * - * ```ts - * plugins.dashboard.locator.getLocation({ ... }); - * ``` - */ - dashboardUrlGenerator?: DashboardUrlGenerator; locator?: DashboardAppLocator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; } @@ -157,11 +134,6 @@ export class DashboardPlugin private stopUrlTracking: (() => void) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig; - - /** - * @deprecated Use locator instead. - */ - private dashboardUrlGenerator?: DashboardUrlGenerator; private locator?: DashboardAppLocator; public setup( @@ -178,20 +150,6 @@ export class DashboardPlugin ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); - const startServices = core.getStartServices(); - - if (share) { - this.dashboardUrlGenerator = share.urlGenerators.registerUrlGenerator( - createDashboardUrlGenerator(async () => { - const [coreStart, , selfStart] = await startServices; - return { - appBasePath: coreStart.application.getUrlForApp('dashboards'), - useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), - savedDashboardLoader: selfStart.getSavedDashboardLoader(), - }; - }) - ); - } const getPlaceholderEmbeddableStartServices = async () => { const [coreStart] = await core.getStartServices(); @@ -253,10 +211,13 @@ export class DashboardPlugin filter( ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) ), - map(({ state }) => ({ - ...state, - filters: state.filters?.filter(esFilters.isFilterPinned), - })) + map(async ({ state }) => { + const { isFilterPinned } = await import('@kbn/es-query'); + return { + ...state, + filters: state.filters?.filter(isFilterPinned), + }; + }) ), }, ], @@ -455,7 +416,6 @@ export class DashboardPlugin factory: dashboardContainerFactory as DashboardContainerFactory, }); }, - dashboardUrlGenerator: this.dashboardUrlGenerator, locator: this.locator, dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!, }; diff --git a/src/plugins/dashboard/public/services/data_views.ts b/src/plugins/dashboard/public/services/data_views.ts new file mode 100644 index 000000000000..4fb2bbaf0850 --- /dev/null +++ b/src/plugins/dashboard/public/services/data_views.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from '../../../data_views/public'; diff --git a/src/plugins/dashboard/public/services/share.ts b/src/plugins/dashboard/public/services/share.ts index 7ed9b8657159..77a9f44a3cf0 100644 --- a/src/plugins/dashboard/public/services/share.ts +++ b/src/plugins/dashboard/public/services/share.ts @@ -6,9 +6,5 @@ * Side Public License, v 1. */ -export type { - SharePluginStart, - SharePluginSetup, - UrlGeneratorContract, -} from '../../../share/public'; +export type { SharePluginStart, SharePluginSetup } from '../../../share/public'; export { downloadMultipleAs } from '../../../share/public'; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index b7b146aeba34..4de07974203a 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -17,22 +17,25 @@ import type { KibanaExecutionContext, } from 'kibana/public'; import { History } from 'history'; +import type { Filter } from '@kbn/es-query'; import { AnyAction, Dispatch } from 'redux'; import { BehaviorSubject, Subject } from 'rxjs'; -import { Query, Filter, IndexPattern, RefreshInterval, TimeRange } from './services/data'; -import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; + +import { DataView } from './services/data_views'; import { SharePluginStart } from './services/share'; import { EmbeddableStart } from './services/embeddable'; import { DashboardSessionStorage } from './application/lib'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { UsageCollectionSetup } from './services/usage_collection'; import { NavigationPublicPluginStart } from './services/navigation'; +import { Query, RefreshInterval, TimeRange } from './services/data'; import { DashboardPanelState, SavedDashboardPanel } from '../common/types'; import { SavedObjectsTaggingApi } from './services/saved_objects_tagging_oss'; -import { DataPublicPluginStart, IndexPatternsContract } from './services/data'; +import { DataPublicPluginStart, DataViewsContract } from './services/data'; +import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; import { SavedObjectLoader, SavedObjectsStart } from './services/saved_objects'; -import { IKbnUrlStateStorage } from './services/kibana_utils'; import type { ScreenshotModePluginStart } from './services/screenshot_mode'; +import { IKbnUrlStateStorage } from './services/kibana_utils'; import type { DashboardContainer, DashboardSavedObject } from '.'; import { VisualizationsStart } from '../../visualizations/public'; import { DashboardAppLocatorParams } from './locator'; @@ -102,7 +105,7 @@ export interface DashboardContainerInput extends ContainerInput { */ export interface DashboardAppState { hasUnsavedChanges?: boolean; - indexPatterns?: IndexPattern[]; + dataViews?: DataView[]; updateLastSavedState?: () => void; resetToLastSavedState?: () => void; savedDashboard?: DashboardSavedObject; @@ -119,7 +122,7 @@ export interface DashboardAppState { export type DashboardBuildContext = Pick< DashboardAppServices, | 'embeddable' - | 'indexPatterns' + | 'dataViews' | 'savedDashboards' | 'usageCollection' | 'initializerContext' @@ -198,7 +201,7 @@ export interface DashboardAppServices { savedDashboards: SavedObjectLoader; scopedHistory: () => ScopedHistory; visualizations: VisualizationsStart; - indexPatterns: IndexPatternsContract; + dataViews: DataViewsContract; usageCollection?: UsageCollectionSetup; navigation: NavigationPublicPluginStart; dashboardCapabilities: DashboardAppCapabilities; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts deleted file mode 100644 index 9a1204f116c7..000000000000 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ /dev/null @@ -1,356 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { createDashboardUrlGenerator } from './url_generator'; -import { hashedItemStore } from '../../kibana_utils/public'; -import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters, Filter } from '../../data/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; - -const APP_BASE_PATH: string = 'xyz/app/dashboards'; - -const createMockDashboardLoader = ( - dashboardToFilters: { - [dashboardId: string]: () => Filter[]; - } = {} -) => { - return { - get: async (dashboardId: string) => { - return { - searchSource: { - getField: (field: string) => { - if (field === 'filter') - return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : []; - throw new Error( - `createMockDashboardLoader > searchSource > getField > ${field} is not mocked` - ); - }, - }, - }; - }, - } as SavedObjectLoader; -}; - -describe('dashboard url generator', () => { - beforeEach(() => { - // @ts-ignore - hashedItemStore.storage = mockStorage; - }); - - test('creates a link to a saved dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({}); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/create?_a=()&_g=()"`); - }); - - test('creates a link with global time range set up', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - }, - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - }, - ], - query: { query: 'bye', language: 'kuery' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('searchSessionId', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [], - query: { query: 'bye', language: 'kuery' }, - searchSessionId: '__sessionSearchId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__"` - ); - }); - - test('savedQuery', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - savedQuery: '__savedQueryId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(savedQuery:__savedQueryId__)&_g=()"` - ); - expect(url).toContain('__savedQueryId__'); - }); - - test('panels', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - panels: [{ fakePanelContent: 'fakePanelContent' } as any], - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()"` - ); - }); - - test('if no useHash setting is given, uses the one was start services', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a false useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: true, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a true useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: false, - }); - expect(url.indexOf('relative')).toBeGreaterThan(1); - }); - - describe('preserving saved filters', () => { - const savedFilter1 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter1' }, - }; - - const savedFilter2 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter2' }, - }; - - const appliedFilter = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'appliedfilter' }, - }; - - test('attaches filters from destination dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - ['dashboard2']: () => [savedFilter2], - }), - }) - ); - - const urlToDashboard1 = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1')); - expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter')); - - const urlToDashboard2 = await generator.createUrl!({ - dashboardId: 'dashboard2', - filters: [appliedFilter], - }); - - expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2')); - expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test("doesn't fail if can't retrieve filters from destination dashboard", async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => { - throw new Error('Not found'); - }, - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test('can enforce empty filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [], - preserveSavedFilters: false, - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).not.toEqual(expect.stringContaining('query:appliedfilter')); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"` - ); - }); - - test('no filters in result url if no filters applied', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - }); - expect(url).not.toEqual(expect.stringContaining('filters')); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/view/dashboard1?_a=()&_g=()"`); - }); - - test('can turn off preserving filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - preserveSavedFilters: false, - }); - - expect(urlWithPreservedFiltersTurnedOff).not.toEqual( - expect.stringContaining('query:savedfilter1') - ); - expect(urlWithPreservedFiltersTurnedOff).toEqual( - expect.stringContaining('query:appliedfilter') - ); - }); - }); -}); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts deleted file mode 100644 index 5c0cd32ee5a1..000000000000 --- a/src/plugins/dashboard/public/url_generator.ts +++ /dev/null @@ -1,170 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - TimeRange, - Filter, - Query, - esFilters, - QueryState, - RefreshInterval, -} from '../../data/public'; -import { setStateToKbnUrl } from '../../kibana_utils/public'; -import { UrlGeneratorsDefinition } from '../../share/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; -import { ViewMode } from '../../embeddable/public'; -import { DashboardConstants } from './dashboard_constants'; -import { SavedDashboardPanel } from '../common/types'; - -export const STATE_STORAGE_KEY = '_a'; -export const GLOBAL_STATE_STORAGE_KEY = '_g'; - -export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR'; - -/** - * @deprecated Use dashboard locator instead. - */ -export interface DashboardUrlGeneratorState { - /** - * If given, the dashboard saved object with this id will be loaded. If not given, - * a new, unsaved dashboard will be loaded up. - */ - dashboardId?: string; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - - /** - * Optionally set the refresh interval. - */ - refreshInterval?: RefreshInterval; - - /** - * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has filters saved with it, this will _replace_ those filters. - */ - filters?: Filter[]; - /** - * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has a query saved with it, this will _replace_ that query. - */ - query?: Query; - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - useHash?: boolean; - - /** - * When `true` filters from saved filters from destination dashboard as merged with applied filters - * When `false` applied filters take precedence and override saved filters - * - * true is default - */ - preserveSavedFilters?: boolean; - - /** - * View mode of the dashboard. - */ - viewMode?: ViewMode; - - /** - * Search search session ID to restore. - * (Background search) - */ - searchSessionId?: string; - - /** - * List of dashboard panels - */ - panels?: SavedDashboardPanel[]; - - /** - * Saved query ID - */ - savedQuery?: string; -} - -/** - * @deprecated Use dashboard locator instead. - */ -export const createDashboardUrlGenerator = ( - getStartServices: () => Promise<{ - appBasePath: string; - useHashedUrl: boolean; - savedDashboardLoader: SavedObjectLoader; - }> -): UrlGeneratorsDefinition => ({ - id: DASHBOARD_APP_URL_GENERATOR, - createUrl: async (state) => { - const startServices = await getStartServices(); - const useHash = state.useHash ?? startServices.useHashedUrl; - const appBasePath = startServices.appBasePath; - const hash = state.dashboardId ? `view/${state.dashboardId}` : `create`; - - const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { - if (state.preserveSavedFilters === false) return []; - if (!state.dashboardId) return []; - try { - const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId); - return dashboard?.searchSource?.getField('filter') ?? []; - } catch (e) { - // in case dashboard is missing, built the url without those filters - // dashboard app will handle redirect to landing page with toast message - return []; - } - }; - - const cleanEmptyKeys = (stateObj: Record) => { - Object.keys(stateObj).forEach((key) => { - if (stateObj[key] === undefined) { - delete stateObj[key]; - } - }); - return stateObj; - }; - - // leave filters `undefined` if no filters was applied - // in this case dashboard will restore saved filters on its own - const filters = state.filters && [ - ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), - ...state.filters, - ]; - - let url = setStateToKbnUrl( - STATE_STORAGE_KEY, - cleanEmptyKeys({ - query: state.query, - filters: filters?.filter((f) => !esFilters.isFilterPinned(f)), - viewMode: state.viewMode, - panels: state.panels, - savedQuery: state.savedQuery, - }), - { useHash }, - `${appBasePath}#/${hash}` - ); - - url = setStateToKbnUrl( - GLOBAL_STATE_STORAGE_KEY, - cleanEmptyKeys({ - time: state.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), - refreshInterval: state.refreshInterval, - }), - { useHash }, - url - ); - - if (state.searchSessionId) { - url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`; - } - - return url; - }, -}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index e0cd410ce5e8..ed8f87ad9b51 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -25,7 +25,7 @@ import { convertSavedDashboardPanelToPanelState, } from '../../common/embeddable/embeddable_saved_object_converters'; import { SavedObjectEmbeddableInput } from '../../../embeddable/common'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../data/common'; import { mergeMigrationFunctionMaps, MigrateFunction, @@ -49,7 +49,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; doc.references.push({ name: searchSource.indexRefName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: searchSource.index, }); delete searchSource.index; @@ -62,7 +62,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; doc.references.push({ name: filterRow.meta.indexRefName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: filterRow.meta.index, }); delete filterRow.meta.index; diff --git a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts index 8980bd190332..4000bed0c28a 100644 --- a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts +++ b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { esFilters, Filter } from 'src/plugins/data/public'; +import { FilterStateStore, Filter } from '@kbn/es-query'; import { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; const filter: Filter = { meta: { disabled: false, negate: false, alias: '' }, query: {}, - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, }; const queryFilter: Pre600FilterQuery = { @@ -27,7 +27,7 @@ test('Migrates an old filter query into the query field', () => { expect(newSearchSource).toEqual({ filter: [ { - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, meta: { alias: '', disabled: false, diff --git a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts index ddd1c45841b9..e2ea076de774 100644 --- a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts +++ b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts @@ -7,14 +7,14 @@ */ import type { SavedObjectMigrationFn } from 'kibana/server'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../data/common'; export const replaceIndexPatternReference: SavedObjectMigrationFn = (doc) => ({ ...doc, references: Array.isArray(doc.references) ? doc.references.map((reference) => { if (reference.type === 'index_pattern') { - reference.type = INDEX_PATTERN_SAVED_OBJECT_TYPE; + reference.type = DATA_VIEW_SAVED_OBJECT_TYPE; } return reference; }) diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 680d06780543..55049447aee5 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../../core/tsconfig.json" }, { "path": "../inspector/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../controls/tsconfig.json" }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss new file mode 100644 index 000000000000..09abf97829be --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss @@ -0,0 +1,4 @@ +.crawlSelectDomainsModal { + width: 50rem; + max-width: 90%; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx new file mode 100644 index 000000000000..79898d9f15e9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiModal, EuiModalFooter, EuiButton, EuiButtonEmpty } from '@elastic/eui'; + +import { rerender } from '../../../../../test_helpers'; + +import { CrawlSelectDomainsModal } from './crawl_select_domains_modal'; +import { SimplifiedSelectable } from './simplified_selectable'; + +const MOCK_VALUES = { + // CrawlerLogic + domains: [{ url: 'https://www.elastic.co' }, { url: 'https://www.swiftype.com' }], + // CrawlSelectDomainsModalLogic + selectedDomainUrls: ['https://www.elastic.co'], + isModalVisible: true, +}; + +const MOCK_ACTIONS = { + // CrawlSelectDomainsModalLogic + hideModal: jest.fn(), + onSelectDomainUrls: jest.fn(), + // CrawlerLogic + startCrawl: jest.fn(), +}; + +describe('CrawlSelectDomainsModal', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(MOCK_VALUES); + setMockActions(MOCK_ACTIONS); + + wrapper = shallow(); + }); + + it('is empty when the modal is hidden', () => { + setMockValues({ + ...MOCK_VALUES, + isModalVisible: false, + }); + + rerender(wrapper); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders as a modal when visible', () => { + expect(wrapper.is(EuiModal)).toBe(true); + }); + + it('can be closed', () => { + expect(wrapper.prop('onClose')).toEqual(MOCK_ACTIONS.hideModal); + expect(wrapper.find(EuiModalFooter).find(EuiButtonEmpty).prop('onClick')).toEqual( + MOCK_ACTIONS.hideModal + ); + }); + + it('allows the user to select domains', () => { + expect(wrapper.find(SimplifiedSelectable).props()).toEqual({ + options: ['https://www.elastic.co', 'https://www.swiftype.com'], + selectedOptions: ['https://www.elastic.co'], + onChange: MOCK_ACTIONS.onSelectDomainUrls, + }); + }); + + describe('submit button', () => { + it('is disabled when no domains are selected', () => { + setMockValues({ + ...MOCK_VALUES, + selectedDomainUrls: [], + }); + + rerender(wrapper); + + expect(wrapper.find(EuiModalFooter).find(EuiButton).prop('disabled')).toEqual(true); + }); + + it('starts a crawl and hides the modal', () => { + wrapper.find(EuiModalFooter).find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalledWith({ + domain_allowlist: MOCK_VALUES.selectedDomainUrls, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx new file mode 100644 index 000000000000..211266a779df --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiNotificationBadge, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; +import { SimplifiedSelectable } from './simplified_selectable'; + +import './crawl_select_domains_modal.scss'; + +export const CrawlSelectDomainsModal: React.FC = () => { + const { domains } = useValues(CrawlerLogic); + const domainUrls = domains.map((domain) => domain.url); + + const crawlSelectDomainsModalLogic = CrawlSelectDomainsModalLogic({ domains }); + const { isDataLoading, isModalVisible, selectedDomainUrls } = useValues( + crawlSelectDomainsModalLogic + ); + const { hideModal, onSelectDomainUrls } = useActions(crawlSelectDomainsModalLogic); + + const { startCrawl } = useActions(CrawlerLogic); + + if (!isModalVisible) { + return null; + } + + return ( + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.modalHeaderTitle', + { + defaultMessage: 'Crawl select domains', + } + )} + + + 0 ? 'accent' : 'subdued'} + > + {selectedDomainUrls.length} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.selectedDescriptor', + { + defaultMessage: 'selected', + } + )} + + + + + + + + {CANCEL_BUTTON_LABEL} + { + startCrawl({ domain_allowlist: selectedDomainUrls }); + }} + disabled={selectedDomainUrls.length === 0} + isLoading={isDataLoading} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.startCrawlButtonLabel', + { + defaultMessage: 'Apply and crawl now', + } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts new file mode 100644 index 000000000000..ef6ef4d09fad --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; + +describe('CrawlSelectDomainsModalLogic', () => { + const { mount } = new LogicMounter(CrawlSelectDomainsModalLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(CrawlSelectDomainsModalLogic.values).toEqual({ + isDataLoading: false, + isModalVisible: false, + selectedDomainUrls: [], + }); + }); + + describe('actions', () => { + describe('hideModal', () => { + it('hides the modal', () => { + CrawlSelectDomainsModalLogic.actions.hideModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + + describe('showModal', () => { + it('shows the modal', () => { + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(true); + }); + + it('resets the selected options', () => { + mount({ + selectedDomainUrls: ['https://www.elastic.co', 'https://www.swiftype.com'], + }); + + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([]); + }); + }); + + describe('onSelectDomainUrls', () => { + it('saves the urls', () => { + mount({ + selectedDomainUrls: [], + }); + + CrawlSelectDomainsModalLogic.actions.onSelectDomainUrls([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + }); + }); + + describe('[CrawlerLogic.actionTypes.startCrawl]', () => { + it('enables loading state', () => { + mount({ + isDataLoading: false, + }); + + CrawlerLogic.actions.startCrawl(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(true); + }); + }); + + describe('[CrawlerLogic.actionTypes.onStartCrawlRequestComplete]', () => { + it('disables loading state and hides the modal', () => { + mount({ + isDataLoading: true, + isModalVisible: true, + }); + + CrawlerLogic.actions.onStartCrawlRequestComplete(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(false); + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts new file mode 100644 index 000000000000..088950cbffd3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlerDomain } from '../../types'; + +export interface CrawlSelectDomainsLogicProps { + domains: CrawlerDomain[]; +} + +export interface CrawlSelectDomainsLogicValues { + isDataLoading: boolean; + isModalVisible: boolean; + selectedDomainUrls: string[]; +} + +export interface CrawlSelectDomainsModalLogicActions { + hideModal(): void; + onSelectDomainUrls(domainUrls: string[]): { domainUrls: string[] }; + showModal(): void; +} + +export const CrawlSelectDomainsModalLogic = kea< + MakeLogicType< + CrawlSelectDomainsLogicValues, + CrawlSelectDomainsModalLogicActions, + CrawlSelectDomainsLogicProps + > +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawl_select_domains_modal'], + actions: () => ({ + hideModal: true, + onSelectDomainUrls: (domainUrls) => ({ domainUrls }), + showModal: true, + }), + reducers: () => ({ + isDataLoading: [ + false, + { + [CrawlerLogic.actionTypes.startCrawl]: () => true, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + isModalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + selectedDomainUrls: [ + [], + { + showModal: () => [], + onSelectDomainUrls: (_, { domainUrls }) => domainUrls, + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx new file mode 100644 index 000000000000..a90259f8dac3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSelectable, EuiSelectableList, EuiSelectableSearch } from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { SimplifiedSelectable } from './simplified_selectable'; + +describe('SimplifiedSelectable', () => { + let wrapper: ShallowWrapper; + + const MOCK_ON_CHANGE = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + wrapper = shallow( + + ); + }); + + it('combines the options and selected options', () => { + expect(wrapper.find(EuiSelectable).prop('options')).toEqual([ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + }); + + it('passes newly selected options to the callback', () => { + wrapper.find(EuiSelectable).simulate('change', [ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + + expect(MOCK_ON_CHANGE).toHaveBeenCalledWith(['cat', 'fish']); + }); + + describe('select all button', () => { + it('it is disabled when all options are already selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="SelectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="SelectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith(['cat', 'dog', 'fish']); + }); + }); + + describe('deselect all button', () => { + it('it is disabled when all no options are selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="DeselectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="DeselectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith([]); + }); + }); + + it('renders a search bar and selectable list', () => { + const fullRender = mountWithIntl( + + ); + + expect(fullRender.find(EuiSelectableSearch)).toHaveLength(1); + expect(fullRender.find(EuiSelectableList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx new file mode 100644 index 000000000000..07ede1c59971 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSelectable } from '@elastic/eui'; +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + options: string[]; + selectedOptions: string[]; + onChange(selectedOptions: string[]): void; +} + +export interface OptionMap { + [key: string]: boolean; +} + +export const SimplifiedSelectable: React.FC = ({ options, selectedOptions, onChange }) => { + const selectedOptionsMap: OptionMap = selectedOptions.reduce( + (acc, selectedOption) => ({ + ...acc, + [selectedOption]: true, + }), + {} + ); + + const selectableOptions: Array> = options.map((option) => ({ + label: option, + checked: selectedOptionsMap[option] ? 'on' : undefined, + })); + + return ( + <> + + + onChange(options)} + disabled={selectedOptions.length === options.length} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.selectAllButtonLabel', + { + defaultMessage: 'Select all', + } + )} + + + + onChange([])} + disabled={selectedOptions.length === 0} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.deselectAllButtonLabel', + { + defaultMessage: 'Deselect all', + } + )} + + + + { + onChange( + newSelectableOptions.filter((option) => option.checked).map((option) => option.label) + ); + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx index c46c360934d0..cc8b1891838b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx @@ -16,6 +16,7 @@ import { EuiButton } from '@elastic/eui'; import { CrawlerDomain, CrawlerStatus } from '../../types'; import { CrawlerStatusIndicator } from './crawler_status_indicator'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; const MOCK_VALUES = { @@ -72,9 +73,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Start a crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); @@ -87,9 +86,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Retry crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx index c02e45f02c40..d750cf100202 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx @@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n'; import { CrawlerLogic } from '../../crawler_logic'; import { CrawlerStatus } from '../../types'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; export const CrawlerStatusIndicator: React.FC = () => { const { domains, mostRecentCrawlRequestStatus } = useValues(CrawlerLogic); - const { startCrawl, stopCrawl } = useActions(CrawlerLogic); + const { stopCrawl } = useActions(CrawlerLogic); const disabledButton = ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', { @@ -40,26 +41,27 @@ export const CrawlerStatusIndicator: React.FC = () => { switch (mostRecentCrawlRequestStatus) { case CrawlerStatus.Success: return ( - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', + + /> ); case CrawlerStatus.Failed: case CrawlerStatus.Canceled: return ( - - {i18n.translate( + + /> ); case CrawlerStatus.Pending: case CrawlerStatus.Suspended: diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx new file mode 100644 index 000000000000..6d9f1cd7be64 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { ReactWrapper, shallow } from 'enzyme'; + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiResizeObserver, +} from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { StartCrawlContextMenu } from './start_crawl_context_menu'; + +const MOCK_ACTIONS = { + startCrawl: jest.fn(), + showModal: jest.fn(), +}; + +describe('StartCrawlContextMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + }); + + it('is initially closed', () => { + const wrapper = shallow(); + + expect(wrapper.is(EuiPopover)).toBe(true); + expect(wrapper.prop('isOpen')).toEqual(false); + }); + + describe('user actions', () => { + let wrapper: ReactWrapper; + let menuItems: ReactWrapper; + + beforeEach(() => { + wrapper = mountWithIntl(); + + wrapper.find(EuiButton).simulate('click'); + + menuItems = wrapper + .find(EuiContextMenuPanel) + .find(EuiResizeObserver) + .find(EuiContextMenuItem); + }); + + it('can be opened', () => { + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); + expect(menuItems.length).toEqual(2); + }); + + it('can start crawls', () => { + menuItems.at(0).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalled(); + }); + + it('can open a modal to start a crawl with selected domains', () => { + menuItems.at(1).simulate('click'); + + expect(MOCK_ACTIONS.showModal).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx new file mode 100644 index 000000000000..1182a845bd4f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from '../crawl_select_domains_modal/crawl_select_domains_modal_logic'; + +interface Props { + menuButtonLabel?: string; + fill?: boolean; +} + +export const StartCrawlContextMenu: React.FC = ({ menuButtonLabel, fill }) => { + const { startCrawl } = useActions(CrawlerLogic); + const { showModal: showCrawlSelectDomainsModal } = useActions(CrawlSelectDomainsModalLogic); + + const [isPopoverOpen, setPopover] = useState(false); + + const togglePopover = () => setPopover(!isPopoverOpen); + + const closePopover = () => setPopover(false); + + return ( + + {menuButtonLabel} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + closePopover(); + startCrawl(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlAllDomainsMenuLabel', + { + defaultMessage: 'Crawl all domains on this engine', + } + )} + , + { + closePopover(); + showCrawlSelectDomainsModal(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlSelectDomainsMenuLabel', + { + defaultMessage: 'Crawl select domains', + } + )} + , + ]} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index e622798e688a..59ec64c69d5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -226,7 +226,7 @@ describe('CrawlerLogic', () => { CrawlerStatus.Running, CrawlerStatus.Canceling, ].forEach((status) => { - it(`creates a new timeout for status ${status}`, async () => { + it(`creates a new timeout for most recent crawl request status ${status}`, async () => { jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlerData'); http.get.mockReturnValueOnce( Promise.resolve({ @@ -260,6 +260,27 @@ describe('CrawlerLogic', () => { expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); }); }); + + it('clears the timeout if no events are active', async () => { + jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); + + http.get.mockReturnValueOnce( + Promise.resolve({ + ...MOCK_SERVER_CRAWLER_DATA, + events: [ + { + status: CrawlerStatus.Failed, + crawl_config: {}, + }, + ], + }) + ); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); + }); }); it('calls flashApiErrors when there is an error on the request for crawler data', async () => { @@ -276,23 +297,36 @@ describe('CrawlerLogic', () => { describe('startCrawl', () => { describe('success path', () => { - it('creates a new crawl request and then fetches the latest crawler data', async () => { + it('creates a new crawl request, fetches latest crawler data, then marks the request complete', async () => { jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); http.post.mockReturnValueOnce(Promise.resolve()); CrawlerLogic.actions.startCrawl(); await nextTick(); expect(http.post).toHaveBeenCalledWith( - '/internal/app_search/engines/some-engine/crawler/crawl_requests' + '/internal/app_search/engines/some-engine/crawler/crawl_requests', + { body: JSON.stringify({ overrides: {} }) } ); expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); }); }); itShowsServerErrorAsFlashMessage(http.post, () => { CrawlerLogic.actions.startCrawl(); }); + + it('marks the request complete even after an error', async () => { + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); + http.post.mockReturnValueOnce(Promise.reject()); + + CrawlerLogic.actions.startCrawl(); + await nextTick(); + + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); + }); }); describe('stopCrawl', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 08a01af67ece..d68dbc59f06d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -48,7 +48,8 @@ interface CrawlerActions { fetchCrawlerData(): void; onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout }; onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData }; - startCrawl(): void; + onStartCrawlRequestComplete(): void; + startCrawl(overrides?: object): { overrides?: object }; stopCrawl(): void; } @@ -60,7 +61,8 @@ export const CrawlerLogic = kea>({ fetchCrawlerData: true, onCreateNewTimeout: (timeoutId) => ({ timeoutId }), onReceiveCrawlerData: (data) => ({ data }), - startCrawl: () => null, + onStartCrawlRequestComplete: true, + startCrawl: (overrides) => ({ overrides }), stopCrawl: () => null, }, reducers: { @@ -135,15 +137,19 @@ export const CrawlerLogic = kea>({ actions.createNewTimeoutForCrawlerData(POLLING_DURATION_ON_FAILURE); } }, - startCrawl: async () => { + startCrawl: async ({ overrides = {} }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; try { - await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`); + await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`, { + body: JSON.stringify({ overrides }), + }); actions.fetchCrawlerData(); } catch (e) { flashAPIErrors(e); + } finally { + actions.onStartCrawlRequestComplete(); } }, stopCrawl: async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 4d72b854bddf..509346542ae1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -23,6 +23,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -215,4 +216,10 @@ describe('CrawlerOverview', () => { expect(wrapper.find(AddDomainFormErrors)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index c68e75790f07..f1f25dfb4dc5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -24,6 +24,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -138,6 +139,7 @@ export const CrawlerOverview: React.FC = () => { )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index ed445b923ea2..addf4093a167 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; import { getPageHeaderActions } from '../../../test_helpers'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -92,4 +93,10 @@ describe('CrawlerSingleDomain', () => { expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index a4b2a9709cd6..63b9c3f080ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -17,6 +17,7 @@ import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; import { CrawlRulesTable } from './components/crawl_rules_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -78,6 +79,7 @@ export const CrawlerSingleDomain: React.FC = () => { + ); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index c9212bca322d..fe225f62d1dc 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -141,6 +141,19 @@ describe('crawler routes', () => { mockRouter.shouldValidate(request); }); + it('validates correctly with overrides', () => { + const request = { + params: { name: 'some-engine' }, + body: { overrides: { domain_allowlist: [] } }, + }; + mockRouter.shouldValidate(request); + }); + + it('validates correctly with empty overrides', () => { + const request = { params: { name: 'some-engine' }, body: { overrides: {} } }; + mockRouter.shouldValidate(request); + }); + it('fails validation without name', () => { const request = { params: {} }; mockRouter.shouldThrow(request); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index f0fdc5c16098..5adffe1ff3ee 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -63,6 +63,13 @@ export function registerCrawlerRoutes({ params: schema.object({ name: schema.string(), }), + body: schema.object({ + overrides: schema.maybe( + schema.object({ + domain_allowlist: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }), }, }, enterpriseSearchRequestHandler.createRequest({ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 743ff40ecf5e..98e96ce59856 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -206,7 +206,12 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { updatedAgentPolicy: NewAgentPolicy ) => { if (selectedTab === SelectedPolicyTab.NEW) { - if (!updatedAgentPolicy.name || !updatedAgentPolicy.namespace) { + if ( + !updatedAgentPolicy.name || + updatedAgentPolicy.name.trim() === '' || + !updatedAgentPolicy.namespace || + updatedAgentPolicy.namespace.trim() === '' + ) { setHasAgentPolicyError(true); } else { setHasAgentPolicyError(false); diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index bf386e7f463a..472f7378028b 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -133,7 +133,7 @@ export const checkVersionIsSame = (version: string, kibanaVersion: string) => { const checkSourceUriAllowed = (sourceUri?: string) => { if (sourceUri && !appContextService.getConfig()?.developer?.allowAgentUpgradeSourceUri) { throw new Error( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); } }; diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index e1398aea6363..d15d73fca733 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -11,9 +11,15 @@ import { agentPolicyStatuses, dataTypes } from '../../../common'; import { PackagePolicySchema, NamespaceSchema } from './package_policy'; +function validateNonEmptyString(val: string) { + if (val.trim() === '') { + return 'Invalid empty string'; + } +} + export const AgentPolicyBaseSchema = { id: schema.maybe(schema.string()), - name: schema.string({ minLength: 1 }), + name: schema.string({ minLength: 1, validate: validateNonEmptyString }), namespace: NamespaceSchema, description: schema.maybe(schema.string()), is_managed: schema.maybe(schema.boolean()), diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts index 555ae7544180..5bc2087dc63a 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts @@ -16,13 +16,15 @@ export const DASHBOARD_REQUEST_BODY = { }; export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { - const createDashboardUrl = useKibana().services.dashboard?.dashboardUrlGenerator?.createUrl; - const savedObjectsClient = useKibana().services.savedObjects.client; + const { + dashboard, + savedObjects: { client: savedObjectsClient }, + } = useKibana().services; const [buttonHref, setButtonHref] = useState(); useEffect(() => { - if (createDashboardUrl && savedObjectsClient) { + if (dashboard?.locator && savedObjectsClient) { savedObjectsClient.find(DASHBOARD_REQUEST_BODY).then( async (DashboardsSO?: { savedObjects?: Array<{ @@ -31,7 +33,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { }>; }) => { if (DashboardsSO?.savedObjects?.length) { - const dashboardUrl = await createDashboardUrl({ + const dashboardUrl = await dashboard?.locator?.getUrl({ dashboardId: DashboardsSO.savedObjects[0].id, timeRange: { to, @@ -43,7 +45,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { } ); } - }, [createDashboardUrl, from, savedObjectsClient, to]); + }, [dashboard, from, savedObjectsClient, to]); return { buttonHref, diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx index 002dc18227f6..5b8bf180da1f 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx @@ -14,40 +14,48 @@ export const useRiskyHostsDashboardLinks = ( from: string, listItems: LinkPanelListItem[] ) => { - const createDashboardUrl = useKibana().services.dashboard?.locator?.getLocation; + const { dashboard } = useKibana().services; + const dashboardId = useRiskyHostsDashboardId(); const [listItemsWithLinks, setListItemsWithLinks] = useState([]); useEffect(() => { let cancelled = false; const createLinks = async () => { - if (createDashboardUrl && dashboardId) { + if (dashboard?.locator && dashboardId) { const dashboardUrls = await Promise.all( - listItems.map((listItem) => - createDashboardUrl({ - dashboardId, - timeRange: { - to, - from, - }, - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { match_phrase: { 'host.name': listItem.title } }, - }, - ], - }) + listItems.reduce( + (acc: Array>, listItem) => + dashboard && dashboard.locator + ? [ + ...acc, + dashboard.locator.getUrl({ + dashboardId, + timeRange: { + to, + from, + }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { match_phrase: { 'host.name': listItem.title } }, + }, + ], + }), + ] + : acc, + [] ) ); - if (!cancelled) { + if (!cancelled && dashboardUrls.length) { setListItemsWithLinks( listItems.map((item, i) => ({ ...item, - path: dashboardUrls[i] as unknown as string, + path: dashboardUrls[i], })) ); } @@ -59,7 +67,7 @@ export const useRiskyHostsDashboardLinks = ( return () => { cancelled = true; }; - }, [createDashboardUrl, dashboardId, from, listItems, to]); + }, [dashboard, dashboardId, from, listItems, to]); return { listItemsWithLinks }; }; diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts index 6cef41347bbf..26e3d726a10c 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts @@ -29,6 +29,26 @@ export const ServiceLocationCodec = t.interface({ url: t.string, }); +export const ServiceLocationErrors = t.array( + t.intersection([ + t.interface({ + locationId: t.string, + error: t.interface({ + reason: t.string, + status: t.number, + }), + }), + t.partial({ + failed_monitors: t.array( + t.interface({ + id: t.string, + message: t.string, + }) + ), + }), + ]) +); + export const ServiceLocationsCodec = t.array(ServiceLocationCodec); export const isServiceLocationInvalid = (location: ServiceLocation) => @@ -42,3 +62,4 @@ export type ManifestLocation = t.TypeOf; export type ServiceLocation = t.TypeOf; export type ServiceLocations = t.TypeOf; export type ServiceLocationsApiResponse = t.TypeOf; +export type ServiceLocationErrors = t.TypeOf; diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index ce197d574aa1..fe8a4960eac1 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -7,8 +7,8 @@ export * from './data_view_permissions'; export * from './uptime.journey'; -export * from './monitor_management.journey'; export * from './step_duration.journey'; export * from './alerts'; export * from './read_only_user'; export * from './monitor_name.journey'; +export * from './monitor_management.journey'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts index beb84a9a003a..456d219adef0 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts @@ -12,7 +12,7 @@ * 2.0. */ -import { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { journey, step, expect, after, before, Page } from '@elastic/synthetics'; import { monitorManagementPageProvider } from '../page_objects/monitor_management'; import { byTestId } from './utils'; @@ -23,6 +23,11 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => await uptime.waitForLoadingToFinish(); }); + after(async () => { + await uptime.navigateToMonitorManagement(); + await uptime.deleteMonitor(); + }); + step('Go to monitor-management', async () => { await uptime.navigateToMonitorManagement(); }); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index 057ce21ec510..fd877708f2bc 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -88,7 +88,7 @@ export function monitorManagementPageProvider({ } else { await page.click('text=Save monitor'); } - return await this.findByTestSubj('uptimeAddMonitorSuccess'); + return await this.findByText('Monitor added successfully.'); }, async fillCodeEditor(value: string) { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx index 8c9dc7ffe627..314347331b5b 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx @@ -17,8 +17,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useSelector } from 'react-redux'; import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { MONITOR_MANAGEMENT_ROUTE } from '../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; @@ -28,6 +29,10 @@ import { SyntheticsMonitor } from '../../../../common/runtime_types'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { TestRun } from '../test_now_mode/test_now_mode'; +import { monitorManagementListSelector } from '../../../state/selectors'; + +import { kibanaService } from '../../../state/kibana_service'; + export interface ActionBarProps { monitor: SyntheticsMonitor; isValid: boolean; @@ -39,11 +44,11 @@ export interface ActionBarProps { export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: ActionBarProps) => { const { monitorId } = useParams<{ monitorId: string }>(); const { basePath } = useContext(UptimeSettingsContext); + const { locations } = useSelector(monitorManagementListSelector); const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false); const [isSaving, setIsSaving] = useState(false); - - const { notifications } = useKibana(); + const [isSuccessful, setIsSuccessful] = useState(false); const { data, status } = useFetcher(() => { if (!isSaving || !isValid) { @@ -55,6 +60,9 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti }); }, [monitor, monitorId, isValid, isSaving]); + const hasErrors = data && Object.keys(data).length; + const loading = status === FETCH_STATUS.LOADING; + const handleOnSave = useCallback(() => { if (onSave) { onSave(); @@ -75,23 +83,57 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti setIsSaving(false); } if (status === FETCH_STATUS.FAILURE) { - notifications.toasts.danger({ - title:

{MONITOR_FAILURE_LABEL}

, + kibanaService.toasts.addDanger({ + title: MONITOR_FAILURE_LABEL, toastLifeTimeMs: 3000, }); - } else if (status === FETCH_STATUS.SUCCESS) { - notifications.toasts.success({ - title: ( -

- {monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL} -

- ), + } else if (status === FETCH_STATUS.SUCCESS && !hasErrors && !loading) { + kibanaService.toasts.addSuccess({ + title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, toastLifeTimeMs: 3000, }); + setIsSuccessful(true); + } else if (hasErrors && !loading) { + Object.values(data).forEach((location) => { + const { status: responseStatus, reason } = location.error || {}; + kibanaService.toasts.addWarning({ + title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { + defaultMessage: `Unable to sync monitor config`, + }), + text: toMountPoint( + <> +

+ {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { + defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, + values: { + location: locations?.find((loc) => loc?.id === location.locationId)?.label, + }, + })} +

+

+ {status + ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { + defaultMessage: 'Status: {status}. ', + values: { status: responseStatus }, + }) + : null} + {reason + ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { + defaultMessage: 'Reason: {reason}.', + values: { reason }, + }) + : null} +

+ + ), + toastLifeTimeMs: 30000, + }); + }); + setIsSuccessful(true); } - }, [data, status, notifications.toasts, isSaving, isValid, monitorId]); + }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); - return status === FETCH_STATUS.SUCCESS ? ( + return isSuccessful ? ( ) : ( @@ -191,7 +233,6 @@ const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( } ); -// TODO: Discuss error states with product const MONITOR_FAILURE_LABEL = i18n.translate( 'xpack.uptime.monitorManagement.monitorFailureMessage', { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx new file mode 100644 index 000000000000..f217631bfe33 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { FETCH_STATUS } from '../../../../../observability/public'; +import { + DataStream, + HTTPFields, + ScheduleUnit, + SyntheticsMonitor, +} from '../../../../common/runtime_types'; +import { spyOnUseFetcher } from '../../../lib/helper/spy_use_fetcher'; +import * as kibana from '../../../state/kibana_service'; +import { ActionBar } from './action_bar'; +import { mockLocationsState } from '../mocks'; + +jest.mock('../../../state/kibana_service', () => ({ + ...jest.requireActual('../../../state/kibana_service'), + kibanaService: { + toasts: { + addWarning: jest.fn(), + }, + }, +})); + +const monitor: SyntheticsMonitor = { + name: 'test-monitor', + schedule: { + unit: ScheduleUnit.MINUTES, + number: '2', + }, + urls: 'https://elastic.co', + type: DataStream.HTTP, +} as unknown as HTTPFields; + +describe(' Service Errors', () => { + let useFetcher: jest.SpyInstance; + const toast = jest.fn(); + + beforeEach(() => { + useFetcher?.mockClear(); + useFetcher = spyOnUseFetcher({}); + }); + + it('Handles service errors', async () => { + jest.spyOn(kibana.kibanaService.toasts, 'addWarning').mockImplementation(toast); + useFetcher.mockReturnValue({ + data: [ + { locationId: 'us_central', error: { reason: 'Invalid config', status: 400 } }, + { locationId: 'us_central', error: { reason: 'Cannot schedule', status: 500 } }, + ], + status: FETCH_STATUS.SUCCESS, + refetch: () => {}, + }); + render(, { state: mockLocationsState }); + userEvent.click(screen.getByText('Save monitor')); + + await waitFor(() => { + expect(toast).toBeCalledTimes(2); + expect(toast).toBeCalledWith( + expect.objectContaining({ + title: 'Unable to sync monitor config', + toastLifeTimeMs: 30000, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts b/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts new file mode 100644 index 000000000000..1ec4437601d5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './locations'; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts b/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts new file mode 100644 index 000000000000..b4f23bed097c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const mockLocation = { + label: 'US Central', + id: 'us_central', + geo: { + lat: 1, + lon: 1, + }, + url: 'url', +}; + +export const mockLocationsState = { + monitorManagementList: { + locations: [mockLocation], + list: { + monitors: [], + perPage: 10, + page: 1, + total: 0, + }, + error: { + serviceLocations: null, + monitorList: null, + }, + loading: { + serviceLocations: false, + monitorList: false, + }, + }, +}; diff --git a/x-pack/plugins/uptime/public/state/api/monitor_management.ts b/x-pack/plugins/uptime/public/state/api/monitor_management.ts index ec2806907baa..206ba07dc4c2 100644 --- a/x-pack/plugins/uptime/public/state/api/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/api/monitor_management.ts @@ -13,6 +13,7 @@ import { ServiceLocations, SyntheticsMonitor, ServiceLocationsApiResponseCodec, + ServiceLocationErrors, } from '../../../common/runtime_types'; import { SyntheticsMonitorSavedObject } from '../../../common/types'; import { apiService } from './utils'; @@ -23,7 +24,7 @@ export const setMonitor = async ({ }: { monitor: SyntheticsMonitor; id?: string; -}): Promise => { +}): Promise => { if (id) { return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor); } else { diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts index 596a64b4d359..1e82ef77e083 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts @@ -11,7 +11,11 @@ import { catchError, tap } from 'rxjs/operators'; import * as https from 'https'; import { SslConfig } from '@kbn/server-http-tools'; import { Logger } from '../../../../../../src/core/server'; -import { MonitorFields, ServiceLocations } from '../../../common/runtime_types'; +import { + MonitorFields, + ServiceLocations, + ServiceLocationErrors, +} from '../../../common/runtime_types'; import { convertToDataStreamFormat } from './formatters/convert_to_data_stream'; import { ServiceConfig } from '../../../common/config'; @@ -109,7 +113,7 @@ export class ServiceAPIClient { }); }; - const pushErrors: Array<{ locationId: string; error: Error }> = []; + const pushErrors: ServiceLocationErrors = []; const promises: Array> = []; @@ -128,7 +132,7 @@ export class ServiceAPIClient { ); }), catchError((err) => { - pushErrors.push({ locationId: id, error: err }); + pushErrors.push({ locationId: id, error: err.response?.data }); this.logger.error(err); // we don't want to throw an unhandled exception here return of(true); diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index e00ea43a0240..417e0c76a9e6 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -101,6 +101,17 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); + it('should return a 400 with an empty name', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: ' ', + namespace: 'default', + }) + .expect(400); + }); + it('should return a 400 with an invalid namespace', async () => { await supertest .post(`/api/fleet/agent_policies`) diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 8901c3166ca1..57e57a6524b0 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -174,7 +174,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); expect(res.body.message).to.eql( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); }); it('should respond 400 if trying to upgrade an agent that is unenrolling', async () => { @@ -591,7 +591,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); expect(res.body.message).to.eql( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); });