[Dashboard] Use SavedObjectResolve (#111040)

* Switch Dashboard to use savedobjects.resolve when loading

* Don't use LegacyURI Redirect if in screenshot mode

* Pass query string on redirects

* Remove unused import

* Fix carrying query params through redirect

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Corey Robertson 2021-10-15 11:17:54 -04:00 committed by GitHub
parent d009e54199
commit cba83335ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 134 additions and 25 deletions

View file

@ -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"]

View file

@ -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<DashboardAppServices>().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}
<div className="dashboardViewport">
<EmbeddableRenderer embeddable={dashboardAppState.dashboardContainer} />
</div>

View file

@ -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']) =>

View file

@ -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,
]);
/**

View file

@ -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);

View file

@ -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;
}

View file

@ -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() {

View file

@ -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);
}

View file

@ -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;
}