Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dashboards: Add alert and panel icon for dashboards that use Angular plugins #70951

Merged
merged 34 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2699779
Add angularDeprecationUI feature toggle
xnyo Jun 30, 2023
a24c027
Add angular notice in angular panel header
xnyo Jun 30, 2023
d90a1a6
Show angular notice for angular datasources
xnyo Jun 30, 2023
f620b2e
Show angular notice at the top of the dashboard
xnyo Jun 30, 2023
e31186b
Changed Angular deprecation messages
xnyo Jul 13, 2023
deefb45
Fix angular deprecation alert displayed for new dashboards
xnyo Jul 13, 2023
7b92ad9
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Jul 13, 2023
42167f4
re-generate feature flags
xnyo Jul 13, 2023
04769b9
Removed unnecessary changes
xnyo Jul 17, 2023
6e0312a
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Jul 17, 2023
f45ade8
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Jul 17, 2023
eeba481
Add angular deprecation dashboard notice tests
xnyo Jul 17, 2023
5e4b3d6
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Jul 17, 2023
a3ae948
Add test for angular deprecation panel icon
xnyo Jul 17, 2023
98272dd
Update test suite name
xnyo Jul 17, 2023
f7edb15
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Jul 18, 2023
03c584e
Moved isAngularDatasourcePlugin to app/features/plugins/angularDeprec…
xnyo Jul 18, 2023
92293c0
Add hasAngularPlugins to DashboardModel
xnyo Jul 18, 2023
21e1abc
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Jul 25, 2023
bbff4fb
re-generate feature toggles
xnyo Jul 25, 2023
b158f11
Fix tests
xnyo Jul 25, 2023
b03b20c
Fix data source spelling
xnyo Jul 25, 2023
b4e3910
Merge remote-tracking branch 'origin/main' into giuseppe/angular-depr…
xnyo Aug 2, 2023
d4b6d8c
Fix typing issues
xnyo Aug 2, 2023
30e1127
Extract plugin type into a separate function
xnyo Aug 2, 2023
e41d759
re-generate feature flags
xnyo Aug 2, 2023
1ec452b
reportInteraction on angular dashboard notice dismiss
xnyo Aug 3, 2023
ab7aa12
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Aug 3, 2023
4fa06ca
re-generate feature flags
xnyo Aug 3, 2023
7da7b3b
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Aug 21, 2023
5b7f023
Re-generate feature flags
xnyo Aug 21, 2023
ced643e
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Aug 28, 2023
49f57c2
Merge branch 'main' into giuseppe/angular-deprecation/dashboard
xnyo Aug 29, 2023
db6d1bf
lint
xnyo Aug 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions packages/grafana-data/src/types/featureToggles.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,5 @@ export interface FeatureToggles {
influxdbSqlSupport?: boolean;
noBasicRole?: boolean;
alertingNoDataErrorExecution?: boolean;
angularDeprecationUI?: boolean;
}
7 changes: 7 additions & 0 deletions pkg/services/featuremgmt/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
)
1 change: 1 addition & 0 deletions pkg/services/featuremgmt/toggles_gen.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions pkg/services/featuremgmt/toggles_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
4 changes: 4 additions & 0 deletions public/app/features/dashboard/containers/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 && (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should have a check here that this is only shown to dashboard users who have edit permissions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could but then it makes it difficult for a viewer to discover that a dashboard they rely on has this issue

<AngularDeprecationNotice dashboardUid={dashboard.uid} />
)}
<DashboardGrid
dashboard={dashboard}
isEditable={!!dashboard.meta.canEdit}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 && (
Expand All @@ -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({
Expand All @@ -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,
}),
};
};
8 changes: 8 additions & 0 deletions public/app/features/dashboard/state/DashboardModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'> {
Expand Down
2 changes: 1 addition & 1 deletion public/app/features/dashboard/state/PanelModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
16 changes: 15 additions & 1 deletion public/app/features/dashboard/utils/getPanelChromeProps.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 && (
Expand All @@ -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}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading
Loading