Skip to content

Dataset Settings: Refactor as React functional components #8719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

hotzenklotz
Copy link
Member

@hotzenklotz hotzenklotz commented Jun 25, 2025

In preparation of restyling this component in a similar fashion as the account and organization settings, I first refactored several datasets settings & tab components as React functional components to use modern hook APIs etc.

I move the beforeUnload into it's own hook. This might be related to issue #5977

URL of deployed dev instance (used for testing):

Issues:


(Please delete unneeded items, merge only when none are left open)

  • Added changelog entry (create a $PR_NUMBER.md file in unreleased_changes or use ./tools/create-changelog-entry.py)
  • Added migration guide entry if applicable (edit the same file as for the changelog)
  • Updated documentation if applicable
  • Adapted wk-libs python client if relevant API parts change
  • Removed dev-only changes like prints and application.conf edits
  • Considered common edge cases
  • Needs datastore update after deployment

@hotzenklotz hotzenklotz self-assigned this Jun 25, 2025
Copy link
Contributor

coderabbitai bot commented Jun 25, 2025

📝 Walkthrough

Walkthrough

This update refactors several dataset settings components in the frontend from class-based or higher-order-component (HOC) patterns to modern React functional components using hooks. It introduces a custom useBeforeUnload hook for unsaved changes, updates message text, and removes Redux and router HOC usage in favor of internal hooks.

Changes

Files/Paths Change Summary
frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx Refactored to use useFetch hook for data, switched from withRouter to useHistory hook.
frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx Refactored to functional component, removed Redux/connect/withRouter, used useWkSelector hook.
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx Major refactor: class → functional component, hooks for state/effects, removed Redux/withRouter HOCs.
frontend/javascripts/dashboard/dataset/team_selection_component.tsx Refactored from class to functional component, state/effects via hooks, renamed props type.
frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts New hook to handle browser/navigation unload events with unsaved changes.
frontend/javascripts/messages.tsx Updated warning message text ("Cancel" → "Ok").
unreleased_changes/8719.md Changelog entry for refactoring dataset settings to functional components.

Possibly related PRs

Suggested labels

enhancement, usability

Poem

In fields of code, the rabbits leap,
From classes old, to hooks so deep.
With useEffect and state anew,
The settings tabs are fresh and true!
A gentle hop, a modern view—
🐇✨ React refactors coming through!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72d4be6 and 90ff565.

📒 Files selected for processing (1)
  • frontend/javascripts/messages.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/messages.tsx
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@hotzenklotz
Copy link
Member Author

@coderabbitai Review!

Copy link
Contributor

coderabbitai bot commented Jun 25, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@hotzenklotz I'll review this PR for you! Let me gather information about the changes first.


🏁 Script executed:

#!/bin/bash
# Get information about the PR and the files that were changed
echo "=== PR Information ==="
gh pr view 8719 --json title,body,headRefName,baseRefName,additions,deletions,changedFiles

echo -e "\n=== Files Changed ==="
gh pr diff 8719 --name-only

echo -e "\n=== PR Diff Summary ==="
gh pr diff 8719 --stat

Length of output: 2349


Let me examine the actual diff content to see the refactoring changes:


🏁 Script executed:

#!/bin/bash
# Get the actual diff content for the main component files
echo "=== Diff for dataset_settings_delete_tab.tsx ==="
gh pr diff 8719 -- frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx

echo -e "\n=== Diff for dataset_settings_sharing_tab.tsx ==="
gh pr diff 8719 -- frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx

echo -e "\n=== Diff for dataset_settings_view.tsx ==="
gh pr diff 8719 -- frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx

Length of output: 704


Let me get the full diff content to examine the refactoring:


🏁 Script executed:

#!/bin/bash
# Get the full PR diff content
echo "=== Full PR Diff ==="
gh pr diff 8719

echo -e "\n=== Changelog Entry ==="
cat unreleased_changes/8719.md 2>/dev/null || echo "Changelog file not found in current directory"

Length of output: 46894


@hotzenklotz I've reviewed this comprehensive refactoring of dataset settings components from class components to functional components. This is excellent modernization work! Here's my detailed analysis:

🎯 Overall Assessment: Excellent Refactoring

This is a well-executed conversion that properly modernizes the React components using current best practices. The code is cleaner, more readable, and follows modern React patterns.

What's Done Well

DatasetSettingsView Component

  • State Management: Properly converted class state to multiple useState hooks with appropriate state separation
  • Lifecycle Conversion: componentDidMountuseEffect with proper dependency arrays
  • Method Optimization: Class methods → useCallback hooks with correct dependencies to prevent unnecessary re-renders
  • Cleanup Handling: Proper cleanup with useRef for unblock callbacks and timeouts

HOC Replacement

  • Redux: connect HOC → useWkSelector hook ✓
  • Router: withRouter HOC → useHistory hook ✓
  • React Query: Context consumption → useQueryClient hook ✓

Custom Hooks Usage

  • useFetch for data fetching in delete tab
  • useEffectOnlyOnce in team selection component
  • useWkSelector for state access

🔍 Potential Areas for Attention

1. useEffect Dependencies

// In DatasetSettingsView - this looks correct but worth double-checking
useEffect(() => {
  fetchData();
  sendAnalyticsEvent("open_dataset_settings", {
    datasetName: dataset ? dataset.name : "Not found dataset",
  });
}, [fetchData]); // dataset dependency removed to avoid infinite loop

The comment indicates dataset dependency was removed to avoid infinite loops. This is handled correctly by accessing dataset inside the fetchData callback.

2. Form Reference Pattern

The component now uses const [form] = Form.useForm() instead of formRef, which is the modern approach. The form is properly passed down to child components.

3. Force Update Replacement

// In handleSubmit - clever solution for force update in functional components
setActiveDataSourceEditMode((prev) => prev); // Force re-render
setTimeout(afterForceUpdateCallback, 0);

This is a creative solution to replace this.forceUpdate(). Consider documenting this pattern more clearly for future maintainers.

💡 Minor Suggestions

1. Consider Extracting Custom Hook

The beforeunload logic in DatasetSettingsView could be extracted into a custom hook:

// Could create useBeforeUnload hook
const useBeforeUnload = (hasUnsavedChanges: boolean, message: string) => {
  // beforeunload logic here
};

2. Memoization Opportunity

Consider memoizing the tabs array in DatasetSettingsView since it's recreated on every render:

const tabs = useMemo(() => [
  // tab definitions
], [activeTabKey, formErrors, dataset, /* other dependencies */]);

📋 Code Quality Highlights

Proper Hook Rules: All hooks are called at the top level and conditionally
Dependency Arrays: Correctly specified for useEffect and useCallback
State Updates: Proper functional updates where needed
Type Safety: TypeScript types maintained throughout
Performance: useCallback used appropriately to prevent unnecessary re-renders

🚀 Technical Debt Reduction

This refactoring significantly reduces technical debt by:

  • Removing deprecated HOC patterns
  • Adopting modern React patterns
  • Improving code readability and maintainability
  • Setting up the codebase for future React features

Changelog

The changelog entry has a small typo: "Reactored" should be "Refactored":

- Reactored dataset settings and child tabs as React functional components.
+ Refactored dataset settings and child tabs as React functional components.

Overall: This is exemplary refactoring work that modernizes the codebase while maintaining functionality. Great job! 🎉

   (\   /)
  (  ._.)
 o_(")(")

The code looks so much cleaner now! This refactoring really showcases modern React best practices.

@hotzenklotz hotzenklotz marked this pull request as ready for review June 26, 2025 11:29
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts (1)

34-37: Consider simplifying the timeout logic.

The current approach temporarily disables the window.onbeforeunload handler and re-enables it after 500ms. This adds complexity and potential race conditions.

Consider a simpler approach that avoids the timeout mechanism:

-          window.onbeforeunload = null;
-          blockTimeoutIdRef.current = window.setTimeout(() => {
-            // @ts-ignore
-            window.onbeforeunload = beforeUnload;
-          }, 500);
+          // Return the message directly without manipulating window.onbeforeunload

The timeout appears to be working around duplicate prompts, but this might be better handled by checking the navigation state or using a flag to track whether a prompt is already showing.

frontend/javascripts/dashboard/dataset/team_selection_component.tsx (1)

81-81: Simplify the disabled prop.

The ternary operator is redundant here.

-      disabled={disabled ? disabled : false}
+      disabled={disabled}
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (2)

375-380: Document the force update workaround for clarity.

The pattern of using setActiveDataSourceEditMode((prev) => prev) with setTimeout to force a re-render is clever but non-obvious. Consider adding a more detailed comment.

    // Need to force update of the SimpleAdvancedDataForm as removing a layer in the advanced tab does not update
    // the form items in the simple tab (only the values are updated). The form items automatically update once
    // the simple tab renders, but this is not the case when the user directly submits the changes.
