mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 06:12:31 +08:00
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:
@ -133,6 +133,7 @@ export interface BootData {
|
||||
* @internal
|
||||
*/
|
||||
export interface GrafanaConfig {
|
||||
isPublicDashboardView: boolean;
|
||||
datasources: { [str: string]: DataSourceInstanceSettings };
|
||||
panels: { [key: string]: PanelPluginMeta };
|
||||
minRefreshInterval: string;
|
||||
|
@ -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: {},
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
11
pkg/middleware/dashboard_public.go
Normal file
11
pkg/middleware/dashboard_public.go
Normal 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
|
||||
}
|
||||
}
|
@ -21,7 +21,8 @@ type ReqContext struct {
|
||||
SkipCache bool
|
||||
Logger log.Logger
|
||||
// RequestNonce is a cryptographic request identifier for use with Content Security Policy.
|
||||
RequestNonce string
|
||||
RequestNonce string
|
||||
IsPublicDashboardView bool
|
||||
|
||||
PerfmonTimer prometheus.Summary
|
||||
LookupTokenErr error
|
||||
|
@ -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()} />
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
@ -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 },
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -59,8 +59,17 @@ export const PanelHeader: FC<Props> = ({ panel, error, isViewing, isEditing, dat
|
||||
/>
|
||||
) : null}
|
||||
<h2 className={styles.titleText}>{title}</h2>
|
||||
<Icon name="angle-down" className="panel-menu-toggle" />
|
||||
<PanelHeaderMenuWrapper panel={panel} dashboard={dashboard} show={panelMenuOpen} onClose={closeMenu} />
|
||||
{!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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{data.request && data.request.timeInfo && (
|
||||
<span className="panel-time-info">
|
||||
<Icon name="clock-nine" size="sm" /> {data.request.timeInfo}
|
||||
|
24
public/app/features/dashboard/routes.ts
Normal file
24
public/app/features/dashboard/routes.ts
Normal 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 [];
|
||||
};
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -66,6 +66,7 @@ export enum DashboardRoutes {
|
||||
New = 'new-dashboard',
|
||||
Normal = 'normal-dashboard',
|
||||
Scripted = 'scripted-dashboard',
|
||||
Public = 'public-dashboard',
|
||||
}
|
||||
|
||||
export enum DashboardInitPhase {
|
||||
|
Reference in New Issue
Block a user