mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Dashboard] Sharing via link to an expanded panel (#190086)
## Summary Closes https://github.com/elastic/kibana/issues/145454 This PR allows users with a dashboard with an expanded (maximized) panel to be shared to other users via url or the share modal link. To implement this, the expanded panel Id is added to the url: `/app/dashboard/{dashboardID}/{expandedPanelID}_g()_a()` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Hannah Mudge <hannah.wright@elastic.co>
This commit is contained in:
parent
92b2c03247
commit
6030231246
5 changed files with 160 additions and 7 deletions
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", 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 { render, waitFor } from '@testing-library/react';
|
||||
import { MemoryHistory, createMemoryHistory } from 'history';
|
||||
import React, { useEffect } from 'react';
|
||||
import type { DashboardRendererProps } from '../dashboard_container/external_api/dashboard_renderer';
|
||||
import { buildMockDashboard } from '../mocks';
|
||||
import { DashboardApp } from './dashboard_app';
|
||||
|
||||
import * as dashboardRendererStuff from '../dashboard_container/external_api/lazy_dashboard_renderer';
|
||||
import { DashboardApi } from '..';
|
||||
|
||||
/* These tests circumvent the need to test the router and legacy code
|
||||
/* the dashboard app will be passed the expanded panel id from the DashboardRouter through mountApp()
|
||||
/* @link https://github.com/elastic/kibana/pull/190086/
|
||||
*/
|
||||
|
||||
describe('Dashboard App', () => {
|
||||
const mockDashboard = buildMockDashboard();
|
||||
let mockHistory: MemoryHistory;
|
||||
// this is in url_utils dashboardApi expandedPanel subscription
|
||||
let historySpy: jest.SpyInstance;
|
||||
// this is in the dashboard app for the renderer when provided an expanded panel id
|
||||
const expandPanelSpy = jest.spyOn(mockDashboard, 'expandPanel');
|
||||
|
||||
beforeAll(() => {
|
||||
mockHistory = createMemoryHistory();
|
||||
historySpy = jest.spyOn(mockHistory, 'replace');
|
||||
|
||||
/**
|
||||
* Mock the LazyDashboardRenderer component to avoid rendering the actual dashboard
|
||||
* and hitting errors that aren't relevant
|
||||
*/
|
||||
jest
|
||||
.spyOn(dashboardRendererStuff, 'LazyDashboardRenderer')
|
||||
// we need overwrite the onApiAvailable prop to get the dashboard Api in the dashboard app
|
||||
.mockImplementation(({ onApiAvailable }: DashboardRendererProps) => {
|
||||
useEffect(() => {
|
||||
onApiAvailable?.(mockDashboard as DashboardApi);
|
||||
}, [onApiAvailable]);
|
||||
|
||||
return <div>Test renderer</div>;
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// reset the spies before each test
|
||||
expandPanelSpy.mockClear();
|
||||
historySpy.mockClear();
|
||||
});
|
||||
|
||||
it('test the default behavior without an expandedPanel id passed as a prop to the DashboardApp', async () => {
|
||||
render(<DashboardApp redirectTo={jest.fn()} history={mockHistory} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(expandPanelSpy).not.toHaveBeenCalled();
|
||||
// this value should be undefined by default
|
||||
expect(mockDashboard.expandedPanelId.getValue()).toBe(undefined);
|
||||
// history should not be called
|
||||
expect(historySpy).toHaveBeenCalledTimes(0);
|
||||
expect(mockHistory.location.pathname).toBe('/');
|
||||
});
|
||||
|
||||
// simulate expanding a panel
|
||||
mockDashboard.expandPanel('123');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDashboard.expandedPanelId.getValue()).toBe('123');
|
||||
expect(historySpy).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistory.location.pathname).toBe('/create/123');
|
||||
});
|
||||
});
|
||||
|
||||
it('test that the expanded panel behavior subject and history is called when passed as a prop to the DashboardApp', async () => {
|
||||
render(<DashboardApp redirectTo={jest.fn()} history={mockHistory} expandedPanelId="456" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(expandPanelSpy).toHaveBeenCalledTimes(1);
|
||||
expect(historySpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
// simulate minimizing a panel
|
||||
mockDashboard.expandedPanelId.next(undefined);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDashboard.expandedPanelId.getValue()).toBe(undefined);
|
||||
expect(historySpy).toHaveBeenCalledTimes(1);
|
||||
expect(mockHistory.location.pathname).toBe('/create');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -23,7 +23,7 @@ import {
|
|||
DashboardAppNoDataPage,
|
||||
isDashboardAppInNoDataState,
|
||||
} from './no_data/dashboard_app_no_data';
|
||||
import { loadAndRemoveDashboardState } from './url/url_utils';
|
||||
import { loadAndRemoveDashboardState, startSyncingExpandedPanelState } from './url/url_utils';
|
||||
import {
|
||||
getSessionURLObservable,
|
||||
getSearchSessionIdFromURL,
|
||||
|
@ -53,6 +53,7 @@ export interface DashboardAppProps {
|
|||
savedDashboardId?: string;
|
||||
redirectTo: DashboardRedirect;
|
||||
embedSettings?: DashboardEmbedSettings;
|
||||
expandedPanelId?: string;
|
||||
}
|
||||
|
||||
export function DashboardApp({
|
||||
|
@ -60,6 +61,7 @@ export function DashboardApp({
|
|||
embedSettings,
|
||||
redirectTo,
|
||||
history,
|
||||
expandedPanelId,
|
||||
}: DashboardAppProps) {
|
||||
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
|
||||
const [regenerateId, setRegenerateId] = useState(uuidv4());
|
||||
|
@ -183,6 +185,12 @@ export function DashboardApp({
|
|||
getScreenshotContext,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboardApi) return;
|
||||
const { stopWatchingExpandedPanel } = startSyncingExpandedPanelState({ dashboardApi, history });
|
||||
return () => stopWatchingExpandedPanel();
|
||||
}, [dashboardApi, history]);
|
||||
|
||||
/**
|
||||
* When the dashboard container is created, or re-created, start syncing dashboard state with the URL
|
||||
*/
|
||||
|
@ -221,7 +229,14 @@ export function DashboardApp({
|
|||
<DashboardRenderer
|
||||
key={regenerateId}
|
||||
locator={locator}
|
||||
onApiAvailable={setDashboardApi}
|
||||
onApiAvailable={(dashboard) => {
|
||||
if (dashboard && !dashboardApi) {
|
||||
setDashboardApi(dashboard);
|
||||
if (expandedPanelId) {
|
||||
dashboard?.expandPanel(expandedPanelId);
|
||||
}
|
||||
}
|
||||
}}
|
||||
dashboardRedirect={redirectTo}
|
||||
savedObjectId={savedDashboardId}
|
||||
showPlainSpinner={showPlainSpinner}
|
||||
|
|
|
@ -101,7 +101,9 @@ export async function mountApp({
|
|||
};
|
||||
};
|
||||
|
||||
const renderDashboard = (routeProps: RouteComponentProps<{ id?: string }>) => {
|
||||
const renderDashboard = (
|
||||
routeProps: RouteComponentProps<{ id?: string; expandedPanelId?: string }>
|
||||
) => {
|
||||
const routeParams = parse(routeProps.history.location.search);
|
||||
if (routeParams.embed === 'true' && !globalEmbedSettings) {
|
||||
globalEmbedSettings = getDashboardEmbedSettings(routeParams);
|
||||
|
@ -112,6 +114,7 @@ export async function mountApp({
|
|||
embedSettings={globalEmbedSettings}
|
||||
savedDashboardId={routeProps.match.params.id}
|
||||
redirectTo={redirect}
|
||||
expandedPanelId={routeProps.match.params.expandedPanelId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -154,7 +157,11 @@ export async function mountApp({
|
|||
<HashRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path={[CREATE_NEW_DASHBOARD_URL, `${VIEW_DASHBOARD_URL}/:id`]}
|
||||
path={[
|
||||
CREATE_NEW_DASHBOARD_URL,
|
||||
`${VIEW_DASHBOARD_URL}/:id/:expandedPanelId`,
|
||||
`${VIEW_DASHBOARD_URL}/:id`,
|
||||
]}
|
||||
render={renderDashboard}
|
||||
/>
|
||||
<Route exact path={LANDING_PAGE_PATH} render={renderListingPage} />
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { skip } from 'rxjs';
|
||||
import semverSatisfies from 'semver/functions/satisfies';
|
||||
import { History } from 'history';
|
||||
|
||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
|
||||
|
@ -21,9 +23,10 @@ import {
|
|||
} from '../../../common';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
|
||||
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
|
||||
import { DASHBOARD_STATE_STORAGE_KEY, createDashboardEditUrl } from '../../dashboard_constants';
|
||||
import { SavedDashboardPanel } from '../../../common/content_management';
|
||||
import { migrateLegacyQuery } from '../../services/dashboard_content_management/lib/load_dashboard_state';
|
||||
import { DashboardApi } from '../../dashboard_api/types';
|
||||
|
||||
/**
|
||||
* We no longer support loading panels from a version older than 7.3 in the URL.
|
||||
|
@ -84,3 +87,25 @@ export const loadAndRemoveDashboardState = (
|
|||
|
||||
return partialState;
|
||||
};
|
||||
|
||||
export const startSyncingExpandedPanelState = ({
|
||||
dashboardApi,
|
||||
history,
|
||||
}: {
|
||||
dashboardApi: DashboardApi;
|
||||
history: History;
|
||||
}) => {
|
||||
const expandedPanelSubscription = dashboardApi?.expandedPanelId
|
||||
// skip the first value because we don't want to trigger a history.replace on initial load
|
||||
.pipe(skip(1))
|
||||
.subscribe((expandedPanelId) => {
|
||||
history.replace({
|
||||
...history.location,
|
||||
pathname: `${createDashboardEditUrl(dashboardApi.savedObjectId.value)}${
|
||||
Boolean(expandedPanelId) ? `/${expandedPanelId}` : ''
|
||||
}`,
|
||||
});
|
||||
});
|
||||
const stopWatchingExpandedPanel = () => expandedPanelSubscription.unsubscribe();
|
||||
return { stopWatchingExpandedPanel };
|
||||
};
|
||||
|
|
|
@ -154,8 +154,16 @@ export class DashboardPageObject extends FtrService {
|
|||
public getDashboardIdFromUrl(url: string) {
|
||||
const urlSubstring = '#/view/';
|
||||
const startOfIdIndex = url.indexOf(urlSubstring) + urlSubstring.length;
|
||||
const endIndex = url.indexOf('?');
|
||||
const id = url.substring(startOfIdIndex, endIndex < 0 ? url.length : endIndex);
|
||||
const endIndexOfFilters = url.indexOf('?');
|
||||
const endIndexOfMax = url.substring(startOfIdIndex).indexOf('/');
|
||||
if (endIndexOfMax === -1) {
|
||||
return url.substring(startOfIdIndex, endIndexOfFilters);
|
||||
}
|
||||
const endIndex =
|
||||
endIndexOfFilters + startOfIdIndex > endIndexOfMax
|
||||
? endIndexOfFilters + startOfIdIndex
|
||||
: endIndexOfMax + startOfIdIndex;
|
||||
const id = url.substring(startOfIdIndex, endIndex < 0 ? url.length : endIndex + startOfIdIndex);
|
||||
return id;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue