PublicDashboards: Paused or deleted public dashboard screen (#63970)

This commit is contained in:
juanicabanas 2023-03-03 10:12:29 -03:00 committed by GitHub
parent 92f47e72e1
commit c59682fad6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 172 additions and 10 deletions

View File

@ -144,7 +144,7 @@ e2e.scenario({
.clearCookies() .clearCookies()
.request({ url: getPublicDashboardAPIUrl(String(url)), failOnStatusCode: false }) .request({ url: getPublicDashboardAPIUrl(String(url)), failOnStatusCode: false })
.then((resp) => { .then((resp) => {
expect(resp.status).to.eq(404); expect(resp.status).to.eq(403);
}); });
}); });
}, },

View File

@ -208,6 +208,14 @@ export const Pages = {
}, },
}, },
}, },
PublicDashboard: {
page: 'public-dashboard-page',
NotAvailable: {
container: 'public-dashboard-not-available',
title: 'public-dashboard-title',
pausedDescription: 'public-dashboard-paused-description',
},
},
Explore: { Explore: {
url: '/explore', url: '/explore',
General: { General: {

View File

@ -43,6 +43,7 @@ func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Respons
IsFolder: false, IsFolder: false,
FolderId: dash.FolderID, FolderId: dash.FolderID,
PublicDashboardAccessToken: pubdash.AccessToken, PublicDashboardAccessToken: pubdash.AccessToken,
PublicDashboardEnabled: pubdash.IsEnabled,
} }
dash.Data.Get("timepicker").Set("hidden", !pubdash.TimeSelectionEnabled) dash.Data.Get("timepicker").Set("hidden", !pubdash.TimeSelectionEnabled)

View File

@ -190,11 +190,11 @@ func (d *PublicDashboardStoreImpl) ExistsEnabledByAccessToken(ctx context.Contex
return hasPublicDashboard, err return hasPublicDashboard, err
} }
// GetOrgIdByAccessToken Returns the public dashboard OrgId if exists and is enabled. // GetOrgIdByAccessToken Returns the public dashboard OrgId if exists.
func (d *PublicDashboardStoreImpl) GetOrgIdByAccessToken(ctx context.Context, accessToken string) (int64, error) { func (d *PublicDashboardStoreImpl) GetOrgIdByAccessToken(ctx context.Context, accessToken string) (int64, error) {
var orgId int64 var orgId int64
err := d.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { err := d.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
sql := "SELECT org_id FROM dashboard_public WHERE access_token=? AND is_enabled=true" sql := "SELECT org_id FROM dashboard_public WHERE access_token=?"
_, err := dbSession.SQL(sql, accessToken).Get(&orgId) _, err := dbSession.SQL(sql, accessToken).Get(&orgId)
if err != nil { if err != nil {

View File

@ -597,7 +597,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
assert.Equal(t, savedDashboard.OrgID, orgId) assert.Equal(t, savedDashboard.OrgID, orgId)
}) })
t.Run("GetOrgIdByAccessToken will return 0 when IsEnabled=false", func(t *testing.T) { t.Run("GetOrgIdByAccessToken will return current OrgId when IsEnabled=false", func(t *testing.T) {
setup() setup()
cmd := SavePublicDashboardCommand{ cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{ PublicDashboard: PublicDashboard{
@ -616,7 +616,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken") orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken")
require.NoError(t, err) require.NoError(t, err)
assert.NotEqual(t, savedDashboard.OrgID, orgId) assert.Equal(t, savedDashboard.OrgID, orgId)
}) })
t.Run("GetOrgIdByAccessToken will return 0 when no public dashboard has matching access token", func(t *testing.T) { t.Run("GetOrgIdByAccessToken will return 0 when no public dashboard has matching access token", func(t *testing.T) {

View File

@ -20,4 +20,6 @@ var (
ErrInvalidMaxDataPoints = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.maxDataPoints", errutil.WithPublicMessage("maxDataPoints should be greater than 0")) ErrInvalidMaxDataPoints = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.maxDataPoints", errutil.WithPublicMessage("maxDataPoints should be greater than 0"))
ErrInvalidTimeRange = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.invalidTimeRange", errutil.WithPublicMessage("Invalid time range")) ErrInvalidTimeRange = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.invalidTimeRange", errutil.WithPublicMessage("Invalid time range"))
ErrInvalidShareType = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.invalidShareType", errutil.WithPublicMessage("Invalid share type")) ErrInvalidShareType = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.invalidShareType", errutil.WithPublicMessage("Invalid share type"))
ErrPublicDashboardNotEnabled = errutil.NewBase(errutil.StatusForbidden, "publicdashboards.notEnabled", errutil.WithPublicMessage("Public dashboard paused"))
) )

View File

@ -113,7 +113,7 @@ func (pd *PublicDashboardServiceImpl) FindPublicDashboardAndDashboardByAccessTok
} }
if !pubdash.IsEnabled { if !pubdash.IsEnabled {
return nil, nil, ErrPublicDashboardNotFound.Errorf("FindPublicDashboardAndDashboardByAccessToken: Public dashboard is disabled accessToken: %s", accessToken) return nil, nil, ErrPublicDashboardNotEnabled.Errorf("FindPublicDashboardAndDashboardByAccessToken: Public dashboard is paused accessToken: %s", accessToken)
} }
dash, err := pd.store.FindDashboard(ctx, pubdash.OrgId, pubdash.DashboardUid) dash, err := pd.store.FindDashboard(ctx, pubdash.OrgId, pubdash.DashboardUid)

View File

@ -0,0 +1,68 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { useStyles2 } from '@grafana/ui/src';
import { Branding } from '../../../../core/components/Branding/Branding';
import { getLoginStyles } from '../../../../core/components/Login/LoginLayout';
const selectors = e2eSelectors.pages.PublicDashboard.NotAvailable;
export const PublicDashboardNotAvailable = ({ paused }: { paused?: boolean }) => {
const styles = useStyles2(getStyles);
const loginStyles = useStyles2(getLoginStyles);
const loginBoxBackground = Branding.LoginBoxBackground();
return (
<Branding.LoginBackground className={styles.container} data-testid={selectors.container}>
<div className={cx(styles.box, loginBoxBackground)}>
<Branding.LoginLogo className={loginStyles.loginLogo} />
<p className={styles.title} data-testid={selectors.title}>
{paused
? 'The dashboard has been temporarily paused by the administrator.'
: 'The dashboard your are trying to access does not exist.'}
</p>
{paused && (
<p className={styles.description} data-testid={selectors.pausedDescription}>
Please check again soon.
</p>
)}
</div>
</Branding.LoginBackground>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
container: css`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
:before {
opacity: 1;
}
`,
box: css`
width: 608px;
display: flex;
align-items: center;
flex-direction: column;
gap: ${theme.spacing(4)};
z-index: 1;
border-radius: ${theme.shape.borderRadius(4)};
padding: ${theme.spacing(6, 8)};
opacity: 1;
`,
title: css`
font-size: ${theme.typography.h3.fontSize};
text-align: center;
margin: 0;
`,
description: css`
font-size: ${theme.typography.h5.fontSize};
margin: 0;
`,
});

View File

@ -82,6 +82,7 @@ const renderWithProvider = ({
}; };
const selectors = e2eSelectors.components; const selectors = e2eSelectors.components;
const publicDashboardSelector = e2eSelectors.pages.PublicDashboard;
const getTestDashboard = (overrides?: Partial<Dashboard>, metaOverrides?: Partial<DashboardMeta>): DashboardModel => { const getTestDashboard = (overrides?: Partial<Dashboard>, metaOverrides?: Partial<DashboardMeta>): DashboardModel => {
const data: Dashboard = Object.assign( const data: Dashboard = Object.assign(
@ -214,6 +215,10 @@ describe('PublicDashboardPage', () => {
expect(screen.queryByTestId(selectors.RefreshPicker.runButtonV2)).not.toBeInTheDocument(); expect(screen.queryByTestId(selectors.RefreshPicker.runButtonV2)).not.toBeInTheDocument();
expect(screen.queryByTestId(selectors.RefreshPicker.intervalButtonV2)).not.toBeInTheDocument(); expect(screen.queryByTestId(selectors.RefreshPicker.intervalButtonV2)).not.toBeInTheDocument();
}); });
it('Should not render paused or deleted screen', () => {
expect(screen.queryByTestId(publicDashboardSelector.NotAvailable.container)).not.toBeInTheDocument();
});
}); });
dashboardPageScenario('Given a public dashboard with time range enabled', (ctx) => { dashboardPageScenario('Given a public dashboard with time range enabled', (ctx) => {
@ -240,4 +245,50 @@ describe('PublicDashboardPage', () => {
expect(screen.getByTestId(selectors.RefreshPicker.intervalButtonV2)).toBeInTheDocument(); expect(screen.getByTestId(selectors.RefreshPicker.intervalButtonV2)).toBeInTheDocument();
}); });
}); });
dashboardPageScenario('Given paused public dashboard', (ctx) => {
ctx.setup(() => {
ctx.mount();
ctx.rerender({
newState: {
dashboard: {
getModel: () => getTestDashboard(undefined, { publicDashboardEnabled: false, dashboardNotFound: false }),
initError: null,
initPhase: DashboardInitPhase.Completed,
permissions: [],
},
},
});
});
it('Should render public dashboard paused screen', () => {
expect(screen.queryByTestId(publicDashboardSelector.page)).not.toBeInTheDocument();
expect(screen.getByTestId(publicDashboardSelector.NotAvailable.title)).toBeInTheDocument();
expect(screen.getByTestId(publicDashboardSelector.NotAvailable.pausedDescription)).toBeInTheDocument();
});
});
dashboardPageScenario('Given deleted public dashboard', (ctx) => {
ctx.setup(() => {
ctx.mount();
ctx.rerender({
newState: {
dashboard: {
getModel: () => getTestDashboard(undefined, { dashboardNotFound: true }),
initError: null,
initPhase: DashboardInitPhase.Completed,
permissions: [],
},
},
});
});
it('Should render public dashboard deleted screen', () => {
expect(screen.queryByTestId(publicDashboardSelector.page)).not.toBeInTheDocument();
expect(screen.getByTestId(publicDashboardSelector.NotAvailable.title)).toBeInTheDocument();
expect(screen.queryByTestId(publicDashboardSelector.NotAvailable.pausedDescription)).not.toBeInTheDocument();
});
});
}); });

