Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] [Dashboard] Sharing via link to an expanded panel (#190086) #193458

Merged
merged 1 commit into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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