Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .config/.cprc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"version": "5.25.8"
"version": "6.7.8",
"features": {}
}
43 changes: 43 additions & 0 deletions .config/bundler/externals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Configuration, ExternalItemFunctionData } from 'webpack';

type ExternalsType = Configuration['externals'];

export const externals: ExternalsType = [
// Required for dynamic publicPath resolution
{ 'amd-module': 'module' },
'lodash',
'jquery',
'moment',
'slate',
'emotion',
'@emotion/react',
'@emotion/css',
'prismjs',
'slate-plain-serializer',
'@grafana/slate-react',
'react',
'react-dom',
'react-redux',
'redux',
'rxjs',
'i18next',
'react-router',
'd3',
'angular',
/^@grafana\/ui/i,
/^@grafana\/runtime/i,
/^@grafana\/data/i,

// Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
({ request }: ExternalItemFunctionData, callback: (error?: Error, result?: string) => void) => {
const prefix = 'grafana/';
const hasPrefix = (request: string) => request.indexOf(prefix) === 0;
const stripPrefix = (request: string) => request.slice(prefix.length);

if (request && hasPrefix(request)) {
return callback(undefined, stripPrefix(request));
}

callback();
},
];
1 change: 1 addition & 0 deletions .config/types/setupTests.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
50 changes: 8 additions & 42 deletions .config/webpack/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import VirtualModulesPlugin from 'webpack-virtual-modules';
import { BuildModeWebpackPlugin } from './BuildModeWebpackPlugin.ts';
import { DIST_DIR, SOURCE_DIR } from './constants.ts';
import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils.ts';
import { externals } from '../bundler/externals.ts';

const pluginJson = getPluginJson();
const cpVersion = getCPConfigVersion();
const pluginVersion = getPackageJson().version;

