Skip to content

Commit

Permalink
Update to React 18 (#8048)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hotzenklotz authored Sep 11, 2024
1 parent ede0ecc commit 6036cdc
Show file tree
Hide file tree
Showing 31 changed files with 581 additions and 563 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- 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)
- 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)
- Updated frontend package management to yarn version 4. [8061](https://github.com/scalableminds/webknossos/pull/8061)
- Updated React to version 18. Updated many peer dependencies inlcuding Redux, React-Router, antd, and FlexLayout. [#8048](https://github.com/scalableminds/webknossos/pull/8048)

### Fixed

Expand Down
4 changes: 3 additions & 1 deletion frontend/javascripts/admin/team/team_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ export function filterTeamMembersOf(team: APITeam, user: APIUser): boolean {
export function renderUsersForTeam(
team: APITeam,
allUsers: APIUser[] | null,
renderAdditionalContent = (_teamMember: APIUser, _team: APITeam) => {},
renderAdditionalContent = (_teamMember: APIUser, _team: APITeam): React.ReactNode => {
return null;
},
) {
if (allUsers === null) return;
const teamMembers = allUsers.filter((user) => filterTeamMembersOf(team, user));
Expand Down
3 changes: 2 additions & 1 deletion frontend/javascripts/admin/voxelytics/ai_model_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { JobState } from "admin/job/job_list_view";
import { Link } from "react-router-dom";
import { useGuardedFetch } from "libs/react_helpers";
import { PageNotAvailableToNormalUser } from "components/permission_enforcer";
import type { Key } from "react";

export default function AiModelListView() {
const activeUser = useSelector((state: OxalisState) => state.activeUser);
Expand Down Expand Up @@ -63,7 +64,7 @@ export default function AiModelListView() {
value: username,
}),
),
onFilter: (value: string | number | boolean, model: AiModel) =>
onFilter: (value: Key | boolean, model: AiModel) =>
formatUserName(null, model.user).startsWith(String(value)),
filterSearch: true,
},
Expand Down
17 changes: 9 additions & 8 deletions frontend/javascripts/admin/voxelytics/task_view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { JSONTree } from "react-json-tree";
import { JSONTree, type ShouldExpandNodeInitially, type LabelRenderer } from "react-json-tree";
import { Progress, Tabs, type TabsProps, Tooltip } from "antd";
import Markdown from "libs/markdown_adapter";
import {
Expand All @@ -14,11 +14,11 @@ import LogTab from "./log_tab";
import StatisticsTab from "./statistics_tab";
import { runStateToStatus, useTheme } from "./utils";
import { formatNumber } from "libs/format_utils";
function labelRenderer(_keyPath: Array<string | number>) {

const labelRenderer: LabelRenderer = function (_keyPath) {
const keyPath = _keyPath.slice().reverse();
const divWithId = <div id={`label-${keyPath.join(".")}`}>{keyPath.slice(-1)[0]}</div>;
return divWithId;
}
return <div id={`label-${keyPath.join(".")}`}>{keyPath.slice(-1)[0]}</div>;
};

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

const ingoingEdges = dag.edges.filter((edge) => edge.target === taskName);
const [theme, invertTheme] = useTheme();
Expand All @@ -54,7 +55,7 @@ function TaskView({
<JSONTree
data={task.config}
hideRoot
shouldExpandNode={shouldExpandNode}
shouldExpandNodeInitially={shouldExpandNode}
labelRenderer={labelRenderer}
theme={theme}
invertTheme={invertTheme}
Expand Down
6 changes: 3 additions & 3 deletions frontend/javascripts/admin/voxelytics/workflow_list_view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type React from "react";
import { useEffect, useMemo, useState } from "react";
import { type Key, useEffect, useMemo, useState } from "react";
import { SyncOutlined } from "@ant-design/icons";
import { Table, Progress, Tooltip, Button, Input } from "antd";
import { Link } from "react-router-dom";
Expand Down Expand Up @@ -211,7 +211,7 @@ export default function WorkflowListView() {
text: username || "",
value: username || "",
})),
onFilter: (value: string | number | boolean, run: RenderRunInfo) =>
onFilter: (value: Key | boolean, run: RenderRunInfo) =>
run.userDisplayName?.startsWith(String(value)) || false,
filterSearch: true,
},
Expand All @@ -223,7 +223,7 @@ export default function WorkflowListView() {
text: hostname,
value: hostname,
})),
onFilter: (value: string | number | boolean, run: RenderRunInfo) =>
onFilter: (value: Key | boolean, run: RenderRunInfo) =>
run.hostName.startsWith(String(value)),
filterSearch: true,
},
Expand Down
26 changes: 14 additions & 12 deletions frontend/javascripts/components/pricing_enforcers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import UpgradePricingPlanModal from "admin/organization/upgrade_plan_modal";
import type { APIOrganization, APIUser } from "types/api_flow_types";
import type { TooltipPlacement } from "antd/lib/tooltip";
import { SwitchSetting } from "oxalis/view/components/setting_input_views";
import type { PopoverProps } from "antd/lib";

