AngularMigration: Allow dashboard by dashboard migration (#84100)

This commit is contained in:
Adela Almasan 2024-03-27 15:24:24 -06:00 committed by GitHub
parent 1ffeb7c365
commit e5d1cd8ea5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 275 additions and 32 deletions

View File

@ -2759,7 +2759,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"]
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "5"]
],
"public/app/features/dashboard/containers/DashboardPageProxy.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -214,7 +214,9 @@ export class GrafanaBootConfig implements GrafanaConfig {
systemDateFormats.update(this.dateFormats);
}
overrideFeatureTogglesFromUrl(this);
if (this.buildInfo.env === 'development') {
overrideFeatureTogglesFromUrl(this);
}
overrideFeatureTogglesFromLocalStorage(this);
if (this.featureToggles.disableAngular) {
@ -251,9 +253,7 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) {
return;
}
const isLocalDevEnv = config.buildInfo.env === 'development';
const prodUrlAllowedFeatureFlags = new Set([
const migrationFeatureFlags = new Set([
'autoMigrateOldPanels',
'autoMigrateGraphPanel',
'autoMigrateTablePanel',
@ -269,7 +269,8 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) {
const featureToggles = config.featureToggles as Record<string, boolean>;
const featureName = key.substring(10);
if (!isLocalDevEnv && !prodUrlAllowedFeatureFlags.has(featureName)) {
// skip the migration feature flags
if (migrationFeatureFlags.has(featureName)) {
return;
}

View File

@ -17,6 +17,7 @@ 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 { AngularMigrationNotice } from 'app/features/plugins/angularDeprecation/AngularMigrationNotice';
import { getPageNavFromSlug, getRootContentNavModel } from 'app/features/storage/StorageFolderPage';
import { DashboardRoutes, KioskMode, StoreState } from 'app/types';
import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
@ -36,6 +37,7 @@ import { SubMenu } from '../components/SubMenu/SubMenu';
import { DashboardGrid } from '../dashgrid/DashboardGrid';
import { liveTimer } from '../dashgrid/liveTimer';
import { getTimeSrv } from '../services/TimeSrv';
import { explicitlyControlledMigrationPanels, autoMigrateAngular } from '../state/PanelModel';
import { cleanUpDashboardAndVariables } from '../state/actions';
import { initDashboard } from '../state/initDashboard';
@ -319,6 +321,50 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
);
}
const migrationFeatureFlags = new Set([
'autoMigrateOldPanels',
'autoMigrateGraphPanel',
'autoMigrateTablePanel',
'autoMigratePiechartPanel',
'autoMigrateWorldmapPanel',
'autoMigrateStatPanel',
'disableAngular',
]);
const isAutoMigrationFlagSet = () => {
const urlParams = new URLSearchParams(window.location.search);
let isFeatureFlagSet = false;
urlParams.forEach((value, key) => {
if (key.startsWith('__feature.')) {
const featureName = key.substring(10);
const toggleState = value === 'true' || value === '';
const featureToggles = config.featureToggles as Record<string, boolean>;
if (featureToggles[featureName]) {
return;
}
if (migrationFeatureFlags.has(featureName) && toggleState) {
isFeatureFlagSet = true;
return;
}
}
});
return isFeatureFlagSet;
};
const dashboardWasAngular = dashboard.panels.some(
(panel) => panel.autoMigrateFrom && autoMigrateAngular[panel.autoMigrateFrom] != null
);
const showDashboardMigrationNotice =
config.featureToggles.angularDeprecationUI &&
dashboardWasAngular &&
isAutoMigrationFlagSet() &&
dashboard.uid !== null;
return (
<>
<Page
@ -349,8 +395,14 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
</section>
)}
{config.featureToggles.angularDeprecationUI && dashboard.hasAngularPlugins() && dashboard.uid !== null && (
<AngularDeprecationNotice dashboardUid={dashboard.uid} />
<AngularDeprecationNotice
dashboardUid={dashboard.uid}
showAutoMigrateLink={dashboard.panels.some((panel) =>
explicitlyControlledMigrationPanels.includes(panel.type)
)}
/>
)}
{showDashboardMigrationNotice && <AngularMigrationNotice dashboardUid={dashboard.uid} />}
<DashboardGrid
dashboard={dashboard}
isEditable={!!dashboard.meta.canEdit}

View File

@ -44,7 +44,7 @@ import { getTimeSrv } from '../services/TimeSrv';
import { mergePanels, PanelMergeInfo } from '../utils/panelMerge';
import { DashboardMigrator } from './DashboardMigrator';
import { PanelModel } from './PanelModel';
import { explicitlyControlledMigrationPanels, PanelModel } from './PanelModel';
import { TimeModel } from './TimeModel';
import { deleteScopeVars, isOnTheSameGridRow } from './utils';
@ -1294,8 +1294,10 @@ export class DashboardModel implements TimeModel {
return this.panels.some((panel) => {
// Return false for plugins that are angular but have angular.hideDeprecation = false
// We cannot use panel.plugin.isAngularPlugin() because panel.plugin may not be initialized at this stage.
// We also have to check for old core angular plugins (explicitlyControlledMigrationPanels).
const isAngularPanel =
config.panels[panel.type]?.angular?.detected && !config.panels[panel.type]?.angular?.hideDeprecation;
(config.panels[panel.type]?.angular?.detected || explicitlyControlledMigrationPanels.includes(panel.type)) &&
!config.panels[panel.type]?.angular?.hideDeprecation;
let isAngularDs = false;
if (panel.datasource?.uid) {
isAngularDs = isAngularDatasourcePluginAndNotHidden(panel.datasource?.uid);

View File

@ -7,12 +7,30 @@ export function getPanelPluginToMigrateTo(panel: any, forceMigration?: boolean):
return autoMigrateRemovedPanelPlugins[panel.type];
}
const isUrlFeatureFlagEnabled = (featureName: string) => {
const flag = '__feature.' + featureName;
const urlParams = new URLSearchParams(window.location.search);
const featureFlagValue = urlParams.get(flag);
return featureFlagValue === 'true' || featureFlagValue === '';
};
// Auto-migrate old angular panels
const shouldMigrateAllAngularPanels =
forceMigration || !config.angularSupportEnabled || config.featureToggles.autoMigrateOldPanels;
forceMigration ||
!config.angularSupportEnabled ||
config.featureToggles.autoMigrateOldPanels ||
isUrlFeatureFlagEnabled('autoMigrateOldPanels') ||
isUrlFeatureFlagEnabled('disableAngular');
// Graph needs special logic as it can be migrated to multiple panels
if (panel.type === 'graph' && (shouldMigrateAllAngularPanels || config.featureToggles.autoMigrateGraphPanel)) {
if (
panel.type === 'graph' &&
(shouldMigrateAllAngularPanels ||
config.featureToggles.autoMigrateGraphPanel ||
isUrlFeatureFlagEnabled('autoMigrateGraphPanel'))
) {
if (panel.xaxis?.mode === 'series') {
return 'barchart';
}
@ -28,21 +46,31 @@ export function getPanelPluginToMigrateTo(panel: any, forceMigration?: boolean):
return autoMigrateAngular[panel.type];
}
if (panel.type === 'table-old' && config.featureToggles.autoMigrateTablePanel) {
if (
panel.type === 'table-old' &&
(config.featureToggles.autoMigrateTablePanel || isUrlFeatureFlagEnabled('autoMigrateTablePanel'))
) {
return 'table';
}
if (panel.type === 'grafana-piechart-panel' && config.featureToggles.autoMigratePiechartPanel) {
if (
panel.type === 'grafana-piechart-panel' &&
(config.featureToggles.autoMigratePiechartPanel || isUrlFeatureFlagEnabled('autoMigratePiechartPanel'))
) {
return 'piechart';
}
if (panel.type === 'grafana-worldmap-panel' && config.featureToggles.autoMigrateWorldmapPanel) {
if (
panel.type === 'grafana-worldmap-panel' &&
(config.featureToggles.autoMigrateWorldmapPanel || isUrlFeatureFlagEnabled('autoMigrateWorldmapPanel'))
) {
return 'geomap';
}
if (
(panel.type === 'singlestat' || panel.type === 'grafana-singlestat-panel') &&
config.featureToggles.autoMigrateStatPanel
((panel.type === 'singlestat' || panel.type === 'grafana-singlestat-panel') &&
config.featureToggles.autoMigrateStatPanel) ||
isUrlFeatureFlagEnabled('autoMigrateStatPanel')
) {
return 'stat';
}

View File

@ -12,7 +12,7 @@ jest.mock('@grafana/runtime', () => ({
}));
function localStorageKey(dsUid: string) {
return `grafana.angularDeprecation.dashboardNotice.isDismissed.${dsUid}`;
return `grafana.angularDeprecation.dashboardNoticeAndMigration.isDismissed.${dsUid}`;
}
describe('AngularDeprecationNotice', () => {
@ -63,4 +63,24 @@ describe('AngularDeprecationNotice', () => {
await userEvent.click(closeButton);
expect(reportInteraction).toHaveBeenCalledWith('angular_deprecation_notice_dismissed');
});
describe('auto migrate button', () => {
const autoMigrateText = 'Try migration';
it('should display auto migrate button if showAutoMigrateLink is true', () => {
render(<AngularDeprecationNotice dashboardUid={dsUid} showAutoMigrateLink={true} />);
const autoMigrateButton = screen.getByRole('button', { name: /Try migration/i });
expect(autoMigrateButton).toBeInTheDocument();
});
it('should not display auto migrate button if showAutoMigrateLink is false', () => {
render(<AngularDeprecationNotice dashboardUid={dsUid} showAutoMigrateLink={false} />);
expect(screen.queryByText(autoMigrateText)).not.toBeInTheDocument();
});
it('should not display auto migrate link if showAutoMigrateLink is not provided', () => {
render(<AngularDeprecationNotice dashboardUid={dsUid} />);
expect(screen.queryByText(autoMigrateText)).not.toBeInTheDocument();
});
});
});

View File

@ -1,10 +1,10 @@
import React from 'react';
import { reportInteraction } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import { Alert, Button } from '@grafana/ui';
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
const LOCAL_STORAGE_KEY_PREFIX = 'grafana.angularDeprecation.dashboardNotice.isDismissed';
const LOCAL_STORAGE_KEY_PREFIX = 'grafana.angularDeprecation.dashboardNoticeAndMigration.isDismissed';
function localStorageKey(dashboardUid: string): string {
return LOCAL_STORAGE_KEY_PREFIX + '.' + dashboardUid;
@ -12,9 +12,19 @@ function localStorageKey(dashboardUid: string): string {
export interface Props {
dashboardUid: string;
showAutoMigrateLink?: boolean;
}
export function AngularDeprecationNotice({ dashboardUid }: Props) {
function tryMigration() {
const autoMigrateParam = '__feature.autoMigrateOldPanels';
const url = new URL(window.location.toString());
if (!url.searchParams.has(autoMigrateParam)) {
url.searchParams.append(autoMigrateParam, 'true');
}
window.open(url.toString(), '_self');
}
export function AngularDeprecationNotice({ dashboardUid, showAutoMigrateLink }: Props) {
return (
<LocalStorageValueProvider<boolean> storageKey={localStorageKey(dashboardUid)} defaultValue={false}>
{(isDismissed, onDismiss) => {
@ -32,18 +42,21 @@ export function AngularDeprecationNotice({ dashboardUid }: Props) {
}}
>
<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>
<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>
<br />
{showAutoMigrateLink && (
<Button fill="outline" size="sm" onClick={tryMigration} style={{ marginTop: 10 }}>
Try migration
</Button>
)}
</div>
</Alert>
</div>

View File

@ -0,0 +1,50 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { AngularMigrationNotice } from './AngularMigrationNotice';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
}));
describe('AngularMigrationNotice', () => {
const noticeText =
/This dashboard was migrated from Angular. Please make sure everything is behaving as expected and save and refresh this dashboard to persist the migration./i;
const dsUid = 'abc';
afterAll(() => {
jest.resetAllMocks();
});
beforeEach(() => {
jest.clearAllMocks();
});
it('should render', () => {
render(<AngularMigrationNotice dashboardUid={dsUid} />);
expect(screen.getByText(noticeText)).toBeInTheDocument();
});
it('should be dismissable', async () => {
render(<AngularMigrationNotice dashboardUid={dsUid} />);
const closeButton = screen.getByRole('button', { name: /Close alert/i });
expect(closeButton).toBeInTheDocument();
await userEvent.click(closeButton);
expect(screen.queryByText(noticeText)).not.toBeInTheDocument();
});
describe('Migration alert buttons', () => {
it('should display the "Revert migration" button', () => {
render(<AngularMigrationNotice dashboardUid={dsUid} />);
const revertMigrationButton = screen.getByRole('button', { name: /Revert migration/i });
expect(revertMigrationButton).toBeInTheDocument();
});
it('should display the "Report issue" button', () => {
render(<AngularMigrationNotice dashboardUid={dsUid} />);
const reportIssueButton = screen.getByRole('button', { name: /Report issue/i });
expect(reportIssueButton).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,66 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Button, useStyles2 } from '@grafana/ui';
import { migrationFeatureFlags } from './utils';
interface Props {
dashboardUid: string;
}
const revertAutoMigrateUrlFlag = () => {
const url = new URL(window.location.toString());
const urlParams = new URLSearchParams(url.search);
urlParams.forEach((value, key) => {
if (key.startsWith('__feature.')) {
const featureName = key.substring(10);
if (migrationFeatureFlags.has(featureName)) {
urlParams.delete(key);
}
}
});
window.location.href = new URL(url.origin + url.pathname + '?' + urlParams.toString()).toString();
};
const reportIssue = () => {
window.open(
'https://github.com/grafana/grafana/issues/new?assignees=&labels=&projects=&template=0-bug-report.yaml&title=Product+Area%3A+Short+description+of+bug'
);
};
export function AngularMigrationNotice({ dashboardUid }: Props) {
const styles = useStyles2(getStyles);
const [showAlert, setShowAlert] = useState(true);
if (showAlert) {
return (
<Alert
severity="info"
title="This dashboard was migrated from Angular. Please make sure everything is behaving as expected and save and refresh this dashboard to persist the migration."
onRemove={() => setShowAlert(false)}
>
<div className="markdown-html">
<Button fill="outline" size="sm" className={styles.linkButton} onClick={reportIssue}>
Report issue
</Button>
<Button fill="outline" size="sm" className={styles.linkButton} onClick={revertAutoMigrateUrlFlag}>
Revert migration
</Button>
</div>
</Alert>
);
}
return null;
}
const getStyles = (theme: GrafanaTheme2) => ({
linkButton: css({
marginRight: 10,
}),
});

View File

@ -14,3 +14,13 @@ export function isAngularDatasourcePluginAndNotHidden(dsUid: string): boolean {
const settings = getDsInstanceSettingsByUid(dsUid);
return (settings?.meta.angular?.detected && !settings?.meta.angular.hideDeprecation) ?? false;
}
export const migrationFeatureFlags = new Set([
'autoMigrateOldPanels',
'autoMigrateGraphPanel',
'autoMigrateTablePanel',
'autoMigratePiechartPanel',
'autoMigrateWorldmapPanel',
'autoMigrateStatPanel',
'disableAngular',
]);