Skip to content

Commit

Permalink
AngularMigration: Allow dashboard by dashboard migration (#84100)
Browse files Browse the repository at this point in the history
  • Loading branch information
adela-almasan committed Mar 27, 2024
1 parent 1ffeb7c commit e5d1cd8
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 32 deletions.
3 changes: 2 additions & 1 deletion .betterer.results
Expand Up @@ -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"]
Expand Down
11 changes: 6 additions & 5 deletions packages/grafana-runtime/src/config.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -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',
Expand All @@ -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;
}

Expand Down
54 changes: 53 additions & 1 deletion public/app/features/dashboard/containers/DashboardPage.tsx
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
6 changes: 4 additions & 2 deletions public/app/features/dashboard/state/DashboardModel.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down
42 changes: 35 additions & 7 deletions public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts
Expand Up @@ -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';
}
Expand All @@ -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';
}
Expand Down
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});
@@ -1,20 +1,30 @@
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;
}

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) => {
Expand All @@ -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>
Expand Down
@@ -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();
});
});
});

0 comments on commit e5d1cd8

Please sign in to comment.