Panel: Embed URL is now correctly generated for a panel in the home dashboard (#44706)

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* test tidyup

* Add comment for route

Co-authored-by: kay delaney <kay@grafana.com>
Co-authored-by: Alexandra Vargas <alexa1866@gmail.com>
Co-authored-by: joshhunt <josh@trtr.co>
Co-authored-by: Muaaz Saleem <muaazsaleem@Muaazs-MacBook-Pro.local>
This commit is contained in:
Ashley Harrison 2022-02-02 13:38:23 +00:00 committed by GitHub
parent 924deda589
commit 0cb3037b55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 220 additions and 13 deletions

View File

@ -0,0 +1,132 @@
import React from 'react';
import { ShareEmbed } from './ShareEmbed';
import { render, screen } from '@testing-library/react';
import config from 'app/core/config';
import { DashboardModel, PanelModel } from '../../state';
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
getTimeSrv: () => ({
timeRange: () => {
return { from: new Date(1000), to: new Date(2000) };
},
}),
}));
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
sidemenu: true,
user: {},
isSignedIn: false,
isGrafanaAdmin: false,
isEditor: false,
hasEditPermissionFolders: false,
},
}));
function mockLocationHref(href: string) {
const location = window.location;
let search = '';
const searchPos = href.indexOf('?');
if (searchPos >= 0) {
search = href.substring(searchPos);
}
// @ts-ignore
delete window.location;
(window as any).location = {
...location,
href,
origin: new URL(href).origin,
search,
};
}
describe('ShareEmbed', () => {
let originalBootData: any;
beforeAll(() => {
originalBootData = config.bootData;
config.appUrl = 'http://dashboards.grafana.com/';
config.bootData = {
user: {
orgId: 1,
},
};
});
afterAll(() => {
config.bootData = originalBootData;
});
it('generates the correct embed url for a dashboard', () => {
const mockDashboard = new DashboardModel({
uid: 'mockDashboardUid',
});
const mockPanel = new PanelModel({
id: 'mockPanelId',
});
mockLocationHref(`http://dashboards.grafana.com/d/${mockDashboard.uid}?orgId=1`);
render(<ShareEmbed dashboard={mockDashboard} panel={mockPanel} />);
const embedUrl = screen.getByTestId('share-embed-html');
expect(embedUrl).toBeInTheDocument();
expect(embedUrl).toHaveTextContent(
`http://dashboards.grafana.com/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}`
);
});
it('generates the correct embed url for a dashboard set to the homepage in the grafana config', () => {
mockLocationHref('http://dashboards.grafana.com/?orgId=1');
const mockDashboard = new DashboardModel({
uid: 'mockDashboardUid',
});
const mockPanel = new PanelModel({
id: 'mockPanelId',
});
render(<ShareEmbed dashboard={mockDashboard} panel={mockPanel} />);
const embedUrl = screen.getByTestId('share-embed-html');
expect(embedUrl).toBeInTheDocument();
expect(embedUrl).toHaveTextContent(
`http://dashboards.grafana.com/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}`
);
});
it('generates the correct embed url for a snapshot', () => {
const mockSlug = 'mockSlug';
mockLocationHref(`http://dashboards.grafana.com/dashboard/snapshot/${mockSlug}?orgId=1`);
const mockDashboard = new DashboardModel({
uid: 'mockDashboardUid',
});
const mockPanel = new PanelModel({
id: 'mockPanelId',
});
render(<ShareEmbed dashboard={mockDashboard} panel={mockPanel} />);
const embedUrl = screen.getByTestId('share-embed-html');
expect(embedUrl).toBeInTheDocument();
expect(embedUrl).toHaveTextContent(
`http://dashboards.grafana.com/dashboard-solo/snapshot/${mockSlug}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}`
);
});
it('generates the correct embed url for a scripted dashboard', () => {
const mockSlug = 'scripted.js';
mockLocationHref(`http://dashboards.grafana.com/dashboard/script/${mockSlug}?orgId=1`);
const mockDashboard = new DashboardModel({
uid: 'mockDashboardUid',
});
const mockPanel = new PanelModel({
id: 'mockPanelId',
});
render(<ShareEmbed dashboard={mockDashboard} panel={mockPanel} />);
const embedUrl = screen.getByTestId('share-embed-html');
expect(embedUrl).toBeInTheDocument();
expect(embedUrl).toHaveTextContent(
`http://dashboards.grafana.com/dashboard-solo/script/${mockSlug}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}`
);
});
});

View File

