From bf4d025012af42ce023b1166e95626f86e819855 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Tue, 29 Aug 2023 16:05:47 +0200 Subject: [PATCH] Dashboards: Add alert and panel icon for dashboards that use Angular plugins (#70951) * Add angularDeprecationUI feature toggle * Add angular notice in angular panel header * Show angular notice for angular datasources * Show angular notice at the top of the dashboard * Changed Angular deprecation messages * Fix angular deprecation alert displayed for new dashboards * re-generate feature flags * Removed unnecessary changes * Add angular deprecation dashboard notice tests * Add test for angular deprecation panel icon * Update test suite name * Moved isAngularDatasourcePlugin to app/features/plugins/angularDeprecation * Add hasAngularPlugins to DashboardModel * re-generate feature toggles * Fix tests * Fix data source spelling * Fix typing issues * Extract plugin type into a separate function * re-generate feature flags * reportInteraction on angular dashboard notice dismiss * re-generate feature flags * Re-generate feature flags * lint --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../dashboard/containers/DashboardPage.tsx | 4 + .../PanelHeaderTitleItems.test.tsx | 76 +++++++++++++++++++ .../PanelHeader/PanelHeaderTitleItems.tsx | 32 +++++++- .../dashboard/state/DashboardModel.ts | 8 ++ .../features/dashboard/state/PanelModel.ts | 2 +- .../dashboard/utils/getPanelChromeProps.tsx | 16 +++- .../AngularDeprecationNotice.test.tsx | 66 ++++++++++++++++ .../AngularDeprecationNotice.tsx | 54 +++++++++++++ 13 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.test.tsx create mode 100644 public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.test.tsx create mode 100644 public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.tsx diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 077ec28fa3d..7f7e01735ad 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -130,6 +130,7 @@ Experimental features might be changed or removed without prior notice. | `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | | `influxdbSqlSupport` | Enable InfluxDB SQL query language support with new querying UI | | `noBasicRole` | Enables a new role that has no permissions by default | +| `angularDeprecationUI` | Display new Angular deprecation-related UI features | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 1d38d3e08c0..f9df87b34f9 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -120,4 +120,5 @@ export interface FeatureToggles { influxdbSqlSupport?: boolean; noBasicRole?: boolean; alertingNoDataErrorExecution?: boolean; + angularDeprecationUI?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index fd8b11cc602..42ae8feee65 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -708,5 +708,12 @@ var ( Owner: grafanaAlertingSquad, RequiresRestart: true, }, + { + Name: "angularDeprecationUI", + Description: "Display new Angular deprecation-related UI features", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaPluginsPlatformSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index cb128dd5f9a..d537a0c075c 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -101,3 +101,4 @@ configurableSchedulerTick,experimental,@grafana/alerting-squad,false,false,true, influxdbSqlSupport,experimental,@grafana/observability-metrics,false,false,false,false noBasicRole,experimental,@grafana/grafana-authnz-team,false,false,true,true alertingNoDataErrorExecution,privatePreview,@grafana/alerting-squad,false,false,true,false +angularDeprecationUI,experimental,@grafana/plugins-platform-backend,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 2b1a1440750..b871cd85e05 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -414,4 +414,8 @@ const ( // FlagAlertingNoDataErrorExecution // Changes how Alerting state manager handles execution of NoData/Error FlagAlertingNoDataErrorExecution = "alertingNoDataErrorExecution" + + // FlagAngularDeprecationUI + // Display new Angular deprecation-related UI features + FlagAngularDeprecationUI = "angularDeprecationUI" ) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 60afb2173cc..45aac84d0bc 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -16,6 +16,7 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { PanelModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; +import { AngularDeprecationNotice } from 'app/features/plugins/angularDeprecation/AngularDeprecationNotice'; import { getPageNavFromSlug, getRootContentNavModel } from 'app/features/storage/StorageFolderPage'; import { DashboardRoutes, KioskMode, StoreState } from 'app/types'; import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; @@ -387,6 +388,9 @@ export class UnthemedDashboardPage extends PureComponent { )} + {config.featureToggles.angularDeprecationUI && dashboard.hasAngularPlugins() && dashboard.uid !== null && ( + + )} ) { + render( + + ); +} + +describe('PanelHeaderTitleItems angular deprecation', () => { + const iconSelector = 'angular-deprecation-icon'; + it('should render angular warning icon for angular plugins', () => { + renderComponent(); + expect(screen.getByTestId(iconSelector)).toBeInTheDocument(); + }); + + it('should not render angular warning icon for non-angular plugins', () => { + renderComponent({ show: false }); + expect(screen.queryByTestId(iconSelector)).not.toBeInTheDocument(); + }); + + describe('Tooltip text', () => { + const tests = [ + { + name: 'panel', + isAngularPanel: true, + isAngularDatasource: false, + expect: /This panel requires Angular/i, + }, + { + name: 'datasource', + isAngularPanel: false, + isAngularDatasource: true, + expect: /This data source requires Angular/i, + }, + { + name: 'unknown (generic)', + isAngularPanel: false, + isAngularDatasource: false, + expect: /This panel or data source requires Angular/i, + }, + ]; + tests.forEach((test) => { + it(`should render the correct tooltip depending on plugin type for {test.name}`, async () => { + renderComponent({ + isAngularDatasource: test.isAngularDatasource, + isAngularPanel: test.isAngularPanel, + }); + await userEvent.hover(screen.getByTestId(iconSelector)); + await waitFor(() => { + expect(screen.getByText(test.expect)).toBeInTheDocument(); + }); + }); + }); + }); +}); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx index 64cc2795c96..83f4cb7508b 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx @@ -8,16 +8,23 @@ import { PanelLinks } from '../PanelLinks'; import { PanelHeaderNotices } from './PanelHeaderNotices'; +export interface AngularNotice { + show: boolean; + isAngularPanel: boolean; + isAngularDatasource: boolean; +} + export interface Props { alertState?: string; data: PanelData; panelId: number; onShowPanelLinks?: () => Array>; panelLinks?: DataLink[]; + angularNotice?: AngularNotice; } export function PanelHeaderTitleItems(props: Props) { - const { alertState, data, panelId, onShowPanelLinks, panelLinks } = props; + const { alertState, data, panelId, onShowPanelLinks, panelLinks, angularNotice } = props; const styles = useStyles2(getStyles); // panel health @@ -47,6 +54,15 @@ export function PanelHeaderTitleItems(props: Props) { ); + const message = `This ${pluginType(angularNotice)} requires Angular (deprecated).`; + const angularNoticeTooltip = ( + + + + + + ); + return ( <> {panelLinks && panelLinks.length > 0 && onShowPanelLinks && ( @@ -56,10 +72,21 @@ export function PanelHeaderTitleItems(props: Props) { {} {timeshift} {alertState && alertStateItem} + {angularNotice?.show && angularNoticeTooltip} ); } +const pluginType = (angularNotice?: AngularNotice): string => { + if (angularNotice?.isAngularPanel) { + return 'panel'; + } + if (angularNotice?.isAngularDatasource) { + return 'data source'; + } + return 'panel or data source'; +}; + const getStyles = (theme: GrafanaTheme2) => { return { ok: css({ @@ -80,5 +107,8 @@ const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.emphasize(theme.colors.text.link, 0.03), }, }), + angularNotice: css({ + color: theme.colors.warning.text, + }), }; }; diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index c0456568f05..3ad83834279 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -23,6 +23,7 @@ import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants'; import { contextSrv } from 'app/core/services/context_srv'; import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; +import { isAngularDatasourcePlugin } from 'app/features/plugins/angularDeprecation/utils'; import { variableAdapters } from 'app/features/variables/adapters'; import { onTimeRangeUpdated } from 'app/features/variables/state/actions'; import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors'; @@ -1303,6 +1304,13 @@ export class DashboardModel implements TimeModel { getOriginalDashboard() { return this.originalDashboard; } + + hasAngularPlugins(): boolean { + return this.panels.some( + (panel) => + panel.isAngularPlugin() || (panel.datasource?.uid ? isAngularDatasourcePlugin(panel.datasource?.uid) : false) + ); + } } function isPanelWithLegend(panel: PanelModel): panel is PanelModel & Pick, 'legend'> { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 8e51834629d..41fe17497ee 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -614,7 +614,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { } isAngularPlugin(): boolean { - return (this.plugin && this.plugin.angularPanelCtrl) !== undefined; + return (this.plugin && this.plugin.angularPanelCtrl) !== undefined || (this.plugin?.meta?.angularDetected ?? false); } destroy() { diff --git a/public/app/features/dashboard/utils/getPanelChromeProps.tsx b/public/app/features/dashboard/utils/getPanelChromeProps.tsx index 21ec9e72296..9f7c4527804 100644 --- a/public/app/features/dashboard/utils/getPanelChromeProps.tsx +++ b/public/app/features/dashboard/utils/getPanelChromeProps.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { LinkModel, PanelData, PanelPlugin, renderMarkdown } from '@grafana/data'; -import { getTemplateSrv, locationService, reportInteraction } from '@grafana/runtime'; +import { config, getTemplateSrv, locationService, reportInteraction } from '@grafana/runtime'; import { PanelPadding } from '@grafana/ui'; import { InspectTab } from 'app/features/inspector/types'; import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; +import { isAngularDatasourcePlugin } from 'app/features/plugins/angularDeprecation/utils'; import { PanelHeaderTitleItems } from '../dashgrid/PanelHeader/PanelHeaderTitleItems'; import { DashboardModel, PanelModel } from '../state'; @@ -83,10 +84,18 @@ export function getPanelChromeProps(props: CommonProps) { const padding: PanelPadding = props.plugin.noPadding ? 'none' : 'md'; const alertState = props.data.alertState?.state; + const isAngularDatasource = props.panel.datasource?.uid + ? isAngularDatasourcePlugin(props.panel.datasource?.uid) + : false; + const isAngularPanel = props.panel.isAngularPlugin(); + const showAngularNotice = + (config.featureToggles.angularDeprecationUI ?? false) && (isAngularDatasource || isAngularPanel); + const showTitleItems = (props.panel.links && props.panel.links.length > 0 && onShowPanelLinks) || (props.data.series.length > 0 && props.data.series.some((v) => (v.meta?.notices?.length ?? 0) > 0)) || (props.data.request && props.data.request.timeInfo) || + showAngularNotice || alertState; const titleItems = showTitleItems && ( @@ -95,6 +104,11 @@ export function getPanelChromeProps(props: CommonProps) { data={props.data} panelId={props.panel.id} panelLinks={props.panel.links} + angularNotice={{ + show: showAngularNotice, + isAngularDatasource, + isAngularPanel, + }} onShowPanelLinks={onShowPanelLinks} /> ); diff --git a/public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.test.tsx b/public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.test.tsx new file mode 100644 index 00000000000..9c1efcedbf8 --- /dev/null +++ b/public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { reportInteraction } from '@grafana/runtime'; + +import { AngularDeprecationNotice } from './AngularDeprecationNotice'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), +})); + +function localStorageKey(dsUid: string) { + return `grafana.angularDeprecation.dashboardNotice.isDismissed.${dsUid}`; +} + +describe('AngularDeprecationNotice', () => { + const noticeText = /This dashboard depends on Angular/i; + const dsUid = 'abc'; + + afterAll(() => { + jest.resetAllMocks(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + window.localStorage.clear(); + }); + + it('should render', () => { + render(); + expect(screen.getByText(noticeText)).toBeInTheDocument(); + }); + + it('should be dismissable', async () => { + render(); + const closeButton = screen.getByRole('button'); + expect(closeButton).toBeInTheDocument(); + await userEvent.click(closeButton); + expect(screen.queryByText(noticeText)).not.toBeInTheDocument(); + }); + + it('should persist dismission status in localstorage', async () => { + render(); + expect(window.localStorage.getItem(localStorageKey(dsUid))).toBeNull(); + const closeButton = screen.getByRole('button'); + expect(closeButton).toBeInTheDocument(); + await userEvent.click(closeButton); + expect(window.localStorage.getItem(localStorageKey(dsUid))).toBe('true'); + }); + + it('should not re-render alert if already dismissed', () => { + window.localStorage.setItem(localStorageKey(dsUid), 'true'); + render(); + expect(screen.queryByText(noticeText)).not.toBeInTheDocument(); + }); + + it('should call reportInteraction when dismissing', async () => { + render(); + const closeButton = screen.getByRole('button'); + expect(closeButton).toBeInTheDocument(); + await userEvent.click(closeButton); + expect(reportInteraction).toHaveBeenCalledWith('angular_deprecation_notice_dismissed'); + }); +}); diff --git a/public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.tsx b/public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.tsx new file mode 100644 index 00000000000..f697e183f9c --- /dev/null +++ b/public/app/features/plugins/angularDeprecation/AngularDeprecationNotice.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import { reportInteraction } from '@grafana/runtime'; +import { Alert } from '@grafana/ui'; +import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; + +const LOCAL_STORAGE_KEY_PREFIX = 'grafana.angularDeprecation.dashboardNotice.isDismissed'; + +function localStorageKey(dashboardUid: string): string { + return LOCAL_STORAGE_KEY_PREFIX + '.' + dashboardUid; +} + +export interface Props { + dashboardUid: string; +} + +export function AngularDeprecationNotice({ dashboardUid }: Props) { + return ( + storageKey={localStorageKey(dashboardUid)} defaultValue={false}> + {(isDismissed, onDismiss) => { + if (isDismissed) { + return null; + } + return ( +
+ { + reportInteraction('angular_deprecation_notice_dismissed'); + onDismiss(true); + }} + > + + +
+ ); + }} + + ); +}