diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 94a0b4c6b3c..63062fc2bcb 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1840,14 +1840,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/pages/DashboardScenePage.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 2 - }, - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 9a6b5194abb..68c4c80788b 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -174,6 +174,56 @@ describe('DashboardScenePage', () => { expect(await screen.findByText('Content B')).toBeInTheDocument(); }); + it('shows Powered by footer in kiosk mode', async () => { + setup({ routeProps: { queryParams: { kiosk: true } } }); + + await waitForDashboardToRender(); + + expect(await screen.findByTestId(selectors.pages.PublicDashboard.footer)).toBeInTheDocument(); + }); + + it('uses kiosk dashboard CTA url', async () => { + setup({ routeProps: { queryParams: { kiosk: true } } }); + + await waitForDashboardToRender(); + + const footer = await screen.findByTestId(selectors.pages.PublicDashboard.footer); + const link = footer.querySelector('a'); + expect(link).toHaveAttribute('href', 'https://grafana.com/?src=grafananet&cnt=kiosk-dashboard'); + }); + + it('hides Powered by footer in kiosk mode when hideLogo is present', async () => { + setup({ routeProps: { queryParams: { kiosk: true, hideLogo: true } } }); + + await waitForDashboardToRender(); + + expect(screen.queryByTestId(selectors.pages.PublicDashboard.footer)).not.toBeInTheDocument(); + }); + + it('hides Powered by footer in kiosk mode when hideLogo=1', async () => { + setup({ routeProps: { queryParams: { kiosk: true, hideLogo: '1' } } }); + + await waitForDashboardToRender(); + + expect(screen.queryByTestId(selectors.pages.PublicDashboard.footer)).not.toBeInTheDocument(); + }); + + it('shows Powered by footer in kiosk mode when hideLogo=0', async () => { + setup({ routeProps: { queryParams: { kiosk: true, hideLogo: '0' } } }); + + await waitForDashboardToRender(); + + expect(await screen.findByTestId(selectors.pages.PublicDashboard.footer)).toBeInTheDocument(); + }); + + it('shows Powered by footer in kiosk mode when hideLogo=false', async () => { + setup({ routeProps: { queryParams: { kiosk: true, hideLogo: 'false' } } }); + + await waitForDashboardToRender(); + + expect(await screen.findByTestId(selectors.pages.PublicDashboard.footer)).toBeInTheDocument(); + }); + it('routeReloadCounter should trigger reload', async () => { const { rerender, props } = setup(); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index 51f400eb39d..eaca42c62c7 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -9,6 +9,7 @@ import { Box } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; +import { PublicDashboardFooter } from 'app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter'; import { DashboardPageError } from 'app/features/dashboard/containers/DashboardPageError'; import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; import { getDashboardSceneProfiler } from 'app/features/dashboard/services/DashboardProfiler'; @@ -21,6 +22,25 @@ import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSes import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager'; +const KIOSK_DASHBOARD_FOOTER_URL = 'https://grafana.com/?src=grafananet&cnt=kiosk-dashboard'; + +function shouldHideDashboardKioskFooter(hideLogo?: string | true): boolean { + if (hideLogo === undefined || hideLogo === null) { + return false; + } + + if (hideLogo === true || hideLogo === '1') { + return true; + } + + const normalized = String(hideLogo).trim().toLowerCase(); + if (normalized === '') { + return true; + } + + return normalized !== 'false' && normalized !== '0'; +} + export interface Props extends Omit, 'match'> {} @@ -33,7 +53,32 @@ export function DashboardScenePage({ route, queryParams, location }: Props) { const stateManager = getDashboardScenePageStateManager(); const { dashboard, isLoading, loadError } = stateManager.useState(); // After scene migration is complete and we get rid of old dashboard we should refactor dashboardWatcher so this route reload is not need - const routeReloadCounter = (location.state as any)?.routeReloadCounter; + const routeReloadCounter = (() => { + const state = location.state; + if (state && typeof state === 'object') { + const value = Reflect.get(state, 'routeReloadCounter'); + return typeof value === 'number' ? value : undefined; + } + + return undefined; + })(); + + const dashboardRoute = (() => { + switch (route.routeName) { + case DashboardRoutes.Home: + case DashboardRoutes.New: + case DashboardRoutes.Template: + case DashboardRoutes.Normal: + case DashboardRoutes.Provisioning: + case DashboardRoutes.Scripted: + case DashboardRoutes.Public: + case DashboardRoutes.Embedded: + case DashboardRoutes.Report: + return route.routeName; + default: + return DashboardRoutes.Normal; + } + })(); const prevParams = useRef>(params); useEffect(() => { @@ -44,7 +89,7 @@ export function DashboardScenePage({ route, queryParams, location }: Props) { uid: (route.routeName === DashboardRoutes.Provisioning ? path : uid) ?? '', type, slug, - route: route.routeName as DashboardRoutes, + route: dashboardRoute, urlFolderUid: queryParams.folderUid, }); } @@ -106,12 +151,18 @@ export function DashboardScenePage({ route, queryParams, location }: Props) { return null; } + const showKioskFooter = queryParams.kiosk === '1' || queryParams.kiosk === true; + const hideKioskFooter = shouldHideDashboardKioskFooter(queryParams.hideLogo); + return ( + {showKioskFooter && !hideKioskFooter && ( + + )} ); } diff --git a/public/app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter.tsx b/public/app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter.tsx index 884f22dedc8..61d86dacf66 100644 --- a/public/app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter.tsx +++ b/public/app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter.tsx @@ -1,20 +1,52 @@ import { css } from '@emotion/css'; +import type { CSSProperties } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { useStyles2 } from '@grafana/ui'; +import { useStyles2, useTheme2 } from '@grafana/ui'; import { useGetPublicDashboardConfig } from './usePublicDashboardConfig'; const selectors = e2eSelectors.pages.PublicDashboard; -export const PublicDashboardFooter = function () { +export interface PublicDashboardFooterProps { + /** + * Applies horizontal padding to the footer container. + * Useful when rendering the footer in layouts that don't already have page padding (e.g. kiosk mode). + */ + paddingX?: number; + /** + * When true, avoids clipping in containers with `overflow: hidden` by not relying on a fixed height. + */ + useMinHeight?: boolean; + /** + * Overrides the CTA link URL. + * Useful when reusing the footer outside public dashboards. + */ + linkUrl?: string; +} + +export const PublicDashboardFooter = function ({ paddingX, useMinHeight, linkUrl }: PublicDashboardFooterProps) { const styles = useStyles2(getStyles); + const theme = useTheme2(); const conf = useGetPublicDashboardConfig(); + const footerStyle: CSSProperties = {}; + if (paddingX !== undefined) { + footerStyle.boxSizing = 'border-box'; + footerStyle.paddingLeft = theme.spacing(paddingX); + footerStyle.paddingRight = theme.spacing(paddingX); + } + + if (useMinHeight) { + footerStyle.height = 'auto'; + footerStyle.minHeight = '30px'; + footerStyle.alignItems = 'center'; + } + return conf.footerHide ? null : ( -
- +
+ {conf.footerText}
diff --git a/public/app/features/dashboard/containers/types.ts b/public/app/features/dashboard/containers/types.ts index ff2f673ff03..5eeff417c32 100644 --- a/public/app/features/dashboard/containers/types.ts +++ b/public/app/features/dashboard/containers/types.ts @@ -18,6 +18,7 @@ export type DashboardPageRouteSearchParams = { to?: string; refresh?: string; kiosk?: string | true; + hideLogo?: string | true; scenes?: boolean; shareView?: string; ref?: string; // used for repo preview