PublicDashboards: Frontend routing for public dashboards (#48834)

* add isPublic to dashboard

* refactor routes to use route group and add placeholder method for sharing apii

* add sharing pane and utils for public dashboard config to sharing modal

* Sharing modal now persists data through the api

* moves ShareDashboard endpoint to new file and starts adding tests

* generates mocks. Adds tests for public dashboard feature flag

* Adds ability to pass in array of features to enable for the test

* test to update public flag on dashboard WIP

* Adds mock for SaveDashboardSharingConfig

* Fixes tests. Had to use FakeDashboardService

* Adds React tests for public dashboards toggle

* removes semicolons

* refactors SharePublic component to use hooks

* rename from `share publicly` to `public dashboard config`

* checkpoint. debugging tests. need to verify name changes

* checkpoint. test bugs fixed. need to finish returning proper response codes

* finish renaming. fix test

* Update pkg/api/api.go

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>

* update backend url

* rename internal objects and commands. fix configuration modal labels

* add endpoint for retrieving public dashboard configuration and populate the frontend state from it

* add test for dashboardCanBePublic

* adds backend routes

* copy DashboardPage component into component for public dashboards. WIP

* adds react routes, and doesnt render main nav bar when viewing a public route

* removes extra react route from testing

* updates component name

* Wrap the original dashboard component so we can pass props relevant to public dashboards, turn kiosk mode on/off, etc

* Wraps DashboardPage in PublicDashboardPage component. DashboardPage gets rendered in kiosk mode when public prop is passed.

* removes commented out code from exploratory work

* Makes public dashboard routes require no auth

* extracts helper to own util file to check if were viewing a public page

* Hides panel dropdown when its being viewed publicly

* formatting

* use function from utils file for determining if publicly viewed. If public, hides app notifications, searchwrapper, and commandpalette.

* adds unit tests for util function used to see if page is being viewed publicly

* cant added annotations to panel when being publicly viewed

* removes useless comment

* hides backend and frontend pubdash routes behind feature flag

* consider feature flag when checking url path to see if on public dashboard

* renames function

* still render app notifications when in public view

* Extract pubdash route logic into own file

* fixes failing tests

* Determines path using location locationUtils. This covers the case when grafana is being hosted on a subpath. Updates tests.

* renames pubdash web route to be more understandable

* rename route

* fixes failing test

* fixes failing test. Needed to update pubdash urls

* sets flag on grafana boot config for if viewing public dashboard. Removes hacky check that looks at the url

* fixes failing tests. Uses config to determine if viewing public dashboard

* renders the blue panel timeInfo on public dashboard panel

* Extracts conditional logic for rendering components out into their own functions

* removes publicDashboardView check, and uses dashboard meta instead

* the timeInfo is always displayed on the panel

* After fetch of public dashboard dto, the meta isPublic flag gets set and used to determine if viewing public dashboard for child components. Fixes tests for PanelHeader.

* Fixes failing test. Needed to add isPublic flag to dashboard meta.

Co-authored-by: Jeff Levin <jeff@levinology.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
owensmallwood
2022-06-02 14:57:55 -06:00
committed by GitHub
parent d452322aa8
commit cfdea1ee30
17 changed files with 153 additions and 14 deletions

View File

@ -133,6 +133,7 @@ export interface BootData {
* @internal
*/
export interface GrafanaConfig {
isPublicDashboardView: boolean;
datasources: { [str: string]: DataSourceInstanceSettings };
panels: { [key: string]: PanelPluginMeta };
minRefreshInterval: string;

View File

@ -24,6 +24,7 @@ export interface AzureSettings {
}
export class GrafanaBootConfig implements GrafanaConfig {
isPublicDashboardView: boolean;
datasources: { [str: string]: DataSourceInstanceSettings } = {};
panels: { [key: string]: PanelPluginMeta } = {};
minRefreshInterval = '';
@ -124,6 +125,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
this.theme2 = createTheme({ colors: { mode } });
this.theme = this.theme2.v1;
this.bootData = options.bootData;
this.isPublicDashboardView = options.bootData.settings.isPublicDashboardView;
const defaults = {
datasources: {},

View File

@ -89,6 +89,11 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/a/:id/*", reqSignedIn, hs.Index) // App Root Page
r.Get("/a/:id", reqSignedIn, hs.Index)
//pubdash
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
r.Get("/public-dashboards/:uid", middleware.SetPublicDashboardFlag(), hs.Index)
}
r.Get("/d/:uid/:slug", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index)
r.Get("/d/:uid", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index)
r.Get("/dashboard/script/*", reqSignedIn, hs.Index)

View File

@ -717,6 +717,10 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
return nil, err
}
if c.IsPublicDashboardView {
settings["isPublicDashboardView"] = true
}
data := dtos.IndexViewData{
User: &dtos.CurrentUser{
Id: c.UserId,

View File

@ -0,0 +1,11 @@
package middleware
import (
"github.com/grafana/grafana/pkg/models"
)
func SetPublicDashboardFlag() func(c *models.ReqContext) {
return func(c *models.ReqContext) {
c.IsPublicDashboardView = true
}
}

View File

@ -22,6 +22,7 @@ type ReqContext struct {
Logger log.Logger
// RequestNonce is a cryptographic request identifier for use with Content Security Policy.
RequestNonce string
IsPublicDashboardView bool
PerfmonTimer prometheus.Summary
LookupTokenErr error

View File

@ -95,6 +95,14 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
});
};
const commandPaletteEnabled = () => !config.isPublicDashboardView && config.featureToggles.commandPalette;
const renderNavBar = () => {
return !config.isPublicDashboardView && ready && <>{newNavigationEnabled ? <NavBarNext /> : <NavBar />}</>;
};
const searchBarEnabled = () => !config.isPublicDashboardView;
return (
<Provider store={store}>
<I18nProvider>
@ -107,10 +115,10 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
>
<ModalsProvider>
<GlobalStyles />
{config.featureToggles.commandPalette && <CommandPalette />}
{commandPaletteEnabled() && <CommandPalette />}
<div className="grafana-app">
<Router history={locationService.getHistory()}>
{ready && <>{newNavigationEnabled ? <NavBarNext /> : <NavBar />}</>}
{renderNavBar()}
<main className="main-view">
{pageBanners.map((Banner, index) => (
<Banner key={index.toString()} />
@ -118,7 +126,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
<AngularRoot />
<AppNotificationList />
<SearchWrapper />
{searchBarEnabled() && <SearchWrapper />}
{ready && this.renderRoutes()}
{bodyRenderHooks.map((Hook, index) => (
<Hook key={index.toString()} />

View File

@ -39,7 +39,7 @@ export interface DashboardPageRouteParams {
slug?: string;
}
type DashboardPageRouteSearchParams = {
export type DashboardPageRouteSearchParams = {
tab?: string;
folderId?: string;
editPanel?: string;
@ -67,7 +67,12 @@ const mapDispatchToProps = {
const connector = connect(mapStateToProps, mapDispatchToProps);
export type Props = Themeable2 &
type OwnProps = {
isPublic?: boolean;
};
export type Props = OwnProps &
Themeable2 &
GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams> &
ConnectedProps<typeof connector>;
@ -112,7 +117,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
}
initDashboard() {
const { dashboard, match, queryParams } = this.props;
const { dashboard, isPublic, match, queryParams } = this.props;
if (dashboard) {
this.closeDashboard();
@ -124,7 +129,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
urlType: match.params.type,
urlFolderId: queryParams.folderId,
routeName: this.props.route.routeName,
fixUrl: true,
fixUrl: !isPublic,
});
// small delay to start live updates
@ -312,9 +317,9 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
}
render() {
const { dashboard, initError, queryParams, theme } = this.props;
const { dashboard, initError, queryParams, theme, isPublic } = this.props;
const { editPanel, viewPanel, updateScrollTop } = this.state;
const kioskMode = getKioskMode();
const kioskMode = !isPublic ? getKioskMode() : KioskMode.Full;
const styles = getStyles(theme, kioskMode);
if (!dashboard) {

View File

@ -0,0 +1,13 @@
import React from 'react';
import { GrafanaRouteComponentProps } from '../../../core/navigation/types';
import DashboardPage, { DashboardPageRouteParams, DashboardPageRouteSearchParams } from './DashboardPage';
export type Props = GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams>;
const PublicDashboardPage = (props: Props) => {
return <DashboardPage isPublic {...props} />;
};
export default PublicDashboardPage;

View File

@ -47,6 +47,9 @@ function setupTestContext(options: Partial<Props>) {
panelInitialized: jest.fn(),
getTimezone: () => 'browser',
events: new EventBusSrv(),
meta: {
isPublic: false,
},
} as unknown as DashboardModel,
plugin: {
meta: { skipDataQuery: false },

View File

@ -20,10 +20,9 @@ import {
toUtc,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { locationService, RefreshEvent } from '@grafana/runtime';
import { config, locationService, RefreshEvent } from '@grafana/runtime';
import { VizLegendOptions } from '@grafana/schema';
import { ErrorBoundary, PanelContext, PanelContextProvider, SeriesVisibilityChangeMode } from '@grafana/ui';
import config from 'app/core/config';
import { PANEL_BORDER } from 'app/core/constants';
import { profiler } from 'app/core/profiler';
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';

View File

@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { createEmptyQueryResponse } from '../../../explore/state/utils';
import { DashboardModel, PanelModel } from '../../state';
import { PanelHeader } from './PanelHeader';
let panelModel = new PanelModel({
id: 1,
gridPos: { x: 1, y: 1, w: 1, h: 1 },
type: 'type',
title: 'title',
});
let panelData = createEmptyQueryResponse();
describe('Panel Header', () => {
const dashboardModel = new DashboardModel({}, { isPublic: true });
it('will render header title but not render dropdown icon when dashboard is being viewed publicly', () => {
window.history.pushState({}, 'Test Title', '/public-dashboards/abc123');
render(
<PanelHeader panel={panelModel} dashboard={dashboardModel} isViewing={false} isEditing={false} data={panelData} />
);
expect(screen.getByText('title')).toBeDefined();
expect(screen.queryByTestId('panel-dropdown')).toBeNull();
});
it('will render header title and dropdown icon when dashboard is not being viewed publicly', () => {
const dashboardModel = new DashboardModel({}, { isPublic: false });
window.history.pushState({}, 'Test Title', '/d/abc/123');
render(
<PanelHeader panel={panelModel} dashboard={dashboardModel} isViewing={false} isEditing={false} data={panelData} />
);
expect(screen.getByText('title')).toBeDefined();
expect(screen.getByTestId('panel-dropdown')).toBeDefined();
});
});

View File

@ -59,8 +59,17 @@ export const PanelHeader: FC<Props> = ({ panel, error, isViewing, isEditing, dat
/>
) : null}
<h2 className={styles.titleText}>{title}</h2>
{!dashboard.meta.isPublic && (
<div data-testid="panel-dropdown">
<Icon name="angle-down" className="panel-menu-toggle" />
<PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} show={panelMenuOpen} onClose={closeMenu} />
<PanelHeaderMenuWrapper
panel={panel}
dashboard={dashboard}
show={panelMenuOpen}
onClose={closeMenu}
/>
</div>
)}
{data.request && data.request.timeInfo && (
<span className="panel-time-info">
<Icon name="clock-nine" size="sm" /> {data.request.timeInfo}

View File

@ -0,0 +1,24 @@
import { SafeDynamicImport } from '../../core/components/DynamicImports/SafeDynamicImport';
import { config } from '../../core/config';
import { RouteDescriptor } from '../../core/navigation/types';
import { DashboardRoutes } from '../../types';
export const getPublicDashboardRoutes = (): RouteDescriptor[] => {
if (config.featureToggles.publicDashboards) {
return [
{
path: '/public-dashboards/:uid',
pageClass: 'page-dashboard',
routeName: DashboardRoutes.Public,
component: SafeDynamicImport(
() =>
import(
/* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPage'
)
),
},
];
}
return [];
};

View File

@ -60,6 +60,15 @@ async function fetchDashboard(
dashDTO.meta.canStar = false;
return dashDTO;
}
case DashboardRoutes.Public: {
const dashDTO: DashboardDTO = await dashboardLoaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid);
// Make sure new endpoint to fetch dashboard DTO sets these as false
dashDTO.meta.canEdit = false;
dashDTO.meta.canMakeEditable = false;
dashDTO.meta.isPublic = true;
return dashDTO;
}
case DashboardRoutes.Normal: {
const dashDTO: DashboardDTO = await dashboardLoaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid);

View File

@ -16,6 +16,7 @@ import { AccessControlAction, DashboardRoutes } from 'app/types';
import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamicImport';
import { RouteDescriptor } from '../core/navigation/types';
import { getPublicDashboardRoutes } from '../features/dashboard/routes';
export const extraRoutes: RouteDescriptor[] = [];
@ -429,6 +430,7 @@ export function getAppRoutes(): RouteDescriptor[] {
...getAlertingRoutes(),
...getProfileRoutes(),
...extraRoutes,
...getPublicDashboardRoutes(),
{
path: '/*',
component: ErrorPage,

View File

@ -66,6 +66,7 @@ export enum DashboardRoutes {
New = 'new-dashboard',
Normal = 'normal-dashboard',
Scripted = 'scripted-dashboard',
Public = 'public-dashboard',
}
export enum DashboardInitPhase {