From c59682fad69550efdb4b445d67144fdb335caf7f Mon Sep 17 00:00:00 2001 From: juanicabanas Date: Fri, 3 Mar 2023 10:12:29 -0300 Subject: [PATCH] PublicDashboards: Paused or deleted public dashboard screen (#63970) --- .../dashboard-public-create.spec.ts | 2 +- .../src/selectors/pages.ts | 8 +++ pkg/services/publicdashboards/api/query.go | 1 + .../publicdashboards/database/database.go | 4 +- .../database/database_test.go | 4 +- .../publicdashboards/models/errors.go | 2 + .../publicdashboards/service/service.go | 2 +- .../PublicDashboardNotAvailable.tsx | 68 +++++++++++++++++++ .../containers/PublicDashboardPage.test.tsx | 51 ++++++++++++++ .../containers/PublicDashboardPage.tsx | 13 ++++ .../dashboard/services/DashboardLoaderSrv.ts | 27 ++++++-- 11 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx diff --git a/e2e/dashboards-suite/dashboard-public-create.spec.ts b/e2e/dashboards-suite/dashboard-public-create.spec.ts index c920cdc7022..304f502dfa3 100644 --- a/e2e/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-public-create.spec.ts @@ -144,7 +144,7 @@ e2e.scenario({ .clearCookies() .request({ url: getPublicDashboardAPIUrl(String(url)), failOnStatusCode: false }) .then((resp) => { - expect(resp.status).to.eq(404); + expect(resp.status).to.eq(403); }); }); }, diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 69ec204810e..15fece4f375 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -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: { url: '/explore', General: { diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go index 4f4583d621f..a5ef7eea240 100644 --- a/pkg/services/publicdashboards/api/query.go +++ b/pkg/services/publicdashboards/api/query.go @@ -43,6 +43,7 @@ func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Respons IsFolder: false, FolderId: dash.FolderID, PublicDashboardAccessToken: pubdash.AccessToken, + PublicDashboardEnabled: pubdash.IsEnabled, } dash.Data.Get("timepicker").Set("hidden", !pubdash.TimeSelectionEnabled) diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index 7087e8cd625..180612ca12c 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -190,11 +190,11 @@ func (d *PublicDashboardStoreImpl) ExistsEnabledByAccessToken(ctx context.Contex 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) { var orgId int64 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) if err != nil { diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 754b9136808..4dbb7eaff9c 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -597,7 +597,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { 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() cmd := SavePublicDashboardCommand{ PublicDashboard: PublicDashboard{ @@ -616,7 +616,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken") 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) { diff --git a/pkg/services/publicdashboards/models/errors.go b/pkg/services/publicdashboards/models/errors.go index 780aacbe792..7a122248440 100644 --- a/pkg/services/publicdashboards/models/errors.go +++ b/pkg/services/publicdashboards/models/errors.go @@ -20,4 +20,6 @@ var ( 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")) ErrInvalidShareType = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.invalidShareType", errutil.WithPublicMessage("Invalid share type")) + + ErrPublicDashboardNotEnabled = errutil.NewBase(errutil.StatusForbidden, "publicdashboards.notEnabled", errutil.WithPublicMessage("Public dashboard paused")) ) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 23fd5417843..a1c0a45b7a8 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -113,7 +113,7 @@ func (pd *PublicDashboardServiceImpl) FindPublicDashboardAndDashboardByAccessTok } 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) diff --git a/public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx b/public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx new file mode 100644 index 00000000000..363afd720ba --- /dev/null +++ b/public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx @@ -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 ( + +
+ +

+ {paused + ? 'The dashboard has been temporarily paused by the administrator.' + : 'The dashboard your are trying to access does not exist.'} +

+ {paused && ( +

+ Please check again soon. +

+ )} +
+
+ ); +}; + +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; + `, +}); diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index f012e8886af..86d3315db95 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -82,6 +82,7 @@ const renderWithProvider = ({ }; const selectors = e2eSelectors.components; +const publicDashboardSelector = e2eSelectors.pages.PublicDashboard; const getTestDashboard = (overrides?: Partial, metaOverrides?: Partial): DashboardModel => { const data: Dashboard = Object.assign( @@ -214,6 +215,10 @@ describe('PublicDashboardPage', () => { expect(screen.queryByTestId(selectors.RefreshPicker.runButtonV2)).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) => { @@ -240,4 +245,50 @@ describe('PublicDashboardPage', () => { 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(); + }); + }); }); diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 52ba5bc257a..bf4ee765c2a 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'; import { usePrevious } from 'react-use'; import { GrafanaTheme2, PageLayoutType, TimeZone } from '@grafana/data'; +import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { PageToolbar, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { useGrafana } from 'app/core/context/GrafanaContext'; @@ -14,6 +15,7 @@ import { DashNavTimeControls } from '../components/DashNav/DashNavTimeControls'; import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed'; import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading'; import { PublicDashboardFooter } from '../components/PublicDashboardFooter/PublicDashboardsFooter'; +import { PublicDashboardNotAvailable } from '../components/PublicDashboardNotAvailable/PublicDashboardNotAvailable'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { getTimeSrv } from '../services/TimeSrv'; import { DashboardModel } from '../state'; @@ -31,6 +33,8 @@ interface PublicDashboardPageRouteSearchParams { export type Props = GrafanaRouteComponentProps; +const selectors = e2eSelectors.pages.PublicDashboard; + const Toolbar = ({ dashboard }: { dashboard: DashboardModel }) => { const dispatch = useDispatch(); @@ -91,11 +95,20 @@ const PublicDashboardPage = (props: Props) => { return ; } + if (dashboard.meta.publicDashboardEnabled === false) { + return ; + } + + if (dashboard.meta.dashboardNotFound) { + return ; + } + return ( } + data-testid={selectors.page} > {dashboardState.initError && }
diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index 3fb7f7da586..aa425478e4c 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -9,7 +9,7 @@ import impressionSrv from 'app/core/services/impression_srv'; import kbn from 'app/core/utils/kbn'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; 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'; @@ -17,7 +17,10 @@ import { getDashboardSrv } from './DashboardSrv'; export class DashboardLoaderSrv { constructor() {} - _dashboardLoadFailed(title: string, snapshot?: boolean) { + _dashboardLoadFailed( + title: string, + snapshot?: boolean + ): { meta: DashboardMeta; dashboard: Partial } { snapshot = snapshot || false; return { meta: { @@ -51,8 +54,24 @@ export class DashboardLoaderSrv { .then((result: any) => { return result; }) - .catch(() => { - return this._dashboardLoadFailed('Public Dashboard Not found', true); + .catch((e) => { + 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 { promise = backendSrv