mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[dashboard] Lazy DashboardRenderer (#192754)
Changes 1. expose DashboardRenderer as lazy loaded component to reduce static page load size 2. Use `onApiAvailable` prop to pass DashboardApi to parent instead of `forwardRef` 3. Decouple DashboardApi from legacy embeddable system. This changed functions such as `updateInput` and using redux `select` to access state. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
2d78f23dde
commit
e2380afd7b
45 changed files with 667 additions and 736 deletions
|
@ -12,55 +12,65 @@ import React, { useMemo } from 'react';
|
|||
import { useAsync } from 'react-use/lib';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
||||
import { AppMountParameters } from '@kbn/core/public';
|
||||
import { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { DashboardListingTable } from '@kbn/dashboard-plugin/public';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
import { DualReduxExample } from './dual_redux_example';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { DualDashboardsExample } from './dual_dashboards_example';
|
||||
import { StartDeps } from './plugin';
|
||||
import { StaticByValueExample } from './static_by_value_example';
|
||||
import { StaticByReferenceExample } from './static_by_reference_example';
|
||||
import { DynamicByReferenceExample } from './dynamically_add_panels_example';
|
||||
import { DashboardWithControlsExample } from './dashboard_with_controls_example';
|
||||
|
||||
const DASHBOARD_DEMO_PATH = '/dashboardDemo';
|
||||
const DASHBOARD_LIST_PATH = '/listingDemo';
|
||||
|
||||
export const renderApp = async (
|
||||
coreStart: CoreStart,
|
||||
{ data, dashboard }: StartDeps,
|
||||
{ element, history }: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
<PortableDashboardsDemos data={data} history={history} dashboard={dashboard} />,
|
||||
<PortableDashboardsDemos
|
||||
coreStart={coreStart}
|
||||
data={data}
|
||||
history={history}
|
||||
dashboard={dashboard}
|
||||
/>,
|
||||
element
|
||||
);
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
|
||||
const PortableDashboardsDemos = ({
|
||||
coreStart,
|
||||
data,
|
||||
dashboard,
|
||||
history,
|
||||
}: {
|
||||
coreStart: CoreStart;
|
||||
data: StartDeps['data'];
|
||||
dashboard: StartDeps['dashboard'];
|
||||
history: AppMountParameters['history'];
|
||||
}) => {
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
<Route exact path="/">
|
||||
<Redirect to={DASHBOARD_DEMO_PATH} />
|
||||
</Route>
|
||||
<Route path={DASHBOARD_LIST_PATH}>
|
||||
<PortableDashboardListingDemo history={history} />
|
||||
</Route>
|
||||
<Route path={DASHBOARD_DEMO_PATH}>
|
||||
<DashboardsDemo data={data} dashboard={dashboard} history={history} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
<KibanaRenderContextProvider i18n={coreStart.i18n} theme={coreStart.theme}>
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
<Route exact path="/">
|
||||
<Redirect to={DASHBOARD_DEMO_PATH} />
|
||||
</Route>
|
||||
<Route path={DASHBOARD_LIST_PATH}>
|
||||
<PortableDashboardListingDemo history={history} />
|
||||
</Route>
|
||||
<Route path={DASHBOARD_DEMO_PATH}>
|
||||
<DashboardsDemo data={data} dashboard={dashboard} history={history} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
</KibanaRenderContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -91,9 +101,7 @@ const DashboardsDemo = ({
|
|||
<>
|
||||
<DashboardWithControlsExample dataView={dataViews[0]} />
|
||||
<EuiSpacer size="xl" />
|
||||
<DynamicByReferenceExample />
|
||||
<EuiSpacer size="xl" />
|
||||
<DualReduxExample />
|
||||
<DualDashboardsExample />
|
||||
<EuiSpacer size="xl" />
|
||||
<StaticByReferenceExample dashboardId={logsSampleDashboardId} dataView={dataViews[0]} />
|
||||
<EuiSpacer size="xl" />
|
||||
|
|
|
@ -14,41 +14,29 @@ import type { DataView } from '@kbn/data-views-plugin/public';
|
|||
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { controlGroupStateBuilder } from '@kbn/controls-plugin/public';
|
||||
import {
|
||||
AwaitingDashboardAPI,
|
||||
DashboardApi,
|
||||
DashboardRenderer,
|
||||
DashboardCreationOptions,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
import { apiHasUniqueId } from '@kbn/presentation-publishing';
|
||||
import { FILTER_DEBUGGER_EMBEDDABLE_ID } from './constants';
|
||||
|
||||
export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => {
|
||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||
const [dashboard, setDashboard] = useState<DashboardApi | undefined>();
|
||||
|
||||
// add a filter debugger panel as soon as the dashboard becomes available
|
||||
useEffect(() => {
|
||||
if (!dashboard) return;
|
||||
(async () => {
|
||||
const api = await dashboard.addNewPanel(
|
||||
dashboard
|
||||
.addNewPanel(
|
||||
{
|
||||
panelType: FILTER_DEBUGGER_EMBEDDABLE_ID,
|
||||
initialState: {},
|
||||
},
|
||||
true
|
||||
);
|
||||
if (!apiHasUniqueId(api)) {
|
||||
return;
|
||||
}
|
||||
const prevPanelState = dashboard.getExplicitInput().panels[api.uuid];
|
||||
// resize the new panel so that it fills up the entire width of the dashboard
|
||||
dashboard.updateInput({
|
||||
panels: {
|
||||
[api.uuid]: {
|
||||
...prevPanelState,
|
||||
gridData: { i: api.uuid, x: 0, y: 0, w: 48, h: 12 },
|
||||
},
|
||||
},
|
||||
)
|
||||
.catch(() => {
|
||||
// ignore error - its an example
|
||||
});
|
||||
})();
|
||||
}, [dashboard]);
|
||||
|
||||
return (
|
||||
|
@ -88,7 +76,7 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
|||
}),
|
||||
};
|
||||
}}
|
||||
ref={setDashboard}
|
||||
onApiAvailable={setDashboard}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
|
|
|
@ -18,19 +18,16 @@ import {
|
|||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
AwaitingDashboardAPI,
|
||||
DashboardAPI,
|
||||
DashboardRenderer,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
import { DashboardApi, DashboardRenderer } from '@kbn/dashboard-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
|
||||
export const DualReduxExample = () => {
|
||||
const [firstDashboardContainer, setFirstDashboardContainer] = useState<AwaitingDashboardAPI>();
|
||||
const [secondDashboardContainer, setSecondDashboardContainer] = useState<AwaitingDashboardAPI>();
|
||||
export const DualDashboardsExample = () => {
|
||||
const [firstDashboardApi, setFirstDashboardApi] = useState<DashboardApi | undefined>();
|
||||
const [secondDashboardApi, setSecondDashboardApi] = useState<DashboardApi | undefined>();
|
||||
|
||||
const ButtonControls = ({ dashboard }: { dashboard: DashboardAPI }) => {
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const ButtonControls = ({ dashboardApi }: { dashboardApi: DashboardApi }) => {
|
||||
const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode);
|
||||
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
|
@ -48,7 +45,7 @@ export const DualReduxExample = () => {
|
|||
},
|
||||
]}
|
||||
idSelected={viewMode}
|
||||
onChange={(id, value) => dashboard.dispatch.setViewMode(value)}
|
||||
onChange={(id, value) => dashboardApi.setViewMode(value)}
|
||||
type="single"
|
||||
/>
|
||||
);
|
||||
|
@ -57,12 +54,12 @@ export const DualReduxExample = () => {
|
|||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Dual redux example</h2>
|
||||
<h2>Dual dashboards example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
Use the redux contexts from two different dashboard containers to independently set the
|
||||
view mode of each dashboard.
|
||||
Use the APIs from different dashboards to independently set the view mode of each
|
||||
dashboard.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
|
@ -73,18 +70,18 @@ export const DualReduxExample = () => {
|
|||
<h3>Dashboard #1</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
{firstDashboardContainer && <ButtonControls dashboard={firstDashboardContainer} />}
|
||||
{firstDashboardApi && <ButtonControls dashboardApi={firstDashboardApi} />}
|
||||
<EuiSpacer size="m" />
|
||||
<DashboardRenderer ref={setFirstDashboardContainer} />
|
||||
<DashboardRenderer onApiAvailable={setFirstDashboardApi} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Dashboard #2</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
{secondDashboardContainer && <ButtonControls dashboard={secondDashboardContainer} />}
|
||||
{secondDashboardApi && <ButtonControls dashboardApi={secondDashboardApi} />}
|
||||
<EuiSpacer size="m" />
|
||||
<DashboardRenderer ref={setSecondDashboardContainer} />
|
||||
<DashboardRenderer onApiAvailable={setSecondDashboardApi} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
|
@ -1,151 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { AwaitingDashboardAPI, DashboardRenderer } from '@kbn/dashboard-plugin/public';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
VisualizeEmbeddable,
|
||||
VisualizeInput,
|
||||
VisualizeOutput,
|
||||
} from '@kbn/visualizations-plugin/public/legacy/embeddable/visualize_embeddable';
|
||||
|
||||
const INPUT_KEY = 'portableDashboard:saveExample:input';
|
||||
|
||||
export const DynamicByReferenceExample = () => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [dashboard, setdashboard] = useState<AwaitingDashboardAPI>();
|
||||
|
||||
const onSave = async () => {
|
||||
if (!dashboard) return;
|
||||
setIsSaving(true);
|
||||
localStorage.setItem(INPUT_KEY, JSON.stringify(dashboard.getInput()));
|
||||
// simulated async save await
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const getPersistableInput = () => {
|
||||
let input = {};
|
||||
const inputAsString = localStorage.getItem(INPUT_KEY);
|
||||
if (inputAsString) {
|
||||
try {
|
||||
input = JSON.parse(inputAsString);
|
||||
} catch (e) {
|
||||
// ignore parse errors
|
||||
}
|
||||
return input;
|
||||
}
|
||||
};
|
||||
|
||||
const resetPersistableInput = () => {
|
||||
localStorage.removeItem(INPUT_KEY);
|
||||
if (dashboard) {
|
||||
const children = dashboard.getChildIds();
|
||||
children.map((childId) => {
|
||||
dashboard.removeEmbeddable(childId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addByValue = async () => {
|
||||
if (!dashboard) return;
|
||||
dashboard.addNewEmbeddable<VisualizeInput, VisualizeOutput, VisualizeEmbeddable>(
|
||||
'visualization',
|
||||
{
|
||||
title: 'Sample Markdown Vis',
|
||||
savedVis: {
|
||||
type: 'markdown',
|
||||
title: '',
|
||||
data: { aggs: [], searchSource: {} },
|
||||
params: {
|
||||
fontSize: 12,
|
||||
openLinksInNewTab: false,
|
||||
markdown: '### By Value Visualization\nThis is a sample by value panel.',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const disableButtons = useMemo(() => {
|
||||
return !dashboard || isSaving;
|
||||
}, [dashboard, isSaving]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Edit and save example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>Customize the dashboard and persist the state to local storage.</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiButton onClick={addByValue} isDisabled={disableButtons}>
|
||||
Add visualization by value
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton onClick={() => dashboard?.addFromLibrary()} isDisabled={disableButtons}>
|
||||
Add visualization from library
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiButton fill onClick={onSave} isLoading={isSaving} isDisabled={disableButtons}>
|
||||
Save to local storage
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
onClick={resetPersistableInput}
|
||||
isLoading={isSaving}
|
||||
isDisabled={disableButtons}
|
||||
>
|
||||
Empty dashboard and reset local storage
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<DashboardRenderer
|
||||
getCreationOptions={async () => {
|
||||
const persistedInput = getPersistableInput();
|
||||
return {
|
||||
getInitialInput: () => ({
|
||||
...persistedInput,
|
||||
timeRange: { from: 'now-30d', to: 'now' }, // need to set the time range for the by value vis
|
||||
}),
|
||||
};
|
||||
}}
|
||||
ref={setdashboard}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { AppMountParameters, CoreSetup, Plugin } from '@kbn/core/public';
|
||||
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
|
@ -34,9 +34,9 @@ export class PortableDashboardsExamplePlugin implements Plugin<void, void, Setup
|
|||
title: 'Portable dashboard examples',
|
||||
visibleIn: [],
|
||||
async mount(params: AppMountParameters) {
|
||||
const [, depsStart] = await core.getStartServices();
|
||||
const [coreStart, depsStart] = await core.getStartServices();
|
||||
const { renderApp } = await import('./app');
|
||||
return renderApp(depsStart, params);
|
||||
return renderApp(coreStart, depsStart, params);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -53,7 +53,12 @@ export class PortableDashboardsExamplePlugin implements Plugin<void, void, Setup
|
|||
});
|
||||
}
|
||||
|
||||
public async start() {}
|
||||
public async start(core: CoreStart, deps: StartDeps) {
|
||||
deps.dashboard.registerDashboardPanelPlacementSetting(FILTER_DEBUGGER_EMBEDDABLE_ID, () => ({
|
||||
width: 48,
|
||||
height: 12,
|
||||
}));
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,11 @@
|
|||
"@kbn/navigation-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/visualizations-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/controls-plugin",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/presentation-publishing"
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/react-kibana-context-render"
|
||||
]
|
||||
}
|
||||
|
|
62
src/plugins/dashboard/public/dashboard_api/types.ts
Normal file
62
src/plugins/dashboard/public/dashboard_api/types.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import {
|
||||
CanExpandPanels,
|
||||
PresentationContainer,
|
||||
TracksOverlays,
|
||||
} from '@kbn/presentation-containers';
|
||||
import {
|
||||
HasAppContext,
|
||||
HasType,
|
||||
PublishesDataViews,
|
||||
PublishesPanelTitle,
|
||||
PublishesSavedObjectId,
|
||||
PublishesUnifiedSearch,
|
||||
PublishesViewMode,
|
||||
PublishingSubject,
|
||||
ViewMode,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { DashboardPanelMap } from '../../common';
|
||||
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
|
||||
|
||||
export type DashboardApi = CanExpandPanels &
|
||||
HasAppContext &
|
||||
HasType<'dashboard'> &
|
||||
PresentationContainer &
|
||||
PublishesDataViews &
|
||||
Pick<PublishesPanelTitle, 'panelTitle'> &
|
||||
PublishesSavedObjectId &
|
||||
PublishesUnifiedSearch &
|
||||
PublishesViewMode &
|
||||
TracksOverlays & {
|
||||
addFromLibrary: () => void;
|
||||
asyncResetToLastSavedState: () => Promise<void>;
|
||||
controlGroupApi$: PublishingSubject<ControlGroupApi | undefined>;
|
||||
fullScreenMode$: PublishingSubject<boolean | undefined>;
|
||||
focusedPanelId$: PublishingSubject<string | undefined>;
|
||||
forceRefresh: () => void;
|
||||
getPanelsState: () => DashboardPanelMap;
|
||||
hasOverlays$: PublishingSubject<boolean | undefined>;
|
||||
hasRunMigrations$: PublishingSubject<boolean | undefined>;
|
||||
hasUnsavedChanges$: PublishingSubject<boolean | undefined>;
|
||||
managed$: PublishingSubject<boolean | undefined>;
|
||||
runInteractiveSave: (interactionMode: ViewMode) => Promise<SaveDashboardReturn | undefined>;
|
||||
runQuickSave: () => Promise<void>;
|
||||
scrollToTop: () => void;
|
||||
setFilters: (filters?: Filter[] | undefined) => void;
|
||||
setFullScreenMode: (fullScreenMode: boolean) => void;
|
||||
setQuery: (query?: Query | undefined) => void;
|
||||
setTags: (tags: string[]) => void;
|
||||
setTimeRange: (timeRange?: TimeRange | undefined) => void;
|
||||
setViewMode: (viewMode: ViewMode) => void;
|
||||
openSettingsFlyout: () => void;
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import { DashboardApi } from './types';
|
||||
|
||||
export const DashboardContext = createContext<DashboardApi | undefined>(undefined);
|
||||
|
||||
export const useDashboardApi = (): DashboardApi => {
|
||||
const api = useContext<DashboardApi | undefined>(DashboardContext);
|
||||
if (!api) {
|
||||
throw new Error('useDashboardApi must be used inside DashboardContext');
|
||||
}
|
||||
return api;
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { ViewMode } from '@kbn/presentation-publishing';
|
||||
|
||||
export const getDashboardPageTitle = () =>
|
||||
i18n.translate('dashboard.dashboardPageTitle', {
|
||||
|
@ -42,9 +42,13 @@ export const dashboardManagedBadge = {
|
|||
* @param viewMode {DashboardViewMode} the current mode. If in editing state, prepends 'Editing ' to the title.
|
||||
* @returns {string} A title to display to the user based on the above parameters.
|
||||
*/
|
||||
export function getDashboardTitle(title: string, viewMode: ViewMode, isNew: boolean): string {
|
||||
const isEditMode = viewMode === ViewMode.EDIT;
|
||||
const dashboardTitle = isNew ? getNewDashboardTitle() : title;
|
||||
export function getDashboardTitle(
|
||||
title: string | undefined,
|
||||
viewMode: ViewMode,
|
||||
isNew: boolean
|
||||
): string {
|
||||
const isEditMode = viewMode === 'edit';
|
||||
const dashboardTitle = isNew || !Boolean(title) ? getNewDashboardTitle() : (title as string);
|
||||
return isEditMode
|
||||
? i18n.translate('dashboard.strings.dashboardEditTitle', {
|
||||
defaultMessage: 'Editing {title}',
|
||||
|
|
|
@ -7,43 +7,46 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { History } from 'history';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import {
|
||||
DashboardAppNoDataPage,
|
||||
isDashboardAppInNoDataState,
|
||||
} from './no_data/dashboard_app_no_data';
|
||||
import {
|
||||
loadAndRemoveDashboardState,
|
||||
startSyncingDashboardUrlState,
|
||||
} from './url/sync_dashboard_url_state';
|
||||
import { loadAndRemoveDashboardState } from './url/url_utils';
|
||||
import {
|
||||
getSessionURLObservable,
|
||||
getSearchSessionIdFromURL,
|
||||
removeSearchSessionIdFromURL,
|
||||
createSessionRestorationDataProvider,
|
||||
} from './url/search_sessions_integration';
|
||||
import { DashboardAPI, DashboardRenderer } from '..';
|
||||
import { DashboardApi, DashboardRenderer } from '..';
|
||||
import { type DashboardEmbedSettings } from './types';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { AwaitingDashboardAPI } from '../dashboard_container';
|
||||
import { DashboardRedirect } from '../dashboard_container/types';
|
||||
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
|
||||
import { createDashboardEditUrl, DASHBOARD_APP_ID } from '../dashboard_constants';
|
||||
import {
|
||||
createDashboardEditUrl,
|
||||
DASHBOARD_APP_ID,
|
||||
DASHBOARD_STATE_STORAGE_KEY,
|
||||
} from '../dashboard_constants';
|
||||
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
|
||||
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
|
||||
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
|
||||
import { DashboardTopNav } from '../dashboard_top_nav';
|
||||
import { DashboardTabTitleSetter } from './tab_title_setter/dashboard_tab_title_setter';
|
||||
import { useObservabilityAIAssistantContext } from './hooks/use_observability_ai_assistant_context';
|
||||
import { SharedDashboardState } from '../../common';
|
||||
|
||||
export interface DashboardAppProps {
|
||||
history: History;
|
||||
|
@ -52,16 +55,6 @@ export interface DashboardAppProps {
|
|||
embedSettings?: DashboardEmbedSettings;
|
||||
}
|
||||
|
||||
export const DashboardAPIContext = createContext<AwaitingDashboardAPI>(null);
|
||||
|
||||
export const useDashboardAPI = (): DashboardAPI => {
|
||||
const api = useContext<AwaitingDashboardAPI>(DashboardAPIContext);
|
||||
if (api == null) {
|
||||
throw new Error('useDashboardAPI must be used inside DashboardAPIContext');
|
||||
}
|
||||
return api!;
|
||||
};
|
||||
|
||||
export function DashboardApp({
|
||||
savedDashboardId,
|
||||
embedSettings,
|
||||
|
@ -69,11 +62,12 @@ export function DashboardApp({
|
|||
history,
|
||||
}: DashboardAppProps) {
|
||||
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
|
||||
const [regenerateId, setRegenerateId] = useState(uuidv4());
|
||||
|
||||
useMount(() => {
|
||||
(async () => setShowNoDataPage(await isDashboardAppInNoDataState()))();
|
||||
});
|
||||
const [dashboardAPI, setDashboardAPI] = useState<AwaitingDashboardAPI>(null);
|
||||
const [dashboardApi, setDashboardApi] = useState<DashboardApi | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Unpack & set up dashboard services
|
||||
|
@ -94,7 +88,7 @@ export function DashboardApp({
|
|||
|
||||
useObservabilityAIAssistantContext({
|
||||
observabilityAIAssistant: observabilityAIAssistant.start,
|
||||
dashboardAPI,
|
||||
dashboardApi,
|
||||
search,
|
||||
dataViews,
|
||||
});
|
||||
|
@ -193,45 +187,46 @@ export function DashboardApp({
|
|||
* When the dashboard container is created, or re-created, start syncing dashboard state with the URL
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!dashboardAPI) return;
|
||||
const { stopWatchingAppStateInUrl } = startSyncingDashboardUrlState({
|
||||
kbnUrlStateStorage,
|
||||
dashboardAPI,
|
||||
});
|
||||
return () => stopWatchingAppStateInUrl();
|
||||
}, [dashboardAPI, kbnUrlStateStorage, savedDashboardId]);
|
||||
if (!dashboardApi) return;
|
||||
const appStateSubscription = kbnUrlStateStorage
|
||||
.change$(DASHBOARD_STATE_STORAGE_KEY)
|
||||
.pipe(debounceTime(10)) // debounce URL updates so react has time to unsubscribe when changing URLs
|
||||
.subscribe(() => {
|
||||
const rawAppStateInUrl = kbnUrlStateStorage.get<SharedDashboardState>(
|
||||
DASHBOARD_STATE_STORAGE_KEY
|
||||
);
|
||||
if (rawAppStateInUrl) setRegenerateId(uuidv4());
|
||||
});
|
||||
return () => appStateSubscription.unsubscribe();
|
||||
}, [dashboardApi, kbnUrlStateStorage, savedDashboardId]);
|
||||
|
||||
const locator = useMemo(() => url?.locators.get(DASHBOARD_APP_LOCATOR), [url]);
|
||||
|
||||
return (
|
||||
return showNoDataPage ? (
|
||||
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />
|
||||
) : (
|
||||
<>
|
||||
{showNoDataPage && (
|
||||
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />
|
||||
)}
|
||||
{!showNoDataPage && (
|
||||
{dashboardApi && (
|
||||
<>
|
||||
{dashboardAPI && (
|
||||
<>
|
||||
<DashboardTabTitleSetter dashboardContainer={dashboardAPI} />
|
||||
<DashboardTopNav
|
||||
redirectTo={redirectTo}
|
||||
embedSettings={embedSettings}
|
||||
dashboardContainer={dashboardAPI}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{getLegacyConflictWarning?.()}
|
||||
<DashboardRenderer
|
||||
locator={locator}
|
||||
ref={setDashboardAPI}
|
||||
dashboardRedirect={redirectTo}
|
||||
savedObjectId={savedDashboardId}
|
||||
showPlainSpinner={showPlainSpinner}
|
||||
getCreationOptions={getCreationOptions}
|
||||
<DashboardTabTitleSetter dashboardApi={dashboardApi} />
|
||||
<DashboardTopNav
|
||||
redirectTo={redirectTo}
|
||||
embedSettings={embedSettings}
|
||||
dashboardApi={dashboardApi}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{getLegacyConflictWarning?.()}
|
||||
<DashboardRenderer
|
||||
key={regenerateId}
|
||||
locator={locator}
|
||||
onApiAvailable={setDashboardApi}
|
||||
dashboardRedirect={redirectTo}
|
||||
savedObjectId={savedDashboardId}
|
||||
showPlainSpinner={showPlainSpinner}
|
||||
getCreationOptions={getCreationOptions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import { useEffect } from 'react';
|
||||
import type { Embeddable } from '@kbn/embeddable-plugin/public';
|
||||
import { getESQLQueryColumns } from '@kbn/esql-utils';
|
||||
import type { ISearchStart } from '@kbn/data-plugin/public';
|
||||
import {
|
||||
|
@ -29,7 +28,7 @@ import {
|
|||
} from '@kbn/lens-embeddable-utils/config_builder';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { LensEmbeddableInput } from '@kbn/lens-plugin/public';
|
||||
import type { AwaitingDashboardAPI } from '../../dashboard_container';
|
||||
import { DashboardApi } from '../../dashboard_api/types';
|
||||
|
||||
const chartTypes = [
|
||||
'xy',
|
||||
|
@ -47,12 +46,12 @@ const chartTypes = [
|
|||
|
||||
export function useObservabilityAIAssistantContext({
|
||||
observabilityAIAssistant,
|
||||
dashboardAPI,
|
||||
dashboardApi,
|
||||
search,
|
||||
dataViews,
|
||||
}: {
|
||||
observabilityAIAssistant: ObservabilityAIAssistantPublicStart | undefined;
|
||||
dashboardAPI: AwaitingDashboardAPI;
|
||||
dashboardApi: DashboardApi | undefined;
|
||||
search: ISearchStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
}) {
|
||||
|
@ -69,7 +68,7 @@ export function useObservabilityAIAssistantContext({
|
|||
return setScreenContext({
|
||||
screenDescription:
|
||||
'The user is looking at the dashboard app. Here they can add visualizations to a dashboard and save them',
|
||||
actions: dashboardAPI
|
||||
actions: dashboardApi
|
||||
? [
|
||||
createScreenContextAction(
|
||||
{
|
||||
|
@ -361,8 +360,8 @@ export function useObservabilityAIAssistantContext({
|
|||
query: dataset,
|
||||
})) as LensEmbeddableInput;
|
||||
|
||||
return dashboardAPI
|
||||
.addNewPanel<Embeddable>({
|
||||
return dashboardApi
|
||||
.addNewPanel({
|
||||
panelType: 'lens',
|
||||
initialState: embeddableInput,
|
||||
})
|
||||
|
@ -383,5 +382,5 @@ export function useObservabilityAIAssistantContext({
|
|||
]
|
||||
: [],
|
||||
});
|
||||
}, [observabilityAIAssistant, dashboardAPI, search, dataViews]);
|
||||
}, [observabilityAIAssistant, dashboardApi, search, dataViews]);
|
||||
}
|
||||
|
|
|
@ -9,29 +9,25 @@
|
|||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/common';
|
||||
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { DashboardAPI } from '../..';
|
||||
import { getDashboardTitle } from '../_dashboard_app_strings';
|
||||
import { DashboardApi } from '../..';
|
||||
import { getNewDashboardTitle } from '../_dashboard_app_strings';
|
||||
|
||||
export const DashboardTabTitleSetter = ({
|
||||
dashboardContainer,
|
||||
}: {
|
||||
dashboardContainer: DashboardAPI;
|
||||
}) => {
|
||||
export const DashboardTabTitleSetter = ({ dashboardApi }: { dashboardApi: DashboardApi }) => {
|
||||
const {
|
||||
chrome: { docTitle: chromeDocTitle },
|
||||
} = pluginServices.getServices();
|
||||
const title = dashboardContainer.select((state) => state.explicitInput.title);
|
||||
const lastSavedId = dashboardContainer.select((state) => state.componentState.lastSavedId);
|
||||
const [title, lastSavedId] = useBatchedPublishingSubjects(
|
||||
dashboardApi.panelTitle,
|
||||
dashboardApi.savedObjectId
|
||||
);
|
||||
|
||||
/**
|
||||
* Set chrome tab title when dashboard's title changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
/** We do not want the tab title to include the "Editing" prefix, so always send in view mode */
|
||||
chromeDocTitle.change(getDashboardTitle(title, ViewMode.VIEW, !lastSavedId));
|
||||
chromeDocTitle.change(!lastSavedId ? getNewDashboardTitle() : title ?? lastSavedId);
|
||||
}, [title, chromeDocTitle, lastSavedId]);
|
||||
|
||||
return null;
|
||||
|
|
|
@ -11,7 +11,7 @@ import React from 'react';
|
|||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { getAddControlButtonTitle } from '../../_dashboard_app_strings';
|
||||
import { useDashboardAPI } from '../../dashboard_app';
|
||||
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
|
||||
|
||||
interface Props {
|
||||
closePopover: () => void;
|
||||
|
@ -19,9 +19,9 @@ interface Props {
|
|||
}
|
||||
|
||||
export const AddDataControlButton = ({ closePopover, controlGroupApi, ...rest }: Props) => {
|
||||
const dashboard = useDashboardAPI();
|
||||
const dashboardApi = useDashboardApi();
|
||||
const onSave = () => {
|
||||
dashboard.scrollToTop();
|
||||
dashboardApi.scrollToTop();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
getAddTimeSliderControlButtonTitle,
|
||||
getOnlyOneTimeSliderControlMsg,
|
||||
} from '../../_dashboard_app_strings';
|
||||
import { useDashboardAPI } from '../../dashboard_app';
|
||||
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
|
||||
|
||||
interface Props {
|
||||
closePopover: () => void;
|
||||
|
@ -27,7 +27,7 @@ interface Props {
|
|||
|
||||
export const AddTimeSliderControlButton = ({ closePopover, controlGroupApi, ...rest }: Props) => {
|
||||
const [hasTimeSliderControl, setHasTimeSliderControl] = useState(false);
|
||||
const dashboard = useDashboardAPI();
|
||||
const dashboardApi = useDashboardApi();
|
||||
|
||||
useEffect(() => {
|
||||
if (!controlGroupApi) {
|
||||
|
@ -58,7 +58,7 @@ export const AddTimeSliderControlButton = ({ closePopover, controlGroupApi, ...r
|
|||
id: uuidv4(),
|
||||
},
|
||||
});
|
||||
dashboard.scrollToTop();
|
||||
dashboardApi.scrollToTop();
|
||||
closePopover();
|
||||
}}
|
||||
data-test-subj="controls-create-timeslider-button"
|
||||
|
|
|
@ -18,10 +18,10 @@ import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
|||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings';
|
||||
import { EditorMenu } from './editor_menu';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { ControlsToolbarButton } from './controls_toolbar_button';
|
||||
import { DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
|
||||
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
|
||||
|
||||
export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }) {
|
||||
const {
|
||||
|
@ -32,7 +32,7 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
|
|||
} = pluginServices.getServices();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const dashboard = useDashboardAPI();
|
||||
const dashboardApi = useDashboardApi();
|
||||
|
||||
const stateTransferService = getStateTransfer();
|
||||
|
||||
|
@ -70,13 +70,13 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
|
|||
stateTransferService.navigateToEditor(appId, {
|
||||
path,
|
||||
state: {
|
||||
originatingApp: dashboard.getAppContext()?.currentAppId,
|
||||
originatingPath: dashboard.getAppContext()?.getCurrentPath?.(),
|
||||
originatingApp: dashboardApi.getAppContext()?.currentAppId,
|
||||
originatingPath: dashboardApi.getAppContext()?.getCurrentPath?.(),
|
||||
searchSessionId: search.session.getSessionId(),
|
||||
},
|
||||
});
|
||||
},
|
||||
[stateTransferService, dashboard, search.session, trackUiMetric]
|
||||
[stateTransferService, dashboardApi, search.session, trackUiMetric]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -85,11 +85,11 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
|
|||
* dismissNotification: Optional, if not passed a toast will appear in the dashboard
|
||||
*/
|
||||
|
||||
const controlGroupApi = useStateFromPublishingSubject(dashboard.controlGroupApi$);
|
||||
const controlGroupApi = useStateFromPublishingSubject(dashboardApi.controlGroupApi$);
|
||||
const extraButtons = [
|
||||
<EditorMenu createNewVisType={createNewVisType} isDisabled={isDisabled} api={dashboard} />,
|
||||
<EditorMenu createNewVisType={createNewVisType} isDisabled={isDisabled} />,
|
||||
<AddFromLibraryButton
|
||||
onClick={() => dashboard.addFromLibrary()}
|
||||
onClick={() => dashboardApi.addFromLibrary()}
|
||||
size="s"
|
||||
data-test-subj="dashboardAddFromLibraryButton"
|
||||
isDisabled={isDisabled}
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from 'react';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { EditorMenu } from './editor_menu';
|
||||
import { DashboardAPIContext } from '../dashboard_app';
|
||||
import { buildMockDashboard } from '../../mocks';
|
||||
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
|
||||
import { DashboardApi } from '../../dashboard_api/types';
|
||||
|
||||
jest.mock('../../services/plugin_services', () => {
|
||||
const module = jest.requireActual('../../services/plugin_services');
|
||||
|
@ -37,21 +37,14 @@ jest.mock('../../services/plugin_services', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked<PresentationContainer>;
|
||||
|
||||
describe('editor menu', () => {
|
||||
const defaultProps: ComponentProps<typeof EditorMenu> = {
|
||||
api: mockApi,
|
||||
createNewVisType: jest.fn(),
|
||||
};
|
||||
|
||||
it('renders without crashing', async () => {
|
||||
render(<EditorMenu {...defaultProps} />, {
|
||||
render(<EditorMenu createNewVisType={jest.fn()} />, {
|
||||
wrapper: ({ children }) => {
|
||||
return (
|
||||
<DashboardAPIContext.Provider value={buildMockDashboard()}>
|
||||
<DashboardContext.Provider value={buildMockDashboard() as DashboardApi}>
|
||||
{children}
|
||||
</DashboardAPIContext.Provider>
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,15 +17,15 @@ import { ToolbarButton } from '@kbn/shared-ux-button-toolbar';
|
|||
|
||||
import { useGetDashboardPanels, DashboardPanelSelectionListFlyout } from './add_new_panel';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
|
||||
|
||||
interface EditorMenuProps
|
||||
extends Pick<Parameters<typeof useGetDashboardPanels>[0], 'api' | 'createNewVisType'> {
|
||||
extends Pick<Parameters<typeof useGetDashboardPanels>[0], 'createNewVisType'> {
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProps) => {
|
||||
const dashboardAPI = useDashboardAPI();
|
||||
export const EditorMenu = ({ createNewVisType, isDisabled }: EditorMenuProps) => {
|
||||
const dashboardApi = useDashboardApi();
|
||||
|
||||
const {
|
||||
overlays,
|
||||
|
@ -34,16 +34,16 @@ export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProp
|
|||
} = pluginServices.getServices();
|
||||
|
||||
const fetchDashboardPanels = useGetDashboardPanels({
|
||||
api,
|
||||
api: dashboardApi,
|
||||
createNewVisType,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// ensure opened dashboard is closed if a navigation event happens;
|
||||
return () => {
|
||||
dashboardAPI.clearOverlays();
|
||||
dashboardApi.clearOverlays();
|
||||
};
|
||||
}, [dashboardAPI]);
|
||||
}, [dashboardApi]);
|
||||
|
||||
const openDashboardPanelSelectionFlyout = useCallback(
|
||||
function openDashboardPanelSelectionFlyout() {
|
||||
|
@ -55,10 +55,10 @@ export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProp
|
|||
React.createElement(function () {
|
||||
return (
|
||||
<DashboardPanelSelectionListFlyout
|
||||
close={dashboardAPI.clearOverlays}
|
||||
close={dashboardApi.clearOverlays}
|
||||
{...{
|
||||
paddingSize: flyoutPanelPaddingSize,
|
||||
fetchDashboardPanels: fetchDashboardPanels.bind(null, dashboardAPI.clearOverlays),
|
||||
fetchDashboardPanels: fetchDashboardPanels.bind(null, dashboardApi.clearOverlays),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -66,7 +66,7 @@ export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProp
|
|||
{ analytics, theme, i18n: i18nStart }
|
||||
);
|
||||
|
||||
dashboardAPI.openOverlay(
|
||||
dashboardApi.openOverlay(
|
||||
overlays.openFlyout(mount, {
|
||||
size: 'm',
|
||||
maxWidth: 500,
|
||||
|
@ -74,13 +74,13 @@ export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProp
|
|||
'aria-labelledby': 'addPanelsFlyout',
|
||||
'data-test-subj': 'dashboardPanelSelectionFlyout',
|
||||
onClose(overlayRef) {
|
||||
dashboardAPI.clearOverlays();
|
||||
dashboardApi.clearOverlays();
|
||||
overlayRef.close();
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[analytics, theme, i18nStart, dashboardAPI, overlays, fetchDashboardPanels]
|
||||
[analytics, theme, i18nStart, dashboardApi, overlays, fetchDashboardPanels]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -77,7 +77,7 @@ describe('ShowShareModal', () => {
|
|||
return {
|
||||
isDirty: true,
|
||||
anchorElement: document.createElement('div'),
|
||||
getDashboardState: () => ({} as DashboardContainerInput),
|
||||
getPanelsState: () => ({}),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -125,19 +125,17 @@ describe('ShowShareModal', () => {
|
|||
query: { query: 'bye', language: 'kuery' },
|
||||
} as unknown as DashboardContainerInput;
|
||||
const showModalProps = getPropsAndShare(unsavedDashboardState);
|
||||
showModalProps.getDashboardState = () => {
|
||||
showModalProps.getPanelsState = () => {
|
||||
return {
|
||||
panels: {
|
||||
panel_1: {
|
||||
type: 'panel_type',
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
|
||||
panelRefName: 'superPanel',
|
||||
explicitInput: {
|
||||
id: 'superPanel',
|
||||
},
|
||||
panel_1: {
|
||||
type: 'panel_type',
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
|
||||
panelRefName: 'superPanel',
|
||||
explicitInput: {
|
||||
id: 'superPanel',
|
||||
},
|
||||
},
|
||||
} as unknown as DashboardContainerInput;
|
||||
};
|
||||
};
|
||||
ShowShareModal(showModalProps);
|
||||
expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1);
|
||||
|
@ -171,36 +169,6 @@ describe('ShowShareModal', () => {
|
|||
},
|
||||
};
|
||||
const props = getPropsAndShare(unsavedDashboardState);
|
||||
const getCurrentState: () => DashboardContainerInput = () => {
|
||||
return {
|
||||
panels: {
|
||||
panel_1: {
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
|
||||
type: 'superType',
|
||||
explicitInput: {
|
||||
id: 'whatever',
|
||||
changedKey1: 'NOT changed',
|
||||
},
|
||||
},
|
||||
panel_2: {
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
|
||||
type: 'superType',
|
||||
explicitInput: {
|
||||
id: 'whatever2',
|
||||
changedKey2: 'definitely NOT changed',
|
||||
},
|
||||
},
|
||||
panel_3: {
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
|
||||
type: 'superType',
|
||||
explicitInput: {
|
||||
id: 'whatever2',
|
||||
changedKey3: 'should still exist',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as DashboardContainerInput;
|
||||
};
|
||||
pluginServices.getServices().dashboardBackup.getState = jest.fn().mockReturnValue({
|
||||
dashboardState: unsavedDashboardState,
|
||||
panels: {
|
||||
|
@ -208,7 +176,32 @@ describe('ShowShareModal', () => {
|
|||
panel_2: { changedKey2: 'definitely changed' },
|
||||
},
|
||||
});
|
||||
props.getDashboardState = getCurrentState;
|
||||
props.getPanelsState = () => ({
|
||||
panel_1: {
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
|
||||
type: 'superType',
|
||||
explicitInput: {
|
||||
id: 'whatever',
|
||||
changedKey1: 'NOT changed',
|
||||
},
|
||||
},
|
||||
panel_2: {
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
|
||||
type: 'superType',
|
||||
explicitInput: {
|
||||
id: 'whatever2',
|
||||
changedKey2: 'definitely NOT changed',
|
||||
},
|
||||
},
|
||||
panel_3: {
|
||||
gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
|
||||
type: 'superType',
|
||||
explicitInput: {
|
||||
id: 'whatever2',
|
||||
changedKey3: 'should still exist',
|
||||
},
|
||||
},
|
||||
});
|
||||
ShowShareModal(props);
|
||||
expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1);
|
||||
const shareLocatorParams = (
|
||||
|
|
|
@ -17,11 +17,7 @@ import { getStateFromKbnUrl, setStateToKbnUrl, unhashUrl } from '@kbn/kibana-uti
|
|||
import { omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import {
|
||||
convertPanelMapToSavedPanels,
|
||||
DashboardContainerInput,
|
||||
DashboardPanelMap,
|
||||
} from '../../../../common';
|
||||
import { convertPanelMapToSavedPanels, DashboardPanelMap } from '../../../../common';
|
||||
import { DashboardLocatorParams } from '../../../dashboard_container';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { dashboardUrlParams } from '../../dashboard_router';
|
||||
|
@ -35,7 +31,7 @@ export interface ShowShareModalProps {
|
|||
savedObjectId?: string;
|
||||
dashboardTitle?: string;
|
||||
anchorElement: HTMLElement;
|
||||
getDashboardState: () => DashboardContainerInput;
|
||||
getPanelsState: () => DashboardPanelMap;
|
||||
}
|
||||
|
||||
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
|
||||
|
@ -51,7 +47,7 @@ export function ShowShareModal({
|
|||
anchorElement,
|
||||
savedObjectId,
|
||||
dashboardTitle,
|
||||
getDashboardState,
|
||||
getPanelsState,
|
||||
}: ShowShareModalProps) {
|
||||
const {
|
||||
dashboardCapabilities: { createShortUrl: allowShortUrl },
|
||||
|
@ -140,7 +136,7 @@ export function ShowShareModal({
|
|||
return;
|
||||
}
|
||||
|
||||
const latestPanels = getDashboardState().panels;
|
||||
const latestPanels = getPanelsState();
|
||||
// apply modifications to panels.
|
||||
const modifiedPanels = panelModifications
|
||||
? Object.entries(panelModifications).reduce((acc, [panelId, unsavedPanel]) => {
|
||||
|
|
|
@ -14,14 +14,15 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
|
|||
import { TopNavMenuData } from '@kbn/navigation-plugin/public';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { UI_SETTINGS } from '../../../common';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
import { topNavStrings } from '../_dashboard_app_strings';
|
||||
import { ShowShareModal } from './share/show_share_modal';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
|
||||
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
|
||||
import { SaveDashboardReturn } from '../../services/dashboard_content_management/types';
|
||||
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
|
||||
|
||||
export const useDashboardMenuItems = ({
|
||||
isLabsShown,
|
||||
|
@ -52,17 +53,25 @@ export const useDashboardMenuItems = ({
|
|||
/**
|
||||
* Unpack dashboard state from redux
|
||||
*/
|
||||
const dashboard = useDashboardAPI();
|
||||
const dashboardApi = useDashboardApi();
|
||||
|
||||
const hasRunMigrations = dashboard.select(
|
||||
(state) => state.componentState.hasRunClientsideMigrations
|
||||
const [
|
||||
dashboardTitle,
|
||||
hasOverlays,
|
||||
hasRunMigrations,
|
||||
hasUnsavedChanges,
|
||||
lastSavedId,
|
||||
managed,
|
||||
viewMode,
|
||||
] = useBatchedPublishingSubjects(
|
||||
dashboardApi.panelTitle,
|
||||
dashboardApi.hasOverlays$,
|
||||
dashboardApi.hasRunMigrations$,
|
||||
dashboardApi.hasUnsavedChanges$,
|
||||
dashboardApi.savedObjectId,
|
||||
dashboardApi.managed$,
|
||||
dashboardApi.viewMode
|
||||
);
|
||||
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
|
||||
const hasOverlays = dashboard.select((state) => state.componentState.hasOverlays);
|
||||
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const managed = dashboard.select((state) => state.componentState.managed);
|
||||
const disableTopNav = isSaveInProgress || hasOverlays;
|
||||
|
||||
/**
|
||||
|
@ -75,10 +84,10 @@ export const useDashboardMenuItems = ({
|
|||
anchorElement,
|
||||
savedObjectId: lastSavedId,
|
||||
isDirty: Boolean(hasUnsavedChanges),
|
||||
getDashboardState: () => dashboard.getState().explicitInput,
|
||||
getPanelsState: dashboardApi.getPanelsState,
|
||||
});
|
||||
},
|
||||
[dashboardTitle, hasUnsavedChanges, lastSavedId, dashboard]
|
||||
[dashboardTitle, hasUnsavedChanges, lastSavedId, dashboardApi]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -86,17 +95,17 @@ export const useDashboardMenuItems = ({
|
|||
*/
|
||||
const quickSaveDashboard = useCallback(() => {
|
||||
setIsSaveInProgress(true);
|
||||
dashboard
|
||||
dashboardApi
|
||||
.runQuickSave()
|
||||
.then(() => setTimeout(() => setIsSaveInProgress(false), CHANGE_CHECK_DEBOUNCE));
|
||||
}, [dashboard]);
|
||||
}, [dashboardApi]);
|
||||
|
||||
/**
|
||||
* initiate interactive dashboard copy action
|
||||
*/
|
||||
const dashboardInteractiveSave = useCallback(() => {
|
||||
dashboard.runInteractiveSave(viewMode).then((result) => maybeRedirect(result));
|
||||
}, [maybeRedirect, dashboard, viewMode]);
|
||||
dashboardApi.runInteractiveSave(viewMode).then((result) => maybeRedirect(result));
|
||||
}, [maybeRedirect, dashboardApi, viewMode]);
|
||||
|
||||
/**
|
||||
* Show the dashboard's "Confirm reset changes" modal. If confirmed:
|
||||
|
@ -106,10 +115,10 @@ export const useDashboardMenuItems = ({
|
|||
const [isResetting, setIsResetting] = useState(false);
|
||||
const resetChanges = useCallback(
|
||||
(switchToViewMode: boolean = false) => {
|
||||
dashboard.clearOverlays();
|
||||
dashboardApi.clearOverlays();
|
||||
const switchModes = switchToViewMode
|
||||
? () => {
|
||||
dashboard.dispatch.setViewMode(ViewMode.VIEW);
|
||||
dashboardApi.setViewMode(ViewMode.VIEW);
|
||||
dashboardBackup.storeViewMode(ViewMode.VIEW);
|
||||
}
|
||||
: undefined;
|
||||
|
@ -120,15 +129,15 @@ export const useDashboardMenuItems = ({
|
|||
confirmDiscardUnsavedChanges(() => {
|
||||
batch(async () => {
|
||||
setIsResetting(true);
|
||||
await dashboard.asyncResetToLastSavedState();
|
||||
await dashboardApi.asyncResetToLastSavedState();
|
||||
if (isMounted()) {
|
||||
setIsResetting(false);
|
||||
switchModes?.();
|
||||
}
|
||||
});
|
||||
}, viewMode);
|
||||
}, viewMode as ViewMode);
|
||||
},
|
||||
[dashboard, dashboardBackup, hasUnsavedChanges, viewMode, isMounted]
|
||||
[dashboardApi, dashboardBackup, hasUnsavedChanges, viewMode, isMounted]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -141,7 +150,7 @@ export const useDashboardMenuItems = ({
|
|||
...topNavStrings.fullScreen,
|
||||
id: 'full-screen',
|
||||
testId: 'dashboardFullScreenMode',
|
||||
run: () => dashboard.dispatch.setFullScreenMode(true),
|
||||
run: () => dashboardApi.setFullScreenMode(true),
|
||||
disableButton: disableTopNav,
|
||||
} as TopNavMenuData,
|
||||
|
||||
|
@ -161,8 +170,8 @@ export const useDashboardMenuItems = ({
|
|||
className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
|
||||
run: () => {
|
||||
dashboardBackup.storeViewMode(ViewMode.EDIT);
|
||||
dashboard.dispatch.setViewMode(ViewMode.EDIT);
|
||||
dashboard.clearOverlays();
|
||||
dashboardApi.setViewMode(ViewMode.EDIT);
|
||||
dashboardApi.clearOverlays();
|
||||
},
|
||||
disableButton: disableTopNav,
|
||||
} as TopNavMenuData,
|
||||
|
@ -218,7 +227,7 @@ export const useDashboardMenuItems = ({
|
|||
id: 'settings',
|
||||
testId: 'dashboardSettingsButton',
|
||||
disableButton: disableTopNav,
|
||||
run: () => dashboard.showSettings(),
|
||||
run: () => dashboardApi.openSettingsFlyout(),
|
||||
},
|
||||
};
|
||||
}, [
|
||||
|
@ -230,7 +239,7 @@ export const useDashboardMenuItems = ({
|
|||
dashboardInteractiveSave,
|
||||
viewMode,
|
||||
showShare,
|
||||
dashboard,
|
||||
dashboardApi,
|
||||
setIsLabsShown,
|
||||
isLabsShown,
|
||||
dashboardBackup,
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import semverSatisfies from 'semver/functions/satisfies';
|
||||
|
||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -20,7 +19,6 @@ import {
|
|||
convertSavedPanelsToPanelMap,
|
||||
DashboardContainerInput,
|
||||
} from '../../../common';
|
||||
import { DashboardAPI } from '../../dashboard_container';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
|
||||
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
|
||||
|
@ -86,23 +84,3 @@ export const loadAndRemoveDashboardState = (
|
|||
|
||||
return partialState;
|
||||
};
|
||||
|
||||
export const startSyncingDashboardUrlState = ({
|
||||
kbnUrlStateStorage,
|
||||
dashboardAPI,
|
||||
}: {
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||
dashboardAPI: DashboardAPI;
|
||||
}) => {
|
||||
const appStateSubscription = kbnUrlStateStorage
|
||||
.change$(DASHBOARD_STATE_STORAGE_KEY)
|
||||
.pipe(debounceTime(10)) // debounce URL updates so react has time to unsubscribe when changing URLs
|
||||
.subscribe(() => {
|
||||
const stateFromUrl = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
||||
if (Object.keys(stateFromUrl).length === 0) return;
|
||||
dashboardAPI.updateInput(stateFromUrl);
|
||||
});
|
||||
|
||||
const stopWatchingAppStateInUrl = () => appStateSubscription.unsubscribe();
|
||||
return { stopWatchingAppStateInUrl };
|
||||
};
|
|
@ -301,23 +301,42 @@ export class DashboardContainer
|
|||
this.select = reduxTools.select;
|
||||
|
||||
this.savedObjectId = new BehaviorSubject(this.getDashboardSavedObjectId());
|
||||
this.expandedPanelId = new BehaviorSubject(this.getExpandedPanelId());
|
||||
this.focusedPanelId$ = new BehaviorSubject(this.getState().componentState.focusedPanelId);
|
||||
this.managed$ = new BehaviorSubject(this.getState().componentState.managed);
|
||||
this.fullScreenMode$ = new BehaviorSubject(this.getState().componentState.fullScreenMode);
|
||||
this.hasRunMigrations$ = new BehaviorSubject(
|
||||
this.getState().componentState.hasRunClientsideMigrations
|
||||
);
|
||||
this.hasUnsavedChanges$ = new BehaviorSubject(this.getState().componentState.hasUnsavedChanges);
|
||||
this.hasOverlays$ = new BehaviorSubject(this.getState().componentState.hasOverlays);
|
||||
this.publishingSubscription.add(
|
||||
this.onStateChange(() => {
|
||||
if (this.savedObjectId.value === this.getDashboardSavedObjectId()) return;
|
||||
this.savedObjectId.next(this.getDashboardSavedObjectId());
|
||||
})
|
||||
);
|
||||
this.publishingSubscription.add(
|
||||
this.savedObjectId.subscribe(() => {
|
||||
this.hadContentfulRender = false;
|
||||
})
|
||||
);
|
||||
|
||||
this.expandedPanelId = new BehaviorSubject(this.getDashboardSavedObjectId());
|
||||
this.publishingSubscription.add(
|
||||
this.onStateChange(() => {
|
||||
if (this.expandedPanelId.value === this.getExpandedPanelId()) return;
|
||||
this.expandedPanelId.next(this.getExpandedPanelId());
|
||||
const state = this.getState();
|
||||
if (this.savedObjectId.value !== this.getDashboardSavedObjectId()) {
|
||||
this.savedObjectId.next(this.getDashboardSavedObjectId());
|
||||
}
|
||||
if (this.expandedPanelId.value !== this.getExpandedPanelId()) {
|
||||
this.expandedPanelId.next(this.getExpandedPanelId());
|
||||
}
|
||||
if (this.focusedPanelId$.value !== state.componentState.focusedPanelId) {
|
||||
this.focusedPanelId$.next(state.componentState.focusedPanelId);
|
||||
}
|
||||
if (this.managed$.value !== state.componentState.managed) {
|
||||
this.managed$.next(state.componentState.managed);
|
||||
}
|
||||
if (this.fullScreenMode$.value !== state.componentState.fullScreenMode) {
|
||||
this.fullScreenMode$.next(state.componentState.fullScreenMode);
|
||||
}
|
||||
if (this.hasRunMigrations$.value !== state.componentState.hasRunClientsideMigrations) {
|
||||
this.hasRunMigrations$.next(state.componentState.hasRunClientsideMigrations);
|
||||
}
|
||||
if (this.hasUnsavedChanges$.value !== state.componentState.hasUnsavedChanges) {
|
||||
this.hasUnsavedChanges$.next(state.componentState.hasUnsavedChanges);
|
||||
}
|
||||
if (this.hasOverlays$.value !== state.componentState.hasOverlays) {
|
||||
this.hasOverlays$.next(state.componentState.hasOverlays);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -519,7 +538,7 @@ export class DashboardContainer
|
|||
public runInteractiveSave = runInteractiveSave;
|
||||
public runQuickSave = runQuickSave;
|
||||
|
||||
public showSettings = showSettings;
|
||||
public openSettingsFlyout = showSettings;
|
||||
public addFromLibrary = addFromLibrary;
|
||||
|
||||
public duplicatePanel(id: string) {
|
||||
|
@ -533,6 +552,12 @@ export class DashboardContainer
|
|||
|
||||
public savedObjectId: BehaviorSubject<string | undefined>;
|
||||
public expandedPanelId: BehaviorSubject<string | undefined>;
|
||||
public focusedPanelId$: BehaviorSubject<string | undefined>;
|
||||
public managed$: BehaviorSubject<boolean | undefined>;
|
||||
public fullScreenMode$: BehaviorSubject<boolean | undefined>;
|
||||
public hasRunMigrations$: BehaviorSubject<boolean | undefined>;
|
||||
public hasUnsavedChanges$: BehaviorSubject<boolean | undefined>;
|
||||
public hasOverlays$: BehaviorSubject<boolean | undefined>;
|
||||
|
||||
public async replacePanel(idToRemove: string, { panelType, initialState }: PanelPackage) {
|
||||
const newId = await this.replaceEmbeddable(
|
||||
|
@ -795,10 +820,30 @@ export class DashboardContainer
|
|||
return this.getState().componentState.expandedPanelId;
|
||||
};
|
||||
|
||||
public getPanelsState = () => {
|
||||
return this.getState().explicitInput.panels;
|
||||
};
|
||||
|
||||
public setExpandedPanelId = (newId?: string) => {
|
||||
this.dispatch.setExpandedPanelId(newId);
|
||||
};
|
||||
|
||||
public setViewMode = (viewMode: ViewMode) => {
|
||||
this.dispatch.setViewMode(viewMode);
|
||||
};
|
||||
|
||||
public setFullScreenMode = (fullScreenMode: boolean) => {
|
||||
this.dispatch.setFullScreenMode(fullScreenMode);
|
||||
};
|
||||
|
||||
public setQuery = (query?: Query | undefined) => this.updateInput({ query });
|
||||
|
||||
public setFilters = (filters?: Filter[] | undefined) => this.updateInput({ filters });
|
||||
|
||||
public setTags = (tags: string[]) => {
|
||||
this.updateInput({ tags });
|
||||
};
|
||||
|
||||
public openOverlay = (ref: OverlayRef, options?: { focusedPanelId?: string }) => {
|
||||
this.clearOverlays();
|
||||
this.dispatch.setHasOverlays(true);
|
||||
|
|
|
@ -9,23 +9,10 @@
|
|||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { CanDuplicatePanels, CanExpandPanels, TracksOverlays } from '@kbn/presentation-containers';
|
||||
import {
|
||||
HasType,
|
||||
HasTypeDisplayName,
|
||||
PublishesUnifiedSearch,
|
||||
PublishesPanelTitle,
|
||||
PublishesSavedObjectId,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { HasTypeDisplayName, PublishesSavedObjectId } from '@kbn/presentation-publishing';
|
||||
import { DashboardPanelState } from '../../../common';
|
||||
import { DashboardContainer } from '../embeddable/dashboard_container';
|
||||
|
||||
// TODO lock down DashboardAPI
|
||||
export type DashboardAPI = DashboardContainer &
|
||||
Partial<
|
||||
HasType<'dashboard'> & PublishesUnifiedSearch & PublishesPanelTitle & PublishesSavedObjectId
|
||||
>;
|
||||
export type AwaitingDashboardAPI = DashboardAPI | null;
|
||||
|
||||
export const buildApiFromDashboardContainer = (container?: DashboardContainer) => container ?? null;
|
||||
|
||||
export type DashboardExternallyAccessibleApi = HasTypeDisplayName &
|
||||
|
|
|
@ -22,6 +22,7 @@ import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
|
|||
import { DashboardContainer } from '../embeddable/dashboard_container';
|
||||
import { DashboardCreationOptions } from '../embeddable/dashboard_container_factory';
|
||||
import { setStubKibanaServices as setPresentationPanelMocks } from '@kbn/presentation-panel-plugin/public/mocks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('dashboard renderer', () => {
|
||||
let mockDashboardContainer: DashboardContainer;
|
||||
|
@ -246,6 +247,7 @@ describe('dashboard renderer', () => {
|
|||
navigateToDashboard: jest.fn(),
|
||||
select: jest.fn().mockReturnValue('WhatAnExpandedPanel'),
|
||||
getInput: jest.fn().mockResolvedValue({}),
|
||||
expandedPanelId: new BehaviorSubject('panel1'),
|
||||
} as unknown as DashboardContainer;
|
||||
const mockSuccessFactory = {
|
||||
create: jest.fn().mockReturnValue(mockSuccessEmbeddable),
|
||||
|
|
|
@ -10,15 +10,7 @@
|
|||
import '../_dashboard_container.scss';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import useUnmount from 'react-use/lib/useUnmount';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
@ -27,6 +19,7 @@ import { ErrorEmbeddable, isErrorEmbeddable } from '@kbn/embeddable-plugin/publi
|
|||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
|
||||
|
||||
import { LocatorPublic } from '@kbn/share-plugin/common';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from '..';
|
||||
import { DashboardContainerInput } from '../../../common';
|
||||
import type { DashboardContainer } from '../embeddable/dashboard_container';
|
||||
|
@ -37,13 +30,11 @@ import {
|
|||
} from '../embeddable/dashboard_container_factory';
|
||||
import { DashboardLocatorParams, DashboardRedirect } from '../types';
|
||||
import { Dashboard404Page } from './dashboard_404';
|
||||
import {
|
||||
AwaitingDashboardAPI,
|
||||
buildApiFromDashboardContainer,
|
||||
DashboardAPI,
|
||||
} from './dashboard_api';
|
||||
import { DashboardApi } from '../../dashboard_api/types';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
|
||||
export interface DashboardRendererProps {
|
||||
onApiAvailable?: (api: DashboardApi) => void;
|
||||
savedObjectId?: string;
|
||||
showPlainSpinner?: boolean;
|
||||
dashboardRedirect?: DashboardRedirect;
|
||||
|
@ -51,150 +42,136 @@ export interface DashboardRendererProps {
|
|||
locator?: Pick<LocatorPublic<DashboardLocatorParams>, 'navigate' | 'getRedirectUrl'>;
|
||||
}
|
||||
|
||||
export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRendererProps>(
|
||||
({ savedObjectId, getCreationOptions, dashboardRedirect, showPlainSpinner, locator }, ref) => {
|
||||
const dashboardRoot = useRef(null);
|
||||
const dashboardViewport = useRef(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [screenshotMode, setScreenshotMode] = useState(false);
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
|
||||
const [fatalError, setFatalError] = useState<ErrorEmbeddable | undefined>();
|
||||
const [dashboardMissing, setDashboardMissing] = useState(false);
|
||||
export function DashboardRenderer({
|
||||
savedObjectId,
|
||||
getCreationOptions,
|
||||
dashboardRedirect,
|
||||
showPlainSpinner,
|
||||
locator,
|
||||
onApiAvailable,
|
||||
}: DashboardRendererProps) {
|
||||
const dashboardRoot = useRef(null);
|
||||
const dashboardViewport = useRef(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
|
||||
const [fatalError, setFatalError] = useState<ErrorEmbeddable | undefined>();
|
||||
const [dashboardMissing, setDashboardMissing] = useState(false);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => buildApiFromDashboardContainer(dashboardContainer) as DashboardAPI,
|
||||
[dashboardContainer]
|
||||
);
|
||||
const { embeddable, screenshotMode } = pluginServices.getServices();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
// Lazy loading all services is required in this component because it is exported and contributes to the bundle size.
|
||||
const { pluginServices } = await import('../../services/plugin_services');
|
||||
const {
|
||||
screenshotMode: { isScreenshotMode },
|
||||
} = pluginServices.getServices();
|
||||
setScreenshotMode(isScreenshotMode());
|
||||
})();
|
||||
}, []);
|
||||
const id = useMemo(() => uuidv4(), []);
|
||||
|
||||
const id = useMemo(() => uuidv4(), []);
|
||||
useEffect(() => {
|
||||
/* In case the locator prop changes, we need to reassign the value in the container */
|
||||
if (dashboardContainer) dashboardContainer.locator = locator;
|
||||
}, [dashboardContainer, locator]);
|
||||
|
||||
useEffect(() => {
|
||||
/* In case the locator prop changes, we need to reassign the value in the container */
|
||||
if (dashboardContainer) dashboardContainer.locator = locator;
|
||||
}, [dashboardContainer, locator]);
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Here we attempt to build a dashboard or navigate to a new dashboard. Clear all error states
|
||||
* if they exist in case this dashboard loads correctly.
|
||||
*/
|
||||
fatalError?.destroy();
|
||||
setDashboardMissing(false);
|
||||
setFatalError(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Here we attempt to build a dashboard or navigate to a new dashboard. Clear all error states
|
||||
* if they exist in case this dashboard loads correctly.
|
||||
*/
|
||||
fatalError?.destroy();
|
||||
setDashboardMissing(false);
|
||||
setFatalError(undefined);
|
||||
if (dashboardContainer) {
|
||||
// When a dashboard already exists, don't rebuild it, just set a new id.
|
||||
dashboardContainer.navigateToDashboard(savedObjectId).catch((e) => {
|
||||
dashboardContainer?.destroy();
|
||||
setDashboardContainer(undefined);
|
||||
setFatalError(new ErrorEmbeddable(e, { id }));
|
||||
if (e instanceof SavedObjectNotFound) {
|
||||
setDashboardMissing(true);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (dashboardContainer) {
|
||||
// When a dashboard already exists, don't rebuild it, just set a new id.
|
||||
dashboardContainer.navigateToDashboard(savedObjectId).catch((e) => {
|
||||
dashboardContainer?.destroy();
|
||||
setDashboardContainer(undefined);
|
||||
setFatalError(new ErrorEmbeddable(e, { id }));
|
||||
if (e instanceof SavedObjectNotFound) {
|
||||
setDashboardMissing(true);
|
||||
}
|
||||
});
|
||||
setLoading(true);
|
||||
let canceled = false;
|
||||
(async () => {
|
||||
const creationOptions = await getCreationOptions?.();
|
||||
|
||||
const dashboardFactory = embeddable.getEmbeddableFactory(
|
||||
DASHBOARD_CONTAINER_TYPE
|
||||
) as DashboardContainerFactory & {
|
||||
create: DashboardContainerFactoryDefinition['create'];
|
||||
};
|
||||
const container = await dashboardFactory?.create(
|
||||
{ id } as unknown as DashboardContainerInput, // Input from creationOptions is used instead.
|
||||
undefined,
|
||||
creationOptions,
|
||||
savedObjectId
|
||||
);
|
||||
setLoading(false);
|
||||
|
||||
if (canceled || !container) {
|
||||
setDashboardContainer(undefined);
|
||||
container?.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
let canceled = false;
|
||||
(async () => {
|
||||
const creationOptions = await getCreationOptions?.();
|
||||
|
||||
// Lazy loading all services is required in this component because it is exported and contributes to the bundle size.
|
||||
const { pluginServices } = await import('../../services/plugin_services');
|
||||
const { embeddable } = pluginServices.getServices();
|
||||
|
||||
const dashboardFactory = embeddable.getEmbeddableFactory(
|
||||
DASHBOARD_CONTAINER_TYPE
|
||||
) as DashboardContainerFactory & {
|
||||
create: DashboardContainerFactoryDefinition['create'];
|
||||
};
|
||||
const container = await dashboardFactory?.create(
|
||||
{ id } as unknown as DashboardContainerInput, // Input from creationOptions is used instead.
|
||||
undefined,
|
||||
creationOptions,
|
||||
savedObjectId
|
||||
);
|
||||
setLoading(false);
|
||||
|
||||
if (canceled || !container) {
|
||||
setDashboardContainer(undefined);
|
||||
container?.destroy();
|
||||
return;
|
||||
if (isErrorEmbeddable(container)) {
|
||||
setFatalError(container);
|
||||
if (container.error instanceof SavedObjectNotFound) {
|
||||
setDashboardMissing(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isErrorEmbeddable(container)) {
|
||||
setFatalError(container);
|
||||
if (container.error instanceof SavedObjectNotFound) {
|
||||
setDashboardMissing(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (dashboardRoot.current) {
|
||||
container.render(dashboardRoot.current);
|
||||
}
|
||||
|
||||
if (dashboardRoot.current) {
|
||||
container.render(dashboardRoot.current);
|
||||
}
|
||||
|
||||
setDashboardContainer(container);
|
||||
})();
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
// Disabling exhaustive deps because embeddable should only be created on first render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [savedObjectId]);
|
||||
|
||||
useUnmount(() => {
|
||||
fatalError?.destroy();
|
||||
dashboardContainer?.destroy();
|
||||
});
|
||||
|
||||
const viewportClasses = classNames(
|
||||
'dashboardViewport',
|
||||
{ 'dashboardViewport--screenshotMode': screenshotMode },
|
||||
{ 'dashboardViewport--loading': loading }
|
||||
);
|
||||
|
||||
const loadingSpinner = showPlainSpinner ? (
|
||||
<EuiLoadingSpinner size="xxl" />
|
||||
) : (
|
||||
<EuiLoadingElastic size="xxl" />
|
||||
);
|
||||
|
||||
const renderDashboardContents = () => {
|
||||
if (dashboardMissing) return <Dashboard404Page dashboardRedirect={dashboardRedirect} />;
|
||||
if (fatalError) return fatalError.render();
|
||||
if (loading) return loadingSpinner;
|
||||
return <div ref={dashboardRoot} />;
|
||||
setDashboardContainer(container);
|
||||
onApiAvailable?.(container as DashboardApi);
|
||||
})();
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
// Disabling exhaustive deps because embeddable should only be created on first render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [savedObjectId]);
|
||||
|
||||
return (
|
||||
<div ref={dashboardViewport} className={viewportClasses}>
|
||||
{dashboardViewport?.current &&
|
||||
dashboardContainer &&
|
||||
!isErrorEmbeddable(dashboardContainer) && (
|
||||
<ParentClassController
|
||||
viewportRef={dashboardViewport.current}
|
||||
dashboard={dashboardContainer}
|
||||
/>
|
||||
)}
|
||||
{renderDashboardContents()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
useUnmount(() => {
|
||||
fatalError?.destroy();
|
||||
dashboardContainer?.destroy();
|
||||
});
|
||||
|
||||
const viewportClasses = classNames(
|
||||
'dashboardViewport',
|
||||
{ 'dashboardViewport--screenshotMode': screenshotMode },
|
||||
{ 'dashboardViewport--loading': loading }
|
||||
);
|
||||
|
||||
const loadingSpinner = showPlainSpinner ? (
|
||||
<EuiLoadingSpinner size="xxl" />
|
||||
) : (
|
||||
<EuiLoadingElastic size="xxl" />
|
||||
);
|
||||
|
||||
const renderDashboardContents = () => {
|
||||
if (dashboardMissing) return <Dashboard404Page dashboardRedirect={dashboardRedirect} />;
|
||||
if (fatalError) return fatalError.render();
|
||||
if (loading) return loadingSpinner;
|
||||
return <div ref={dashboardRoot} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={dashboardViewport} className={viewportClasses}>
|
||||
{dashboardViewport?.current &&
|
||||
dashboardContainer &&
|
||||
!isErrorEmbeddable(dashboardContainer) && (
|
||||
<ParentClassController
|
||||
viewportRef={dashboardViewport.current}
|
||||
dashboardApi={dashboardContainer as DashboardApi}
|
||||
/>
|
||||
)}
|
||||
{renderDashboardContents()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximizing a panel in Dashboard only works if the parent div has a certain class. This
|
||||
|
@ -202,13 +179,13 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
|||
* the class to whichever element renders the Dashboard.
|
||||
*/
|
||||
const ParentClassController = ({
|
||||
dashboard,
|
||||
dashboardApi,
|
||||
viewportRef,
|
||||
}: {
|
||||
dashboard: DashboardContainer;
|
||||
dashboardApi: DashboardApi;
|
||||
viewportRef: HTMLDivElement;
|
||||
}) => {
|
||||
const maximizedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
|
||||
const maximizedPanelId = useStateFromPublishingSubject(dashboardApi.expandedPanelId);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const parentDiv = viewportRef.parentElement;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import type { DashboardRendererProps } from './dashboard_renderer';
|
||||
|
||||
const Component = dynamic(async () => {
|
||||
const { DashboardRenderer } = await import('./dashboard_renderer');
|
||||
return {
|
||||
default: DashboardRenderer,
|
||||
};
|
||||
});
|
||||
|
||||
export function LazyDashboardRenderer(props: DashboardRendererProps) {
|
||||
return <Component {...props} />;
|
||||
}
|
|
@ -21,7 +21,6 @@ export {
|
|||
DashboardContainerFactoryDefinition,
|
||||
} from './embeddable/dashboard_container_factory';
|
||||
|
||||
export { DashboardRenderer } from './external_api/dashboard_renderer';
|
||||
export type { DashboardAPI, AwaitingDashboardAPI } from './external_api/dashboard_api';
|
||||
export { LazyDashboardRenderer } from './external_api/lazy_dashboard_renderer';
|
||||
export type { DashboardLocatorParams } from './types';
|
||||
export type { IProvidesLegacyPanelPlacementSettings } from './panel_placement';
|
||||
|
|
|
@ -8,20 +8,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { DashboardAPIContext } from '../dashboard_app/dashboard_app';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import {
|
||||
InternalDashboardTopNav,
|
||||
InternalDashboardTopNavProps,
|
||||
} from './internal_dashboard_top_nav';
|
||||
import { DashboardContext } from '../dashboard_api/use_dashboard_api';
|
||||
import { DashboardApi } from '../dashboard_api/types';
|
||||
export interface DashboardTopNavProps extends InternalDashboardTopNavProps {
|
||||
dashboardContainer: DashboardContainer;
|
||||
dashboardApi: DashboardApi;
|
||||
}
|
||||
|
||||
export const DashboardTopNavWithContext = (props: DashboardTopNavProps) => (
|
||||
<DashboardAPIContext.Provider value={props.dashboardContainer}>
|
||||
<DashboardContext.Provider value={props.dashboardApi}>
|
||||
<InternalDashboardTopNav {...props} />
|
||||
</DashboardAPIContext.Provider>
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -13,8 +13,9 @@ import { buildMockDashboard } from '../mocks';
|
|||
import { InternalDashboardTopNav } from './internal_dashboard_top_nav';
|
||||
import { setMockedPresentationUtilServices } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DashboardAPIContext } from '../dashboard_app/dashboard_app';
|
||||
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||
import { DashboardContext } from '../dashboard_api/use_dashboard_api';
|
||||
import { DashboardApi } from '../dashboard_api/types';
|
||||
|
||||
describe('Internal dashboard top nav', () => {
|
||||
const mockTopNav = (badges: TopNavMenuProps['badges'] | undefined[]) => {
|
||||
|
@ -42,9 +43,9 @@ describe('Internal dashboard top nav', () => {
|
|||
|
||||
it('should not render the managed badge by default', async () => {
|
||||
const component = render(
|
||||
<DashboardAPIContext.Provider value={buildMockDashboard()}>
|
||||
<DashboardContext.Provider value={buildMockDashboard() as DashboardApi}>
|
||||
<InternalDashboardTopNav redirectTo={jest.fn()} />
|
||||
</DashboardAPIContext.Provider>
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
|
||||
expect(component.queryByText('Managed')).toBeNull();
|
||||
|
@ -54,9 +55,9 @@ describe('Internal dashboard top nav', () => {
|
|||
const container = buildMockDashboard();
|
||||
container.dispatch.setManaged(true);
|
||||
const component = render(
|
||||
<DashboardAPIContext.Provider value={container}>
|
||||
<DashboardContext.Provider value={container as DashboardApi}>
|
||||
<InternalDashboardTopNav redirectTo={jest.fn()} />
|
||||
</DashboardAPIContext.Provider>
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
|
||||
expect(component.getByText('Managed')).toBeInTheDocument();
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
LazyLabsFlyout,
|
||||
getContextProvider as getPresentationUtilContextProvider,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { TopNavMenuBadgeProps, TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||
import {
|
||||
EuiBreadcrumb,
|
||||
|
@ -29,7 +28,8 @@ import {
|
|||
import { MountPoint } from '@kbn/core/public';
|
||||
import { getManagedContentBadge } from '@kbn/managed-content-badge';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import {
|
||||
getDashboardTitle,
|
||||
leaveConfirmStrings,
|
||||
|
@ -38,7 +38,6 @@ import {
|
|||
dashboardManagedBadge,
|
||||
} from '../dashboard_app/_dashboard_app_strings';
|
||||
import { UI_SETTINGS } from '../../common';
|
||||
import { useDashboardAPI } from '../dashboard_app/dashboard_app';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { useDashboardMenuItems } from '../dashboard_app/top_nav/use_dashboard_menu_items';
|
||||
import { DashboardEmbedSettings } from '../dashboard_app/types';
|
||||
|
@ -48,6 +47,7 @@ import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../dashboard_constants
|
|||
import './_dashboard_top_nav.scss';
|
||||
import { DashboardRedirect } from '../dashboard_container/types';
|
||||
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
|
||||
import { useDashboardApi } from '../dashboard_api/use_dashboard_api';
|
||||
|
||||
export interface InternalDashboardTopNavProps {
|
||||
customLeadingBreadCrumbs?: EuiBreadcrumb[];
|
||||
|
@ -98,24 +98,35 @@ export function InternalDashboardTopNav({
|
|||
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
|
||||
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
|
||||
|
||||
const dashboard = useDashboardAPI();
|
||||
const dashboardApi = useDashboardApi();
|
||||
const PresentationUtilContextProvider = getPresentationUtilContextProvider();
|
||||
const hasRunMigrations = dashboard.select(
|
||||
(state) => state.componentState.hasRunClientsideMigrations
|
||||
|
||||
const [
|
||||
allDataViews,
|
||||
focusedPanelId,
|
||||
fullScreenMode,
|
||||
hasRunMigrations,
|
||||
hasUnsavedChanges,
|
||||
lastSavedId,
|
||||
managed,
|
||||
query,
|
||||
title,
|
||||
viewMode,
|
||||
] = useBatchedPublishingSubjects(
|
||||
dashboardApi.dataViews,
|
||||
dashboardApi.focusedPanelId$,
|
||||
dashboardApi.fullScreenMode$,
|
||||
dashboardApi.hasRunMigrations$,
|
||||
dashboardApi.hasUnsavedChanges$,
|
||||
dashboardApi.savedObjectId,
|
||||
dashboardApi.managed$,
|
||||
dashboardApi.query$,
|
||||
dashboardApi.panelTitle,
|
||||
dashboardApi.viewMode
|
||||
);
|
||||
const hasUnsavedChanges = dashboard.select((state) => state.componentState.hasUnsavedChanges);
|
||||
const fullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
|
||||
const savedQueryId = dashboard.select((state) => state.componentState.savedQueryId);
|
||||
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||
const focusedPanelId = dashboard.select((state) => state.componentState.focusedPanelId);
|
||||
const managed = dashboard.select((state) => state.componentState.managed);
|
||||
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const query = dashboard.select((state) => state.explicitInput.query);
|
||||
const title = dashboard.select((state) => state.explicitInput.title);
|
||||
|
||||
const [savedQueryId, setSavedQueryId] = useState<string | undefined>();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const allDataViews = useStateFromPublishingSubject(dashboard.dataViews);
|
||||
|
||||
const dashboardTitle = useMemo(() => {
|
||||
return getDashboardTitle(title, viewMode, !lastSavedId);
|
||||
|
@ -132,7 +143,7 @@ export function InternalDashboardTopNav({
|
|||
* Manage chrome visibility when dashboard is embedded.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!embedSettings) setChromeVisibility(viewMode !== ViewMode.PRINT);
|
||||
if (!embedSettings) setChromeVisibility(viewMode !== 'print');
|
||||
}, [embedSettings, setChromeVisibility, viewMode]);
|
||||
|
||||
/**
|
||||
|
@ -142,12 +153,12 @@ export function InternalDashboardTopNav({
|
|||
const subscription = getChromeIsVisible$().subscribe((visible) => setIsChromeVisible(visible));
|
||||
if (lastSavedId && title) {
|
||||
chromeRecentlyAccessed.add(
|
||||
getFullEditPath(lastSavedId, viewMode === ViewMode.EDIT),
|
||||
getFullEditPath(lastSavedId, viewMode === 'edit'),
|
||||
title,
|
||||
lastSavedId
|
||||
);
|
||||
dashboardRecentlyAccessed.add(
|
||||
getFullEditPath(lastSavedId, viewMode === ViewMode.EDIT),
|
||||
getFullEditPath(lastSavedId, viewMode === 'edit'),
|
||||
title,
|
||||
lastSavedId
|
||||
);
|
||||
|
@ -170,14 +181,14 @@ export function InternalDashboardTopNav({
|
|||
const dashboardTitleBreadcrumbs = [
|
||||
{
|
||||
text:
|
||||
viewMode === ViewMode.EDIT ? (
|
||||
viewMode === 'edit' ? (
|
||||
<>
|
||||
{dashboardTitle}
|
||||
<EuiIcon
|
||||
size="s"
|
||||
type="pencil"
|
||||
className="dshTitleBreadcrumbs__updateIcon"
|
||||
onClick={() => dashboard.showSettings()}
|
||||
onClick={() => dashboardApi.openSettingsFlyout()}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -213,7 +224,7 @@ export function InternalDashboardTopNav({
|
|||
setBreadcrumbs,
|
||||
redirectTo,
|
||||
dashboardTitle,
|
||||
dashboard,
|
||||
dashboardApi,
|
||||
viewMode,
|
||||
serverless,
|
||||
customLeadingBreadCrumbs,
|
||||
|
@ -224,11 +235,7 @@ export function InternalDashboardTopNav({
|
|||
*/
|
||||
useEffect(() => {
|
||||
onAppLeave((actions) => {
|
||||
if (
|
||||
viewMode === ViewMode.EDIT &&
|
||||
hasUnsavedChanges &&
|
||||
!getStateTransfer().isTransferInProgress
|
||||
) {
|
||||
if (viewMode === 'edit' && hasUnsavedChanges && !getStateTransfer().isTransferInProgress) {
|
||||
return actions.confirm(
|
||||
leaveConfirmStrings.getLeaveSubtitle(),
|
||||
leaveConfirmStrings.getLeaveTitle()
|
||||
|
@ -252,7 +259,7 @@ export function InternalDashboardTopNav({
|
|||
const showQueryInput = Boolean(forceHideUnifiedSearch)
|
||||
? false
|
||||
: shouldShowNavBarComponent(
|
||||
Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
|
||||
Boolean(embedSettings?.forceShowQueryInput || viewMode === 'edit')
|
||||
);
|
||||
const showDatePicker = Boolean(forceHideUnifiedSearch)
|
||||
? false
|
||||
|
@ -300,12 +307,12 @@ export function InternalDashboardTopNav({
|
|||
});
|
||||
|
||||
UseUnmount(() => {
|
||||
dashboard.clearOverlays();
|
||||
dashboardApi.clearOverlays();
|
||||
});
|
||||
|
||||
const badges = useMemo(() => {
|
||||
const allBadges: TopNavMenuProps['badges'] = [];
|
||||
if (hasUnsavedChanges && viewMode === ViewMode.EDIT) {
|
||||
if (hasUnsavedChanges && viewMode === 'edit') {
|
||||
allBadges.push({
|
||||
'data-test-subj': 'dashboardUnsavedChangesBadge',
|
||||
badgeText: unsavedChangesBadgeStrings.getUnsavedChangedBadgeText(),
|
||||
|
@ -317,7 +324,7 @@ export function InternalDashboardTopNav({
|
|||
} as EuiToolTipProps,
|
||||
});
|
||||
}
|
||||
if (hasRunMigrations && viewMode === ViewMode.EDIT) {
|
||||
if (hasRunMigrations && viewMode === 'edit') {
|
||||
allBadges.push({
|
||||
'data-test-subj': 'dashboardSaveRecommendedBadge',
|
||||
badgeText: unsavedChangesBadgeStrings.getHasRunMigrationsText(),
|
||||
|
@ -357,7 +364,7 @@ export function InternalDashboardTopNav({
|
|||
<EuiLink
|
||||
id="dashboardManagedContentPopoverButton"
|
||||
onClick={() => {
|
||||
dashboard
|
||||
dashboardApi
|
||||
.runInteractiveSave(viewMode)
|
||||
.then((result) => maybeRedirect(result));
|
||||
}}
|
||||
|
@ -385,7 +392,7 @@ export function InternalDashboardTopNav({
|
|||
showWriteControls,
|
||||
managed,
|
||||
isPopoverOpen,
|
||||
dashboard,
|
||||
dashboardApi,
|
||||
maybeRedirect,
|
||||
]);
|
||||
|
||||
|
@ -399,7 +406,7 @@ export function InternalDashboardTopNav({
|
|||
>{`${getDashboardBreadcrumb()} - ${dashboardTitle}`}</h1>
|
||||
<TopNavMenu
|
||||
{...visibilityProps}
|
||||
query={query}
|
||||
query={query as Query | undefined}
|
||||
badges={badges}
|
||||
screenTitle={title}
|
||||
useDefaultBehaviors={true}
|
||||
|
@ -407,7 +414,7 @@ export function InternalDashboardTopNav({
|
|||
indexPatterns={allDataViews ?? []}
|
||||
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
|
||||
appName={LEGACY_DASHBOARD_APP_ID}
|
||||
visible={viewMode !== ViewMode.PRINT}
|
||||
visible={viewMode !== 'print'}
|
||||
setMenuMountPoint={
|
||||
embedSettings || fullScreenMode
|
||||
? setCustomHeaderActionMenu ?? undefined
|
||||
|
@ -416,28 +423,24 @@ export function InternalDashboardTopNav({
|
|||
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
|
||||
config={
|
||||
visibilityProps.showTopNavMenu
|
||||
? viewMode === ViewMode.EDIT
|
||||
? viewMode === 'edit'
|
||||
? editModeTopNavConfig
|
||||
: viewModeTopNavConfig
|
||||
: undefined
|
||||
}
|
||||
onQuerySubmit={(_payload, isUpdate) => {
|
||||
if (isUpdate === false) {
|
||||
dashboard.forceRefresh();
|
||||
dashboardApi.forceRefresh();
|
||||
}
|
||||
}}
|
||||
onSavedQueryIdChange={(newId: string | undefined) =>
|
||||
dashboard.dispatch.setSavedQueryId(newId)
|
||||
}
|
||||
onSavedQueryIdChange={setSavedQueryId}
|
||||
/>
|
||||
{viewMode !== ViewMode.PRINT && isLabsEnabled && isLabsShown ? (
|
||||
{viewMode !== 'print' && isLabsEnabled && isLabsShown ? (
|
||||
<PresentationUtilContextProvider>
|
||||
<LabsFlyout solutions={['dashboard']} onClose={() => setIsLabsShown(false)} />
|
||||
</PresentationUtilContextProvider>
|
||||
) : null}
|
||||
{viewMode === ViewMode.EDIT ? (
|
||||
<DashboardEditingToolbar isDisabled={!!focusedPanelId} />
|
||||
) : null}
|
||||
{viewMode === 'edit' ? <DashboardEditingToolbar isDisabled={!!focusedPanelId} /> : null}
|
||||
{showBorderBottom && <EuiHorizontalRule margin="none" />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,10 +17,9 @@ export {
|
|||
DASHBOARD_GRID_COLUMN_COUNT,
|
||||
PanelPlacementStrategy,
|
||||
} from './dashboard_constants';
|
||||
export type { DashboardApi } from './dashboard_api/types';
|
||||
export {
|
||||
type DashboardAPI,
|
||||
type AwaitingDashboardAPI,
|
||||
DashboardRenderer,
|
||||
LazyDashboardRenderer as DashboardRenderer,
|
||||
DASHBOARD_CONTAINER_TYPE,
|
||||
type DashboardCreationOptions,
|
||||
type DashboardLocatorParams,
|
||||
|
|
|
@ -200,6 +200,7 @@ export const legacyEmbeddableToApi = (
|
|||
const filters$: BehaviorSubject<Filter[] | undefined> = new BehaviorSubject<Filter[] | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const query$: BehaviorSubject<Query | AggregateQuery | undefined> = new BehaviorSubject<
|
||||
Query | AggregateQuery | undefined
|
||||
>(undefined);
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { TimefilterContract } from '@kbn/data-plugin/public';
|
|||
import { firstValueFrom } from 'rxjs';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type {
|
||||
DashboardAPI,
|
||||
DashboardApi,
|
||||
DashboardLocatorParams,
|
||||
DashboardStart,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
|
@ -78,7 +78,7 @@ export class QuickJobCreatorBase {
|
|||
end: number | undefined;
|
||||
startJob: boolean;
|
||||
runInRealTime: boolean;
|
||||
dashboard?: DashboardAPI;
|
||||
dashboard?: DashboardApi;
|
||||
}) {
|
||||
const datafeedId = createDatafeedId(jobId);
|
||||
const datafeed = { ...datafeedConfig, job_id: jobId, datafeed_id: datafeedId };
|
||||
|
@ -225,7 +225,7 @@ export class QuickJobCreatorBase {
|
|||
return mergedQueries;
|
||||
}
|
||||
|
||||
private async createDashboardLink(dashboard: DashboardAPI, datafeedConfig: estypes.MlDatafeed) {
|
||||
private async createDashboardLink(dashboard: DashboardApi, datafeedConfig: estypes.MlDatafeed) {
|
||||
const savedObjectId = dashboard.savedObjectId?.value;
|
||||
if (!savedObjectId) {
|
||||
return null;
|
||||
|
@ -260,7 +260,7 @@ export class QuickJobCreatorBase {
|
|||
return { url_name: urlName, url_value: url, time_range: 'auto' };
|
||||
}
|
||||
|
||||
private async getCustomUrls(dashboard: DashboardAPI, datafeedConfig: estypes.MlDatafeed) {
|
||||
private async getCustomUrls(dashboard: DashboardApi, datafeedConfig: estypes.MlDatafeed) {
|
||||
const customUrls = await this.createDashboardLink(dashboard, datafeedConfig);
|
||||
return dashboard !== undefined && customUrls !== null ? { custom_urls: [customUrls] } : {};
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { layerTypes } from '@kbn/lens-plugin/public';
|
|||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils';
|
||||
import type { LensApi } from '@kbn/lens-plugin/public';
|
||||
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
|
||||
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
|
||||
import { ML_PAGES, ML_APP_LOCATOR } from '../../../../../common/constants/locator';
|
||||
|
||||
export const COMPATIBLE_SERIES_TYPES = [
|
||||
|
@ -78,7 +78,7 @@ export async function getJobsItemsFromEmbeddable(embeddable: LensApi, lens?: Len
|
|||
}
|
||||
|
||||
const dashboardApi = apiIsOfType(embeddable.parentApi, 'dashboard')
|
||||
? (embeddable.parentApi as DashboardAPI)
|
||||
? (embeddable.parentApi as DashboardApi)
|
||||
: undefined;
|
||||
|
||||
const timeRange = embeddable.timeRange$?.value ?? dashboardApi?.timeRange$?.value;
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { Query } from '@kbn/es-query';
|
|||
import { apiIsOfType } from '@kbn/presentation-publishing';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { MapApi } from '@kbn/maps-plugin/public';
|
||||
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
|
||||
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
|
||||
import { ML_PAGES, ML_APP_LOCATOR } from '../../../../../common/constants/locator';
|
||||
|
||||
export async function redirectToGeoJobWizard(
|
||||
|
@ -53,7 +53,7 @@ export function isCompatibleMapVisualization(api: MapApi) {
|
|||
|
||||
export async function getJobsItemsFromEmbeddable(embeddable: MapApi) {
|
||||
const dashboardApi = apiIsOfType(embeddable.parentApi, 'dashboard')
|
||||
? (embeddable.parentApi as DashboardAPI)
|
||||
? (embeddable.parentApi as DashboardApi)
|
||||
: undefined;
|
||||
const timeRange = embeddable.timeRange$?.value ?? dashboardApi?.timeRange$?.value;
|
||||
if (timeRange === undefined) {
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { useState, useEffect } from 'react';
|
|||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
AwaitingDashboardAPI,
|
||||
DashboardApi,
|
||||
DashboardCreationOptions,
|
||||
DashboardRenderer,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
|
@ -28,7 +28,7 @@ import { useApmParams } from '../../../../hooks/use_apm_params';
|
|||
import { convertSavedDashboardToPanels, MetricsDashboardProps } from './helper';
|
||||
|
||||
export function JsonMetricsDashboard(dashboardProps: MetricsDashboardProps) {
|
||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||
const [dashboard, setDashboard] = useState<DashboardApi | undefined>(undefined);
|
||||
const { dataView } = dashboardProps;
|
||||
const {
|
||||
query: { environment, kuery, rangeFrom, rangeTo },
|
||||
|
@ -42,24 +42,20 @@ export function JsonMetricsDashboard(dashboardProps: MetricsDashboardProps) {
|
|||
|
||||
useEffect(() => {
|
||||
if (!dashboard) return;
|
||||
dashboard.updateInput({
|
||||
timeRange: { from: rangeFrom, to: rangeTo },
|
||||
query: { query: kuery, language: 'kuery' },
|
||||
});
|
||||
dashboard.setTimeRange({ from: rangeFrom, to: rangeTo });
|
||||
dashboard.setQuery({ query: kuery, language: 'kuery' });
|
||||
}, [kuery, dashboard, rangeFrom, rangeTo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboard) return;
|
||||
|
||||
dashboard.updateInput({
|
||||
filters: dataView ? getFilters(serviceName, environment, dataView) : [],
|
||||
});
|
||||
dashboard.setFilters(dataView ? getFilters(serviceName, environment, dataView) : []);
|
||||
}, [dataView, serviceName, environment, dashboard]);
|
||||
|
||||
return (
|
||||
<DashboardRenderer
|
||||
getCreationOptions={() => getCreationOptions(dashboardProps, notifications, dataView)}
|
||||
ref={setDashboard}
|
||||
onApiAvailable={setDashboard}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
AwaitingDashboardAPI,
|
||||
DashboardApi,
|
||||
DashboardCreationOptions,
|
||||
DashboardRenderer,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
|
@ -53,7 +53,7 @@ export function ServiceDashboards({ checkForEntities = false }: { checkForEntiti
|
|||
'/services/{serviceName}/dashboards',
|
||||
'/mobile-services/{serviceName}/dashboards'
|
||||
);
|
||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||
const [dashboard, setDashboard] = useState<DashboardApi | undefined>();
|
||||
const [serviceDashboards, setServiceDashboards] = useState<MergedServiceDashboard[]>([]);
|
||||
const [currentDashboard, setCurrentDashboard] = useState<MergedServiceDashboard>();
|
||||
const { data: allAvailableDashboards } = useDashboardFetcher();
|
||||
|
@ -110,16 +110,15 @@ export function ServiceDashboards({ checkForEntities = false }: { checkForEntiti
|
|||
useEffect(() => {
|
||||
if (!dashboard) return;
|
||||
|
||||
dashboard.updateInput({
|
||||
filters:
|
||||
dataView &&
|
||||
dashboard.setFilters(
|
||||
dataView &&
|
||||
currentDashboard?.serviceEnvironmentFilterEnabled &&
|
||||
currentDashboard?.serviceNameFilterEnabled
|
||||
? getFilters(serviceName, environment, dataView)
|
||||
: [],
|
||||
timeRange: { from: rangeFrom, to: rangeTo },
|
||||
query: { query: kuery, language: 'kuery' },
|
||||
});
|
||||
? getFilters(serviceName, environment, dataView)
|
||||
: []
|
||||
);
|
||||
dashboard.setQuery({ query: kuery, language: 'kuery' });
|
||||
dashboard.setTimeRange({ from: rangeFrom, to: rangeTo });
|
||||
}, [dataView, serviceName, environment, kuery, dashboard, rangeFrom, rangeTo, currentDashboard]);
|
||||
|
||||
const getLocatorParams = useCallback(
|
||||
|
@ -213,7 +212,7 @@ export function ServiceDashboards({ checkForEntities = false }: { checkForEntiti
|
|||
locator={locator}
|
||||
savedObjectId={dashboardId}
|
||||
getCreationOptions={getCreationOptions}
|
||||
ref={setDashboard}
|
||||
onApiAvailable={setDashboard}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
AwaitingDashboardAPI,
|
||||
DashboardApi,
|
||||
DashboardCreationOptions,
|
||||
DashboardRenderer,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
|
@ -61,7 +61,7 @@ export function Dashboards() {
|
|||
const {
|
||||
services: { share, telemetry },
|
||||
} = useKibanaContextForPlugin();
|
||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||
const [dashboard, setDashboard] = useState<DashboardApi | undefined>();
|
||||
const [customDashboards, setCustomDashboards] = useState<DashboardItemWithTitle[]>([]);
|
||||
const [currentDashboard, setCurrentDashboard] = useState<DashboardItemWithTitle>();
|
||||
const [trackingEventProperties, setTrackingEventProperties] = useState({});
|
||||
|
@ -143,15 +143,13 @@ export function Dashboards() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!dashboard) return;
|
||||
dashboard.updateInput({
|
||||
filters:
|
||||
metrics.dataView && currentDashboard?.dashboardFilterAssetIdEnabled
|
||||
? buildAssetIdFilter(asset.name, asset.type, metrics.dataView)
|
||||
: [],
|
||||
timeRange: { from: dateRange.from, to: dateRange.to },
|
||||
// forces data reload
|
||||
lastReloadRequestTime: Date.now(),
|
||||
});
|
||||
dashboard.setFilters(
|
||||
metrics.dataView && currentDashboard?.dashboardFilterAssetIdEnabled
|
||||
? buildAssetIdFilter(asset.name, asset.type, metrics.dataView)
|
||||
: []
|
||||
);
|
||||
dashboard.setTimeRange({ from: dateRange.from, to: dateRange.to });
|
||||
dashboard.forceRefresh();
|
||||
}, [
|
||||
metrics.dataView,
|
||||
asset.name,
|
||||
|
@ -274,7 +272,7 @@ export function Dashboards() {
|
|||
<DashboardRenderer
|
||||
savedObjectId={urlState?.dashboardId}
|
||||
getCreationOptions={getCreationOptions}
|
||||
ref={setDashboard}
|
||||
onApiAvailable={setDashboard}
|
||||
locator={locator}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
|
||||
import type { DashboardContainerInput } from '@kbn/dashboard-plugin/common';
|
||||
import type {
|
||||
DashboardAPI,
|
||||
DashboardApi,
|
||||
DashboardCreationOptions,
|
||||
DashboardLocatorParams,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
|
@ -43,11 +43,11 @@ const DashboardRendererComponent = ({
|
|||
viewMode = ViewMode.VIEW,
|
||||
}: {
|
||||
canReadDashboard: boolean;
|
||||
dashboardContainer?: DashboardAPI;
|
||||
dashboardContainer?: DashboardApi;
|
||||
filters?: Filter[];
|
||||
id: string;
|
||||
inputId?: InputsModelId.global | InputsModelId.timeline;
|
||||
onDashboardContainerLoaded?: (dashboardContainer: DashboardAPI) => void;
|
||||
onDashboardContainerLoaded?: (dashboardContainer: DashboardApi) => void;
|
||||
query?: Query;
|
||||
savedObjectId: string | undefined;
|
||||
timeRange: {
|
||||
|
@ -142,12 +142,19 @@ const DashboardRendererComponent = ({
|
|||
}, [dispatch, id, inputId, refetchByForceRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
dashboardContainer?.updateInput({ timeRange, query, filters });
|
||||
}, [dashboardContainer, filters, query, timeRange]);
|
||||
dashboardContainer?.setFilters(filters);
|
||||
}, [dashboardContainer, filters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreateDashboard && firstSecurityTagId)
|
||||
dashboardContainer?.updateInput({ tags: [firstSecurityTagId] });
|
||||
dashboardContainer?.setQuery(query);
|
||||
}, [dashboardContainer, query]);
|
||||
|
||||
useEffect(() => {
|
||||
dashboardContainer?.setTimeRange(timeRange);
|
||||
}, [dashboardContainer, timeRange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreateDashboard && firstSecurityTagId) dashboardContainer?.setTags([firstSecurityTagId]);
|
||||
}, [dashboardContainer, firstSecurityTagId, isCreateDashboard]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -166,7 +173,7 @@ const DashboardRendererComponent = ({
|
|||
setDashboardContainerRenderer(
|
||||
<DashboardContainerRenderer
|
||||
locator={locator}
|
||||
ref={onDashboardContainerLoaded}
|
||||
onApiAvailable={onDashboardContainerLoaded}
|
||||
savedObjectId={savedObjectId}
|
||||
getCreationOptions={getCreationOptions}
|
||||
/>
|
||||
|
|
|
@ -5,21 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
|
||||
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { EDIT_DASHBOARD_TITLE } from '../pages/details/translations';
|
||||
|
||||
const DashboardTitleComponent = ({
|
||||
dashboardContainer,
|
||||
onTitleLoaded,
|
||||
}: {
|
||||
dashboardContainer: DashboardAPI;
|
||||
dashboardContainer: DashboardApi;
|
||||
onTitleLoaded: (title: string) => void;
|
||||
}) => {
|
||||
const dashboardTitle = dashboardContainer.select((state) => state.explicitInput.title).trim();
|
||||
const title =
|
||||
dashboardTitle && dashboardTitle.length !== 0 ? dashboardTitle : EDIT_DASHBOARD_TITLE;
|
||||
const dashboardTitle = useStateFromPublishingSubject(dashboardContainer.panelTitle);
|
||||
const title = useMemo(() => {
|
||||
return dashboardTitle && dashboardTitle.length !== 0 ? dashboardTitle : EDIT_DASHBOARD_TITLE;
|
||||
}, [dashboardTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
onTitleLoaded(title);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { DashboardToolBar } from './dashboard_tool_bar';
|
||||
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
|
||||
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { DashboardTopNav } from '@kbn/dashboard-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
|
@ -34,9 +34,7 @@ jest.mock('@kbn/dashboard-plugin/public', () => ({
|
|||
const mockCore = coreMock.createStart();
|
||||
const mockNavigateTo = jest.fn();
|
||||
const mockGetAppUrl = jest.fn();
|
||||
const mockDashboardContainer = {
|
||||
select: jest.fn(),
|
||||
} as unknown as DashboardAPI;
|
||||
const mockDashboardContainer = {} as unknown as DashboardApi;
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<TestProviders>
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
|
||||
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
|
||||
import { DashboardTopNav, LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import type { ChromeBreadcrumb } from '@kbn/core/public';
|
||||
import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common';
|
||||
import type { RedirectToProps } from '@kbn/dashboard-plugin/public/dashboard_container/types';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { SecurityPageName } from '../../../common';
|
||||
import { useCapabilities, useKibana, useNavigation } from '../../common/lib/kibana';
|
||||
import { APP_NAME } from '../../../common/constants';
|
||||
|
@ -21,13 +22,12 @@ const DashboardToolBarComponent = ({
|
|||
dashboardContainer,
|
||||
onLoad,
|
||||
}: {
|
||||
dashboardContainer: DashboardAPI;
|
||||
dashboardContainer: DashboardApi;
|
||||
onLoad?: (mode: ViewMode) => void;
|
||||
}) => {
|
||||
const { setHeaderActionMenu } = useKibana().services;
|
||||
|
||||
const viewMode =
|
||||
dashboardContainer?.select((state) => state.explicitInput.viewMode) ?? ViewMode.VIEW;
|
||||
const viewMode = useStateFromPublishingSubject(dashboardContainer.viewMode);
|
||||
|
||||
const { navigateTo, getAppUrl } = useNavigation();
|
||||
const redirectTo = useCallback(
|
||||
|
@ -56,7 +56,7 @@ const DashboardToolBarComponent = ({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
onLoad?.(viewMode);
|
||||
onLoad?.((viewMode as ViewMode) ?? 'view');
|
||||
}, [onLoad, viewMode]);
|
||||
|
||||
const embedSettings = useMemo(
|
||||
|
@ -73,7 +73,7 @@ const DashboardToolBarComponent = ({
|
|||
return showWriteControls ? (
|
||||
<DashboardTopNav
|
||||
customLeadingBreadCrumbs={landingBreadcrumb}
|
||||
dashboardContainer={dashboardContainer}
|
||||
dashboardApi={dashboardContainer}
|
||||
forceHideUnifiedSearch={true}
|
||||
embedSettings={embedSettings}
|
||||
redirectTo={redirectTo}
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
|
||||
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
|
||||
|
||||
import { useDashboardRenderer } from './use_dashboard_renderer';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const mockDashboardContainer = { getExplicitInput: () => ({ tags: ['tagId'] }) } as DashboardAPI;
|
||||
const mockDashboardContainer = {} as DashboardApi;
|
||||
|
||||
describe('useDashboardRenderer', () => {
|
||||
it('should set dashboard container correctly when dashboard is loaded', async () => {
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
|
||||
import type { DashboardApi } from '@kbn/dashboard-plugin/public';
|
||||
|
||||
export const useDashboardRenderer = () => {
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardAPI>();
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardApi>();
|
||||
|
||||
const handleDashboardLoaded = useCallback((container: DashboardAPI) => {
|
||||
const handleDashboardLoaded = useCallback((container: DashboardApi) => {
|
||||
setDashboardContainer(container);
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -223,6 +223,7 @@
|
|||
"@kbn/cloud-security-posture",
|
||||
"@kbn/security-solution-distribution-bar",
|
||||
"@kbn/cloud-security-posture-common",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/entityManager-plugin",
|
||||
"@kbn/entities-schema",
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue