mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Dashboard] Sharing via link to an expanded panel (#190086)](https://github.com/elastic/kibana/pull/190086) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Rachel Shen","email":"rshen@elastic.co"},"sourceCommit":{"committedDate":"2024-09-19T17:38:21Z","message":"[Dashboard] Sharing via link to an expanded panel (#190086)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/145454\r\n\r\nThis PR allows users with a dashboard with an expanded (maximized) panel\r\nto be shared to other users via url or the share modal link. To\r\nimplement this, the expanded panel Id is added to the url:\r\n\r\n`/app/dashboard/{dashboardID}/{expandedPanelID}_g()_a()`\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\nCo-authored-by: Hannah Mudge <hannah.wright@elastic.co>","sha":"603023124681429ee900ff3a73793ce31a9cad58","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Presentation","loe:medium","impact:medium","v9.0.0","backport:prev-minor","v8.16.0"],"title":"[Dashboard] Sharing via link to an expanded panel","number":190086,"url":"https://github.com/elastic/kibana/pull/190086","mergeCommit":{"message":"[Dashboard] Sharing via link to an expanded panel (#190086)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/145454\r\n\r\nThis PR allows users with a dashboard with an expanded (maximized) panel\r\nto be shared to other users via url or the share modal link. To\r\nimplement this, the expanded panel Id is added to the url:\r\n\r\n`/app/dashboard/{dashboardID}/{expandedPanelID}_g()_a()`\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\nCo-authored-by: Hannah Mudge <hannah.wright@elastic.co>","sha":"603023124681429ee900ff3a73793ce31a9cad58"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/190086","number":190086,"mergeCommit":{"message":"[Dashboard] Sharing via link to an expanded panel (#190086)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/145454\r\n\r\nThis PR allows users with a dashboard with an expanded (maximized) panel\r\nto be shared to other users via url or the share modal link. To\r\nimplement this, the expanded panel Id is added to the url:\r\n\r\n`/app/dashboard/{dashboardID}/{expandedPanelID}_g()_a()`\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\nCo-authored-by: Hannah Mudge <hannah.wright@elastic.co>","sha":"603023124681429ee900ff3a73793ce31a9cad58"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Rachel Shen <rshen@elastic.co>
This commit is contained in:
parent
0977302186
commit
331de8d1bb
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