Skip to content

Commit 6036cdc

Browse files
authored
Update to React 18 (#8048)
* update libs to react 18.3 * update reactDom to createRoot * remove antd resolutions in package.json * update antd to version 5.17.4 * updated redux to v4.0.5 * updated react.router-dom to v5.3.4 * replace react-virtualized with react-virtualized-auto-sizer lib * replace react-sortable-hoc with dnd-kit 1/2 * replace react-sortable-hoc with dnd-kit 2/2 * fix typescript errors * updated yarn lock * updated tanstack/query * updated tanstack/query * update flex layout * update react-json-tree * fix remaining typescript errors * mock renderIndenpently in unit tests * formatting * fix unit tests * fix dark mode overwrite for flex layout * changelog * apply PR feedback for TS typing * fix dark mode layer handles * format
1 parent ede0ecc commit 6036cdc

31 files changed

+581
-563
lines changed

CHANGELOG.unreleased.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1717
- Clicking on a bounding box within the bounding box tab centers it within the viewports and focusses it in the list. [#8049](https://github.com/scalableminds/webknossos/pull/8049)
1818
- For self-hosted versions, the text in the data set upload view was updated to recommend switching to webknossos.org. [#7996](https://github.com/scalableminds/webknossos/pull/7996)
1919
- Updated frontend package management to yarn version 4. [8061](https://github.com/scalableminds/webknossos/pull/8061)
20+
- Updated React to version 18. Updated many peer dependencies inlcuding Redux, React-Router, antd, and FlexLayout. [#8048](https://github.com/scalableminds/webknossos/pull/8048)
2021

2122
### Fixed
2223

frontend/javascripts/admin/team/team_list_view.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export function filterTeamMembersOf(team: APITeam, user: APIUser): boolean {
5454
export function renderUsersForTeam(
5555
team: APITeam,
5656
allUsers: APIUser[] | null,
57-
renderAdditionalContent = (_teamMember: APIUser, _team: APITeam) => {},
57+
renderAdditionalContent = (_teamMember: APIUser, _team: APITeam): React.ReactNode => {
58+
return null;
59+
},
5860
) {
5961
if (allUsers === null) return;
6062
const teamMembers = allUsers.filter((user) => filterTeamMembersOf(team, user));

frontend/javascripts/admin/voxelytics/ai_model_list_view.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { JobState } from "admin/job/job_list_view";
1212
import { Link } from "react-router-dom";
1313
import { useGuardedFetch } from "libs/react_helpers";
1414
import { PageNotAvailableToNormalUser } from "components/permission_enforcer";
15+
import type { Key } from "react";
1516

1617
export default function AiModelListView() {
1718
const activeUser = useSelector((state: OxalisState) => state.activeUser);
@@ -63,7 +64,7 @@ export default function AiModelListView() {
6364
value: username,
6465
}),
6566
),
66-
onFilter: (value: string | number | boolean, model: AiModel) =>
67+
onFilter: (value: Key | boolean, model: AiModel) =>
6768
formatUserName(null, model.user).startsWith(String(value)),
6869
filterSearch: true,
6970
},

frontend/javascripts/admin/voxelytics/task_view.tsx

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { JSONTree } from "react-json-tree";
2+
import { JSONTree, type ShouldExpandNodeInitially, type LabelRenderer } from "react-json-tree";
33
import { Progress, Tabs, type TabsProps, Tooltip } from "antd";
44
import Markdown from "libs/markdown_adapter";
55
import {
@@ -14,11 +14,11 @@ import LogTab from "./log_tab";
1414
import StatisticsTab from "./statistics_tab";
1515
import { runStateToStatus, useTheme } from "./utils";
1616
import { formatNumber } from "libs/format_utils";
17-
function labelRenderer(_keyPath: Array<string | number>) {
17+
18+
const labelRenderer: LabelRenderer = function (_keyPath) {
1819
const keyPath = _keyPath.slice().reverse();
19-
const divWithId = <div id={`label-${keyPath.join(".")}`}>{keyPath.slice(-1)[0]}</div>;
20-
return divWithId;
21-
}
20+
return <div id={`label-${keyPath.join(".")}`}>{keyPath.slice(-1)[0]}</div>;
21+
};
2222

2323
function TaskView({
2424
taskName,
@@ -39,9 +39,10 @@ function TaskView({
3939
taskInfo: VoxelyticsTaskInfo;
4040
onSelectTask: (id: string) => void;
4141
}) {
42-
const shouldExpandNode = (_keyPath: Array<string | number>, data: any) =>
42+
const shouldExpandNode: ShouldExpandNodeInitially = function (_keyPath, data) {
4343
// Expand all with at most 10 keys
44-
(data.length || 0) <= 10;
44+
return ((data as any[]).length || 0) <= 10;
45+
};
4546

4647
const ingoingEdges = dag.edges.filter((edge) => edge.target === taskName);
4748
const [theme, invertTheme] = useTheme();
@@ -54,7 +55,7 @@ function TaskView({
5455
<JSONTree
5556
data={task.config}
5657
hideRoot
57-
shouldExpandNode={shouldExpandNode}
58+
shouldExpandNodeInitially={shouldExpandNode}
5859
labelRenderer={labelRenderer}
5960
theme={theme}
6061
invertTheme={invertTheme}

frontend/javascripts/admin/voxelytics/workflow_list_view.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type React from "react";
2-
import { useEffect, useMemo, useState } from "react";
2+
import { type Key, useEffect, useMemo, useState } from "react";
33
import { SyncOutlined } from "@ant-design/icons";
44
import { Table, Progress, Tooltip, Button, Input } from "antd";
55
import { Link } from "react-router-dom";
@@ -211,7 +211,7 @@ export default function WorkflowListView() {
211211
text: username || "",
212212
value: username || "",
213213
})),
214-
onFilter: (value: string | number | boolean, run: RenderRunInfo) =>
214+
onFilter: (value: Key | boolean, run: RenderRunInfo) =>
215215
run.userDisplayName?.startsWith(String(value)) || false,
216216
filterSearch: true,
217217
},
@@ -223,7 +223,7 @@ export default function WorkflowListView() {
223223
text: hostname,
224224
value: hostname,
225225
})),
226-
onFilter: (value: string | number | boolean, run: RenderRunInfo) =>
226+
onFilter: (value: Key | boolean, run: RenderRunInfo) =>
227227
run.hostName.startsWith(String(value)),
228228
filterSearch: true,
229229
},

frontend/javascripts/components/pricing_enforcers.tsx

+14-12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import UpgradePricingPlanModal from "admin/organization/upgrade_plan_modal";
1616
import type { APIOrganization, APIUser } from "types/api_flow_types";
1717
import type { TooltipPlacement } from "antd/lib/tooltip";
1818
import { SwitchSetting } from "oxalis/view/components/setting_input_views";
19+
import type { PopoverProps } from "antd/lib";
1920

2021
const PRIMARY_COLOR_HEX = rgbToHex(PRIMARY_COLOR);
2122

@@ -50,20 +51,21 @@ const useActiveUserAndOrganization = (): [APIUser | null | undefined, APIOrganiz
5051
return [activeUser, activeOrganization];
5152
};
5253

53-
type PopoverEnforcedProps = RequiredPricingProps & {
54-
activeUser: APIUser | null | undefined;
55-
activeOrganization: APIOrganization | null;
56-
placement?: TooltipPlacement;
57-
zIndex?: number;
58-
};
59-
const PricingEnforcedPopover: React.FunctionComponent<PopoverEnforcedProps> = ({
54+
type PopoverEnforcedProps = RequiredPricingProps &
55+
PopoverProps & {
56+
activeUser: APIUser | null | undefined;
57+
activeOrganization: APIOrganization | null;
58+
placement?: TooltipPlacement;
59+
zIndex?: number;
60+
};
61+
const PricingEnforcedPopover = ({
6062
children,
6163
requiredPricingPlan,
6264
activeUser,
6365
activeOrganization,
6466
placement,
6567
zIndex,
66-
}) => {
68+
}: React.PropsWithChildren<PopoverEnforcedProps>) => {
6769
return (
6870
<Popover
6971
color={PRIMARY_COLOR_HEX}
@@ -82,10 +84,10 @@ const PricingEnforcedPopover: React.FunctionComponent<PopoverEnforcedProps> = ({
8284
);
8385
};
8486

85-
export const PricingEnforcedSpan: React.FunctionComponent<RequiredPricingProps> = ({
87+
export const PricingEnforcedSpan = ({
8688
children,
8789
requiredPricingPlan,
88-
}) => {
90+
}: React.PropsWithChildren<RequiredPricingProps>) => {
8991
const [activeUser, activeOrganization] = useActiveUserAndOrganization();
9092
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);
9193

@@ -173,11 +175,11 @@ export const PricingEnforcedSwitchSetting: React.FunctionComponent<
173175
);
174176
};
175177

176-
export const PricingEnforcedBlur: React.FunctionComponent<RequiredPricingProps> = ({
178+
export const PricingEnforcedBlur = ({
177179
children,
178180
requiredPricingPlan,
179181
...restProps
180-
}) => {
182+
}: React.PropsWithChildren<RequiredPricingProps>) => {
181183
const [activeUser, activeOrganization] = useActiveUserAndOrganization();
182184
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);
183185

Original file line numberDiff line numberDiff line change
@@ -1,42 +1,59 @@
11
import { MenuOutlined, InfoCircleOutlined } from "@ant-design/icons";
22
import { List, Collapse, Tooltip, type CollapseProps } from "antd";
3-
import React from "react";
4-
import type { SortEnd } from "react-sortable-hoc";
5-
import { SortableContainer, SortableElement, SortableHandle } from "react-sortable-hoc";
63
import { settings, settingsTooltips } from "messages";
4+
import { DndContext, type DragEndEvent } from "@dnd-kit/core";
5+
import { CSS } from "@dnd-kit/utilities";
6+
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
77

8-
// Example taken and modified from https://4x.ant.design/components/table/#components-table-demo-drag-sorting-handler.
8+
// Example taken and modified from https://ant.design/components/table/#components-table-demo-drag-sorting-handler.
99

10-
const DragHandle = SortableHandle(() => <MenuOutlined style={{ cursor: "grab", color: "#999" }} />);
10+
function SortableListItem({ colorLayerName }: { colorLayerName: string }) {
11+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
12+
id: colorLayerName,
13+
});
1114

12-
const SortableItem = SortableElement(({ name }: { name: string }) => (
13-
<List.Item key={name}>
14-
<DragHandle /> {name}
15-
</List.Item>
16-
));
15+
const style = {
16+
transform: CSS.Transform.toString(transform),
17+
transition,
18+
zIndex: isDragging ? "100" : "auto",
19+
opacity: isDragging ? 0.3 : 1,
20+
};
1721

18-
const SortableLayerSettingsContainer = SortableContainer(({ children }: { children: any }) => {
19-
return <div style={{ paddingTop: -16, paddingBottom: -16 }}>{children}</div>;
20-
});
22+
return (
23+
<List.Item id={colorLayerName} ref={setNodeRef} style={style}>
24+
<MenuOutlined style={{ cursor: "grab", color: "#999" }} {...listeners} {...attributes} />{" "}
25+
{colorLayerName}
26+
</List.Item>
27+
);
28+
}
2129

2230
export default function ColorLayerOrderingTable({
2331
colorLayerNames,
2432
onChange,
2533
}: {
2634
colorLayerNames?: string[];
2735
onChange?: (newColorLayerNames: string[]) => void;
28-
}): JSX.Element {
29-
const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
30-
document.body.classList.remove("is-dragging");
31-
if (oldIndex !== newIndex && onChange && colorLayerNames) {
32-
const movedElement = colorLayerNames[oldIndex];
33-
const newColorLayerNames = colorLayerNames.filter((_, index) => index !== oldIndex);
34-
newColorLayerNames.splice(newIndex, 0, movedElement);
35-
onChange(newColorLayerNames);
36+
}) {
37+
const onSortEnd = (event: DragEndEvent) => {
38+
const { active, over } = event;
39+
40+
if (active && over && colorLayerNames) {
41+
const oldIndex = colorLayerNames.indexOf(active.id as string);
42+
const newIndex = colorLayerNames.indexOf(over.id as string);
43+
44+
document.body.classList.remove("is-dragging");
45+
46+
if (oldIndex !== newIndex && onChange) {
47+
const movedElement = colorLayerNames[oldIndex];
48+
const newColorLayerNames = colorLayerNames.filter((_, index) => index !== oldIndex);
49+
newColorLayerNames.splice(newIndex, 0, movedElement);
50+
onChange(newColorLayerNames);
51+
}
3652
}
3753
};
3854

3955
const isSettingEnabled = colorLayerNames && colorLayerNames.length > 1;
56+
const sortingItems = isSettingEnabled ? colorLayerNames.map((name) => name) : [];
4057
const collapsibleDisabledExplanation =
4158
"The order of layers can only be configured when the dataset has multiple color layers.";
4259

@@ -55,29 +72,25 @@ export default function ColorLayerOrderingTable({
5572
{
5673
label: panelTitle,
5774
key: "1",
58-
children: (
59-
<SortableLayerSettingsContainer
60-
onSortEnd={onSortEnd}
61-
onSortStart={() =>
62-
colorLayerNames &&
63-
colorLayerNames.length > 1 &&
64-
document.body.classList.add("is-dragging")
65-
}
66-
useDragHandle
67-
>
68-
{colorLayerNames?.map((name, index) => (
69-
<SortableItem key={name} index={index} name={name} />
70-
))}
71-
</SortableLayerSettingsContainer>
72-
),
75+
children: sortingItems.map((name) => <SortableListItem key={name} colorLayerName={name} />),
7376
},
7477
];
7578

7679
return (
77-
<Collapse
78-
defaultActiveKey={[]}
79-
collapsible={isSettingEnabled ? "header" : "disabled"}
80-
items={collapseItems}
81-
/>
80+
<DndContext
81+
autoScroll={false}
82+
onDragStart={() => {
83+
colorLayerNames && colorLayerNames.length > 1 && document.body.classList.add("is-dragging");
84+
}}
85+
onDragEnd={onSortEnd}
86+
>
87+
<SortableContext items={sortingItems} strategy={verticalListSortingStrategy}>
88+
<Collapse
89+
defaultActiveKey={[]}
90+
collapsible={isSettingEnabled ? "header" : "disabled"}
91+
items={collapseItems}
92+
/>
93+
</SortableContext>
94+
</DndContext>
8295
);
8396
}

frontend/javascripts/dashboard/explorative_annotations_view.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ class ExplorativeAnnotationsView extends React.PureComponent<Props, State> {
679679
width: 300,
680680
filters: ownerAndTeamsFilters,
681681
filterMode: "tree",
682-
onFilter: (value: string | number | boolean, tracing: APIAnnotationInfo) =>
682+
onFilter: (value: React.Key | boolean, tracing: APIAnnotationInfo) =>
683683
(tracing.owner != null && tracing.owner.id === value.toString()) ||
684684
tracing.teams.some((team) => team.id === value),
685685
sorter: Utils.localeCompareBy((annotation) => annotation.owner?.firstName || ""),

frontend/javascripts/dashboard/folders/folder_tree.tsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import {
1010
import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons";
1111
import { Dropdown, Modal, type MenuProps, Tree } from "antd";
1212
import Toast from "libs/toast";
13-
import type { DataNode, DirectoryTreeProps } from "antd/lib/tree";
13+
import type { AntTreeNodeSelectedEvent, DataNode, DirectoryTreeProps } from "antd/lib/tree";
1414
import memoizeOne from "memoize-one";
1515
import classNames from "classnames";
1616
import type { FolderItem } from "types/api_flow_types";
1717
import { PricingEnforcedSpan } from "components/pricing_enforcers";
1818
import { PricingPlanEnum } from "admin/organization/pricing_plan_utils";
19+
import { AntTreeNodeBaseEvent } from "antd/es/tree/Tree";
1920

2021
const { DirectoryTree } = Tree;
2122

@@ -78,18 +79,20 @@ export function FolderTreeSidebar({
7879
});
7980

8081
const onSelect: DirectoryTreeProps["onSelect"] = useCallback(
81-
(keys, event) => {
82+
(keys: React.Key[], { nativeEvent }: { nativeEvent: MouseEvent }) => {
8283
// Without the following check, the onSelect callback would also be called by antd
8384
// when the user clicks on a menu entry in the context menu (e.g., deleting a folder
8485
// would directly select it afterwards).
8586
// Since the context menu is inserted at the root of the DOM, it's not a child node of
8687
// the ant-tree container. Therefore, we can use this property to filter out those
8788
// click events.
8889
// The classic preventDefault() didn't work as an alternative workaround.
89-
const doesEventReferToTreeUi = event.nativeEvent.target.closest(".ant-tree") != null;
90-
if (keys.length > 0 && doesEventReferToTreeUi) {
91-
context.setActiveFolderId(keys[0] as string);
92-
context.setSelectedDatasets([]);
90+
if (nativeEvent.target && nativeEvent.target instanceof HTMLElement) {
91+
const doesEventReferToTreeUi = nativeEvent.target.closest(".ant-tree") != null;
92+
if (keys.length > 0 && doesEventReferToTreeUi) {
93+
context.setActiveFolderId(keys[0] as string);
94+
context.setSelectedDatasets([]);
95+
}
9396
}
9497
},
9598
[context],

frontend/javascripts/libs/react_helpers.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ export function useGuardedFetch<T>(
9797
updates.
9898
*/
9999
export function usePolledState(callback: (arg0: OxalisState) => void, interval: number = 1000) {
100-
const store = useStore();
101-
const oldState = useRef(null);
100+
const store = useStore<OxalisState>();
101+
const oldState = useRef<OxalisState | null>(null);
102102
useInterval(() => {
103103
const state = store.getState();
104104

frontend/javascripts/libs/render_independently.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
33
import { document } from "libs/window";
44
import { Provider } from "react-redux";
55
import GlobalThemeProvider from "theme";
6+
import { createRoot } from "react-dom/client";
67

78
type DestroyFunction = () => void; // The returned promise gets resolved once the element is destroyed.
89

@@ -14,6 +15,7 @@ export default function renderIndependently(
1415
import("oxalis/throttled_store").then((_Store) => {
1516
const Store = _Store.default;
1617
const div = document.createElement("div");
18+
const react_root = createRoot(div);
1719

1820
if (!document.body) {
1921
resolve();
@@ -23,21 +25,20 @@ export default function renderIndependently(
2325
document.body.appendChild(div);
2426

2527
function destroy() {
26-
const unmountResult = ReactDOM.unmountComponentAtNode(div);
28+
react_root.unmount();
2729

28-
if (unmountResult && div.parentNode) {
30+
if (div.parentNode) {
2931
div.parentNode.removeChild(div);
3032
}
3133

3234
resolve();
3335
}
3436

35-
ReactDOM.render(
37+
react_root.render(
3638
// @ts-ignore
3739
<Provider store={Store}>
3840
<GlobalThemeProvider isMainProvider={false}>{getComponent(destroy)}</GlobalThemeProvider>
3941
</Provider>,
40-
div,
4142
);
4243
});
4344
});

0 commit comments

Comments
 (0)