View File

@ -3,6 +3,7 @@ import React, { useEffect } from 'react';
import { usePrevious } from 'react-use'; import { usePrevious } from 'react-use';
import { GrafanaTheme2, PageLayoutType, TimeZone } from '@grafana/data'; import { GrafanaTheme2, PageLayoutType, TimeZone } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { PageToolbar, useStyles2 } from '@grafana/ui'; import { PageToolbar, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
@ -14,6 +15,7 @@ import { DashNavTimeControls } from '../components/DashNav/DashNavTimeControls';
import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed'; import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading'; import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading';
import { PublicDashboardFooter } from '../components/PublicDashboardFooter/PublicDashboardsFooter'; import { PublicDashboardFooter } from '../components/PublicDashboardFooter/PublicDashboardsFooter';
import { PublicDashboardNotAvailable } from '../components/PublicDashboardNotAvailable/PublicDashboardNotAvailable';
import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashboardGrid } from '../dashgrid/DashboardGrid';
import { getTimeSrv } from '../services/TimeSrv'; import { getTimeSrv } from '../services/TimeSrv';
import { DashboardModel } from '../state'; import { DashboardModel } from '../state';
@ -31,6 +33,8 @@ interface PublicDashboardPageRouteSearchParams {
export type Props = GrafanaRouteComponentProps<PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams>; export type Props = GrafanaRouteComponentProps<PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams>;
const selectors = e2eSelectors.pages.PublicDashboard;
const Toolbar = ({ dashboard }: { dashboard: DashboardModel }) => { const Toolbar = ({ dashboard }: { dashboard: DashboardModel }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -91,11 +95,20 @@ const PublicDashboardPage = (props: Props) => {
return <DashboardLoading initPhase={dashboardState.initPhase} />; return <DashboardLoading initPhase={dashboardState.initPhase} />;
} }
if (dashboard.meta.publicDashboardEnabled === false) {
return <PublicDashboardNotAvailable paused />;
}
if (dashboard.meta.dashboardNotFound) {
return <PublicDashboardNotAvailable />;
}
return ( return (
<Page <Page
pageNav={{ text: dashboard.title }} pageNav={{ text: dashboard.title }}
layout={PageLayoutType.Custom} layout={PageLayoutType.Custom}
toolbar={<Toolbar dashboard={dashboard} />} toolbar={<Toolbar dashboard={dashboard} />}
data-testid={selectors.page}
> >
{dashboardState.initError && <DashboardFailed initError={dashboardState.initError} />} {dashboardState.initError && <DashboardFailed initError={dashboardState.initError} />}
<div className={styles.gridContainer}> <div className={styles.gridContainer}>

View File

@ -9,7 +9,7 @@ import impressionSrv from 'app/core/services/impression_srv';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getGrafanaStorage } from 'app/features/storage/storage'; import { getGrafanaStorage } from 'app/features/storage/storage';
import { DashboardDTO, DashboardRoutes } from 'app/types'; import { DashboardDataDTO, DashboardDTO, DashboardMeta, DashboardRoutes } from 'app/types';
import { appEvents } from '../../../core/core'; import { appEvents } from '../../../core/core';
@ -17,7 +17,10 @@ import { getDashboardSrv } from './DashboardSrv';
export class DashboardLoaderSrv { export class DashboardLoaderSrv {
constructor() {} constructor() {}
_dashboardLoadFailed(title: string, snapshot?: boolean) { _dashboardLoadFailed(
title: string,
snapshot?: boolean
): { meta: DashboardMeta; dashboard: Partial<DashboardDataDTO> } {
snapshot = snapshot || false; snapshot = snapshot || false;
return { return {
meta: { meta: {
@ -51,8 +54,24 @@ export class DashboardLoaderSrv {
.then((result: any) => { .then((result: any) => {
return result; return result;
}) })
.catch(() => { .catch((e) => {
return this._dashboardLoadFailed('Public Dashboard Not found', true); const isPublicDashboardPaused =
e.data.statusCode === 403 && e.data.messageId === 'publicdashboards.notEnabled';
const isPublicDashboardNotFound =
e.data.statusCode === 404 && e.data.messageId === 'publicdashboards.notFound';
const dashboardModel = this._dashboardLoadFailed(
isPublicDashboardPaused ? 'Public Dashboard paused' : 'Public Dashboard Not found',
true
);
return {
...dashboardModel,
meta: {
...dashboardModel.meta,
publicDashboardEnabled: isPublicDashboardNotFound ? undefined : !isPublicDashboardPaused,
dashboardNotFound: isPublicDashboardNotFound,
},
};
}); });
} else { } else {
promise = backendSrv promise = backendSrv