const PRIMARY_COLOR_HEX = rgbToHex(PRIMARY_COLOR);

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

type PopoverEnforcedProps = RequiredPricingProps & {
activeUser: APIUser | null | undefined;
activeOrganization: APIOrganization | null;
placement?: TooltipPlacement;
zIndex?: number;
};
const PricingEnforcedPopover: React.FunctionComponent<PopoverEnforcedProps> = ({
type PopoverEnforcedProps = RequiredPricingProps &
PopoverProps & {
activeUser: APIUser | null | undefined;
activeOrganization: APIOrganization | null;
placement?: TooltipPlacement;
zIndex?: number;
};
const PricingEnforcedPopover = ({
children,
requiredPricingPlan,
activeUser,
activeOrganization,
placement,
zIndex,
}) => {
}: React.PropsWithChildren<PopoverEnforcedProps>) => {
return (
<Popover
color={PRIMARY_COLOR_HEX}
Expand All @@ -82,10 +84,10 @@ const PricingEnforcedPopover: React.FunctionComponent<PopoverEnforcedProps> = ({
);
};

export const PricingEnforcedSpan: React.FunctionComponent<RequiredPricingProps> = ({
export const PricingEnforcedSpan = ({
children,
requiredPricingPlan,
}) => {
}: React.PropsWithChildren<RequiredPricingProps>) => {
const [activeUser, activeOrganization] = useActiveUserAndOrganization();
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);

Expand Down Expand Up @@ -173,11 +175,11 @@ export const PricingEnforcedSwitchSetting: React.FunctionComponent<
);
};

export const PricingEnforcedBlur: React.FunctionComponent<RequiredPricingProps> = ({
export const PricingEnforcedBlur = ({
children,
requiredPricingPlan,
...restProps
}) => {
}: React.PropsWithChildren<RequiredPricingProps>) => {
const [activeUser, activeOrganization] = useActiveUserAndOrganization();
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,59 @@
import { MenuOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { List, Collapse, Tooltip, type CollapseProps } from "antd";
import React from "react";
import type { SortEnd } from "react-sortable-hoc";
import { SortableContainer, SortableElement, SortableHandle } from "react-sortable-hoc";
import { settings, settingsTooltips } from "messages";
import { DndContext, type DragEndEvent } from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";

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

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

const SortableItem = SortableElement(({ name }: { name: string }) => (
<List.Item key={name}>
<DragHandle /> {name}
</List.Item>
));
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? "100" : "auto",
opacity: isDragging ? 0.3 : 1,
};

const SortableLayerSettingsContainer = SortableContainer(({ children }: { children: any }) => {
return <div style={{ paddingTop: -16, paddingBottom: -16 }}>{children}</div>;
});
return (
<List.Item id={colorLayerName} ref={setNodeRef} style={style}>
<MenuOutlined style={{ cursor: "grab", color: "#999" }} {...listeners} {...attributes} />{" "}
{colorLayerName}
</List.Item>
);
}

export default function ColorLayerOrderingTable({
colorLayerNames,
onChange,
}: {
colorLayerNames?: string[];
onChange?: (newColorLayerNames: string[]) => void;
}): JSX.Element {
const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
document.body.classList.remove("is-dragging");
if (oldIndex !== newIndex && onChange && colorLayerNames) {
const movedElement = colorLayerNames[oldIndex];
const newColorLayerNames = colorLayerNames.filter((_, index) => index !== oldIndex);
newColorLayerNames.splice(newIndex, 0, movedElement);
onChange(newColorLayerNames);
}) {
const onSortEnd = (event: DragEndEvent) => {
const { active, over } = event;

if (active && over && colorLayerNames) {
const oldIndex = colorLayerNames.indexOf(active.id as string);
const newIndex = colorLayerNames.indexOf(over.id as string);

document.body.classList.remove("is-dragging");

if (oldIndex !== newIndex && onChange) {
const movedElement = colorLayerNames[oldIndex];
const newColorLayerNames = colorLayerNames.filter((_, index) => index !== oldIndex);
newColorLayerNames.splice(newIndex, 0, movedElement);
onChange(newColorLayerNames);
}
}
};

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

Expand All @@ -55,29 +72,25 @@ export default function ColorLayerOrderingTable({
{
label: panelTitle,
key: "1",
children: (
<SortableLayerSettingsContainer
onSortEnd={onSortEnd}
onSortStart={() =>
colorLayerNames &&
colorLayerNames.length > 1 &&
document.body.classList.add("is-dragging")
}
useDragHandle
>
{colorLayerNames?.map((name, index) => (
<SortableItem key={name} index={index} name={name} />
))}
</SortableLayerSettingsContainer>
),
children: sortingItems.map((name) => <SortableListItem key={name} colorLayerName={name} />),
},
];

return (
<Collapse
defaultActiveKey={[]}
collapsible={isSettingEnabled ? "header" : "disabled"}
items={collapseItems}
/>
<DndContext
autoScroll={false}
onDragStart={() => {
colorLayerNames && colorLayerNames.length > 1 && document.body.classList.add("is-dragging");
}}
onDragEnd={onSortEnd}
>
<SortableContext items={sortingItems} strategy={verticalListSortingStrategy}>
<Collapse
defaultActiveKey={[]}
collapsible={isSettingEnabled ? "header" : "disabled"}
items={collapseItems}
/>
</SortableContext>
</DndContext>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ class ExplorativeAnnotationsView extends React.PureComponent<Props, State> {
width: 300,
filters: ownerAndTeamsFilters,
filterMode: "tree",
onFilter: (value: string | number | boolean, tracing: APIAnnotationInfo) =>
onFilter: (value: React.Key | boolean, tracing: APIAnnotationInfo) =>
(tracing.owner != null && tracing.owner.id === value.toString()) ||
tracing.teams.some((team) => team.id === value),
sorter: Utils.localeCompareBy((annotation) => annotation.owner?.firstName || ""),
Expand Down
15 changes: 9 additions & 6 deletions frontend/javascripts/dashboard/folders/folder_tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import {
import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons";
import { Dropdown, Modal, type MenuProps, Tree } from "antd";
import Toast from "libs/toast";
import type { DataNode, DirectoryTreeProps } from "antd/lib/tree";
import type { AntTreeNodeSelectedEvent, DataNode, DirectoryTreeProps } from "antd/lib/tree";
import memoizeOne from "memoize-one";
import classNames from "classnames";
import type { FolderItem } from "types/api_flow_types";
import { PricingEnforcedSpan } from "components/pricing_enforcers";
import { PricingPlanEnum } from "admin/organization/pricing_plan_utils";
import { AntTreeNodeBaseEvent } from "antd/es/tree/Tree";

const { DirectoryTree } = Tree;

Expand Down Expand Up @@ -78,18 +79,20 @@ export function FolderTreeSidebar({
});

const onSelect: DirectoryTreeProps["onSelect"] = useCallback(
(keys, event) => {
(keys: React.Key[], { nativeEvent }: { nativeEvent: MouseEvent }) => {
// Without the following check, the onSelect callback would also be called by antd
// when the user clicks on a menu entry in the context menu (e.g., deleting a folder
// would directly select it afterwards).
// Since the context menu is inserted at the root of the DOM, it's not a child node of
// the ant-tree container. Therefore, we can use this property to filter out those
// click events.
// The classic preventDefault() didn't work as an alternative workaround.
const doesEventReferToTreeUi = event.nativeEvent.target.closest(".ant-tree") != null;
if (keys.length > 0 && doesEventReferToTreeUi) {
context.setActiveFolderId(keys[0] as string);
context.setSelectedDatasets([]);
if (nativeEvent.target && nativeEvent.target instanceof HTMLElement) {
const doesEventReferToTreeUi = nativeEvent.target.closest(".ant-tree") != null;
if (keys.length > 0 && doesEventReferToTreeUi) {
context.setActiveFolderId(keys[0] as string);
context.setSelectedDatasets([]);
}
}
},
[context],
Expand Down
4 changes: 2 additions & 2 deletions frontend/javascripts/libs/react_helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ export function useGuardedFetch<T>(
updates.
*/
export function usePolledState(callback: (arg0: OxalisState) => void, interval: number = 1000) {
const store = useStore();
const oldState = useRef(null);
const store = useStore<OxalisState>();
const oldState = useRef<OxalisState | null>(null);
useInterval(() => {
const state = store.getState();

Expand Down
9 changes: 5 additions & 4 deletions frontend/javascripts/libs/render_independently.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
import { document } from "libs/window";
import { Provider } from "react-redux";
import GlobalThemeProvider from "theme";
import { createRoot } from "react-dom/client";

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

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

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

function destroy() {
const unmountResult = ReactDOM.unmountComponentAtNode(div);
react_root.unmount();

if (unmountResult && div.parentNode) {
if (div.parentNode) {
div.parentNode.removeChild(div);
}

resolve();
}

ReactDOM.render(
react_root.render(
// @ts-ignore
<Provider store={Store}>
<GlobalThemeProvider isMainProvider={false}>{getComponent(destroy)}</GlobalThemeProvider>
</Provider>,
div,
);
});
});
Expand Down
Loading

0 comments on commit 6036cdc

Please sign in to comment.