mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
6b9f51c209
commit
bf4d025012
@ -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
|
||||
|
||||
|
@ -120,4 +120,5 @@ export interface FeatureToggles {
|
||||
influxdbSqlSupport?: boolean;
|
||||
noBasicRole?: boolean;
|
||||
alertingNoDataErrorExecution?: boolean;
|
||||
angularDeprecationUI?: boolean;
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -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
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
@ -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<Props, State> {
|
||||
<SubMenu dashboard={dashboard} annotations={dashboard.annotations.list} links={dashboard.links} />
|
||||
</section>
|
||||
)}
|
||||
{config.featureToggles.angularDeprecationUI && dashboard.hasAngularPlugins() && dashboard.uid !== null && (
|
||||
<AngularDeprecationNotice dashboardUid={dashboard.uid} />
|
||||
)}
|
||||
<DashboardGrid
|
||||
dashboard={dashboard}
|
||||
isEditable={!!dashboard.meta.canEdit}
|
||||
|
@ -0,0 +1,76 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { LoadingState, TimeRange } from '@grafana/data';
|
||||
|
||||
import { AngularNotice, PanelHeaderTitleItems } from './PanelHeaderTitleItems';
|
||||
|
||||
function renderComponent(angularNoticeOverride?: Partial<AngularNotice>) {
|
||||
render(
|
||||
<PanelHeaderTitleItems
|
||||
data={{
|
||||
series: [],
|
||||
state: LoadingState.Done,
|
||||
timeRange: {} as TimeRange,
|
||||
}}
|
||||
panelId={1}
|
||||
angularNotice={{
|
||||
...{
|
||||
show: true,
|
||||
isAngularDatasource: false,
|
||||
isAngularPanel: false,
|
||||
},
|
||||
...angularNoticeOverride,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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<LinkModel<PanelModel>>;
|
||||
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 = (
|
||||
<Tooltip content={message}>
|
||||
<PanelChrome.TitleItem className={styles.angularNotice} data-testid="angular-deprecation-icon">
|
||||
<Icon name="exclamation-triangle" size="md" />
|
||||
</PanelChrome.TitleItem>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{panelLinks && panelLinks.length > 0 && onShowPanelLinks && (
|
||||
@ -56,10 +72,21 @@ export function PanelHeaderTitleItems(props: Props) {
|
||||
{<PanelHeaderNotices panelId={panelId} frames={data.series} />}
|
||||
{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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -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<Required<PanelModel>, 'legend'> {
|
||||
|
@ -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() {
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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(<AngularDeprecationNotice dashboardUid={dsUid} />);
|
||||
expect(screen.getByText(noticeText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be dismissable', async () => {
|
||||
render(<AngularDeprecationNotice dashboardUid={dsUid} />);
|
||||
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(<AngularDeprecationNotice dashboardUid={dsUid} />);
|
||||
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(<AngularDeprecationNotice dashboardUid={dsUid} />);
|
||||
expect(screen.queryByText(noticeText)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call reportInteraction when dismissing', async () => {
|
||||
render(<AngularDeprecationNotice dashboardUid={dsUid} />);
|
||||
const closeButton = screen.getByRole('button');
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
await userEvent.click(closeButton);
|
||||
expect(reportInteraction).toHaveBeenCalledWith('angular_deprecation_notice_dismissed');
|
||||
});
|
||||
});
|
@ -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 (
|
||||
<LocalStorageValueProvider<boolean> storageKey={localStorageKey(dashboardUid)} defaultValue={false}>
|
||||
{(isDismissed, onDismiss) => {
|
||||
if (isDismissed) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="This dashboard depends on Angular, which is deprecated and will stop working in future releases of Grafana."
|
||||
onRemove={() => {
|
||||
reportInteraction('angular_deprecation_notice_dismissed');
|
||||
onDismiss(true);
|
||||
}}
|
||||
>
|
||||
<div className="markdown-html">
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/developers/angular_deprecation/"
|
||||
className="external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read our deprecation notice and migration advice.
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</LocalStorageValueProvider>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user