@ -34,10 +34,10 @@ export class ShareEmbed extends PureComponent<Props, State> {
}
buildIframeHtml = () => {
const { panel } = this.props;
const { panel, dashboard } = this.props;
const { useCurrentTimeRange, selectedTheme } = this.state;
const iframeHtml = buildIframeHtml(useCurrentTimeRange, selectedTheme, panel);
const iframeHtml = buildIframeHtml(useCurrentTimeRange, dashboard.uid, selectedTheme, panel);
this.setState({ iframeHtml });
};
@ -92,6 +92,7 @@ export class ShareEmbed extends PureComponent<Props, State> {
the user viewing that page need to be signed into Grafana for the graph to load."
>
<TextArea
data-testid="share-embed-html"
id="share-panel-embed-embed-html-textarea"
rows={5}
value={iframeHtml}

View File

@ -6,7 +6,7 @@ import { Props, ShareLink, State } from './ShareLink';
import { initTemplateSrv } from '../../../../../test/helpers/initTemplateSrv';
import { variableAdapters } from '../../../variables/adapters';
import { createQueryVariableAdapter } from '../../../variables/query/adapter';
import { PanelModel } from '../../state';
import { DashboardModel, PanelModel } from '../../state';
import { getDefaultTimeRange } from '@grafana/data';
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
@ -31,6 +31,7 @@ function mockLocationHref(href: string) {
(window as any).location = {
...location,
href,
origin: new URL(href).origin,
search,
};
}
@ -189,3 +190,39 @@ describe('ShareModal', () => {
});
});
});
describe('when default_home_dashboard_path is set in the grafana config', () => {
let originalBootData: any;
beforeAll(() => {
originalBootData = config.bootData;
config.appUrl = 'http://dashboards.grafana.com/';
config.bootData = {
user: {
orgId: 1,
},
};
});
afterAll(() => {
config.bootData = originalBootData;
});
it('should render the correct link', async () => {
const mockDashboard = new DashboardModel({
uid: 'mockDashboardUid',
});
const mockPanel = new PanelModel({
id: 'mockPanelId',
});
mockLocationHref('http://dashboards.grafana.com/?orgId=1');
const test: ShallowWrapper<Props, State, ShareLink> = shallow(
<ShareLink dashboard={mockDashboard} panel={mockPanel} />
);
await test.instance().buildUrl();
expect(test.state().imageUrl).toBe(
`http://dashboards.grafana.com/render/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}&width=1000&height=500&tz=UTC`
);
});
});

View File

@ -51,11 +51,11 @@ export class ShareLink extends PureComponent<Props, State> {
}
buildUrl = async () => {
const { panel } = this.props;
const { panel, dashboard } = this.props;
const { useCurrentTimeRange, useShortUrl, selectedTheme } = this.state;
const shareUrl = await buildShareUrl(useCurrentTimeRange, selectedTheme, panel, useShortUrl);
const imageUrl = buildImageUrl(useCurrentTimeRange, selectedTheme, panel);
const imageUrl = buildImageUrl(useCurrentTimeRange, dashboard.uid, selectedTheme, panel);
this.setState({ shareUrl, imageUrl });
};

View File

@ -1,7 +1,7 @@
import { config } from '@grafana/runtime';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { createShortLink } from 'app/core/utils/shortLinks';
import { dateTime, PanelModel, TimeRange, urlUtil } from '@grafana/data';
import { dateTime, locationUtil, PanelModel, TimeRange, urlUtil } from '@grafana/data';
export interface BuildParamsArgs {
useCurrentTimeRange: boolean;
@ -68,13 +68,24 @@ export async function buildShareUrl(
return shareUrl;
}
export function buildSoloUrl(useCurrentTimeRange: boolean, selectedTheme?: string, panel?: PanelModel) {
export function buildSoloUrl(
useCurrentTimeRange: boolean,
dashboardUid: string,
selectedTheme?: string,
panel?: PanelModel
) {
const baseUrl = buildBaseUrl();
const params = buildParams({ useCurrentTimeRange, selectedTheme, panel });
let soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
// For handling the case when default_home_dashboard_path is set in the grafana config
const strippedUrl = locationUtil.stripBaseFromUrl(baseUrl);
if (strippedUrl === '/') {
soloUrl = `${config.appUrl}d-solo/${dashboardUid}`;
}
const panelId = params.get('editPanel') ?? params.get('viewPanel') ?? '';
params.set('panelId', panelId);
params.delete('editPanel');
@ -83,17 +94,26 @@ export function buildSoloUrl(useCurrentTimeRange: boolean, selectedTheme?: strin
return urlUtil.appendQueryToUrl(soloUrl, params.toString());
}
export function buildImageUrl(useCurrentTimeRange: boolean, selectedTheme?: string, panel?: PanelModel) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, selectedTheme, panel);
export function buildImageUrl(
useCurrentTimeRange: boolean,
dashboardUid: string,
selectedTheme?: string,
panel?: PanelModel
) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, dashboardUid, selectedTheme, panel);
let imageUrl = soloUrl.replace(config.appSubUrl + '/dashboard-solo/', config.appSubUrl + '/render/dashboard-solo/');
imageUrl = imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/');
imageUrl += '&width=1000&height=500' + getLocalTimeZone();
return imageUrl;
}
export function buildIframeHtml(useCurrentTimeRange: boolean, selectedTheme?: string, panel?: PanelModel) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, selectedTheme, panel);
export function buildIframeHtml(
useCurrentTimeRange: boolean,
dashboardUid: string,
selectedTheme?: string,
panel?: PanelModel
) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, dashboardUid, selectedTheme, panel);
return '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
}

View File

@ -60,6 +60,15 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage')
),
},
// This route handles embedding of snapshot/scripted dashboard panels
{
path: '/dashboard-solo/:type/:slug',
pageClass: 'dashboard-solo',
routeName: DashboardRoutes.Normal,
component: SafeDynamicImport(
() => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage')
),
},
{
path: '/d-solo/:uid',
pageClass: 'dashboard-solo',

View File

@ -9,6 +9,14 @@ describe('isSoloRoute', () => {
)
).toBe(true);
});
it('then it should return true for a dashboard-solo route', () => {
expect(
isSoloRoute(
'http://localhost:3000/render/dashboard-solo/4vEk45n7k/dash?orgId=1&from=1629329071059&to=1629350671060&panelId=5&width=1000&height=500&tz=Europe%2FStockholm'
)
).toBe(true);
});
});
describe('when called without a solo route', () => {

View File

@ -1,3 +1,3 @@
export function isSoloRoute(path: string): boolean {
return path?.toLowerCase().includes('/d-solo/');
return /(d-solo|dashboard-solo)/.test(path?.toLowerCase());
}