diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index d13b833790a22..cb6a5383688dc 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -18,7 +18,13 @@ "presentationUtil", "visualizations" ], - "optionalPlugins": ["home", "spaces", "savedObjectsTaggingOss", "usageCollection"], + "optionalPlugins": [ + "home", + "spaces", + "savedObjectsTaggingOss", + "screenshotMode", + "usageCollection" + ], "server": true, "ui": true, "requiredBundles": ["home", "kibanaReact", "kibanaUtils", "presentationUtil"] diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index dcaf541619d6f..3e6566f0da0a4 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -21,6 +21,7 @@ import { EmbeddableRenderer } from '../services/embeddable'; import { DashboardTopNav, isCompleteDashboardAppState } from './top_nav/dashboard_top_nav'; import { DashboardAppServices, DashboardEmbedSettings, DashboardRedirect } from '../types'; import { createKbnUrlStateStorage, withNotifyOnErrors } from '../services/kibana_utils'; +import { createDashboardEditUrl } from '../dashboard_constants'; export interface DashboardAppProps { history: History; savedDashboardId?: string; @@ -34,7 +35,7 @@ export function DashboardApp({ redirectTo, history, }: DashboardAppProps) { - const { core, chrome, embeddable, onAppLeave, uiSettings, data } = + const { core, chrome, embeddable, onAppLeave, uiSettings, data, spacesService } = useKibana().services; const kbnUrlStateStorage = useMemo( @@ -109,6 +110,18 @@ export function DashboardApp({ embedSettings={embedSettings} dashboardAppState={dashboardAppState} /> + + {dashboardAppState.savedDashboard.outcome === 'conflict' && + dashboardAppState.savedDashboard.id && + dashboardAppState.savedDashboard.aliasId + ? spacesService?.ui.components.getLegacyUrlConflict({ + currentObjectId: dashboardAppState.savedDashboard.id, + otherObjectId: dashboardAppState.savedDashboard.aliasId, + otherObjectPath: `#${createDashboardEditUrl( + dashboardAppState.savedDashboard.aliasId + )}${history.location.search}`, + }) + : null}
diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index f160aef14f3a6..97f7cbc769851 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -84,6 +84,7 @@ export async function mountApp({ savedObjectsTaggingOss, visualizations, presentationUtil, + screenshotMode, } = pluginsStart; const activeSpaceId = @@ -129,6 +130,8 @@ export async function mountApp({ core.notifications.toasts, activeSpaceId || 'default' ), + spacesService: spacesApi, + screenshotModeService: screenshotMode, }; const getUrlStateStorage = (history: RouteComponentProps['history']) => 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 779ae97b2a0e2..fddcc309e1ef1 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 @@ -92,6 +92,8 @@ export const useDashboardAppState = ({ dashboardCapabilities, dashboardSessionStorage, scopedHistory, + spacesService, + screenshotModeService, } = services; const { docTitle } = chrome; const { notifications } = core; @@ -149,6 +151,25 @@ export const useDashboardAppState = ({ if (canceled || !loadSavedDashboardResult) return; const { savedDashboard, savedDashboardState } = loadSavedDashboardResult; + // If the saved dashboard is an alias match, then we will redirect + if (savedDashboard.outcome === 'aliasMatch' && savedDashboard.id && savedDashboard.aliasId) { + // We want to keep the "query" params on our redirect. + // But, these aren't true query params, they are technically part of the hash + // So, to get the new path, we will just replace the current id in the hash + // with the alias id + const path = scopedHistory().location.hash.replace( + savedDashboard.id, + savedDashboard.aliasId + ); + if (screenshotModeService?.isScreenshotMode()) { + scopedHistory().replace(path); + } else { + await spacesService?.ui.redirectLegacyUrl(path); + } + // Return so we don't run any more of the hook and let it rerun after the redirect that just happened + return; + } + /** * Combine initial state from the saved object, session storage, and URL, then dispatch it to Redux. */ @@ -340,6 +361,8 @@ export const useDashboardAppState = ({ search, query, data, + spacesService?.ui, + screenshotModeService, ]); /** 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 3913608c6beff..31579e92bd1ec 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 @@ -54,7 +54,10 @@ export const loadSavedDashboardState = async ({ await indexPatterns.ensureDefaultDataView(); let savedDashboard: DashboardSavedObject | undefined; try { - savedDashboard = (await savedDashboards.get(savedDashboardId)) as DashboardSavedObject; + savedDashboard = (await savedDashboards.get({ + id: savedDashboardId, + useResolve: true, + })) as DashboardSavedObject; } catch (error) { // E.g. a corrupt or deleted dashboard notifications.toasts.addDanger(error.message); diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 496526c08ece8..ff0ac0642ec91 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -12,6 +12,7 @@ import { filter, map } from 'rxjs/operators'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; +import { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public'; import { APP_WRAPPER_CLASS } from '../../../core/public'; import { App, @@ -115,6 +116,7 @@ export interface DashboardStartDependencies { savedObjects: SavedObjectsStart; presentationUtil: PresentationUtilPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; + screenshotMode?: ScreenshotModePluginStart; spaces?: SpacesPluginStart; visualizations: VisualizationsStart; } diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index b81cf57bbc963..8772f14a6ec4c 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { assign, cloneDeep } from 'lodash'; +import { SavedObjectsClientContract } from 'kibana/public'; import { EmbeddableStart } from '../services/embeddable'; import { SavedObject, SavedObjectsStart } from '../services/saved_objects'; import { Filter, ISearchSource, Query, RefreshInterval } from '../services/data'; @@ -32,12 +34,33 @@ export interface DashboardSavedObject extends SavedObject { getQuery(): Query; getFilters(): Filter[]; getFullEditPath: (editMode?: boolean) => string; + outcome?: string; + aliasId?: string; } +const defaults = { + title: '', + hits: 0, + description: '', + panelsJSON: '[]', + optionsJSON: JSON.stringify({ + // for BWC reasons we can't default dashboards that already exist without this setting to true. + useMargins: true, + syncColors: false, + hidePanelTitles: false, + } as DashboardOptions), + version: 1, + timeRestore: false, + timeTo: undefined, + timeFrom: undefined, + refreshInterval: undefined, +}; + // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( savedObjectStart: SavedObjectsStart, - embeddableStart: EmbeddableStart + embeddableStart: EmbeddableStart, + savedObjectsClient: SavedObjectsClientContract ): new (id: string) => DashboardSavedObject { class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type @@ -68,7 +91,10 @@ export function createSavedDashboardClass( public static searchSource = true; public showInRecentlyAccessed = true; - constructor(id: string) { + public outcome?: string; + public aliasId?: string; + + constructor(arg: { id: string; useResolve: boolean } | string) { super({ type: SavedDashboard.type, mapping: SavedDashboard.mapping, @@ -88,28 +114,53 @@ export function createSavedDashboardClass( }, // if this is null/undefined then the SavedObject will be assigned the defaults - id, + id: typeof arg === 'string' ? arg : arg.id, // default values that will get assigned if the doc is new - defaults: { - title: '', - hits: 0, - description: '', - panelsJSON: '[]', - optionsJSON: JSON.stringify({ - // for BWC reasons we can't default dashboards that already exist without this setting to true. - useMargins: true, - syncColors: false, - hidePanelTitles: false, - } as DashboardOptions), - version: 1, - timeRestore: false, - timeTo: undefined, - timeFrom: undefined, - refreshInterval: undefined, - }, + defaults, }); - this.getFullPath = () => `/app/dashboards#${createDashboardEditUrl(this.id)}`; + + const id: string = typeof arg === 'string' ? arg : arg.id; + const useResolve = typeof arg === 'string' ? false : arg.useResolve; + + this.getFullPath = () => `/app/dashboards#${createDashboardEditUrl(this.aliasId || this.id)}`; + + // Overwrite init if we want to use resolve + if (useResolve || true) { + this.init = async () => { + const esType = SavedDashboard.type; + // ensure that the esType is defined + if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); + + if (!id) { + // just assign the defaults and be done + assign(this, defaults); + await this.hydrateIndexPattern!(); + + return this; + } + + const { + outcome, + alias_target_id: aliasId, + saved_object: resp, + } = await savedObjectsClient.resolve(esType, id); + + const respMapped = { + _id: resp.id, + _type: resp.type, + _source: cloneDeep(resp.attributes), + references: resp.references, + found: !!resp._version, + }; + + this.outcome = outcome; + this.aliasId = aliasId; + await this.applyESResp(respMapped); + + return this; + }; + } } getQuery() { diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 014af306a3842..94877b6c3c823 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -27,6 +27,10 @@ export function createSavedDashboardLoader({ savedObjectsClient, embeddableStart, }: Services) { - const SavedDashboard = createSavedDashboardClass(savedObjects, embeddableStart); + const SavedDashboard = createSavedDashboardClass( + savedObjects, + embeddableStart, + savedObjectsClient + ); return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index bc56d1a0896d2..651a51834a794 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,6 +19,7 @@ import type { import { History } from 'history'; import { AnyAction, Dispatch } from 'redux'; import { BehaviorSubject, Subject } from 'rxjs'; +import { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public'; import { Query, Filter, IndexPattern, RefreshInterval, TimeRange } from './services/data'; import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; import { SharePluginStart } from './services/share'; @@ -35,6 +36,7 @@ import { IKbnUrlStateStorage } from './services/kibana_utils'; import { DashboardContainer, DashboardSavedObject } from '.'; import { VisualizationsStart } from '../../visualizations/public'; import { DashboardAppLocatorParams } from './locator'; +import { SpacesPluginStart } from './services/spaces'; export { SavedDashboardPanel }; @@ -203,4 +205,6 @@ export interface DashboardAppServices { dashboardSessionStorage: DashboardSessionStorage; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedQueryService: DataPublicPluginStart['query']['savedQueries']; + spacesService?: SpacesPluginStart; + screenshotModeService?: ScreenshotModePluginStart; }