mirror of
https://github.com/prymitive/karma
synced 2026-05-11 03:46:48 +00:00
fix(ui): update eslint
This commit is contained in:
committed by
Łukasz Mierzwa
parent
3b74969b3c
commit
638531901a
@@ -1,85 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const config = {
|
||||
root: true,
|
||||
ignorePatterns: ["playwright.config.ts", "src/e2e/"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
project: ["./tsconfig.json"],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: [
|
||||
"@typescript-eslint",
|
||||
"prettier",
|
||||
"react",
|
||||
"react-hooks",
|
||||
"jest",
|
||||
],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jest/recommended",
|
||||
"prettier",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2022: true,
|
||||
jest: true,
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-expressions": [
|
||||
"error",
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"**/__mocks__/*.ts",
|
||||
"**/__mocks__/**/*.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"jest/expect-expect": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -48,8 +48,7 @@ update-e2e:
|
||||
|
||||
.PHONY: lint-js
|
||||
lint-js: $(NODE_PATH)/eslint
|
||||
@rm -fr node_modules/.cache/eslint-loader
|
||||
eslint --ext .js,.jsx,.ts,.tsx src
|
||||
eslint
|
||||
tsc --noEmit -p .
|
||||
|
||||
.PHONY: lint-typescript
|
||||
|
||||
85
ui/eslint.config.js
Normal file
85
ui/eslint.config.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import eslintJs from "@eslint/js";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import tseslint from "typescript-eslint";
|
||||
import eslintReact from "@eslint-react/eslint-plugin";
|
||||
import reactCompiler from "eslint-plugin-react-compiler";
|
||||
import jest from "eslint-plugin-jest";
|
||||
import prettier from "eslint-config-prettier";
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores(["playwright.config.ts", "src/e2e/", "coverage/"]),
|
||||
{
|
||||
files: ["src/**/*.ts", "src/**/*.tsx"],
|
||||
extends: [
|
||||
eslintJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
eslintReact.configs["recommended-typescript"],
|
||||
],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-expressions": [
|
||||
"error",
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
},
|
||||
],
|
||||
// setState in useEffect is the standard pattern for async fetches, MobX reactions, and timer-driven updates
|
||||
"@eslint-react/hooks-extra/no-direct-set-state-in-use-effect": "off",
|
||||
// index keys are used in static display-only lists (split strings, SVG samples, error messages) with no stable ID
|
||||
"@eslint-react/no-array-index-key": "off",
|
||||
// cloneElement is used by react-transition-group integration, replacing it would require a major refactor
|
||||
"@eslint-react/no-clone-element": "off",
|
||||
// Children.map is used by the Toast container with react-transition-group TransitionGroup
|
||||
"@eslint-react/no-children-map": "off",
|
||||
// dangerouslySetInnerHTML is intentional for rendering trusted server-provided HTML
|
||||
"@eslint-react/dom/no-dangerously-set-innerhtml": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.ts", "src/**/*.tsx"],
|
||||
...reactCompiler.configs.recommended,
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"src/**/__mocks__/*.ts",
|
||||
"src/**/__mocks__/**/*.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.tsx",
|
||||
],
|
||||
...jest.configs["flat/recommended"],
|
||||
rules: {
|
||||
...jest.configs["flat/recommended"].rules,
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"jest/expect-expect": "off",
|
||||
"react-compiler/react-compiler": "off",
|
||||
"@eslint-react/no-create-ref": "off",
|
||||
"@eslint-react/no-nested-component-definitions": "off",
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
);
|
||||
2967
ui/package-lock.json
generated
2967
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,8 @@
|
||||
"typeface-open-sans": "1.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-react/eslint-plugin": "2.13.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@fetch-mock/jest": "0.2.20",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
@@ -61,17 +63,13 @@
|
||||
"@types/promise-retry": "1.1.6",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"@typescript-eslint/parser": "8.56.1",
|
||||
"@vitejs/plugin-legacy": "7.2.1",
|
||||
"@vitejs/plugin-react": "5.1.4",
|
||||
"csstype": "3.2.3",
|
||||
"eslint": "8.57.1",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-jest": "29.15.0",
|
||||
"eslint-plugin-prettier": "5.5.5",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"fetch-mock": "12.6.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"jest": "30.2.0",
|
||||
@@ -82,6 +80,7 @@
|
||||
"terser": "5.46.0",
|
||||
"ts-jest": "29.4.6",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.56.1",
|
||||
"vite": "7.3.1",
|
||||
"vite-tsconfig-paths": "6.1.1"
|
||||
},
|
||||
|
||||
@@ -30,9 +30,11 @@ interface AppProps {
|
||||
}
|
||||
|
||||
const App: FC<AppProps> = observer(({ defaultFilters, uiDefaults }) => {
|
||||
const [alertStore] = useState<AlertStore>(new AlertStore(null));
|
||||
const [silenceFormStore] = useState<SilenceFormStore>(new SilenceFormStore());
|
||||
const [settingsStore] = useState<Settings>(new Settings(uiDefaults));
|
||||
const [alertStore] = useState<AlertStore>(() => new AlertStore(null));
|
||||
const [silenceFormStore] = useState<SilenceFormStore>(
|
||||
() => new SilenceFormStore(),
|
||||
);
|
||||
const [settingsStore] = useState<Settings>(() => new Settings(uiDefaults));
|
||||
|
||||
useEffect(() => {
|
||||
let filters;
|
||||
@@ -61,7 +63,7 @@ const App: FC<AppProps> = observer(({ defaultFilters, uiDefaults }) => {
|
||||
if (p.params.m && silenceFormStore.data.fromBase64(p.params.m)) {
|
||||
silenceFormStore.toggle.show();
|
||||
}
|
||||
}, [alertStore, defaultFilters, settingsStore]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [alertStore, defaultFilters, settingsStore]);
|
||||
|
||||
const onPopState = useCallback(
|
||||
(event: PopStateEvent) => {
|
||||
@@ -85,7 +87,7 @@ const App: FC<AppProps> = observer(({ defaultFilters, uiDefaults }) => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<span data-theme={`${settingsStore.themeConfig.config.theme}`} />
|
||||
<ThemeContext.Provider
|
||||
<ThemeContext
|
||||
value={{
|
||||
isDark:
|
||||
settingsStore.themeConfig.config.theme ===
|
||||
@@ -122,7 +124,7 @@ const App: FC<AppProps> = observer(({ defaultFilters, uiDefaults }) => {
|
||||
/>
|
||||
<FaviconBadge alertStore={alertStore} />
|
||||
</React.Suspense>
|
||||
</ThemeContext.Provider>
|
||||
</ThemeContext>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
// https://stackoverflow.com/a/58208791/1154047
|
||||
const normalize = (): any => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (_: string, value: any) {
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
return Object.entries(value)
|
||||
|
||||
@@ -32,7 +32,7 @@ export const AlertHistory: FC<{ group: APIAlertGroupT; grid: APIGridT }> = ({
|
||||
}) => {
|
||||
const [ref, inView] = useInView({ triggerOnce: true });
|
||||
|
||||
const [lastUpdate, setLastUpdate] = useState<number>(GetUTCSeconds());
|
||||
const [lastUpdate, setLastUpdate] = useState<number>(() => GetUTCSeconds());
|
||||
const [upstreams, setUpstreams] = useState<UpstreamT[]>([]);
|
||||
const [labels] = useState<{ [key: string]: string }>({
|
||||
...Object.fromEntries(group.labels.map((l) => [l.name, l.value])),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useRef } from "react";
|
||||
import React, { use, FC, useRef } from "react";
|
||||
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
@@ -9,7 +9,7 @@ const DropdownSlide: FC<{
|
||||
in?: boolean;
|
||||
unmountOnExit?: boolean;
|
||||
}> = ({ children, ...props }) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,11 +5,7 @@ import { ThemeContext } from "Components/Theme";
|
||||
import { CenteredMessage } from ".";
|
||||
|
||||
const renderWithTheme = (ui: React.ReactElement) =>
|
||||
render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
{ui}
|
||||
</ThemeContext.Provider>,
|
||||
);
|
||||
render(<ThemeContext value={MockThemeContext}>{ui}</ThemeContext>);
|
||||
|
||||
describe("<CenteredMessage />", () => {
|
||||
const Message = () => <div>Foo</div>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode, useRef } from "react";
|
||||
import { use, FC, ReactNode, useRef } from "react";
|
||||
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
@@ -8,7 +8,7 @@ const CenteredMessage: FC<{
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}> = ({ children, className }) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
const nodeRef = useRef<HTMLHeadingElement>(null);
|
||||
return (
|
||||
<CSSTransition
|
||||
|
||||
@@ -16,7 +16,7 @@ const formatLabel = (timestamp: string) => {
|
||||
};
|
||||
|
||||
export const DateFromNow: FC<{ timestamp: string }> = ({ timestamp }) => {
|
||||
const [label, setLabel] = useState<string>(formatLabel(timestamp));
|
||||
const [label, setLabel] = useState<string>(() => formatLabel(timestamp));
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(
|
||||
|
||||
@@ -10,12 +10,13 @@ const FaviconBadge: FC<{
|
||||
alertStore: AlertStore;
|
||||
}> = ({ alertStore }) => {
|
||||
const [favico] = useState(
|
||||
new Favico({
|
||||
animation: "none",
|
||||
position: "down",
|
||||
bgColor: "#e74c3c",
|
||||
textColor: "#fff",
|
||||
}),
|
||||
() =>
|
||||
new Favico({
|
||||
animation: "none",
|
||||
position: "down",
|
||||
bgColor: "#e74c3c",
|
||||
textColor: "#fff",
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(
|
||||
@@ -29,7 +30,7 @@ const FaviconBadge: FC<{
|
||||
: "?",
|
||||
);
|
||||
}),
|
||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState, FC } from "react";
|
||||
import { use, useEffect, useRef, useState, FC } from "react";
|
||||
|
||||
import { reaction, toJS } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
@@ -18,7 +18,7 @@ import { ThemeContext } from "Components/Theme";
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
|
||||
const PauseButton: FC<{ alertStore: AlertStore }> = ({ alertStore }) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
const nodeRef = useRef<HTMLSpanElement>(null);
|
||||
return (
|
||||
<TooltipWrapper title="Click to resume updates">
|
||||
@@ -42,7 +42,7 @@ const PauseButton: FC<{ alertStore: AlertStore }> = ({ alertStore }) => {
|
||||
};
|
||||
|
||||
const PlayButton: FC<{ alertStore: AlertStore }> = ({ alertStore }) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
const nodeRef = useRef<HTMLSpanElement>(null);
|
||||
return (
|
||||
<TooltipWrapper title="Click to pause updates">
|
||||
@@ -101,7 +101,7 @@ const Fetcher: FC<{
|
||||
alertStore: AlertStore;
|
||||
settingsStore: Settings;
|
||||
}> = observer(({ alertStore, settingsStore }) => {
|
||||
const timer = useRef<number | undefined>(undefined);
|
||||
const timerRef = useRef<number | undefined>(undefined);
|
||||
const [percentLeft, setPercentLeft] = useState<number>(100);
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
|
||||
@@ -191,7 +191,7 @@ const Fetcher: FC<{
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => window.clearInterval(timer.current);
|
||||
return () => window.clearInterval(timerRef.current);
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
@@ -224,7 +224,7 @@ const Fetcher: FC<{
|
||||
},
|
||||
{ fireImmediately: true },
|
||||
),
|
||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
@@ -233,10 +233,10 @@ const Fetcher: FC<{
|
||||
() => alertStore.status.paused,
|
||||
(paused) => {
|
||||
if (paused) {
|
||||
window.clearInterval(timer.current);
|
||||
timer.current = undefined;
|
||||
window.clearInterval(timerRef.current);
|
||||
timerRef.current = undefined;
|
||||
} else {
|
||||
timer.current = window.setInterval(
|
||||
timerRef.current = window.setInterval(
|
||||
() => window.requestAnimationFrame(fetchIfIdle),
|
||||
1000,
|
||||
);
|
||||
@@ -244,7 +244,7 @@ const Fetcher: FC<{
|
||||
},
|
||||
{ fireImmediately: true },
|
||||
),
|
||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const dots = Math.max(0, Math.min(9, percentLeft / 10));
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
forwardRef,
|
||||
Ref,
|
||||
CSSProperties,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { Ref, CSSProperties, useRef, useState, useCallback } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@@ -73,112 +66,110 @@ interface MenuContentProps {
|
||||
afterClick: () => void;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(
|
||||
(
|
||||
{
|
||||
x,
|
||||
y,
|
||||
floating,
|
||||
strategy,
|
||||
group,
|
||||
alert,
|
||||
afterClick,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const actions: APIAnnotationT[] = [
|
||||
...alert.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true),
|
||||
...group.shared.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true),
|
||||
];
|
||||
const MenuContent = ({
|
||||
x,
|
||||
y,
|
||||
floating,
|
||||
strategy,
|
||||
group,
|
||||
alert,
|
||||
afterClick,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
ref,
|
||||
}: MenuContentProps) => {
|
||||
const actions: APIAnnotationT[] = [
|
||||
...alert.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true),
|
||||
...group.shared.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true),
|
||||
];
|
||||
|
||||
return (
|
||||
<FetchPauser alertStore={alertStore}>
|
||||
return (
|
||||
<FetchPauser alertStore={alertStore}>
|
||||
<div
|
||||
className="dropdown-menu d-block shadow m-0"
|
||||
ref={(node) => {
|
||||
if (typeof floating === "function") {
|
||||
floating(node);
|
||||
} else if (floating) {
|
||||
// eslint-disable-next-line react-compiler/react-compiler -- assigning to floating ref in a ref callback is an intentional pattern from @floating-ui
|
||||
floating.current = node;
|
||||
}
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y,
|
||||
left: x,
|
||||
}}
|
||||
>
|
||||
<h6 className="dropdown-header">Alert source links:</h6>
|
||||
{alert.alertmanager.map((am) => (
|
||||
<MenuLink
|
||||
key={am.name}
|
||||
icon={faExternalLinkAlt}
|
||||
text={am.name}
|
||||
uri={am.source}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
<div className="dropdown-divider" />
|
||||
<div
|
||||
className="dropdown-menu d-block shadow m-0"
|
||||
ref={(node) => {
|
||||
if (typeof floating === "function") {
|
||||
floating(node);
|
||||
} else if (floating) {
|
||||
floating.current = node;
|
||||
}
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y,
|
||||
left: x,
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => {
|
||||
copy(JSON.stringify(alertToJSON(group, alert)));
|
||||
afterClick();
|
||||
}}
|
||||
>
|
||||
<h6 className="dropdown-header">Alert source links:</h6>
|
||||
{alert.alertmanager.map((am) => (
|
||||
<MenuLink
|
||||
key={am.name}
|
||||
icon={faExternalLinkAlt}
|
||||
text={am.name}
|
||||
uri={am.source}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
<div className="dropdown-divider" />
|
||||
<div
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => {
|
||||
copy(JSON.stringify(alertToJSON(group, alert)));
|
||||
afterClick();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="me-1" icon={faCopy} />
|
||||
Copy to clipboard
|
||||
</div>
|
||||
{actions.length ? (
|
||||
<>
|
||||
<div className="dropdown-divider" />
|
||||
<h6 className="dropdown-header">Actions:</h6>
|
||||
{actions.map((action) => (
|
||||
<MenuLink
|
||||
key={action.name}
|
||||
icon={faWrench}
|
||||
text={action.name}
|
||||
uri={action.value}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
<div className="dropdown-divider" />
|
||||
<div
|
||||
className={`dropdown-item ${
|
||||
Object.keys(alertStore.data.clustersWithoutReadOnly).length === 0
|
||||
? "disabled"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) {
|
||||
onSilenceClick(alertStore, silenceFormStore, group, alert);
|
||||
afterClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="me-1" icon={faBellSlash} />
|
||||
Silence this alert
|
||||
</div>
|
||||
<FontAwesomeIcon className="me-1" icon={faCopy} />
|
||||
Copy to clipboard
|
||||
</div>
|
||||
</FetchPauser>
|
||||
);
|
||||
},
|
||||
);
|
||||
{actions.length ? (
|
||||
<>
|
||||
<div className="dropdown-divider" />
|
||||
<h6 className="dropdown-header">Actions:</h6>
|
||||
{actions.map((action) => (
|
||||
<MenuLink
|
||||
key={action.name}
|
||||
icon={faWrench}
|
||||
text={action.name}
|
||||
uri={action.value}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
<div className="dropdown-divider" />
|
||||
<div
|
||||
className={`dropdown-item ${
|
||||
Object.keys(alertStore.data.clustersWithoutReadOnly).length === 0
|
||||
? "disabled"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) {
|
||||
onSilenceClick(alertStore, silenceFormStore, group, alert);
|
||||
afterClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="me-1" icon={faBellSlash} />
|
||||
Silence this alert
|
||||
</div>
|
||||
</div>
|
||||
</FetchPauser>
|
||||
);
|
||||
};
|
||||
|
||||
interface AlertMenuProps {
|
||||
group: APIAlertGroupT;
|
||||
|
||||
@@ -56,7 +56,7 @@ const renderAlert = (
|
||||
showOnlyExpandedAnnotations: boolean,
|
||||
) => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Alert
|
||||
alert={alert}
|
||||
group={group}
|
||||
@@ -67,7 +67,7 @@ const renderAlert = (
|
||||
silenceFormStore={silenceFormStore}
|
||||
setIsMenuOpen={MockSetIsMenuOpen}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const RenderNonLinkAnnotation: FC<{
|
||||
}
|
||||
});
|
||||
|
||||
const { ref, props, nodeRef } = useFlashTransition(value);
|
||||
const { ref, props } = useFlashTransition(value);
|
||||
|
||||
const className =
|
||||
"mb-1 p-1 bg-light d-inline-block rounded components-grid-annotation text-break mw-100";
|
||||
@@ -63,21 +63,11 @@ const RenderNonLinkAnnotation: FC<{
|
||||
<CSSTransition {...props}>
|
||||
{allowHTML ? (
|
||||
<span
|
||||
ref={(node) => {
|
||||
ref(node);
|
||||
nodeRef.current = node;
|
||||
}}
|
||||
ref={ref}
|
||||
dangerouslySetInnerHTML={{ __html: value }}
|
||||
></span>
|
||||
) : (
|
||||
<span
|
||||
ref={(node) => {
|
||||
ref(node);
|
||||
nodeRef.current = node;
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<span ref={ref}>{value}</span>
|
||||
)}
|
||||
</CSSTransition>
|
||||
</Linkify>
|
||||
|
||||
@@ -68,7 +68,7 @@ const renderGroupFooter = (props?: {
|
||||
showAnnotations?: boolean;
|
||||
}) => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<GroupFooter
|
||||
group={group}
|
||||
afterUpdate={MockAfterUpdate}
|
||||
@@ -76,7 +76,7 @@ const renderGroupFooter = (props?: {
|
||||
silenceFormStore={silenceFormStore}
|
||||
{...props}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ const MockAlerts = (alertCount: number, totalAlerts: number) => {
|
||||
|
||||
const renderAlertGroup = (afterUpdate: () => void, theme?: ThemeCtx) => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={theme || MockThemeContext}>
|
||||
<ThemeContext value={theme || MockThemeContext}>
|
||||
<AlertGroup
|
||||
afterUpdate={afterUpdate}
|
||||
grid={grid}
|
||||
@@ -100,7 +100,7 @@ const renderAlertGroup = (afterUpdate: () => void, theme?: ThemeCtx) => {
|
||||
gridLabelValue=""
|
||||
groupWidth={420}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
import {
|
||||
use,
|
||||
FC,
|
||||
forwardRef,
|
||||
Ref,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useState,
|
||||
@@ -55,214 +56,209 @@ interface AlertGroupProps {
|
||||
silenceFormStore: SilenceFormStore;
|
||||
groupWidth: number;
|
||||
gridLabelValue: string;
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const AlertGroup = forwardRef<HTMLDivElement, AlertGroupProps>(
|
||||
(
|
||||
{
|
||||
grid,
|
||||
group,
|
||||
afterUpdate,
|
||||
silenceFormStore,
|
||||
alertStore,
|
||||
settingsStore,
|
||||
groupWidth,
|
||||
gridLabelValue,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const defaultRenderCount =
|
||||
settingsStore.alertGroupConfig.config.defaultRenderCount;
|
||||
const AlertGroup = ({
|
||||
grid,
|
||||
group,
|
||||
afterUpdate,
|
||||
silenceFormStore,
|
||||
alertStore,
|
||||
settingsStore,
|
||||
groupWidth,
|
||||
gridLabelValue,
|
||||
ref,
|
||||
}: AlertGroupProps) => {
|
||||
const defaultRenderCount =
|
||||
settingsStore.alertGroupConfig.config.defaultRenderCount;
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(
|
||||
DefaultDetailsCollapseValue(settingsStore),
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() =>
|
||||
DefaultDetailsCollapseValue(settingsStore),
|
||||
);
|
||||
|
||||
// Used to calculate step size when loading more alerts.
|
||||
// Step is calculated from the excessive alert count
|
||||
// (what's > defaultRenderCount) by dividing it into 5 clicks.
|
||||
// Don't use step lower than 5, too much clicking if we have a group of 9:
|
||||
// * we'll show initially 5
|
||||
// * step would be 1
|
||||
// * 4 extra clicks to see the entire group
|
||||
// but ensure that step wouldn't push us above totalSize
|
||||
// With 9 alerts and rendering 5 initially we want to show extra 9 after one
|
||||
// click, and when user clicks showLess we want to go back to 5.
|
||||
const getStepSize = (totalSize: number) => {
|
||||
const val = Math.min(
|
||||
Math.max(Math.round((totalSize - defaultRenderCount) / 5), 5),
|
||||
totalSize - defaultRenderCount,
|
||||
);
|
||||
return val;
|
||||
};
|
||||
|
||||
// Used to calculate step size when loading more alerts.
|
||||
// Step is calculated from the excessive alert count
|
||||
// (what's > defaultRenderCount) by dividing it into 5 clicks.
|
||||
// Don't use step lower than 5, too much clicking if we have a group of 9:
|
||||
// * we'll show initially 5
|
||||
// * step would be 1
|
||||
// * 4 extra clicks to see the entire group
|
||||
// but ensure that step wouldn't push us above totalSize
|
||||
// With 9 alerts and rendering 5 initially we want to show extra 9 after one
|
||||
// click, and when user clicks showLess we want to go back to 5.
|
||||
const getStepSize = (totalSize: number) => {
|
||||
const val = Math.min(
|
||||
Math.max(Math.round((totalSize - defaultRenderCount) / 5), 5),
|
||||
totalSize - defaultRenderCount,
|
||||
);
|
||||
return val;
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
const step = getStepSize(group.totalAlerts);
|
||||
alertStore.ui.setGroupAlertLimit(
|
||||
group.id,
|
||||
Math.min(group.alerts.length + step, group.totalAlerts),
|
||||
);
|
||||
};
|
||||
|
||||
const loadLess = () => {
|
||||
const step = getStepSize(group.totalAlerts);
|
||||
alertStore.ui.setGroupAlertLimit(
|
||||
group.id,
|
||||
Math.max(group.alerts.length - step, 1),
|
||||
);
|
||||
};
|
||||
|
||||
const onAlertGroupCollapseEvent = useCallback(
|
||||
(event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
if (customEvent.detail.gridLabelValue === gridLabelValue) {
|
||||
setIsCollapsed(customEvent.detail.value);
|
||||
}
|
||||
},
|
||||
[gridLabelValue],
|
||||
const loadMore = () => {
|
||||
const step = getStepSize(group.totalAlerts);
|
||||
alertStore.ui.setGroupAlertLimit(
|
||||
group.id,
|
||||
Math.min(group.alerts.length + step, group.totalAlerts),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("alertGroupCollapse", onAlertGroupCollapseEvent);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"alertGroupCollapse",
|
||||
onAlertGroupCollapseEvent,
|
||||
);
|
||||
};
|
||||
}, [onAlertGroupCollapseEvent]);
|
||||
const loadLess = () => {
|
||||
const step = getStepSize(group.totalAlerts);
|
||||
alertStore.ui.setGroupAlertLimit(
|
||||
group.id,
|
||||
Math.max(group.alerts.length - step, 1),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
afterUpdate();
|
||||
});
|
||||
|
||||
let themedCounters = true;
|
||||
let cardBackgroundClass = "bg-light";
|
||||
if (settingsStore.alertGroupConfig.config.colorTitleBar) {
|
||||
const stateList = Object.entries(group.stateCount)
|
||||
.filter(([_, v]) => v !== 0)
|
||||
.map(([k, _]) => k);
|
||||
if (stateList.length === 1) {
|
||||
const state = stateList.pop();
|
||||
cardBackgroundClass = BackgroundClassMap[state as AlertStateT];
|
||||
themedCounters = false;
|
||||
const onAlertGroupCollapseEvent = useCallback(
|
||||
(event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
if (customEvent.detail.gridLabelValue === gridLabelValue) {
|
||||
setIsCollapsed(customEvent.detail.value);
|
||||
}
|
||||
},
|
||||
[gridLabelValue],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("alertGroupCollapse", onAlertGroupCollapseEvent);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"alertGroupCollapse",
|
||||
onAlertGroupCollapseEvent,
|
||||
);
|
||||
};
|
||||
}, [onAlertGroupCollapseEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
afterUpdate();
|
||||
});
|
||||
|
||||
let themedCounters = true;
|
||||
let cardBackgroundClass = "bg-light";
|
||||
if (settingsStore.alertGroupConfig.config.colorTitleBar) {
|
||||
const stateList = Object.entries(group.stateCount)
|
||||
.filter(([_, v]) => v !== 0)
|
||||
.map(([k, _]) => k);
|
||||
if (stateList.length === 1) {
|
||||
const state = stateList.pop();
|
||||
cardBackgroundClass = BackgroundClassMap[state as AlertStateT];
|
||||
themedCounters = false;
|
||||
}
|
||||
}
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`components-grid-alertgrid-alertgroup ${
|
||||
context.animations.duration ? "animate" : ""
|
||||
}`}
|
||||
style={{
|
||||
width: groupWidth,
|
||||
zIndex: isMenuOpen ? 100 : undefined,
|
||||
}}
|
||||
data-defaultrendercount={
|
||||
settingsStore.alertGroupConfig.config.defaultRenderCount
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`components-grid-alertgrid-alertgroup ${
|
||||
context.animations.duration ? "animate" : ""
|
||||
}`}
|
||||
style={{
|
||||
width: groupWidth,
|
||||
zIndex: isMenuOpen ? 100 : undefined,
|
||||
}}
|
||||
data-defaultrendercount={
|
||||
settingsStore.alertGroupConfig.config.defaultRenderCount
|
||||
}
|
||||
className={`card ${cardBackgroundClass}`}
|
||||
data-colortitlebar={settingsStore.alertGroupConfig.config.colorTitleBar}
|
||||
>
|
||||
<div
|
||||
className={`card ${cardBackgroundClass}`}
|
||||
data-colortitlebar={
|
||||
settingsStore.alertGroupConfig.config.colorTitleBar
|
||||
}
|
||||
>
|
||||
<GroupHeader
|
||||
isCollapsed={isCollapsed}
|
||||
setIsCollapsed={setIsCollapsed}
|
||||
<GroupHeader
|
||||
isCollapsed={isCollapsed}
|
||||
setIsCollapsed={setIsCollapsed}
|
||||
group={group}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
themedCounters={themedCounters}
|
||||
setIsMenuOpen={setIsMenuOpen}
|
||||
gridLabelValue={gridLabelValue}
|
||||
/>
|
||||
{isCollapsed ? null : (
|
||||
<div className="card-body px-2 py-1 components-grid-alertgrid-card">
|
||||
{alertStore.settings.values.historyEnabled ? (
|
||||
<AlertHistory group={group} grid={grid} />
|
||||
) : null}
|
||||
<ul className="list-group">
|
||||
{group.alerts
|
||||
.slice(0, alertStore.ui.isIdle ? 1 : group.alerts.length)
|
||||
.map((alert) => (
|
||||
<Alert
|
||||
key={alert.id}
|
||||
group={group}
|
||||
alert={alert}
|
||||
showReceiver={
|
||||
alertStore.data.receivers.length > 1 &&
|
||||
group.alerts.length === 1
|
||||
}
|
||||
showOnlyExpandedAnnotations={alertStore.ui.isIdle}
|
||||
afterUpdate={afterUpdate}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
setIsMenuOpen={setIsMenuOpen}
|
||||
/>
|
||||
))}
|
||||
{group.totalAlerts > defaultRenderCount ? (
|
||||
<li
|
||||
className="list-group-item border-0 p-0 text-center bg-transparent"
|
||||
style={{
|
||||
lineHeight: alertStore.ui.isIdle ? "1rem" : undefined,
|
||||
}}
|
||||
>
|
||||
{alertStore.ui.isIdle ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsisH}
|
||||
className="text-muted"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<LoadButton
|
||||
icon={faMinus}
|
||||
action={loadLess}
|
||||
tooltip="Show fewer alerts in this group"
|
||||
/>
|
||||
<small className="text-muted mx-2">
|
||||
{group.alerts.length}
|
||||
{" of "}
|
||||
{group.totalAlerts}
|
||||
</small>
|
||||
<LoadButton
|
||||
icon={faPlus}
|
||||
action={loadMore}
|
||||
tooltip="Show more alerts in this group"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{isCollapsed === false ? (
|
||||
<GroupFooter
|
||||
group={group}
|
||||
afterUpdate={afterUpdate}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
themedCounters={themedCounters}
|
||||
setIsMenuOpen={setIsMenuOpen}
|
||||
gridLabelValue={gridLabelValue}
|
||||
showAnnotations={!alertStore.ui.isIdle}
|
||||
showSilences={!alertStore.ui.isIdle}
|
||||
showReceiver={
|
||||
!(
|
||||
alertStore.data.receivers.length > 1 &&
|
||||
group.alerts.length === 1
|
||||
)
|
||||
}
|
||||
/>
|
||||
{isCollapsed ? null : (
|
||||
<div className="card-body px-2 py-1 components-grid-alertgrid-card">
|
||||
{alertStore.settings.values.historyEnabled ? (
|
||||
<AlertHistory group={group} grid={grid} />
|
||||
) : null}
|
||||
<ul className="list-group">
|
||||
{group.alerts
|
||||
.slice(0, alertStore.ui.isIdle ? 1 : group.alerts.length)
|
||||
.map((alert) => (
|
||||
<Alert
|
||||
key={alert.id}
|
||||
group={group}
|
||||
alert={alert}
|
||||
showReceiver={
|
||||
alertStore.data.receivers.length > 1 &&
|
||||
group.alerts.length === 1
|
||||
}
|
||||
showOnlyExpandedAnnotations={alertStore.ui.isIdle}
|
||||
afterUpdate={afterUpdate}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
setIsMenuOpen={setIsMenuOpen}
|
||||
/>
|
||||
))}
|
||||
{group.totalAlerts > defaultRenderCount ? (
|
||||
<li
|
||||
className="list-group-item border-0 p-0 text-center bg-transparent"
|
||||
style={{
|
||||
lineHeight: alertStore.ui.isIdle ? "1rem" : undefined,
|
||||
}}
|
||||
>
|
||||
{alertStore.ui.isIdle ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsisH}
|
||||
className="text-muted"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<LoadButton
|
||||
icon={faMinus}
|
||||
action={loadLess}
|
||||
tooltip="Show fewer alerts in this group"
|
||||
/>
|
||||
<small className="text-muted mx-2">
|
||||
{group.alerts.length}
|
||||
{" of "}
|
||||
{group.totalAlerts}
|
||||
</small>
|
||||
<LoadButton
|
||||
icon={faPlus}
|
||||
action={loadMore}
|
||||
tooltip="Show more alerts in this group"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{isCollapsed === false ? (
|
||||
<GroupFooter
|
||||
group={group}
|
||||
afterUpdate={afterUpdate}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
showAnnotations={!alertStore.ui.isIdle}
|
||||
showSilences={!alertStore.ui.isIdle}
|
||||
showReceiver={
|
||||
!(
|
||||
alertStore.data.receivers.length > 1 &&
|
||||
group.alerts.length === 1
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(AlertGroup);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {
|
||||
use,
|
||||
FC,
|
||||
useEffect,
|
||||
useState,
|
||||
@@ -132,12 +133,12 @@ const Grid: FC<{
|
||||
paddingTop,
|
||||
zIndex,
|
||||
}) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
const { ref, repack } = useGrid(gridSizesConfig);
|
||||
const debouncedRepack = useMemo(() => debounce(() => repack(), 10), [repack]);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(
|
||||
!DefaultDetailsCollapseValue(settingsStore),
|
||||
() => !DefaultDetailsCollapseValue(settingsStore),
|
||||
);
|
||||
const toggleIsExpanded = useCallback(() => {
|
||||
setIsExpanded(!isExpanded);
|
||||
|
||||
@@ -199,13 +199,13 @@ describe("<GridLabelSelect />", () => {
|
||||
},
|
||||
]);
|
||||
const { container } = render(
|
||||
<ThemeContext.Provider value={MockThemeContextWithoutAnimations}>
|
||||
<ThemeContext value={MockThemeContextWithoutAnimations}>
|
||||
<AlertGrid
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
|
||||
const toggle = container.querySelector(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {
|
||||
import {
|
||||
use,
|
||||
FC,
|
||||
Ref,
|
||||
CSSProperties,
|
||||
@@ -64,7 +65,7 @@ const GridLabelNameSelect: FC<{
|
||||
callback(options);
|
||||
};
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, MouseEvent } from "react";
|
||||
import { MouseEvent, Ref } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faGrip } from "@fortawesome/free-solid-svg-icons/faGrip";
|
||||
@@ -19,83 +19,87 @@ interface SwimlaneProps {
|
||||
isExpanded: boolean;
|
||||
onToggle: (event: MouseEvent) => void;
|
||||
paddingTop: number;
|
||||
ref?: Ref<HTMLHeadingElement>;
|
||||
}
|
||||
|
||||
const Swimlane = forwardRef<HTMLHeadingElement, SwimlaneProps>(
|
||||
(
|
||||
{ alertStore, settingsStore, grid, isExpanded, onToggle, paddingTop },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<h5
|
||||
ref={ref}
|
||||
className="components-grid-swimlane d-flex flex-row justify-content-between rounded px-2 py-1 my-1 border border-dark"
|
||||
style={{ top: paddingTop }}
|
||||
>
|
||||
<span className="flex-shrink-0 flex-grow-0 d-none d-sm-block">
|
||||
<span className="badge components-label px-0 ms-1 me-3">
|
||||
<FontAwesomeIcon icon={faGrip} className="text-muted" />
|
||||
</span>
|
||||
const Swimlane = ({
|
||||
alertStore,
|
||||
settingsStore,
|
||||
grid,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
paddingTop,
|
||||
ref,
|
||||
}: SwimlaneProps) => {
|
||||
return (
|
||||
<h5
|
||||
ref={ref}
|
||||
className="components-grid-swimlane d-flex flex-row justify-content-between rounded px-2 py-1 my-1 border border-dark"
|
||||
style={{ top: paddingTop }}
|
||||
>
|
||||
<span className="flex-shrink-0 flex-grow-0 d-none d-sm-block">
|
||||
<span className="badge components-label px-0 ms-1 me-3">
|
||||
<FontAwesomeIcon icon={faGrip} className="text-muted" />
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="flex-shrink-1 flex-grow-0 ms-1 ms-sm-0"
|
||||
style={{ minWidth: "0px" }}
|
||||
>
|
||||
{grid.labelName !== "" && grid.labelValue !== "" && (
|
||||
<FilteringLabel
|
||||
key={grid.labelValue}
|
||||
name={grid.labelName}
|
||||
value={grid.labelValue}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{grid.labelName !== "" && grid.labelValue !== "" && (
|
||||
<span
|
||||
className="flex-shrink-1 flex-grow-0 ms-1 ms-sm-0"
|
||||
className="flex-shrink-0 flex-grow-1 px-0"
|
||||
style={{ minWidth: "0px" }}
|
||||
>
|
||||
{grid.labelName !== "" && grid.labelValue !== "" && (
|
||||
<FilteringLabel
|
||||
key={grid.labelValue}
|
||||
name={grid.labelName}
|
||||
value={grid.labelValue}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
)}
|
||||
<GridLabelSelect
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
grid={grid}
|
||||
/>
|
||||
</span>
|
||||
{grid.labelName !== "" && grid.labelValue !== "" && (
|
||||
<span
|
||||
className="flex-shrink-0 flex-grow-1 px-0"
|
||||
style={{ minWidth: "0px" }}
|
||||
>
|
||||
<GridLabelSelect
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
grid={grid}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-shrink-0 flex-grow-0 ms-2 me-0">
|
||||
<FilteringCounterBadge
|
||||
name="@state"
|
||||
value="unprocessed"
|
||||
counter={grid.stateCount.unprocessed}
|
||||
themed={true}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
<FilteringCounterBadge
|
||||
name="@state"
|
||||
value="suppressed"
|
||||
counter={grid.stateCount.suppressed}
|
||||
themed={true}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
<FilteringCounterBadge
|
||||
name="@state"
|
||||
value="active"
|
||||
counter={grid.stateCount.active}
|
||||
themed={true}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
<span
|
||||
className="text-muted cursor-pointer badge with-click with-click-dark components-label ms-1 me-0"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<TooltipWrapper title="Click to toggle this grid details or Alt+Click to toggle all grids">
|
||||
<ToggleIcon isOpen={isExpanded} />
|
||||
</TooltipWrapper>
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-shrink-0 flex-grow-0 ms-2 me-0">
|
||||
<FilteringCounterBadge
|
||||
name="@state"
|
||||
value="unprocessed"
|
||||
counter={grid.stateCount.unprocessed}
|
||||
themed={true}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
<FilteringCounterBadge
|
||||
name="@state"
|
||||
value="suppressed"
|
||||
counter={grid.stateCount.suppressed}
|
||||
themed={true}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
<FilteringCounterBadge
|
||||
name="@state"
|
||||
value="active"
|
||||
counter={grid.stateCount.active}
|
||||
themed={true}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
<span
|
||||
className="text-muted cursor-pointer badge with-click with-click-dark components-label ms-1 me-0"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<TooltipWrapper title="Click to toggle this grid details or Alt+Click to toggle all grids">
|
||||
<ToggleIcon isOpen={isExpanded} />
|
||||
</TooltipWrapper>
|
||||
</span>
|
||||
</h5>
|
||||
);
|
||||
},
|
||||
);
|
||||
</span>
|
||||
</h5>
|
||||
);
|
||||
};
|
||||
|
||||
export { Swimlane };
|
||||
|
||||
@@ -66,13 +66,13 @@ afterEach(() => {
|
||||
|
||||
const renderAlertGrid = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<AlertGrid
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -94,7 +94,7 @@ const MockGrid = () => ({
|
||||
|
||||
const renderGrid = (theme?: ThemeCtx) => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={theme || MockThemeContext}>
|
||||
<ThemeContext value={theme || MockThemeContext}>
|
||||
<Grid
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
@@ -106,7 +106,7 @@ const renderGrid = (theme?: ThemeCtx) => {
|
||||
paddingTop={0}
|
||||
zIndex={101}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -227,7 +227,7 @@ describe("<Grid />", () => {
|
||||
it("appends more groups after clicking 'Load More' button", () => {
|
||||
MockGroupList(40, 5, 70);
|
||||
const { container } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Grid
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
@@ -239,7 +239,7 @@ describe("<Grid />", () => {
|
||||
paddingTop={0}
|
||||
zIndex={101}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
const button = container.querySelector("button");
|
||||
fireEvent.click(button!);
|
||||
@@ -263,7 +263,7 @@ describe("<Grid />", () => {
|
||||
},
|
||||
]);
|
||||
const { container } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Grid
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
@@ -275,7 +275,7 @@ describe("<Grid />", () => {
|
||||
paddingTop={0}
|
||||
zIndex={101}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
const button = container.querySelector("button");
|
||||
fireEvent.click(button!);
|
||||
@@ -309,7 +309,7 @@ describe("<Grid />", () => {
|
||||
};
|
||||
alertStore.data.setGrids([grid]);
|
||||
const { container } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Grid
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
@@ -321,7 +321,7 @@ describe("<Grid />", () => {
|
||||
paddingTop={0}
|
||||
zIndex={101}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(
|
||||
container.querySelectorAll(".components-grid-alertgrid-alertgroup"),
|
||||
@@ -358,7 +358,7 @@ describe("<Grid />", () => {
|
||||
]);
|
||||
const dispatchSpy = jest.spyOn(window, "dispatchEvent");
|
||||
const { container } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Grid
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
@@ -370,7 +370,7 @@ describe("<Grid />", () => {
|
||||
paddingTop={0}
|
||||
zIndex={101}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
|
||||
const toggles = container.querySelectorAll("span.cursor-pointer");
|
||||
@@ -396,7 +396,7 @@ describe("<Grid />", () => {
|
||||
};
|
||||
alertStore.data.setGrids([grid]);
|
||||
const { container } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Grid
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
@@ -408,7 +408,7 @@ describe("<Grid />", () => {
|
||||
paddingTop={0}
|
||||
zIndex={101}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
|
||||
expect(
|
||||
@@ -441,7 +441,7 @@ describe("<Grid />", () => {
|
||||
};
|
||||
alertStore.data.setGrids([MockGrid(), MockGrid()]);
|
||||
const { container } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Grid
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
@@ -453,7 +453,7 @@ describe("<Grid />", () => {
|
||||
paddingTop={0}
|
||||
zIndex={101}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(container.textContent).toMatch(/foo:.*bar/);
|
||||
});
|
||||
@@ -470,7 +470,7 @@ describe("<Grid />", () => {
|
||||
};
|
||||
alertStore.data.setGrids([MockGrid(), MockGrid()]);
|
||||
const { container } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Grid
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
@@ -482,7 +482,7 @@ describe("<Grid />", () => {
|
||||
paddingTop={0}
|
||||
zIndex={101}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(container.querySelector("h5")?.innerHTML).not.toMatch(/foo: bar/);
|
||||
});
|
||||
|
||||
@@ -25,10 +25,10 @@ const AlertGrid: FC<{
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const { observe, width: bodyWidth } = useDimensions();
|
||||
|
||||
const [gridSizesConfig, setGridSizesConfig] = useState<SizeDetail[]>(
|
||||
const [gridSizesConfig, setGridSizesConfig] = useState<SizeDetail[]>(() =>
|
||||
GridSizesConfig(settingsStore.gridConfig.config.groupWidth),
|
||||
);
|
||||
const [groupWidth, setGroupWidth] = useState<number>(
|
||||
const [groupWidth, setGroupWidth] = useState<number>(() =>
|
||||
GetGridElementWidth(
|
||||
bodyWidth || document.body.clientWidth,
|
||||
windowWidth,
|
||||
@@ -52,7 +52,7 @@ const AlertGrid: FC<{
|
||||
),
|
||||
);
|
||||
}),
|
||||
[windowWidth, bodyWidth], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[windowWidth, bodyWidth],
|
||||
);
|
||||
|
||||
useHotkeys("alt+space", alertStore.status.togglePause);
|
||||
|
||||
@@ -7,9 +7,9 @@ import { EmptyGrid } from ".";
|
||||
describe("<EmptyGrid />", () => {
|
||||
it("matches snapshot", () => {
|
||||
const { asFragment } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<EmptyGrid />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -7,9 +7,9 @@ import { FatalError } from ".";
|
||||
describe("<FatalError />", () => {
|
||||
it("matches snapshot", () => {
|
||||
const { asFragment } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<FatalError message="foo bar" />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -7,9 +7,9 @@ import { NoUpstream } from ".";
|
||||
describe("<NoUpstream />", () => {
|
||||
it("matches snapshot", () => {
|
||||
const { asFragment } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<NoUpstream />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -17,11 +17,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
const renderWithTheme = (ui: React.ReactElement) =>
|
||||
render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
{ui}
|
||||
</ThemeContext.Provider>,
|
||||
);
|
||||
render(<ThemeContext value={MockThemeContext}>{ui}</ThemeContext>);
|
||||
|
||||
describe("<ReloadNeeded />", () => {
|
||||
it("matches snapshot", () => {
|
||||
|
||||
@@ -6,16 +6,12 @@ import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
|
||||
|
||||
import { CenteredMessage } from "Components/CenteredMessage";
|
||||
|
||||
const clearReloadTimer = (timer: NodeJS.Timeout): void => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
|
||||
const ReloadNeeded: FC<{
|
||||
reloadAfter: number;
|
||||
}> = ({ reloadAfter }) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => window.location.reload(), reloadAfter);
|
||||
return () => clearReloadTimer(timer);
|
||||
return () => clearTimeout(timer);
|
||||
}, [reloadAfter]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,11 +17,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
const renderWithTheme = (ui: React.ReactElement) =>
|
||||
render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
{ui}
|
||||
</ThemeContext.Provider>,
|
||||
);
|
||||
render(<ThemeContext value={MockThemeContext}>{ui}</ThemeContext>);
|
||||
|
||||
describe("<UpgradeNeeded />", () => {
|
||||
it("matches snapshot", () => {
|
||||
|
||||
@@ -8,17 +8,13 @@ import { CenteredMessage } from "Components/CenteredMessage";
|
||||
|
||||
import "csshake/dist/csshake-slow.css";
|
||||
|
||||
const clearUpgradeTimer = (timer: NodeJS.Timeout): void => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
|
||||
const UpgradeNeeded: FC<{
|
||||
newVersion: string;
|
||||
reloadAfter: number;
|
||||
}> = ({ newVersion, reloadAfter }) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => window.location.reload(), reloadAfter);
|
||||
return () => clearUpgradeTimer(timer);
|
||||
return () => clearTimeout(timer);
|
||||
}, [reloadAfter]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,7 +32,7 @@ const FilteringCounterBadge: FC<{
|
||||
defaultColor = "bg-light",
|
||||
isAppend = true,
|
||||
}) => {
|
||||
const { ref, props, nodeRef } = useFlashTransition(counter);
|
||||
const { ref, props } = useFlashTransition(counter);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
@@ -72,10 +72,7 @@ const FilteringCounterBadge: FC<{
|
||||
>
|
||||
<CSSTransition {...props}>
|
||||
<span
|
||||
ref={(node) => {
|
||||
ref(node);
|
||||
nodeRef.current = node;
|
||||
}}
|
||||
ref={ref}
|
||||
className={
|
||||
themed
|
||||
? cs.className
|
||||
|
||||
@@ -13,9 +13,9 @@ beforeEach(() => {
|
||||
|
||||
const renderConfiguration = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<AlertGroupCollapseConfiguration settingsStore={settingsStore} />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC } from "react";
|
||||
import { use, FC } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@@ -32,7 +32,7 @@ const AlertGroupCollapseConfiguration: FC<{
|
||||
settingsStore.alertGroupConfig.setDefaultCollapseState(newValue);
|
||||
};
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
<div className="mb-0">
|
||||
|
||||
@@ -18,9 +18,9 @@ afterEach(() => {
|
||||
|
||||
const renderConfiguration = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<AlertGroupSortConfiguration settingsStore={settingsStore} />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC } from "react";
|
||||
import { use, FC } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@@ -43,7 +43,7 @@ const AlertGroupSortConfiguration: FC<{
|
||||
settingsStore.gridConfig.config.sortOrder ===
|
||||
settingsStore.gridConfig.options.disabled.value;
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
<div className="mb-0">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC } from "react";
|
||||
import { use, FC } from "react";
|
||||
|
||||
import Creatable from "react-select/creatable";
|
||||
|
||||
@@ -33,7 +33,7 @@ const GridLabelName: FC<{
|
||||
FormatBackendURI(`labelNames.json`),
|
||||
);
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
const defaultValue =
|
||||
settingsStore.multiGridConfig.config.gridLabel === "@auto"
|
||||
|
||||
@@ -24,9 +24,9 @@ afterEach(() => {
|
||||
|
||||
const renderConfiguration = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<MultiGridConfiguration settingsStore={settingsStore} />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC } from "react";
|
||||
import { use, FC } from "react";
|
||||
|
||||
import Creatable from "react-select/creatable";
|
||||
|
||||
@@ -23,7 +23,7 @@ const SortLabelName: FC<{
|
||||
settingsStore.gridConfig.setSortLabel(StaticLabels.AlertName);
|
||||
}
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
<Creatable
|
||||
|
||||
@@ -13,9 +13,9 @@ beforeEach(() => {
|
||||
|
||||
const renderConfiguration = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<ThemeConfiguration settingsStore={settingsStore} />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC } from "react";
|
||||
import { use, FC } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@@ -32,7 +32,7 @@ const ThemeConfiguration: FC<{
|
||||
settingsStore.themeConfig.setTheme(newValue);
|
||||
};
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
|
||||
@@ -24,9 +24,9 @@ describe("<Configuration />", () => {
|
||||
it("matches snapshot", () => {
|
||||
const settingsStore = new Settings(null);
|
||||
const { asFragment } = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Configuration settingsStore={settingsStore} defaultIsOpen={true} />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -29,14 +29,14 @@ afterEach(() => {
|
||||
|
||||
const renderModalContent = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<MainModalContent
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
onHide={onHide}
|
||||
expandAllOptions={true}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ const renderMainModal = async () => {
|
||||
let result: ReturnType<typeof render>;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<MainModal alertStore={alertStore} settingsStore={settingsStore} />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
});
|
||||
return result!;
|
||||
|
||||
@@ -30,16 +30,13 @@ const SilenceIDCopyButton: FC<{
|
||||
id: string;
|
||||
}> = ({ id }) => {
|
||||
const [clickCount, setClickCount] = useState<number>(0);
|
||||
const { ref, props, nodeRef } = useFlashTransition(clickCount);
|
||||
const { ref, props } = useFlashTransition(clickCount);
|
||||
|
||||
return (
|
||||
<TooltipWrapper title="Copy silence ID to the clipboard">
|
||||
<CSSTransition {...props}>
|
||||
<span
|
||||
ref={(node) => {
|
||||
ref(node);
|
||||
nodeRef.current = node;
|
||||
}}
|
||||
ref={ref}
|
||||
className="badge bg-secondary px-1 me-1 components-label cursor-pointer"
|
||||
onClick={() => {
|
||||
copy(id);
|
||||
|
||||
@@ -53,7 +53,7 @@ afterEach(() => {
|
||||
|
||||
const renderManagedSilence = (onDidUpdate?: () => void) => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<ManagedSilence
|
||||
cluster={cluster}
|
||||
alertCount={123}
|
||||
@@ -63,7 +63,7 @@ const renderManagedSilence = (onDidUpdate?: () => void) => {
|
||||
silenceFormStore={silenceFormStore}
|
||||
onDidUpdate={onDidUpdate}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ const ManagedSilence: FC<{
|
||||
silenceFormStore.toggle.show();
|
||||
};
|
||||
|
||||
const [progress, setProgress] = useState<number>(
|
||||
const [progress, setProgress] = useState<number>(() =>
|
||||
calculatePercent(silence.startsAt, silence.endsAt),
|
||||
);
|
||||
|
||||
|
||||
@@ -116,11 +116,11 @@ describe("<ModalInner />", () => {
|
||||
it("uses components-animation-modal class when animations are enabled", () => {
|
||||
const onExited = jest.fn();
|
||||
render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Modal isOpen={true} toggleOpen={fakeToggle} onExited={onExited}>
|
||||
<div />
|
||||
</Modal>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(document.body.querySelector(".modal")).toBeInTheDocument();
|
||||
});
|
||||
@@ -128,11 +128,11 @@ describe("<ModalInner />", () => {
|
||||
it("doesn't use components-animation-modal class when animations are disabled", () => {
|
||||
const onExited = jest.fn();
|
||||
render(
|
||||
<ThemeContext.Provider value={MockThemeContextWithoutAnimations}>
|
||||
<ThemeContext value={MockThemeContextWithoutAnimations}>
|
||||
<Modal isOpen={true} toggleOpen={fakeToggle} onExited={onExited}>
|
||||
<div />
|
||||
</Modal>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(document.body.querySelector(".modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect, useRef } from "react";
|
||||
import React, { use, FC, useEffect, useRef } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
@@ -64,7 +64,7 @@ const Modal: FC<{
|
||||
onExited,
|
||||
children,
|
||||
}) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const backdropRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ const History: FC<{
|
||||
alertStore: AlertStore;
|
||||
settingsStore: Settings;
|
||||
}> = observer(({ alertStore, settingsStore }) => {
|
||||
const [history] = useState<HistoryStorage>(new HistoryStorage());
|
||||
const [history] = useState<HistoryStorage>(() => new HistoryStorage());
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const [maxHeight, setMaxHeight] = useState<number | null>(null);
|
||||
const hide = useCallback(() => setIsVisible(false), []);
|
||||
@@ -246,7 +246,7 @@ const History: FC<{
|
||||
disposeAutorun();
|
||||
history.destroy();
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useOnClickOutside(ref, hide, isVisible);
|
||||
|
||||
@@ -96,14 +96,14 @@ afterEach(() => {
|
||||
|
||||
const renderNavbar = (fixedTop?: boolean) => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<NavBar
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
fixedTop={fixedTop}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -215,13 +215,13 @@ describe("<NavBar />", () => {
|
||||
resizeCallback([{ contentRect: { width: 100, height: 10 } }]);
|
||||
});
|
||||
rerender(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<NavBar
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(
|
||||
window
|
||||
@@ -233,13 +233,13 @@ describe("<NavBar />", () => {
|
||||
resizeCallback([{ contentRect: { width: 100, height: 36 } }]);
|
||||
});
|
||||
rerender(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<NavBar
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
expect(
|
||||
window
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useState, useEffect, useCallback, useRef } from "react";
|
||||
import { use, FC, useState, useEffect, useCallback, useRef } from "react";
|
||||
|
||||
import { reaction } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
@@ -30,7 +30,7 @@ const NavBar: FC<{
|
||||
}> = ({ alertStore, settingsStore, silenceFormStore, fixedTop = true }) => {
|
||||
const [containerClass, setContainerClass] = useState<string>("visible");
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const { observe, height } = useDimensions({});
|
||||
@@ -38,6 +38,7 @@ const NavBar: FC<{
|
||||
const updateBodyPaddingTop = useCallback(
|
||||
(idle: boolean) => {
|
||||
const paddingTop = idle ? 0 : height + 8;
|
||||
// eslint-disable-next-line react-compiler/react-compiler -- intentional DOM side-effect, not React state
|
||||
document.body.style.paddingTop = `${paddingTop}px`;
|
||||
setContainerClass(idle ? "invisible" : "visible");
|
||||
|
||||
@@ -93,7 +94,7 @@ const NavBar: FC<{
|
||||
(paused) => (paused ? pause() : reset()),
|
||||
{ fireImmediately: true },
|
||||
),
|
||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
|
||||
@@ -26,19 +26,14 @@ const OverviewModal: FC<{
|
||||
|
||||
const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]);
|
||||
|
||||
const { ref, props, nodeRef } = useFlashTransition(
|
||||
alertStore.info.totalAlerts,
|
||||
);
|
||||
const { ref, props } = useFlashTransition(alertStore.info.totalAlerts);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TooltipWrapper title="Show alert overview">
|
||||
<CSSTransition {...props}>
|
||||
<div
|
||||
ref={(node) => {
|
||||
ref(node);
|
||||
nodeRef.current = node;
|
||||
}}
|
||||
ref={ref}
|
||||
className={`text-center d-inline-block cursor-pointer navbar-brand m-0 components-navbar-button ${
|
||||
isVisible ? "border-info" : ""
|
||||
}`}
|
||||
|
||||
@@ -63,12 +63,12 @@ beforeEach(() => {
|
||||
|
||||
const renderAlertManagerInput = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<AlertManagerInput
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect } from "react";
|
||||
import { use, FC, useEffect } from "react";
|
||||
|
||||
import { autorun } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
@@ -36,7 +36,7 @@ const AlertManagerInput: FC<{
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
// https://mobx-react.netlify.app/recipes-effects
|
||||
@@ -59,10 +59,10 @@ const AlertManagerInput: FC<{
|
||||
}
|
||||
}
|
||||
}),
|
||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -235,16 +235,19 @@ export const MassDeleteProgress: FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const timers: ReturnType<typeof setTimeout>[] = [];
|
||||
silences.forEach((silence, index) => {
|
||||
const ams = alertStore.data.readWriteAlertmanagers.filter(
|
||||
(u) => u.cluster === silence.cluster,
|
||||
);
|
||||
setTimeout(
|
||||
// eslint-disable-next-line @eslint-react/web-api/no-leaked-timeout -- false positive: timers are collected and cleared in the useEffect cleanup below
|
||||
const timer = setTimeout(
|
||||
() => deleteSilence(silence.cluster, silence.id, ams),
|
||||
50 * index,
|
||||
);
|
||||
timers.push(timer);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return () => timers.forEach((t) => clearTimeout(t));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -83,13 +83,13 @@ const MockSilenceList = (count: number): APIManagedSilenceT[] => {
|
||||
|
||||
const renderBrowser = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<Browser
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {
|
||||
import {
|
||||
use,
|
||||
FC,
|
||||
useState,
|
||||
useEffect,
|
||||
@@ -46,7 +47,7 @@ const FetchError: FC<{
|
||||
const Placeholder: FC<{
|
||||
content: ReactNode;
|
||||
}> = ({ content }) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
|
||||
@@ -246,7 +246,7 @@ const DateTimeSelect: FC<{
|
||||
openTab?: tabT;
|
||||
}> = observer(({ silenceFormStore, openTab = "duration" }) => {
|
||||
const [currentTab, setCurrentTab] = useState<tabT>(openTab);
|
||||
const [timeNow, setTimeNow] = useState<Date>(nowZeroSeconds());
|
||||
const [timeNow, setTimeNow] = useState<Date>(() => nowZeroSeconds());
|
||||
|
||||
const updateTimeNow = useCallback(() => {
|
||||
setTimeNow(nowZeroSeconds());
|
||||
|
||||
@@ -51,14 +51,14 @@ beforeEach(() => {
|
||||
|
||||
const renderSilenceForm = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<SilenceForm
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
previewOpen={false}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ const SilenceForm: FC<{
|
||||
if (alertStore.info.authentication.enabled) {
|
||||
silenceFormStore.data.setAuthor(alertStore.info.authentication.username);
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const addMore = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -21,9 +21,9 @@ afterEach(() => {
|
||||
|
||||
const renderLabelNameInput = (isValid: boolean) => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<LabelNameInput matcher={matcher} isValid={isValid} />
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC } from "react";
|
||||
import { use, FC } from "react";
|
||||
|
||||
import Creatable from "react-select/creatable";
|
||||
|
||||
@@ -19,7 +19,7 @@ const LabelNameInput: FC<{
|
||||
FormatBackendURI(`labelNames.json`),
|
||||
);
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
<Creatable
|
||||
@@ -33,6 +33,7 @@ const LabelNameInput: FC<{
|
||||
}
|
||||
placeholder={isValid ? "Label name" : <ValidationError />}
|
||||
onChange={(option: OnChangeValue<OptionT, false>) => {
|
||||
// eslint-disable-next-line react-compiler/react-compiler -- intentional MobX observable mutation
|
||||
matcher.name = (option as OptionT).value;
|
||||
}}
|
||||
hideSelectedOptions
|
||||
|
||||
@@ -26,13 +26,13 @@ afterEach(() => {
|
||||
|
||||
const renderLabelValueInput = (isValid: boolean) => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<LabelValueInput
|
||||
silenceFormStore={silenceFormStore}
|
||||
matcher={matcher}
|
||||
isValid={isValid}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect } from "react";
|
||||
import { use, FC, useEffect } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@@ -41,6 +41,27 @@ const Placeholder = (props: PlaceholderProps<OptionT, true>) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const makeValueContainer = (
|
||||
silenceFormStore: SilenceFormStore,
|
||||
matcher: MatcherWithIDT,
|
||||
) =>
|
||||
function ValueContainer(props: ValueContainerProps<OptionT, true>) {
|
||||
return (
|
||||
<components.ValueContainer
|
||||
{...(props as ValueContainerProps<OptionT, true>)}
|
||||
>
|
||||
{matcher.values.length > 0 ? (
|
||||
<MatchCounter
|
||||
key={GenerateHashFromMatchers(silenceFormStore, matcher)}
|
||||
silenceFormStore={silenceFormStore}
|
||||
matcher={matcher}
|
||||
/>
|
||||
) : null}
|
||||
{props.children}
|
||||
</components.ValueContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const LabelValueInput: FC<{
|
||||
silenceFormStore: SilenceFormStore;
|
||||
matcher: MatcherWithIDT;
|
||||
@@ -58,22 +79,7 @@ const LabelValueInput: FC<{
|
||||
return () => cancelGet();
|
||||
}, [matcher.name, get, cancelGet]);
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
|
||||
const ValueContainer = (props: ValueContainerProps<OptionT, true>) => (
|
||||
<components.ValueContainer
|
||||
{...(props as ValueContainerProps<OptionT, true>)}
|
||||
>
|
||||
{matcher.values.length > 0 ? (
|
||||
<MatchCounter
|
||||
key={GenerateHashFromMatchers(silenceFormStore, matcher)}
|
||||
silenceFormStore={silenceFormStore}
|
||||
matcher={matcher}
|
||||
/>
|
||||
) : null}
|
||||
{props.children}
|
||||
</components.ValueContainer>
|
||||
);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return (
|
||||
<Creatable
|
||||
@@ -104,7 +110,7 @@ const LabelValueInput: FC<{
|
||||
hideSelectedOptions
|
||||
isMulti
|
||||
components={{
|
||||
ValueContainer: ValueContainer,
|
||||
ValueContainer: makeValueContainer(silenceFormStore, matcher),
|
||||
Placeholder: Placeholder,
|
||||
Menu: AnimatedMenuMultiple,
|
||||
}}
|
||||
|
||||
@@ -49,6 +49,7 @@ const SilenceMatch: FC<{
|
||||
value=""
|
||||
checked={matcher.isEqual}
|
||||
onChange={(event) => {
|
||||
// eslint-disable-next-line react-compiler/react-compiler -- intentional MobX observable mutation
|
||||
matcher.isEqual = event.target.checked;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -49,14 +49,14 @@ const MockOnHide = jest.fn();
|
||||
|
||||
const renderSilenceModalContent = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<SilenceModalContent
|
||||
alertStore={alertStore}
|
||||
settingsStore={settingsStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
onHide={MockOnHide}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ const SilenceSubmitProgress: FC<{
|
||||
}`,
|
||||
});
|
||||
}
|
||||
}, [cluster, error, inProgress, publicURIs, response, responseURI]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [cluster, error, inProgress, publicURIs, response, responseURI]);
|
||||
|
||||
return <FontAwesomeIcon className="text-muted" icon={faCircleNotch} spin />;
|
||||
};
|
||||
|
||||
@@ -47,13 +47,13 @@ afterEach(() => {
|
||||
|
||||
const renderSilenceModal = () => {
|
||||
return render(
|
||||
<ThemeContext.Provider value={MockThemeContext}>
|
||||
<ThemeContext value={MockThemeContext}>
|
||||
<SilenceModal
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
/>
|
||||
</ThemeContext.Provider>,
|
||||
</ThemeContext>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,32 +2,27 @@ import React, { act } from "react";
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
|
||||
import { BodyTheme } from ".";
|
||||
|
||||
const context = {
|
||||
isDark: true,
|
||||
};
|
||||
import { ThemeContext, BodyTheme } from ".";
|
||||
import type { ThemeCtx } from ".";
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.classList.remove("theme-light");
|
||||
document.body.classList.remove("theme-dark");
|
||||
document.documentElement.removeAttribute("data-bs-theme");
|
||||
|
||||
jest.spyOn(React, "useContext").mockImplementation(() => {
|
||||
return context;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
const renderWithTheme = (ctx: Partial<ThemeCtx>) =>
|
||||
render(
|
||||
<ThemeContext value={ctx as ThemeCtx}>
|
||||
<BodyTheme />
|
||||
</ThemeContext>,
|
||||
);
|
||||
|
||||
describe("<BodyTheme />", () => {
|
||||
it("uses light theme when ThemeContext->isDark is false", async () => {
|
||||
// Verifies body class and data-bs-theme attribute are set for light theme when isDark is false
|
||||
context.isDark = false;
|
||||
await act(async () => {
|
||||
render(<BodyTheme />);
|
||||
renderWithTheme({ isDark: false });
|
||||
});
|
||||
expect(document.body.classList.contains("theme-light")).toEqual(true);
|
||||
expect(document.documentElement.getAttribute("data-bs-theme")).toEqual(
|
||||
@@ -37,9 +32,8 @@ describe("<BodyTheme />", () => {
|
||||
|
||||
it("uses dark theme when ThemeContext->isDark is true", async () => {
|
||||
// Verifies body class and data-bs-theme attribute are set for dark theme when isDark is true
|
||||
context.isDark = true;
|
||||
await act(async () => {
|
||||
render(<BodyTheme />);
|
||||
renderWithTheme({ isDark: true });
|
||||
});
|
||||
expect(document.body.classList.contains("theme-dark")).toEqual(true);
|
||||
expect(document.documentElement.getAttribute("data-bs-theme")).toEqual(
|
||||
@@ -49,10 +43,9 @@ describe("<BodyTheme />", () => {
|
||||
|
||||
it("updates theme when ThemeContext->isDark is updated", async () => {
|
||||
// Verifies body class updates when isDark context value changes
|
||||
context.isDark = true;
|
||||
let rerender: (ui: React.ReactElement) => void;
|
||||
await act(async () => {
|
||||
const result = render(<BodyTheme />);
|
||||
const result = renderWithTheme({ isDark: true });
|
||||
rerender = result.rerender;
|
||||
});
|
||||
expect(document.body.classList.contains("theme-dark")).toEqual(true);
|
||||
@@ -60,9 +53,12 @@ describe("<BodyTheme />", () => {
|
||||
document.body.classList.remove("theme-light");
|
||||
document.body.classList.remove("theme-dark");
|
||||
|
||||
context.isDark = false;
|
||||
await act(async () => {
|
||||
rerender!(<BodyTheme />);
|
||||
rerender!(
|
||||
<ThemeContext value={{ isDark: false } as ThemeCtx}>
|
||||
<BodyTheme />
|
||||
</ThemeContext>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(document.body.classList.contains("theme-light")).toEqual(true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect } from "react";
|
||||
import React, { use, FC, useEffect } from "react";
|
||||
|
||||
import type { StylesConfig } from "react-select/dist/declarations/src/styles";
|
||||
|
||||
@@ -18,7 +18,7 @@ const ThemeContext = React.createContext({
|
||||
} as ThemeCtx);
|
||||
|
||||
const BodyTheme: FC = () => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode, useState, useEffect, useRef } from "react";
|
||||
import React, { use, FC, ReactNode, useState, useEffect, useRef } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import TransitionGroup from "react-transition-group/TransitionGroup";
|
||||
@@ -77,7 +77,7 @@ const ToastTransition: FC<{
|
||||
};
|
||||
|
||||
const ToastContainer: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const context = React.useContext(ThemeContext);
|
||||
const context = use(ThemeContext);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className="components-toast-container d-flex flex-column">
|
||||
|
||||
@@ -73,13 +73,18 @@ describe("<ErrorBoundary />", () => {
|
||||
});
|
||||
|
||||
it("reloadApp decrements countdown when more than one second is left", () => {
|
||||
// Verifies that reloadApp uses functional setState to decrement reloadSeconds
|
||||
const boundary = new ErrorBoundary({ children: <span /> });
|
||||
const setStateSpy = jest.spyOn(boundary, "setState");
|
||||
(boundary as any).state = { cachedError: null, reloadSeconds: 2 };
|
||||
|
||||
boundary.reloadApp();
|
||||
|
||||
expect(setStateSpy).toHaveBeenCalledWith({ reloadSeconds: 1 });
|
||||
expect(setStateSpy).toHaveBeenCalledTimes(1);
|
||||
const updater = setStateSpy.mock.calls[0][0] as unknown as (prev: {
|
||||
reloadSeconds: number;
|
||||
}) => { reloadSeconds: number };
|
||||
expect(updater({ reloadSeconds: 2 })).toEqual({ reloadSeconds: 1 });
|
||||
});
|
||||
|
||||
it("reloadApp does not decrement when countdown reaches 1 or less", () => {
|
||||
|
||||
@@ -64,7 +64,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
if (this.state.reloadSeconds <= 1) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.setState({ reloadSeconds: this.state.reloadSeconds - 1 });
|
||||
this.setState((prev) => ({ reloadSeconds: prev.reloadSeconds - 1 }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ const useFetchDelete = (
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [uri, options, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [uri, options, ...deps]);
|
||||
|
||||
return { response, error, isDeleting };
|
||||
};
|
||||
|
||||
@@ -46,14 +46,14 @@ const useFetchGet = <T>(
|
||||
isRetrying: false,
|
||||
retryCount: 0,
|
||||
});
|
||||
const isCanceled = useRef<boolean>(false);
|
||||
const isCanceledRef = useRef<boolean>(false);
|
||||
|
||||
const cancelGet = useCallback(() => {
|
||||
isCanceled.current = true;
|
||||
isCanceledRef.current = true;
|
||||
}, []);
|
||||
|
||||
const get = useCallback(async () => {
|
||||
isCanceled.current = false;
|
||||
isCanceledRef.current = false;
|
||||
|
||||
try {
|
||||
setResponse((r) => ({
|
||||
@@ -78,7 +78,7 @@ const useFetchGet = <T>(
|
||||
},
|
||||
) as RequestInit,
|
||||
).catch((err: Error) => {
|
||||
if (!isCanceled.current) {
|
||||
if (!isCanceledRef.current) {
|
||||
setResponse((r) => ({
|
||||
...r,
|
||||
isRetrying: true,
|
||||
@@ -90,7 +90,7 @@ const useFetchGet = <T>(
|
||||
FetchRetryConfig,
|
||||
);
|
||||
|
||||
if (res !== undefined && !isCanceled.current) {
|
||||
if (res !== undefined && !isCanceledRef.current) {
|
||||
let body;
|
||||
const contentType = res.headers.get("content-type");
|
||||
if (contentType && contentType.indexOf("application/json") !== -1) {
|
||||
@@ -99,7 +99,7 @@ const useFetchGet = <T>(
|
||||
body = await res.text();
|
||||
}
|
||||
|
||||
if (!isCanceled.current) {
|
||||
if (!isCanceledRef.current) {
|
||||
if (res.ok) {
|
||||
setResponse({
|
||||
response: body,
|
||||
@@ -134,7 +134,7 @@ const useFetchGet = <T>(
|
||||
if (autorun) get();
|
||||
|
||||
return () => cancelGet();
|
||||
}, [uri, get, cancelGet, autorun, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [uri, get, cancelGet, autorun, ...deps]);
|
||||
|
||||
return { get, cancelGet, ...response };
|
||||
};
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { useState, useEffect, useRef, ReactNode, RefObject } from "react";
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
} from "react";
|
||||
|
||||
import type { CSSTransitionProps } from "react-transition-group/CSSTransition";
|
||||
|
||||
@@ -16,7 +23,7 @@ const defaultProps: CSSTransitionProps = {
|
||||
const useFlashTransition = (
|
||||
flashOn: ReactNode,
|
||||
): {
|
||||
ref: (node?: Element | null) => void;
|
||||
ref: (node: HTMLElement | null) => void;
|
||||
props: CSSTransitionProps;
|
||||
nodeRef: RefObject<HTMLElement | null>;
|
||||
} => {
|
||||
@@ -45,7 +52,15 @@ const useFlashTransition = (
|
||||
});
|
||||
}, [inView, isPending]);
|
||||
|
||||
return { ref, props, nodeRef };
|
||||
const combinedRef = useCallback(
|
||||
(node: HTMLElement | null) => {
|
||||
ref(node);
|
||||
nodeRef.current = node;
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
return { ref: combinedRef, props, nodeRef };
|
||||
};
|
||||
|
||||
export { useFlashTransition, defaultProps };
|
||||
|
||||
@@ -6,27 +6,28 @@ const useGrid = (
|
||||
sizes: SizeDetail[],
|
||||
): { ref: Ref<HTMLDivElement>; repack: () => void } => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const grid = useRef<Instance | null>(null);
|
||||
const gridRef = useRef<Instance | null>(null);
|
||||
const [repack, setRepack] = useState<() => void>(() => () => {});
|
||||
|
||||
useEffect(() => {
|
||||
if (!grid.current && ref.current) {
|
||||
grid.current = Bricks({
|
||||
if (!gridRef.current && ref.current) {
|
||||
gridRef.current = Bricks({
|
||||
container: ref.current,
|
||||
sizes: sizes,
|
||||
packed: "packed",
|
||||
position: false,
|
||||
});
|
||||
window.addEventListener("resize", grid.current.pack);
|
||||
grid.current.pack();
|
||||
window.addEventListener("resize", gridRef.current.pack);
|
||||
gridRef.current.pack();
|
||||
setRepack(() => () => {
|
||||
grid.current && grid.current.pack();
|
||||
gridRef.current && gridRef.current.pack();
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (grid.current) window.removeEventListener("resize", grid.current.pack);
|
||||
grid.current = null;
|
||||
if (gridRef.current)
|
||||
window.removeEventListener("resize", gridRef.current.pack);
|
||||
gridRef.current = null;
|
||||
};
|
||||
}, [sizes]);
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ describe("useOnClickOutside", () => {
|
||||
enabled: boolean;
|
||||
}> = ({ enabled }) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [isModalOpen, setModalOpen] = useState<boolean>(true);
|
||||
useOnClickOutside(ref, () => setModalOpen(false), enabled);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(true);
|
||||
useOnClickOutside(ref, () => setIsModalOpen(false), enabled);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -13,7 +13,7 @@ function getSize(): Dimensions {
|
||||
}
|
||||
|
||||
function useWindowSize(): Dimensions {
|
||||
const [windowSize, setWindowSize] = useState<Dimensions>(getSize());
|
||||
const [windowSize, setWindowSize] = useState<Dimensions>(() => getSize());
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
|
||||
@@ -234,8 +234,6 @@ const useFetchGetMock = (
|
||||
|
||||
useEffect(() => {
|
||||
if (autorun) get();
|
||||
// eslint doesn't like ...deps
|
||||
// eslint-disable-next-line
|
||||
}, [uri, get, cancelGet, autorun, ...deps]);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user