Skip to content

Commit

Permalink
[8.x] [Dashboard] Sharing via link to an expanded panel (#190086) (#1…
Browse files Browse the repository at this point in the history
…93458)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Dashboard] Sharing via link to an expanded panel
(#190086)](#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":"[email protected]"},"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
<[email protected]>","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
<[email protected]>","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
<[email protected]>","sha":"603023124681429ee900ff3a73793ce31a9cad58"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Rachel Shen <[email protected]>
  • Loading branch information
kibanamachine and rshen91 authored Sep 19, 2024
1 parent 0977302 commit 331de8d
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 7 deletions.
98 changes: 98 additions & 0 deletions src/plugins/dashboard/public/dashboard_app/dashboard_app.test.tsx
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
19 changes: 17 additions & 2 deletions src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -53,13 +53,15 @@ export interface DashboardAppProps {
savedDashboardId?: string;
redirectTo: DashboardRedirect;
embedSettings?: DashboardEmbedSettings;
expandedPanelId?: string;
}

export function DashboardApp({
savedDashboardId,
embedSettings,
redirectTo,
history,
expandedPanelId,
}: DashboardAppProps) {
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
const [regenerateId, setRegenerateId] = useState(uuidv4());
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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}
Expand Down
11 changes: 9 additions & 2 deletions src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -112,6 +114,7 @@ export async function mountApp({
embedSettings={globalEmbedSettings}
savedDashboardId={routeProps.match.params.id}
redirectTo={redirect}
expandedPanelId={routeProps.match.params.expandedPanelId}
/>
);
};
Expand Down Expand Up @@ -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} />
Expand Down
27 changes: 26 additions & 1 deletion src/plugins/dashboard/public/dashboard_app/url/url_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -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 };
};
12 changes: 10 additions & 2 deletions test/functional/page_objects/dashboard_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down

0 comments on commit 331de8d

Please sign in to comment.