const virtualPublicPath = new VirtualModulesPlugin({
'node_modules/grafana-public-path.js': `
Expand Down Expand Up @@ -54,45 +56,7 @@ const config = async (env: Env): Promise<Configuration> => {

entry: await getEntries(),

externals: [
// Required for dynamic publicPath resolution
{ 'amd-module': 'module' },
'lodash',
'jquery',
'moment',
'slate',
'emotion',
'@emotion/react',
'@emotion/css',
'prismjs',
'slate-plain-serializer',
'@grafana/slate-react',
'react',
'react-dom',
'react-redux',
'redux',
'rxjs',
'react-router',
'react-router-dom',
'd3',
'angular',
/^@grafana\/ui/i,
/^@grafana\/runtime/i,
/^@grafana\/data/i,

// Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
({ request }, callback) => {
const prefix = 'grafana/';
const hasPrefix = (request: string) => request.indexOf(prefix) === 0;
const stripPrefix = (request: string) => request.substr(prefix.length);

if (request && hasPrefix(request)) {
return callback(undefined, stripPrefix(request));
}

callback();
},
],
externals,

// Support WebAssembly according to latest spec - makes WebAssembly module async
experiments: {
Expand Down Expand Up @@ -196,7 +160,8 @@ const config = async (env: Env): Promise<Configuration> => {
virtualPublicPath,
// Insert create plugin version information into the bundle
new webpack.BannerPlugin({
banner: '/* [create-plugin] version: ' + cpVersion + ' */',
banner: `/* [create-plugin] version: ${cpVersion} */
/* [create-plugin] plugin: ${pluginJson.id}@${pluginVersion} */`,
raw: true,
entryOnly: true,
}),
Expand All @@ -222,11 +187,12 @@ const config = async (env: Env): Promise<Configuration> => {
new ReplaceInFileWebpackPlugin([
{
dir: DIST_DIR,
files: ['plugin.json', 'README.md'],
test: [/(^|\/)plugin\.json$/, /(^|\/)README\.md$/],

rules: [
{
search: /\%VERSION\%/g,
replace: getPackageJson().version,
replace: pluginVersion,
},
{
search: /\%TODAY\%/g,
Expand Down
2 changes: 1 addition & 1 deletion .cprc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"features": {
"bundleGrafanaUI": false,
"useReactRouterV6": false,
"useReactRouterV6": true,
"useExperimentalRspack": false
}
}
19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/eslint": "9.6.1",
"@types/history": "4.7.11",
"@types/is-base64": "1.1.3",
"@types/jest": "30.0.0",
"@types/k6": "1.5.0",
"@types/lodash": "4.17.21",
"@types/node": "24.10.1",
"@types/prismjs": "1.26.5",
"@types/react": "18.3.0",
"@types/react-dom": "19.2.3",
"@types/react-router-dom": "5.3.3",
"@types/valid-url": "1.0.7",
"@typescript-eslint/eslint-plugin": "8.46.3",
"@typescript-eslint/parser": "8.46.3",
Expand Down Expand Up @@ -108,13 +109,14 @@
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/alerting": "12.3.0",
"@grafana/data": "^12.1.0",
"@grafana/data": "^12.2.0",
"@grafana/faro-web-sdk": "1.19.0",
"@grafana/runtime": "^12.1.0",
"@grafana/i18n": "^12.2.0",
"@grafana/runtime": "^12.2.0",
"@grafana/scenes": "6.52.10",
"@grafana/scenes-react": "6.52.10",
"@grafana/schema": "^12.1.0",
"@grafana/ui": "^12.1.0",
"@grafana/schema": "^12.2.0",
"@grafana/ui": "^12.2.0",
"@hookform/resolvers": "5.2.2",
"@tanstack/react-query": "5.90.11",
"@tanstack/react-query-devtools": "5.91.1",
Expand All @@ -131,15 +133,14 @@
"prismjs": "1.30.0",
"punycode": "^2.1.1",
"rc-slider": "11.1.9",
"react": "18.2.0",
"react": "18.3.0",
"react-async-hook": "4.0.0",
"react-data-table-component": "^7.5.4",
"react-dom": "18.2.0",
"react-dom": "18.3.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "7.66.0",
"react-popper": "^2.2.5",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "^6.27.0",
"react-router-dom": "6.27.0",
"rxjs": "7.8.2",
"use-konami": "^1.0.1",
"usehooks-ts": "3.1.1",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Checkster/contexts/AppContainerContext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createContext, CSSProperties, RefObject, useContext } from 'react';

interface SplitterComponentProps {
ref: RefObject<HTMLDivElement | null>;
ref: RefObject<HTMLDivElement>;
className: string;
style?: CSSProperties;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Checkster/utils/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function getFieldErrorProps<T extends CheckFormFieldPath = CheckFormField
export function getHttpAuthType(
basicAuth?: { username?: string; password?: string },
bearerToken?: string
): HTTPAuthType | undefined {
): HTTPAuthType {
if (basicAuth && (basicAuth.username !== undefined || basicAuth.password !== undefined)) {
return HTTPAuthType.BasicAuth;
}
Expand Down
59 changes: 34 additions & 25 deletions src/components/ConfirmLeavingPage/ConfirmLeavingPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { PropsWithChildren } from 'react';
import { Router } from 'react-router-dom';
import { CompatRouter, Route, Routes } from 'react-router-dom-v5-compat';
import { Route, Router, Routes } from 'react-router-dom';
import { locationService } from '@grafana/runtime';
import { TextLink } from '@grafana/ui';
import { fireEvent, render, screen } from '@testing-library/react';
import userEventLib from '@testing-library/user-event';

import { DataTestIds } from '../../test/dataTestIds';
import { useLocationServiceHistory } from '../../test/helpers/useLocationServiceHistory';
import { ConfirmLeavingPage } from './ConfirmLeavingPage';

const TEST_IDS = {
Expand All @@ -15,28 +15,19 @@ const TEST_IDS = {
OTHER_PAGE: 'ConfirmLeavingPage.other-route',
} as const;

beforeAll(() => {
// History needs to be reset manually between tests as it uses Grafana's global locationService
locationService.replace('/');
});

afterEach(() => {
// History needs to be reset manually between tests as it uses Grafana's global locationService
locationService.replace('/');
});

function Wrapper({ children }: PropsWithChildren<{}>) {
const history = locationService.getHistory();
// History will not automatically be reset between tests
const { history, location } = useLocationServiceHistory('/');

return (
<Router history={history}>
<CompatRouter>
<Routes>
<Route path="/" element={<div data-testid={TEST_IDS.INITIAL_PAGE}>{children}</div>} />
<Route path="*" element={<div data-testid={TEST_IDS.OTHER_PAGE} />} />
</Routes>
</CompatRouter>
<Router navigator={history} location={location}>
<Routes>
<Route path="/" element={<div data-testid={TEST_IDS.INITIAL_PAGE}>{children}</div>} />
<Route path="*" element={<div data-testid={TEST_IDS.OTHER_PAGE} />} />
</Routes>
</Router>
);
}
Expand All @@ -50,13 +41,31 @@ describe('ConfirmLeavingPage', () => {
});

describe('router navigation', () => {
it.each([
['Stay on page', TEST_IDS.INITIAL_PAGE],
['Leave page', TEST_IDS.OTHER_PAGE],
])('should render a modal when navigating away (%s)', async (buttonText, expectedTestId) => {
it('should render a modal when navigating away and stay on page when clicking "Stay on page"', async () => {
render(
<>
<TextLink href="/some-other-route" data-testid={TEST_IDS.LEAVE_PAGE_LINK}>
Leave page
</TextLink>
<ConfirmLeavingPage enabled />
</>,
{ wrapper: Wrapper }
);

expect(await screen.findByTestId(TEST_IDS.INITIAL_PAGE)).toBeInTheDocument();
await new Promise((resolve) => setTimeout(resolve, 0));
const user = userEventLib.setup();
const link = screen.getByTestId(TEST_IDS.LEAVE_PAGE_LINK);
await user.click(link);
expect(await screen.findByTestId(DataTestIds.ConfirmUnsavedModalHeading)).toBeInTheDocument();
await user.click(screen.getByText('Stay on page', { selector: 'button > span' }));
expect(await screen.findByTestId(TEST_IDS.INITIAL_PAGE)).toBeInTheDocument();
});

it('should close modal and allow navigation when clicking "Leave page"', async () => {
render(
<>
<TextLink href="some-other-route" data-testid={TEST_IDS.LEAVE_PAGE_LINK}>
<TextLink href="/some-other-route" data-testid={TEST_IDS.LEAVE_PAGE_LINK}>
Leave page
</TextLink>
<ConfirmLeavingPage enabled />
Expand All @@ -70,16 +79,16 @@ describe('ConfirmLeavingPage', () => {
const link = screen.getByTestId(TEST_IDS.LEAVE_PAGE_LINK);
await user.click(link);
expect(await screen.findByTestId(DataTestIds.ConfirmUnsavedModalHeading)).toBeInTheDocument();
await user.click(screen.getByText(buttonText, { selector: 'button > span' }));
expect(await screen.findByTestId(expectedTestId)).toBeInTheDocument();
await user.click(screen.getByText('Leave page', { selector: 'button > span' }));
expect(screen.queryByTestId(DataTestIds.ConfirmUnsavedModalHeading)).not.toBeInTheDocument();
});
});

describe('onbeforeunload', () => {
it('should trigger confirm on beforeunload', async () => {
render(
<>
<TextLink href="some-other-route" data-testid={TEST_IDS.LEAVE_PAGE_LINK}>
<TextLink href="/some-other-route" data-testid={TEST_IDS.LEAVE_PAGE_LINK}>
Leave page
</TextLink>
<ConfirmLeavingPage enabled />
Expand Down
13 changes: 4 additions & 9 deletions src/components/ConfirmLeavingPage/ConfirmLeavingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { useLocation, useNavigate } from 'react-router-dom';
import { locationService } from '@grafana/runtime';
import { Location, TransitionPromptHook } from 'history';
import { Location } from 'history';

import { useConfirmBeforeUnload } from 'hooks/useConfirmBeforeUnload';

Expand All @@ -20,7 +20,6 @@ interface ConfirmLeavingPageProps {
*
* @see {useConfirmBeforeUnload}
* @param {boolean} enabled Whether or not to actively block transitions
* @constructor
*/
export function ConfirmLeavingPage({ enabled }: ConfirmLeavingPageProps) {
const [showModal, setShowModal] = useState(false);
Expand All @@ -32,12 +31,11 @@ export function ConfirmLeavingPage({ enabled }: ConfirmLeavingPageProps) {

const location = useLocation();

const blockHandler: TransitionPromptHook = useCallback(
const blockHandler = useCallback(
(nextLocation: Location) => {
const path = location.pathname;
const nextPath = nextLocation.pathname;

// Check all the reasons to allow navigation
if (!enabled || path === nextPath || changesDiscarded) {
return;
}
Expand All @@ -51,10 +49,7 @@ export function ConfirmLeavingPage({ enabled }: ConfirmLeavingPageProps) {

useEffect(() => {
const unblock = history.block(blockHandler);

return () => {
unblock();
};
return () => unblock();
}, [blockHandler, blockedLocation, history]);

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/SceneRedirecter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jest.mock('components/RunbookRedirectAlert.utils', () => ({
doRunbookRedirect: jest.fn(),
}));

jest.mock('react-router-dom-v5-compat', () => ({
jest.mock('react-router-dom', () => ({
Navigate: ({ to, replace }: { to: string; replace: boolean }) => (
<div data-testid="navigate" data-to={to} data-replace={replace}>
Navigate to {to}
Expand Down
2 changes: 1 addition & 1 deletion src/components/SceneRedirecter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Navigate } from 'react-router-dom-v5-compat';
import { Navigate } from 'react-router-dom';
import { PluginPage } from '@grafana/runtime';

import { CheckAlertType, CheckAlertWithRunbookUrl } from 'types';
Expand Down
Loading