chore(ws): enforce named imports for react hooks (#414)

Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com>
This commit is contained in:
Paulo Rego 2025-06-16 17:51:08 -03:00 committed by Bhakti Narvekar
parent cd02eb46c6
commit 99538a7f81
54 changed files with 268 additions and 218 deletions

View File

@ -1,4 +1,6 @@
{
const noReactHookNamespace = require('./eslint-local-rules/no-react-hook-namespace');
module.exports = {
"parser": "@typescript-eslint/parser",
"env": {
"browser": true,
@ -216,7 +218,8 @@
"no-useless-return": "error",
"symbol-description": "error",
"yoda": "error",
"func-names": "warn"
"func-names": "warn",
"no-react-hook-namespace": "error"
},
"overrides": [
{
@ -262,6 +265,12 @@
}
]
}
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
'no-react-hook-namespace': 'error',
},
}
]
}
};

View File

@ -0,0 +1,34 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow using React hooks through React namespace',
},
messages: {
avoidNamespaceHook: 'Import React hook "{{hook}}" directly instead of using React.{{hook}}.',
},
schema: [],
},
create(context) {
const hooks = new Set([
'useState', 'useEffect', 'useContext', 'useReducer',
'useCallback', 'useMemo', 'useRef', 'useLayoutEffect',
'useImperativeHandle', 'useDebugValue', 'useDeferredValue',
'useTransition', 'useId', 'useSyncExternalStore',
]);
return {
MemberExpression(node) {
if (
node.object?.name === 'React' &&
hooks.has(node.property?.name)
) {
context.report({
node,
messageId: 'avoidNamespaceHook',
data: { hook: node.property.name },
});
}
},
};
},
};

View File

@ -24,8 +24,8 @@
"test:unit": "npm run test:jest -- --silent",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:fix": "eslint --ext .js,.ts,.jsx,.tsx ./src --fix",
"test:lint": "eslint --max-warnings 0 --ext .js,.ts,.jsx,.tsx ./src",
"test:fix": "eslint --rulesdir eslint-local-rules --ext .js,.ts,.jsx,.tsx ./src --fix",
"test:lint": "eslint --rulesdir eslint-local-rules --max-warnings 0 --ext .js,.ts,.jsx,.tsx ./src",
"cypress:open": "cypress open --project src/__tests__/cypress",
"cypress:open:mock": "CY_MOCK=1 CY_WS_PORT=9002 npm run cypress:open -- ",
"cypress:run": "cypress run -b chrome --project src/__tests__/cypress",

View File