-   // In functional components, we can trigger a re-render by updating state
-   setActiveDataSourceEditMode((prev) => prev); // Force re-render
+   // In functional components, we can trigger a re-render by updating state.
+   // This setState call with the same value forces React to re-render the component,
+   // which is necessary to update the form items before validation.
+   setActiveDataSourceEditMode((prev) => prev);
    setTimeout(afterForceUpdateCallback, 0);

503-576: Consider memoizing the tabs array for performance.

The tabs array is recreated on every render. Since it contains complex JSX and conditional logic, memoizing it could improve performance.

Add at the top of the component:

import { useMemo } from "react";

Then wrap the tabs array:

- const tabs = [
+ const tabs = useMemo(() => [
    {
      label: <span> Data {formErrors.data ? errorIcon : ""}</span>,
      key: "data",
      // ... rest of tabs
    },
    // ... other tabs
- ];
+ ], [formErrors, errorIcon, activeTabKey, dataset, form, activeDataSourceEditMode, 
+     handleDataSourceEditModeChange, datasetId, isUserAdmin, maybeDataSourceId]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c71944c and 40cfa40.

📒 Files selected for processing (7)
  • frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx (2 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx (1 hunks)
  • frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (10 hunks)
  • frontend/javascripts/dashboard/dataset/team_selection_component.tsx (1 hunks)
  • frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts (1 hunks)
  • frontend/javascripts/messages.tsx (1 hunks)
  • unreleased_changes/8719.md (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: philippotto
PR: scalableminds/webknossos#8542
File: frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx:0-0
Timestamp: 2025-05-30T12:36:09.930Z
Learning: The WebKnossos frontend follows Redux immutability principles where reducers act immutably and create new state objects when changes occur. This makes reference equality checks (like `prevVolumeTracing.segments !== volumeTracing.segments`) reliable and efficient for detecting actual state changes, allowing memoization to work correctly.
frontend/javascripts/messages.tsx (1)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx (1)
Learnt from: philippotto
PR: scalableminds/webknossos#8542
File: frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx:0-0
Timestamp: 2025-05-30T12:36:09.930Z
Learning: The WebKnossos frontend follows Redux immutability principles where reducers act immutably and create new state objects when changes occur. This makes reference equality checks (like `prevVolumeTracing.segments !== volumeTracing.segments`) reliable and efficient for detecting actual state changes, allowing memoization to work correctly.
frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts (1)
Learnt from: knollengewaechs
PR: scalableminds/webknossos#8321
File: frontend/javascripts/components/slider.tsx:0-0
Timestamp: 2025-02-05T20:16:45.712Z
Learning: When tracking focus state that needs to be accessed by native event listeners (e.g., wheel events), use `useRef` instead of `useState` to ensure the event handlers always have access to the current value. This is because event listeners added via `addEventListener` would capture the initial state value if `useState` were used.
frontend/javascripts/dashboard/dataset/team_selection_component.tsx (1)
Learnt from: knollengewaechs
PR: scalableminds/webknossos#8266
File: frontend/javascripts/components/mag_selection.tsx:51-60
Timestamp: 2024-12-16T15:48:37.065Z
Learning: In the `MagSelection` component (`frontend/javascripts/components/mag_selection.tsx`), the selected magnification `value` will always be present in `allMags` because only existing values can be selected by the user.
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (1)
Learnt from: MichaelBuessemeyer
PR: scalableminds/webknossos#8485
File: frontend/javascripts/oxalis/model/accessors/dataset_layer_transformation_accessor.ts:384-392
Timestamp: 2025-04-01T09:45:17.527Z
Learning: The function `isRotationAndMirrorMaybeOnly` in the dataset_layer_transformation_accessor.ts is intentionally designed to allow mirroring transformations (negative scale values). It uses the length comparison (`scale.length() === NON_SCALED_VECTOR.length()`) rather than component equality to permit mirrored axes while ensuring the overall scale magnitude remains the same.
🧬 Code Graph Analysis (1)
frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts (1)
frontend/javascripts/admin/dataset/dataset_upload_view.tsx (1)
  • unblockHistory (269-280)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
🔇 Additional comments (11)
frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts (1)

31-31: Clarify the action === undefined condition.

The condition action === undefined seems unusual since history actions should typically be defined (PUSH, REPLACE, POP). This might indicate an edge case or potential bug.

Run this script to check how the history.block callback is used elsewhere in the codebase:

#!/bin/bash
# Search for other uses of history.block to understand expected action values
rg -A 5 -B 2 'history\.block'

# Also search for beforeUnload patterns
rg -A 10 'beforeUnload.*action.*undefined'
frontend/javascripts/messages.tsx (1)

336-336: LGTM! Improved user experience with clearer button text.

Changing "Cancel" to "Ok" better reflects the action of discarding unsaved changes, making the user interface more intuitive.

unreleased_changes/8719.md (1)

1-2: LGTM! Clear and accurate changelog entry.

The changelog entry accurately summarizes the refactoring work performed in this PR.

frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx (1)

4-4: Excellent refactoring to modern React patterns!

The conversion from class/HOC patterns to functional components with hooks significantly improves code readability and maintainability:

  • useFetch hook simplifies dataset fetching and state management
  • useHistory hook eliminates the need for withRouter HOC
  • Functional component syntax is more concise and easier to understand

Also applies to: 7-8, 15-20

frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx (1)

9-9: Excellent modernization removing HOC dependencies!

The refactoring successfully eliminates HOC patterns in favor of direct hook usage:

  • useWkSelector hook provides direct Redux store access, removing the need for connect HOC
  • Functional component syntax improves readability
  • Core functionality and UI remain unchanged

This aligns well with modern React best practices and the overall modernization effort in this PR.

Also applies to: 25-27

frontend/javascripts/dashboard/dataset/team_selection_component.tsx (3)

10-17: LGTM! Good naming convention for component props.

The rename from Props to TeamSelectionComponentProps follows React TypeScript best practices for more descriptive type names.


27-39: Excellent conversion of lifecycle methods to hooks!

The state initialization and effect hooks are properly implemented:

  • useEffect with [value] dependency correctly syncs state with prop changes
  • useEffectOnlyOnce appropriately replaces componentDidMount for initial data fetch

55-57: Good memoization of getAllTeams function!

Proper use of useCallback with correct dependencies prevents unnecessary re-renders in child components.

frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (3)

65-73: Excellent modernization to functional component with hooks!

Great job replacing HOCs with hooks:

  • useWkSelector for Redux state access
  • useQueryClient for React Query cache management
  • Form.useForm() for modern Ant Design form handling

405-411: Good handling of the infinite loop prevention!

The intentional exclusion of dataset from the dependency array with clear documentation is the correct approach here. The effect correctly accesses the latest dataset value when needed without causing re-execution loops.


65-620: Outstanding refactoring work!

This is an exemplary conversion from class to functional component:

  • ✅ Proper use of all React hooks with correct dependencies
  • ✅ Clean state management with multiple useState calls
  • ✅ Appropriate memoization with useCallback
  • ✅ Modern form handling with Form.useForm()
  • ✅ Excellent integration of custom hooks like useBeforeUnload
  • ✅ Maintains all original functionality while improving code quality

The code is now more maintainable and aligns with modern React best practices.

Comment on lines +35 to +36
// @ts-ignore
window.onbeforeunload = beforeUnload;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove TypeScript suppressions and fix type issues.

The @ts-ignore comments indicate unresolved TypeScript issues that should be addressed rather than suppressed. These suppressions can hide real type safety problems.

For line 36, the issue is likely that beforeUnload expects specific types but window.onbeforeunload has a different signature. Consider creating a properly typed wrapper function:

-          // @ts-ignore
-          window.onbeforeunload = beforeUnload;
+          window.onbeforeunload = (event: BeforeUnloadEvent) => {
+            if (hasUnsavedChanges) {
+              event.preventDefault();
+              return message;
+            }
+          };

For line 45, create a separate function for the window beforeunload handler instead of reusing the history block callback.

Also applies to: 45-46

🤖 Prompt for AI Agents
In frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts around lines
35-36 and 45-46, remove the @ts-ignore comments by addressing the type mismatch
between the beforeUnload function and window.onbeforeunload. Create a properly
typed wrapper function that matches the expected signature of
window.onbeforeunload and assign that wrapper to window.onbeforeunload.
Additionally, for lines 45-46, define a separate function for the window
beforeunload handler instead of reusing the history block callback to ensure
correct typing and separation of concerns.

@hotzenklotz hotzenklotz requested a review from philippotto June 26, 2025 11:55
Copy link
Member

@philippotto philippotto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool stuff 👍 I will do a final test after you addressed the small bit of feedback I left.

import { useCallback, useEffect, useRef } from "react";
import { useHistory } from "react-router-dom";

const useBeforeUnload = (hasUnsavedChanges: boolean, message: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the old code had some comments (which are needed in my opinion because the code is not straightforward):

// Only show the prompt if this is a proper beforeUnload event from the browser
// or the pathname changed
// This check has to be done because history.block triggers this function even if only the url hash changed
...
// clear the event handler otherwise it would be called twice. Once from history.block once from the beforeunload event
...
// restore the event handler in case a user chose to stay on the page

please restore them (or some adapted version of it).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (this.props.onChange) {
this.props.onChange(Array.isArray(selectedTeamIdsOrId) ? selectedTeams : selectedTeams[0]);
const allTeams = getAllTeams();
const selected = _.compact(selectedTeamIds.map((id) => allTeams.find((t) => t.id === id)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find selectedTeams clearer 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

onChange={onSelectTeams}
value={selectedTeams.map((t) => t.id)}
filterOption
disabled={disabled ? disabled : false}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
disabled={disabled ? disabled : false}
disabled={disabled ?? false}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

setPossibleTeams(possibleTeams);
setIsFetchingData(false);
if (afterFetchedTeams) {
afterFetchedTeams(possibleTeams);
}
} catch (_exception) {
console.error("Could not load teams.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's independent of the functional refactoring, but I'd use Toast.error here and setIsFetchingData should also be called with false to avoid an infinite loading state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 89 to 92
const unblockRef = useRef<UnregisterCallback | null>(null);
const blockTimeoutIdRef = useRef<number | null>(null);

const unblockHistory = useCallback(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, this can be deleted completely. the refs are only set to null (and never anything else) and the unblock stuff should happen by the new useBeforeUnload hook once the component is unmounted (which should happen on cancel).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 375 to +380
// Need to force update of the SimpleAdvancedDataForm as removing a layer in the advanced tab does not update
// the form items in the simple tab (only the values are updated). The form items automatically update once
// the simple tab renders, but this is not the case when the user directly submits the changes.
this.forceUpdate(afterForceUpdateCallback);
};

submit = async (formValues: FormData) => {
const { dataset, datasetDefaultConfiguration } = this.state;
const datasetChangeValues = { ...formValues.dataset };

if (datasetChangeValues.sortingKey != null) {
datasetChangeValues.sortingKey = datasetChangeValues.sortingKey.valueOf();
}

const teamIds = formValues.dataset.allowedTeams.map((t) => t.id);
await updateDatasetPartial(this.props.datasetId, datasetChangeValues);

if (datasetDefaultConfiguration != null) {
await updateDatasetDefaultConfiguration(
this.props.datasetId,
_.extend({}, datasetDefaultConfiguration, formValues.defaultConfiguration, {
layers: JSON.parse(formValues.defaultConfigurationLayersJson),
}),
);
}

await updateDatasetTeams(this.props.datasetId, teamIds);
const dataSource = JSON.parse(formValues.dataSourceJson);

if (dataset != null && this.didDatasourceChange(dataSource)) {
if (this.didDatasourceIdChange(dataSource)) {
Toast.warning(messages["dataset.settings.updated_datasource_id_warning"]);
}
await updateDatasetDatasource(dataset.directoryName, dataset.dataStore.url, dataSource);
this.setState({
savedDataSourceOnServer: dataSource,
});
}

const verb = this.props.isEditingMode ? "updated" : "imported";
Toast.success(`Successfully ${verb} ${dataset?.name || this.props.datasetId}.`);
this.setState({
hasUnsavedChanges: false,
// In functional components, we can trigger a re-render by updating state
setActiveDataSourceEditMode((prev) => prev); // Force re-render
setTimeout(afterForceUpdateCallback, 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self to test this.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (1)

357-362: The force update pattern won't trigger a re-render

Setting state to the same value (setActiveDataSourceEditMode((prev) => prev)) doesn't trigger a re-render in React functional components. This won't achieve the intended forced update of SimpleAdvancedDataForm.

Consider using a proper force update pattern:

+  const [updateKey, setUpdateKey] = useState(0);

   const handleSubmit = useCallback(() => {
     // Ensure that all form fields are in sync
     syncDataSourceFields(form, activeDataSourceEditMode === "simple" ? "advanced" : "simple");

     const afterForceUpdateCallback = () => {
       // Trigger validation manually...
       setTimeout(
         () =>
           form
             .validateFields()
             .then((formValues) => submit(formValues))
             .catch((errorInfo) => handleValidationFailed(errorInfo)),
         0,
       );
     };

-    // In functional components, we can trigger a re-render by updating state
-    setActiveDataSourceEditMode((prev) => prev); // Force re-render
+    // Force re-render by incrementing a key
+    setUpdateKey((prev) => prev + 1);
     setTimeout(afterForceUpdateCallback, 0);
-  }, [form, activeDataSourceEditMode, submit, handleValidationFailed]);
+  }, [form, activeDataSourceEditMode, submit, handleValidationFailed]);

Then pass updateKey as a key prop to the component that needs to re-render:

<DatasetSettingsDataTab
-  key="SimpleAdvancedDataForm"
+  key={`SimpleAdvancedDataForm-${updateKey}`}
  dataset={dataset}
  form={form}
  activeDataSourceEditMode={activeDataSourceEditMode}
  onChange={handleDataSourceEditModeChange}
/>
🧹 Nitpick comments (1)
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (1)

480-553: Consider memoizing the tabs array for performance

The tabs array is recreated on every render. Since it contains complex JSX structures and conditional logic, memoizing it would improve performance.

Wrap the tabs array construction in useMemo:

-  const tabs = [
+  const tabs = useMemo(() => {
+    const tabItems = [
     {
       label: <span> Data {formErrors.data ? errorIcon : ""}</span>,
       key: "data",
       // ... rest of tabs
     },
   ];

   if (isUserAdmin && features().allowDeleteDatasets)
-    tabs.push({
+    tabItems.push({
       label: <span> Delete Dataset </span>,
       key: "deleteDataset",
       // ... rest of tab config
     });
+
+    return tabItems;
+  }, [formErrors, activeTabKey, dataset, form, activeDataSourceEditMode, 
+      handleDataSourceEditModeChange, datasetId, maybeDataSourceId, isUserAdmin]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40cfa40 and 72d4be6.

📒 Files selected for processing (3)
  • frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (10 hunks)
  • frontend/javascripts/dashboard/dataset/team_selection_component.tsx (1 hunks)
  • frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts
  • frontend/javascripts/dashboard/dataset/team_selection_component.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: philippotto
PR: scalableminds/webknossos#8542
File: frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx:0-0
Timestamp: 2025-05-30T12:36:09.930Z
Learning: The WebKnossos frontend follows Redux immutability principles where reducers act immutably and create new state objects when changes occur. This makes reference equality checks (like `prevVolumeTracing.segments !== volumeTracing.segments`) reliable and efficient for detecting actual state changes, allowing memoization to work correctly.
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (1)
Learnt from: MichaelBuessemeyer
PR: scalableminds/webknossos#8485
File: frontend/javascripts/oxalis/model/accessors/dataset_layer_transformation_accessor.ts:384-392
Timestamp: 2025-04-01T09:45:17.527Z
Learning: The function `isRotationAndMirrorMaybeOnly` in the dataset_layer_transformation_accessor.ts is intentionally designed to allow mirroring transformations (negative scale values). It uses the length comparison (`scale.length() === NON_SCALED_VECTOR.length()`) rather than component equality to permit mirrored axes while ensuring the overall scale magnitude remains the same.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
🔇 Additional comments (6)
frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx (6)

1-42: LGTM! Clean refactoring of imports

The imports have been properly updated to support the functional component refactoring, including the addition of React hooks and the custom useBeforeUnload hook.


64-86: Excellent conversion to functional component with hooks

The state management has been properly migrated from class state to individual useState hooks, and the form management uses the modern Form.useForm() pattern. The Redux state access via useWkSelector is also the correct approach.


88-184: Well-structured data fetching with proper error handling

The fetchData callback is correctly implemented with:

  • Appropriate dependency array [datasetId, form]
  • Proper loading state management
  • Comprehensive error handling
  • Correct form field initialization sequence

186-265: Validation logic properly migrated to callbacks

All validation and state-checking callbacks are correctly wrapped in useCallback with appropriate dependency arrays. The logic has been preserved from the original implementation while adapting to the functional component pattern.


378-388: Proper lifecycle management with hooks

Good use of the custom useBeforeUnload hook for handling unsaved changes. The useEffect correctly excludes dataset from dependencies with a clear comment explaining the reasoning to prevent infinite loops.


555-597: Excellent modernization of the component

The refactoring from class-based to functional component is comprehensive and well-executed:

  • Clean JSX structure with proper form handling
  • Correct usage of all React hooks
  • Maintained all original functionality
  • Improved code readability and maintainability

Great work on this modernization effort! 🎉

@hotzenklotz
Copy link
Member Author

@philippotto Thanks for starting a review here. A lot of this code has been used and further modified in the full PR for the dataset settings. So, some of these changes might be outdated already and not worth digging to deep. See ##8732

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants