mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
AngularMigration: Allow dashboard by dashboard migration (#84100)
This commit is contained in:
parent
1ffeb7c365
commit
e5d1cd8ea5
@ -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"]
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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,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,
|
||||
}),
|
||||
});
|
@ -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',
|
||||
]);
|
||||
|
Loading…
Reference in New Issue
Block a user