@ -1,15 +1,15 @@
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { createComparativeValue, renderHook, standardUseFetchState, testHook } from './hooks';
const useSayHello = (who: string, showCount = false) => {
const countRef = React.useRef(0);
const countRef = useRef(0);
countRef.current++;
return `Hello ${who}!${showCount && countRef.current > 1 ? ` x${countRef.current}` : ''}`;
};
const useSayHelloDelayed = (who: string, delay = 0) => {
const [speech, setSpeech] = React.useState('');
React.useEffect(() => {
const [speech, setSpeech] = useState('');
useEffect(() => {
const handle = setTimeout(() => setSpeech(`Hello ${who}!`), delay);
return () => clearTimeout(handle);
}, [who, delay]);

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useEffect } from 'react';
import '@patternfly/react-core/dist/styles/base.css';
import './app.css';
import {
@ -26,7 +26,7 @@ import { isMUITheme, Theme } from './const';
import { BrowserStorageContextProvider } from './context/BrowserStorageContext';
const App: React.FC = () => {
React.useEffect(() => {
useEffect(() => {
// Apply the theme based on the value of STYLE_THEME
if (isMUITheme()) {
document.documentElement.classList.add(Theme.MUI);

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';
import { AppRoutePaths } from '~/app/routes';
import { WorkspaceForm } from '~/app/pages/Workspaces/Form/WorkspaceForm';

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import { Bullseye, Spinner } from '@patternfly/react-core';
import { useNotebookAPI } from './hooks/useNotebookAPI';

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useState } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import {
Brand,
@ -29,7 +29,7 @@ const NavHref: React.FC<{ item: NavDataHref }> = ({ item }) => {
const NavGroup: React.FC<{ item: NavDataGroup }> = ({ item }) => {
const { children } = item;
const [expanded, setExpanded] = React.useState(false);
const [expanded, setExpanded] = useState(false);
return (
<NavExpandable

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import { SearchInput, SearchInputProps, TextInput } from '@patternfly/react-core';
import FormFieldset from 'app/components/FormFieldset';
import { isMUITheme } from 'app/const';

View File

@ -1,4 +1,12 @@
import React, { createContext, useCallback, useContext, useEffect } from 'react';
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
export interface BrowserStorageContextType {
getValue: (key: string) => unknown;
@ -17,8 +25,8 @@ const BrowserStorageContext = createContext<BrowserStorageContextType>({
export const BrowserStorageContextProvider: React.FC<BrowserStorageContextProviderProps> = ({
children,
}) => {
const [values, setValues] = React.useState<{ [key: string]: unknown }>({});
const valuesRef = React.useRef(values);
const [values, setValues] = useState<{ [key: string]: unknown }>({});
const valuesRef = useRef(values);
useEffect(() => {
valuesRef.current = values;
}, [values]);
@ -49,7 +57,7 @@ export const BrowserStorageContextProvider: React.FC<BrowserStorageContextProvid
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const contextValue = React.useMemo(() => ({ getValue, setValue }), [getValue, setValue, values]);
const contextValue = useMemo(() => ({ getValue, setValue }), [getValue, setValue, values]);
return (
<BrowserStorageContext.Provider value={contextValue}>{children}</BrowserStorageContext.Provider>

View File

@ -1,4 +1,4 @@
import React, { useState, useContext, ReactNode, useMemo, useCallback } from 'react';
import React, { ReactNode, useCallback, useContext, useMemo, useState } from 'react';
import useMount from '~/app/hooks/useMount';
import useNamespaces from '~/app/hooks/useNamespaces';
import { useStorage } from './BrowserStorageContext';

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import { ReactNode } from 'react';
import React, { ReactNode, useMemo } from 'react';
import { BFF_API_VERSION } from '~/app/const';
import EnsureAPIAvailability from '~/app/EnsureAPIAvailability';
import useNotebookAPIState, { NotebookAPIState } from './useNotebookAPIState';
@ -26,7 +25,7 @@ export const NotebookContextProvider: React.FC<NotebookContextProviderProps> = (
return (
<NotebookContext.Provider
value={React.useMemo(
value={useMemo(
() => ({
apiState,
refreshAPIState,

View File

@ -1,4 +1,4 @@
import React from 'react';
import { useCallback } from 'react';
import { NotebookAPIs } from '~/shared/api/notebookApi';
import {
createWorkspace,
@ -48,7 +48,7 @@ const MOCK_API_ENABLED = process.env.WEBPACK_REPLACE__mockApiEnabled === 'true';
const useNotebookAPIState = (
hostPath: string | null,
): [apiState: NotebookAPIState, refreshAPIState: () => void] => {
const createApi = React.useCallback(
const createApi = useCallback(
(path: string): NotebookAPIs => ({
// Health
getHealthCheck: getHealthCheck(path),
@ -75,7 +75,7 @@ const useNotebookAPIState = (
[],
);
const createMockApi = React.useCallback(
const createMockApi = useCallback(
(path: string): NotebookAPIs => ({
// Health
getHealthCheck: mockGetHealthCheck(path),

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import { Button, Split, SplitItem, Title } from '@patternfly/react-core';
import { TimesIcon } from '@patternfly/react-icons';

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import {
ClipboardCopy,
ClipboardCopyVariant,

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import {
Button,
EmptyState,

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useCallback, useRef, useState } from 'react';
export type UpdateObjectAtPropAndValue<T> = <K extends keyof T>(
propKey: K,
@ -13,9 +13,9 @@ export type GenericObjectState<T> = [
];
const useGenericObjectState = <T>(defaultData: T | (() => T)): GenericObjectState<T> => {
const [value, setValue] = React.useState<T>(defaultData);
const [value, setValue] = useState<T>(defaultData);
const setPropValue = React.useCallback<UpdateObjectAtPropAndValue<T>>((propKey, propValue) => {
const setPropValue = useCallback<UpdateObjectAtPropAndValue<T>>((propKey, propValue) => {
setValue((oldValue) => {
if (oldValue[propKey] !== propValue) {
return { ...oldValue, [propKey]: propValue };
@ -24,12 +24,12 @@ const useGenericObjectState = <T>(defaultData: T | (() => T)): GenericObjectStat
});
}, []);
const defaultDataRef = React.useRef(value);
const resetToDefault = React.useCallback(() => {
const defaultDataRef = useRef(value);
const resetToDefault = useCallback(() => {
setValue(defaultDataRef.current);
}, []);
const replace = React.useCallback((newValue: T) => {
const replace = useCallback((newValue: T) => {
setValue(newValue);
}, []);

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useCallback } from 'react';
import useFetchState, {
FetchState,
FetchStateCallbackPromise,
@ -9,7 +9,7 @@ import { Namespace } from '~/shared/api/backendApiTypes';
const useNamespaces = (): FetchState<Namespace[] | null> => {
const { api, apiAvailable } = useNotebookAPI();
const call = React.useCallback<FetchStateCallbackPromise<Namespace[] | null>>(
const call = useCallback<FetchStateCallbackPromise<Namespace[] | null>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useContext } from 'react';
import { NotebookAPIState } from '~/app/context/useNotebookAPIState';
import { NotebookContext } from '~/app/context/NotebookContext';
@ -7,7 +7,7 @@ type UseNotebookAPI = NotebookAPIState & {
};
export const useNotebookAPI = (): UseNotebookAPI => {
const { apiState, refreshAPIState: refreshAllAPI } = React.useContext(NotebookContext);
const { apiState, refreshAPIState: refreshAllAPI } = useContext(NotebookContext);
return {
refreshAllAPI,

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { Workspace, WorkspaceKind } from '~/shared/api/backendApiTypes';
import { WorkspaceCountPerOption } from '~/app/types';
@ -8,11 +8,9 @@ export type WorkspaceCountPerKind = Record<WorkspaceKind['name'], WorkspaceCount
export const useWorkspaceCountPerKind = (): WorkspaceCountPerKind => {
const { api } = useNotebookAPI();
const [workspaceCountPerKind, setWorkspaceCountPerKind] = React.useState<WorkspaceCountPerKind>(
{},
);
const [workspaceCountPerKind, setWorkspaceCountPerKind] = useState<WorkspaceCountPerKind>({});
React.useEffect(() => {
useEffect(() => {
api.listAllWorkspaces({}).then((workspaces) => {
const countPerKind = workspaces.reduce((acc: WorkspaceCountPerKind, workspace: Workspace) => {
acc[workspace.workspaceKind.name] = acc[workspace.workspaceKind.name] ?? {

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useCallback } from 'react';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { WorkspaceFormData } from '~/app/types';
import useFetchState, {
@ -25,7 +25,7 @@ const useWorkspaceFormData = (args: {
}): FetchState<WorkspaceFormData> => {
const { api, apiAvailable } = useNotebookAPI();
const call = React.useCallback<FetchStateCallbackPromise<WorkspaceFormData>>(
const call = useCallback<FetchStateCallbackPromise<WorkspaceFormData>>(
async (opts) => {
if (!apiAvailable) {
throw new Error('API not yet available');

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useCallback } from 'react';
import useFetchState, {
FetchState,
FetchStateCallbackPromise,
@ -9,7 +9,7 @@ import { WorkspaceKind } from '~/shared/api/backendApiTypes';
const useWorkspaceKindByName = (kind: string): FetchState<WorkspaceKind | null> => {
const { api, apiAvailable } = useNotebookAPI();
const call = React.useCallback<FetchStateCallbackPromise<WorkspaceKind | null>>(
const call = useCallback<FetchStateCallbackPromise<WorkspaceKind | null>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useCallback } from 'react';
import useFetchState, {
FetchState,
FetchStateCallbackPromise,
@ -8,7 +8,7 @@ import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
const useWorkspaceKinds = (): FetchState<WorkspaceKind[]> => {
const { api, apiAvailable } = useNotebookAPI();
const call = React.useCallback<FetchStateCallbackPromise<WorkspaceKind[]>>(
const call = useCallback<FetchStateCallbackPromise<WorkspaceKind[]>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useCallback } from 'react';
import useFetchState, {
FetchState,
FetchStateCallbackPromise,
@ -9,7 +9,7 @@ import { Workspace } from '~/shared/api/backendApiTypes';
const useWorkspaces = (namespace: string): FetchState<Workspace[] | null> => {
const { api, apiAvailable } = useNotebookAPI();
const call = React.useCallback<FetchStateCallbackPromise<Workspace[] | null>>(
const call = useCallback<FetchStateCallbackPromise<Workspace[] | null>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import { CubesIcon } from '@patternfly/react-icons';
import {
Button,

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Drawer,
DrawerContent,
@ -48,7 +48,7 @@ export enum ActionType {
export const WorkspaceKinds: React.FunctionComponent = () => {
// Table columns
const columns: WorkspaceKindsColumns = React.useMemo(
const columns: WorkspaceKindsColumns = useMemo(
() => ({
icon: { name: '', label: 'Icon', id: 'icon' },
name: { name: 'Name', label: 'Name', id: 'name' },
@ -65,16 +65,14 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
const [workspaceKinds, workspaceKindsLoaded, workspaceKindsError] = useWorkspaceKinds();
const workspaceCountPerKind = useWorkspaceCountPerKind();
const [selectedWorkspaceKind, setSelectedWorkspaceKind] = React.useState<WorkspaceKind | null>(
null,
);
const [activeActionType, setActiveActionType] = React.useState<ActionType | null>(null);
const [selectedWorkspaceKind, setSelectedWorkspaceKind] = useState<WorkspaceKind | null>(null);
const [activeActionType, setActiveActionType] = useState<ActionType | null>(null);
// Column sorting
const [activeSortIndex, setActiveSortIndex] = React.useState<number | null>(null);
const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null);
const [activeSortIndex, setActiveSortIndex] = useState<number | null>(null);
const [activeSortDirection, setActiveSortDirection] = useState<'asc' | 'desc' | null>(null);
const getSortableRowValues = React.useCallback(
const getSortableRowValues = useCallback(
(workspaceKind: WorkspaceKind): (string | boolean | number)[] => {
const {
icon,
@ -95,7 +93,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[workspaceCountPerKind],
);
const sortedWorkspaceKinds = React.useMemo(() => {
const sortedWorkspaceKinds = useMemo(() => {
if (activeSortIndex === null) {
return workspaceKinds;
}
@ -114,7 +112,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
});
}, [workspaceKinds, activeSortIndex, activeSortDirection, getSortableRowValues]);
const getSortParams = React.useCallback(
const getSortParams = useCallback(
(columnIndex: number): ThProps['sort'] => ({
sortBy: {
index: activeSortIndex || 0,
@ -131,19 +129,19 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
);
// Set up filter - Attribute search.
const [searchNameValue, setSearchNameValue] = React.useState('');
const [searchDescriptionValue, setSearchDescriptionValue] = React.useState('');
const [statusSelection, setStatusSelection] = React.useState('');
const [searchNameValue, setSearchNameValue] = useState('');
const [searchDescriptionValue, setSearchDescriptionValue] = useState('');
const [statusSelection, setStatusSelection] = useState('');
const onSearchNameChange = React.useCallback((value: string) => {
const onSearchNameChange = useCallback((value: string) => {
setSearchNameValue(value);
}, []);
const onSearchDescriptionChange = React.useCallback((value: string) => {
const onSearchDescriptionChange = useCallback((value: string) => {
setSearchDescriptionValue(value);
}, []);
const onFilter = React.useCallback(
const onFilter = useCallback(
(workspaceKind: WorkspaceKind) => {
let nameRegex: RegExp;
let descriptionRegex: RegExp;
@ -178,24 +176,24 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[searchNameValue, searchDescriptionValue, statusSelection],
);
const filteredWorkspaceKinds = React.useMemo(
const filteredWorkspaceKinds = useMemo(
() => sortedWorkspaceKinds.filter(onFilter),
[sortedWorkspaceKinds, onFilter],
);
const clearAllFilters = React.useCallback(() => {
const clearAllFilters = useCallback(() => {
setSearchNameValue('');
setStatusSelection('');
setSearchDescriptionValue('');
}, []);
// Set up status single select
const [isStatusMenuOpen, setIsStatusMenuOpen] = React.useState<boolean>(false);
const statusToggleRef = React.useRef<HTMLButtonElement>(null);
const statusMenuRef = React.useRef<HTMLDivElement>(null);
const statusContainerRef = React.useRef<HTMLDivElement>(null);
const [isStatusMenuOpen, setIsStatusMenuOpen] = useState<boolean>(false);
const statusToggleRef = useRef<HTMLButtonElement>(null);
const statusMenuRef = useRef<HTMLDivElement>(null);
const statusContainerRef = useRef<HTMLDivElement>(null);
const handleStatusMenuKeys = React.useCallback(
const handleStatusMenuKeys = useCallback(
(event: KeyboardEvent) => {
if (isStatusMenuOpen && statusMenuRef.current?.contains(event.target as Node)) {
if (event.key === 'Escape' || event.key === 'Tab') {
@ -207,7 +205,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[isStatusMenuOpen],
);
const handleStatusClickOutside = React.useCallback(
const handleStatusClickOutside = useCallback(
(event: MouseEvent) => {
if (isStatusMenuOpen && !statusMenuRef.current?.contains(event.target as Node)) {
setIsStatusMenuOpen(false);
@ -216,7 +214,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[isStatusMenuOpen],
);
React.useEffect(() => {
useEffect(() => {
window.addEventListener('keydown', handleStatusMenuKeys);
window.addEventListener('click', handleStatusClickOutside);
return () => {
@ -225,7 +223,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
};
}, [isStatusMenuOpen, statusMenuRef, handleStatusClickOutside, handleStatusMenuKeys]);
const onStatusToggleClick = React.useCallback((ev: React.MouseEvent) => {
const onStatusToggleClick = useCallback((ev: React.MouseEvent) => {
ev.stopPropagation();
setTimeout(() => {
const firstElement = statusMenuRef.current?.querySelector('li > button:not(:disabled)');
@ -236,7 +234,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
setIsStatusMenuOpen((prev) => !prev);
}, []);
const onStatusSelect = React.useCallback(
const onStatusSelect = useCallback(
(event: React.MouseEvent | undefined, itemId: string | number | undefined) => {
if (typeof itemId === 'undefined') {
return;
@ -248,7 +246,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[],
);
const statusToggle = React.useMemo(
const statusToggle = useMemo(
() => (
<MenuToggle
ref={statusToggleRef}
@ -262,7 +260,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[isStatusMenuOpen, onStatusToggleClick],
);
const statusMenu = React.useMemo(
const statusMenu = useMemo(
() => (
<Menu
ref={statusMenuRef}
@ -281,7 +279,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[statusSelection, onStatusSelect],
);
const statusSelect = React.useMemo(
const statusSelect = useMemo(
() => (
<div ref={statusContainerRef}>
<Popper
@ -298,15 +296,15 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
);
// Set up attribute selector
const [activeAttributeMenu, setActiveAttributeMenu] = React.useState<
'Name' | 'Description' | 'Status'
>('Name');
const [isAttributeMenuOpen, setIsAttributeMenuOpen] = React.useState(false);
const attributeToggleRef = React.useRef<HTMLButtonElement>(null);
const attributeMenuRef = React.useRef<HTMLDivElement>(null);
const attributeContainerRef = React.useRef<HTMLDivElement>(null);
const [activeAttributeMenu, setActiveAttributeMenu] = useState<'Name' | 'Description' | 'Status'>(
'Name',
);
const [isAttributeMenuOpen, setIsAttributeMenuOpen] = useState(false);
const attributeToggleRef = useRef<HTMLButtonElement>(null);
const attributeMenuRef = useRef<HTMLDivElement>(null);
const attributeContainerRef = useRef<HTMLDivElement>(null);
const handleAttributeMenuKeys = React.useCallback(
const handleAttributeMenuKeys = useCallback(
(event: KeyboardEvent) => {
if (!isAttributeMenuOpen) {
return;
@ -324,7 +322,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[isAttributeMenuOpen],
);
const handleAttributeClickOutside = React.useCallback(
const handleAttributeClickOutside = useCallback(
(event: MouseEvent) => {
if (isAttributeMenuOpen && !attributeMenuRef.current?.contains(event.target as Node)) {
setIsAttributeMenuOpen(false);
@ -333,7 +331,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[isAttributeMenuOpen],
);
React.useEffect(() => {
useEffect(() => {
window.addEventListener('keydown', handleAttributeMenuKeys);
window.addEventListener('click', handleAttributeClickOutside);
return () => {
@ -342,7 +340,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
};
}, [isAttributeMenuOpen, attributeMenuRef, handleAttributeMenuKeys, handleAttributeClickOutside]);
const onAttributeToggleClick = React.useCallback((ev: React.MouseEvent) => {
const onAttributeToggleClick = useCallback((ev: React.MouseEvent) => {
ev.stopPropagation();
setTimeout(() => {
@ -355,7 +353,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
setIsAttributeMenuOpen((prev) => !prev);
}, []);
const attributeToggle = React.useMemo(
const attributeToggle = useMemo(
() => (
<MenuToggle
ref={attributeToggleRef}
@ -369,7 +367,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[isAttributeMenuOpen, onAttributeToggleClick, activeAttributeMenu],
);
const attributeMenu = React.useMemo(
const attributeMenu = useMemo(
() => (
<Menu
ref={attributeMenuRef}
@ -390,7 +388,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[],
);
const attributeDropdown = React.useMemo(
const attributeDropdown = useMemo(
() => (
<div ref={attributeContainerRef}>
<Popper
@ -406,19 +404,19 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[attributeToggle, attributeMenu, isAttributeMenuOpen],
);
const emptyState = React.useMemo(
const emptyState = useMemo(
() => <CustomEmptyState onClearFilters={clearAllFilters} />,
[clearAllFilters],
);
// Actions
const viewDetailsClick = React.useCallback((workspaceKind: WorkspaceKind) => {
const viewDetailsClick = useCallback((workspaceKind: WorkspaceKind) => {
setSelectedWorkspaceKind(workspaceKind);
setActiveActionType(ActionType.ViewDetails);
}, []);
const workspaceKindsDefaultActions = React.useCallback(
const workspaceKindsDefaultActions = useCallback(
(workspaceKind: WorkspaceKind): IActions => [
{
id: 'view-details',

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
DrawerActions,
DrawerCloseButton,
@ -30,7 +30,7 @@ export const WorkspaceKindDetails: React.FunctionComponent<WorkspaceKindDetailsP
workspaceCountPerKind,
onCloseClick,
}) => {
const [activeTabKey, setActiveTabKey] = React.useState<string | number>(0);
const [activeTabKey, setActiveTabKey] = useState<string | number>(0);
const handleTabClick = (
event: React.MouseEvent | React.KeyboardEvent | MouseEvent,

View File

@ -1,3 +1,4 @@
import React from 'react';
import {
ClipboardCopy,
ClipboardCopyVariant,
@ -11,7 +12,6 @@ import {
Tooltip,
} from '@patternfly/react-core';
import { DatabaseIcon, LockedIcon } from '@patternfly/react-icons';
import * as React from 'react';
import { Workspace } from '~/shared/api/backendApiTypes';
interface DataVolumesListProps {

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
DrawerActions,
DrawerCloseButton,
@ -32,7 +32,7 @@ export const WorkspaceDetails: React.FunctionComponent<WorkspaceDetailsProps> =
// onEditClick,
onDeleteClick,
}) => {
const [activeTabKey, setActiveTabKey] = React.useState<string | number>(0);
const [activeTabKey, setActiveTabKey] = useState<string | number>(0);
const handleTabClick = (
event: React.MouseEvent | React.KeyboardEvent | MouseEvent,

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useState } from 'react';
import {
Dropdown,
DropdownList,
@ -18,7 +18,7 @@ export const WorkspaceDetailsActions: React.FC<WorkspaceDetailsActionsProps> = (
// onEditClick,
onDeleteClick,
}) => {
const [isOpen, setOpen] = React.useState(false);
const [isOpen, setOpen] = useState(false);
return (
<Flex>

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import { ExpandableRowContent, Td, Tr } from '@patternfly/react-table';
import { Workspace } from '~/shared/api/backendApiTypes';
import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList';

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Button,
Content,
@ -10,7 +10,6 @@ import {
ProgressStepper,
Stack,
} from '@patternfly/react-core';
import { useCallback, useMemo, useState } from 'react';
import useGenericObjectState from '~/app/hooks/useGenericObjectState';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { WorkspaceFormImageSelection } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection';
@ -48,13 +47,13 @@ const WorkspaceForm: React.FC = () => {
workspaceName,
});
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [currentStep, setCurrentStep] = useState(WorkspaceFormSteps.KindSelection);
const [data, setData, resetData, replaceData] =
useGenericObjectState<WorkspaceFormData>(initialFormData);
React.useEffect(() => {
useEffect(() => {
if (!initialFormDataLoaded || mode === 'create') {
return;
}

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
CardTitle,
Gallery,
@ -28,7 +28,7 @@ export const WorkspaceFormImageList: React.FunctionComponent<WorkspaceFormImageL
}) => {
const [workspaceImages, setWorkspaceImages] = useState<WorkspaceImageConfigValue[]>(images);
const [filters, setFilters] = useState<FilteredColumn[]>([]);
const filterRef = React.useRef<FilterRef>(null);
const filterRef = useRef<FilterRef>(null);
const filterableColumns = useMemo(
() => ({

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState, useCallback } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Content, Split, SplitItem } from '@patternfly/react-core';
import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails';
import { WorkspaceFormImageList } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageList';
@ -19,7 +19,7 @@ const WorkspaceFormImageSelection: React.FunctionComponent<WorkspaceFormImageSel
}) => {
const [selectedLabels, setSelectedLabels] = useState<Map<string, Set<string>>>(new Map());
const [isExpanded, setIsExpanded] = useState(false);
const drawerRef = React.useRef<HTMLSpanElement>(undefined);
const drawerRef = useRef<HTMLSpanElement>(undefined);
const onExpand = useCallback(() => {
if (drawerRef.current) {

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import {
CardBody,
CardTitle,
@ -25,7 +25,7 @@ export const WorkspaceFormKindList: React.FunctionComponent<WorkspaceFormKindLis
onSelect,
}) => {
const [workspaceKinds, setWorkspaceKinds] = useState<WorkspaceKind[]>(allWorkspaceKinds);
const filterRef = React.useRef<FilterRef>(null);
const filterRef = useRef<FilterRef>(null);
const filterableColumns = useMemo(
() => ({

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useMemo, useCallback } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Content } from '@patternfly/react-core';
import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
CardTitle,
Gallery,
@ -26,7 +26,7 @@ export const WorkspaceFormPodConfigList: React.FunctionComponent<
const [workspacePodConfigs, setWorkspacePodConfigs] =
useState<WorkspacePodConfigValue[]>(podConfigs);
const [filters, setFilters] = useState<FilteredColumn[]>([]);
const filterRef = React.useRef<FilterRef>(null);
const filterRef = useRef<FilterRef>(null);
const filterableColumns = useMemo(
() => ({

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Content, Split, SplitItem } from '@patternfly/react-core';
import { WorkspaceFormPodConfigDetails } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails';
import { WorkspaceFormPodConfigList } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList';
@ -17,7 +17,7 @@ const WorkspaceFormPodConfigSelection: React.FunctionComponent<
> = ({ podConfigs, selectedPodConfig, onSelect }) => {
const [selectedLabels, setSelectedLabels] = useState<Map<string, Set<string>>>(new Map());
const [isExpanded, setIsExpanded] = useState(false);
const drawerRef = React.useRef<HTMLSpanElement>(undefined);
const drawerRef = useRef<HTMLSpanElement>(undefined);
const onExpand = useCallback(() => {
if (drawerRef.current) {

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import { useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
Checkbox,
Content,

View File

@ -1,3 +1,4 @@
import React, { useCallback, useState } from 'react';
import {
Button,
Dropdown,
@ -15,7 +16,6 @@ import {
} from '@patternfly/react-core';
import { EllipsisVIcon } from '@patternfly/react-icons';
import { Table, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import React, { useCallback, useState } from 'react';
import { WorkspacePodVolumeMount } from '~/shared/api/backendApiTypes';
interface WorkspaceFormPropertiesVolumesProps {

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
Dropdown,
DropdownItem,
@ -16,7 +16,7 @@ type WorkspaceConnectActionProps = {
export const WorkspaceConnectAction: React.FunctionComponent<WorkspaceConnectActionProps> = ({
workspace,
}) => {
const [open, setIsOpen] = React.useState(false);
const [open, setIsOpen] = useState(false);
const onToggleClick = () => {
setIsOpen(!open);

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Drawer,
DrawerContent,
@ -33,7 +33,6 @@ import {
QuestionCircleIcon,
CodeIcon,
} from '@patternfly/react-icons';
import { useState } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes';
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
@ -68,7 +67,7 @@ export enum ActionType {
export const Workspaces: React.FunctionComponent = () => {
const navigate = useTypedNavigate();
const createWorkspace = React.useCallback(() => {
const createWorkspace = useCallback(() => {
navigate('workspaceCreate');
}, [navigate]);
@ -83,7 +82,7 @@ export const Workspaces: React.FunctionComponent = () => {
const workspaceRedirectStatus = buildWorkspaceRedirectStatus(workspaceKinds);
// Table columns
const columnNames: WorkspacesColumnNames = React.useMemo(
const columnNames: WorkspacesColumnNames = useMemo(
() => ({
redirectStatus: 'Redirect Status',
name: 'Name',
@ -114,20 +113,20 @@ export const Workspaces: React.FunctionComponent = () => {
const [initialWorkspaces, initialWorkspacesLoaded, , initialWorkspacesRefresh] =
useWorkspaces(selectedNamespace);
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [expandedWorkspacesNames, setExpandedWorkspacesNames] = React.useState<string[]>([]);
const [selectedWorkspace, setSelectedWorkspace] = React.useState<Workspace | null>(null);
const [isActionAlertModalOpen, setIsActionAlertModalOpen] = React.useState(false);
const [activeActionType, setActiveActionType] = React.useState<ActionType | null>(null);
const filterRef = React.useRef<FilterRef>(null);
const [expandedWorkspacesNames, setExpandedWorkspacesNames] = useState<string[]>([]);
const [selectedWorkspace, setSelectedWorkspace] = useState<Workspace | null>(null);
const [isActionAlertModalOpen, setIsActionAlertModalOpen] = useState(false);
const [activeActionType, setActiveActionType] = useState<ActionType | null>(null);
const filterRef = useRef<FilterRef>(null);
React.useEffect(() => {
useEffect(() => {
if (!initialWorkspacesLoaded) {
return;
}
setWorkspaces(initialWorkspaces ?? []);
}, [initialWorkspaces, initialWorkspacesLoaded]);
React.useEffect(() => {
useEffect(() => {
if (activeActionType !== ActionType.Edit || !selectedWorkspace) {
return;
}
@ -139,7 +138,7 @@ export const Workspaces: React.FunctionComponent = () => {
});
}, [activeActionType, navigate, selectedWorkspace]);
const selectWorkspace = React.useCallback(
const selectWorkspace = useCallback(
(newSelectedWorkspace: Workspace | null) => {
if (selectedWorkspace?.name === newSelectedWorkspace?.name) {
setSelectedWorkspace(null);
@ -162,7 +161,7 @@ export const Workspaces: React.FunctionComponent = () => {
expandedWorkspacesNames.includes(workspace.name);
// filter function to pass to the filter component
const onFilter = React.useCallback(
const onFilter = useCallback(
(filters: FilteredColumn[]) => {
// Search name with search value
let filteredWorkspaces = initialWorkspaces ?? [];
@ -212,15 +211,15 @@ export const Workspaces: React.FunctionComponent = () => {
[initialWorkspaces, columnNames],
);
const emptyState = React.useMemo(
const emptyState = useMemo(
() => <CustomEmptyState onClearFilters={() => filterRef.current?.clearAll()} />,
[],
);
// Column sorting
const [activeSortIndex, setActiveSortIndex] = React.useState<number | null>(null);
const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null);
const [activeSortIndex, setActiveSortIndex] = useState<number | null>(null);
const [activeSortDirection, setActiveSortDirection] = useState<'asc' | 'desc' | null>(null);
const getSortableRowValues = (workspace: Workspace): (string | number)[] => {
const { redirectStatus, name, kind, image, podConfig, state, homeVol, cpu, ram, lastActivity } =
@ -274,7 +273,7 @@ export const Workspaces: React.FunctionComponent = () => {
// Actions
const viewDetailsClick = React.useCallback((workspace: Workspace) => {
const viewDetailsClick = useCallback((workspace: Workspace) => {
setSelectedWorkspace(workspace);
setActiveActionType(ActionType.ViewDetails);
}, []);
@ -285,7 +284,7 @@ export const Workspaces: React.FunctionComponent = () => {
// setActiveActionType(ActionType.Edit);
// }, []);
const deleteAction = React.useCallback(async () => {
const deleteAction = useCallback(async () => {
if (!selectedWorkspace) {
return;
}
@ -301,19 +300,19 @@ export const Workspaces: React.FunctionComponent = () => {
}
}, [api, initialWorkspacesRefresh, selectedNamespace, selectedWorkspace]);
const startRestartAction = React.useCallback((workspace: Workspace, action: ActionType) => {
const startRestartAction = useCallback((workspace: Workspace, action: ActionType) => {
setSelectedWorkspace(workspace);
setActiveActionType(action);
setIsActionAlertModalOpen(true);
}, []);
const stopAction = React.useCallback((workspace: Workspace) => {
const stopAction = useCallback((workspace: Workspace) => {
setSelectedWorkspace(workspace);
setActiveActionType(ActionType.Stop);
setIsActionAlertModalOpen(true);
}, []);
const handleDeleteClick = React.useCallback((workspace: Workspace) => {
const handleDeleteClick = useCallback((workspace: Workspace) => {
const buttonElement = document.activeElement as HTMLElement;
buttonElement.blur(); // Remove focus from the currently focused button
setSelectedWorkspace(workspace);
@ -482,8 +481,8 @@ export const Workspaces: React.FunctionComponent = () => {
// Pagination
const [page, setPage] = React.useState(1);
const [perPage, setPerPage] = React.useState(10);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const onSetPage = (
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,

View File

@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import { ExpandableSection, Icon, Tab, Tabs, TabTitleText, Content } from '@patternfly/react-core';
import {
ExclamationCircleIcon,
ExclamationTriangleIcon,
InfoCircleIcon,
} from '@patternfly/react-icons';
import * as React from 'react';
import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName';
import { WorkspaceKind } from '~/shared/api/backendApiTypes';
@ -44,14 +44,14 @@ interface WorkspaceRedirectInformationViewProps {
export const WorkspaceRedirectInformationView: React.FC<WorkspaceRedirectInformationViewProps> = ({
kind,
}) => {
const [activeKey, setActiveKey] = React.useState<string | number>(0);
const [activeKey, setActiveKey] = useState<string | number>(0);
const [workspaceKind, workspaceKindLoaded] = useWorkspaceKindByName(kind);
const [imageConfig, setImageConfig] =
React.useState<WorkspaceKind['podTemplate']['options']['imageConfig']>();
useState<WorkspaceKind['podTemplate']['options']['imageConfig']>();
const [podConfig, setPodConfig] =
React.useState<WorkspaceKind['podTemplate']['options']['podConfig']>();
useState<WorkspaceKind['podTemplate']['options']['podConfig']>();
React.useEffect(() => {
useEffect(() => {
if (!workspaceKindLoaded) {
return;
}

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import {
Button,
Content,

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useCallback, useState } from 'react';
import {
Button,
Modal,
@ -30,9 +30,9 @@ export const WorkspaceStartActionModal: React.FC<StartActionAlertProps> = ({
onUpdateAndStart,
onActionDone,
}) => {
const [actionOnGoing, setActionOnGoing] = React.useState<StartAction | null>(null);
const [actionOnGoing, setActionOnGoing] = useState<StartAction | null>(null);
const executeAction = React.useCallback(
const executeAction = useCallback(
async (args: { action: StartAction; callback: () => Promise<void> }) => {
setActionOnGoing(args.action);
try {
@ -44,7 +44,7 @@ export const WorkspaceStartActionModal: React.FC<StartActionAlertProps> = ({
[],
);
const handleStart = React.useCallback(async () => {
const handleStart = useCallback(async () => {
try {
await executeAction({ action: 'start', callback: onStart });
// TODO: alert user about success
@ -58,7 +58,7 @@ export const WorkspaceStartActionModal: React.FC<StartActionAlertProps> = ({
}, [executeAction, onActionDone, onClose, onStart]);
// TODO: combine handleStart and handleUpdateAndStart if they end up being similar
const handleUpdateAndStart = React.useCallback(async () => {
const handleUpdateAndStart = useCallback(async () => {
try {
await executeAction({ action: 'updateAndStart', callback: onUpdateAndStart });
// TODO: alert user about success
@ -71,7 +71,7 @@ export const WorkspaceStartActionModal: React.FC<StartActionAlertProps> = ({
}
}, [executeAction, onActionDone, onClose, onUpdateAndStart]);
const shouldShowActionButton = React.useCallback(
const shouldShowActionButton = useCallback(
(action: StartAction) => !actionOnGoing || actionOnGoing === action,
[actionOnGoing],
);

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useCallback, useState } from 'react';
import {
Button,
Content,
@ -32,9 +32,9 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
onActionDone,
}) => {
const workspacePendingUpdate = workspace?.pendingRestart;
const [actionOnGoing, setActionOnGoing] = React.useState<StopAction | null>(null);
const [actionOnGoing, setActionOnGoing] = useState<StopAction | null>(null);
const executeAction = React.useCallback(
const executeAction = useCallback(
async (args: { action: StopAction; callback: () => Promise<void> }) => {
setActionOnGoing(args.action);
try {
@ -46,7 +46,7 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
[],
);
const handleStop = React.useCallback(async () => {
const handleStop = useCallback(async () => {
try {
await executeAction({ action: 'stop', callback: onStop });
// TODO: alert user about success
@ -60,7 +60,7 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
}, [executeAction, onActionDone, onClose, onStop]);
// TODO: combine handleStop and handleUpdateAndStop if they end up being similar
const handleUpdateAndStop = React.useCallback(async () => {
const handleUpdateAndStop = useCallback(async () => {
try {
await executeAction({ action: 'updateAndStop', callback: onUpdateAndStop });
// TODO: alert user about success
@ -73,7 +73,7 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
}
}, [executeAction, onActionDone, onClose, onUpdateAndStop]);
const shouldShowActionButton = React.useCallback(
const shouldShowActionButton = useCallback(
(action: StopAction) => !actionOnGoing || actionOnGoing === action,
[actionOnGoing],
);

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import {
Button,

View File

@ -1,17 +1,17 @@
import * as React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { APIState } from '~/shared/api/types';
const useAPIState = <T>(
hostPath: string | null,
createAPI: (path: string) => T,
): [apiState: APIState<T>, refreshAPIState: () => void] => {
const [internalAPIToggleState, setInternalAPIToggleState] = React.useState(false);
const [internalAPIToggleState, setInternalAPIToggleState] = useState(false);
const refreshAPIState = React.useCallback(() => {
const refreshAPIState = useCallback(() => {
setInternalAPIToggleState((v) => !v);
}, []);
const apiState = React.useMemo<APIState<T>>(() => {
const apiState = useMemo<APIState<T>>(() => {
let path = hostPath;
if (!path) {
// TODO: we need to figure out maybe a stopgap or something

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useCallback, useState } from 'react';
import { Button } from '@patternfly/react-core';
type ActionButtonProps = {
@ -13,9 +13,9 @@ export const ActionButton: React.FC<ActionButtonProps> = ({
onClick,
...props
}) => {
const [isLoading, setIsLoading] = React.useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleClick = React.useCallback(async () => {
const handleClick = useCallback(async () => {
setIsLoading(true);
try {
await onClick();

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
import {
EmptyState,
EmptyStateBody,

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
Modal,
ModalBody,
@ -34,7 +34,7 @@ const DeleteModal: React.FC<DeleteModalProps> = ({
onDelete,
}) => {
const [inputValue, setInputValue] = useState('');
const [isDeleting, setIsDeleting] = React.useState(false);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
if (!isOpen) {

View File

@ -1,4 +1,11 @@
import * as React from 'react';
import React, {
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import {
Menu,
MenuContent,
@ -38,19 +45,19 @@ export interface FilterRef {
const Filter = React.forwardRef<FilterRef, FilterProps>(
({ id, onFilter, columnNames, toolbarActions }, ref) => {
Filter.displayName = 'Filter';
const [activeFilter, setActiveFilter] = React.useState<FilteredColumn>({
const [activeFilter, setActiveFilter] = useState<FilteredColumn>({
columnName: Object.values(columnNames)[0],
value: '',
});
const [searchValue, setSearchValue] = React.useState<string>('');
const [isFilterMenuOpen, setIsFilterMenuOpen] = React.useState<boolean>(false);
const [filters, setFilters] = React.useState<FilteredColumn[]>([]);
const [searchValue, setSearchValue] = useState<string>('');
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState<boolean>(false);
const [filters, setFilters] = useState<FilteredColumn[]>([]);
const filterToggleRef = React.useRef<MenuToggleElement | null>(null);
const filterMenuRef = React.useRef<HTMLDivElement | null>(null);
const filterContainerRef = React.useRef<HTMLDivElement | null>(null);
const filterToggleRef = useRef<MenuToggleElement | null>(null);
const filterMenuRef = useRef<HTMLDivElement | null>(null);
const filterContainerRef = useRef<HTMLDivElement | null>(null);
const handleFilterMenuKeys = React.useCallback(
const handleFilterMenuKeys = useCallback(
(event: KeyboardEvent) => {
if (!isFilterMenuOpen) {
return;
@ -68,7 +75,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[isFilterMenuOpen, filterMenuRef, filterToggleRef],
);
const handleClickOutside = React.useCallback(
const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (isFilterMenuOpen && !filterMenuRef.current?.contains(event.target as Node)) {
setIsFilterMenuOpen(false);
@ -77,7 +84,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[isFilterMenuOpen, filterMenuRef],
);
React.useEffect(() => {
useEffect(() => {
window.addEventListener('keydown', handleFilterMenuKeys);
window.addEventListener('click', handleClickOutside);
return () => {
@ -86,7 +93,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
};
}, [isFilterMenuOpen, filterMenuRef, handleFilterMenuKeys, handleClickOutside]);
const onFilterToggleClick = React.useCallback(
const onFilterToggleClick = useCallback(
(ev: React.MouseEvent) => {
ev.stopPropagation(); // Stop handleClickOutside from handling
setTimeout(() => {
@ -100,7 +107,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[isFilterMenuOpen],
);
const updateFilters = React.useCallback(
const updateFilters = useCallback(
(filterObj: FilteredColumn) => {
setFilters((prevFilters) => {
const index = prevFilters.findIndex(
@ -128,7 +135,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[onFilter],
);
const onSearchChange = React.useCallback(
const onSearchChange = useCallback(
(value: string) => {
setSearchValue(value);
setActiveFilter((prevActiveFilter) => {
@ -140,7 +147,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[updateFilters],
);
const onDeleteLabelGroup = React.useCallback(
const onDeleteLabelGroup = useCallback(
(filter: FilteredColumn) => {
setFilters((prevFilters) => {
const newFilters = prevFilters.filter(
@ -161,7 +168,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
);
// Expose the clearAllFilters logic via the ref
const clearAllInternal = React.useCallback(() => {
const clearAllInternal = useCallback(() => {
setFilters([]);
setSearchValue('');
setActiveFilter({
@ -171,11 +178,11 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
onFilter([]);
}, [columnNames, onFilter]);
React.useImperativeHandle(ref, () => ({
useImperativeHandle(ref, () => ({
clearAll: clearAllInternal,
}));
const onFilterSelect = React.useCallback(
const onFilterSelect = useCallback(
(itemId: string | number | undefined) => {
// Use the functional update form to toggle the state
setIsFilterMenuOpen((prevIsMenuOpen) => !prevIsMenuOpen); // Fix is here
@ -195,7 +202,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[columnNames, filters],
);
const filterMenuToggle = React.useMemo(
const filterMenuToggle = useMemo(
() => (
<MenuToggle
ref={filterToggleRef}
@ -209,7 +216,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[activeFilter.columnName, isFilterMenuOpen, onFilterToggleClick],
);
const filterMenu = React.useMemo(
const filterMenu = useMemo(
() => (
<Menu ref={filterMenuRef} onSelect={(_ev, itemId) => onFilterSelect(itemId)}>
<MenuContent>
@ -226,7 +233,7 @@ const Filter = React.forwardRef<FilterRef, FilterProps>(
[columnNames, id, onFilterSelect],
);
const filterDropdown = React.useMemo(
const filterDropdown = useMemo(
() => (
<div ref={filterContainerRef}>
<Popper

View File

@ -1,4 +1,4 @@
import React, { FC, useMemo, useState, useEffect } from 'react';
import React, { FC, useEffect, useMemo, useState } from 'react';
import {
Dropdown,
DropdownItem,

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { APIOptions } from '~/shared/api/types';
/**
@ -120,15 +120,15 @@ const useFetchState = <Type>(
/** Configurable features */
{ refreshRate = 0, initialPromisePurity = false }: Partial<FetchOptions> = {},
): FetchState<Type> => {
const initialDefaultStateRef = React.useRef(initialDefaultState);
const [result, setResult] = React.useState<Type>(initialDefaultState);
const [loaded, setLoaded] = React.useState(false);
const [loadError, setLoadError] = React.useState<Error | undefined>(undefined);
const abortCallbackRef = React.useRef<() => void>(() => undefined);
const changePendingRef = React.useRef(true);
const initialDefaultStateRef = useRef(initialDefaultState);
const [result, setResult] = useState<Type>(initialDefaultState);
const [loaded, setLoaded] = useState(false);
const [loadError, setLoadError] = useState<Error | undefined>(undefined);
const abortCallbackRef = useRef<() => void>(() => undefined);
const changePendingRef = useRef(true);
/** Setup on initial hook a singular reset function. DefaultState & resetDataOnNewPromise are initial render states. */
const cleanupRef = React.useRef(() => {
const cleanupRef = useRef(() => {
if (initialPromisePurity) {
setResult(initialDefaultState);
setLoaded(false);
@ -136,11 +136,11 @@ const useFetchState = <Type>(
}
});
React.useEffect(() => {
useEffect(() => {
cleanupRef.current();
}, [fetchCallbackPromise]);
const call = React.useCallback<() => [Promise<Type | undefined>, () => void]>(() => {
const call = useCallback<() => [Promise<Type | undefined>, () => void]>(() => {
let alreadyAborted = false;
const abortController = new AbortController();
@ -208,13 +208,13 @@ const useFetchState = <Type>(
}, [fetchCallbackPromise]);
// Use a memmo to update the `changePendingRef` immediately on change.
React.useMemo(() => {
useMemo(() => {
changePendingRef.current = true;
// React to changes to the `call` reference.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [call]);
React.useEffect(() => {
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
const callAndSave = () => {
@ -237,10 +237,10 @@ const useFetchState = <Type>(
}, [call, refreshRate]);
// Use a reference for `call` to ensure a stable reference to `refresh` is always returned
const callRef = React.useRef(call);
const callRef = useRef(call);
callRef.current = call;
const refresh = React.useCallback<FetchStateRefreshPromise<Type>>(() => {
const refresh = useCallback<FetchStateRefreshPromise<Type>>(() => {
abortCallbackRef.current();
const [callPromise, unload] = callRef.current();
abortCallbackRef.current = unload;