-
Notifications
You must be signed in to change notification settings - Fork 18
feat: userSettings for package #3235
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
Conversation
|
@astandrik I've opened a new pull request, #3237, to work on those changes. Once the pull request is ready, I'll request review from you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional Comments (1)
-
src/components/SplitPane/SplitPane.tsx, line 75-91 (link)logic: Missing dependencies in
useEffecthooks cause stale closure issues. Both effects callsetDefaultSizePanewhich usessaveSizesStringDebounced, but neither includes it in their dependency arrays. This violates React hooks rules and can cause unexpected behavior.
22 files reviewed, 6 comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces remote user settings support for package mode by adding a settingsBackend configuration to the UIFactory. The changes migrate from direct localStorage access to a Redux-based settings management system that can optionally sync with a remote settings service.
Key changes:
- Adds
settingsBackendconfiguration to UIFactory withgetEndpoint()andgetUserId()methods for remote settings - Refactors settings management to use Redux store as source of truth with optional localStorage and remote sync
- Introduces debounced remote writes with retry logic and optimistic updates
- Adds
SettingsBootstrapcomponent to preload settings on app startup
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/hooks/useSetting.ts | Refactored to use new store-based useSetting hook instead of directly dispatching to Redux |
| src/utils/hooks/useSelectedColumns.ts | Migrated from settingsManager to useSetting hook for column preferences |
| src/uiFactory/uiFactory.ts | Added settingsBackend property to base factory configuration |
| src/uiFactory/types.ts | Added settingsBackend interface with endpoint and user ID getters |
| src/types/api/settings.ts | Simplified API types to support new JSON-string-based settings contract |
| src/store/reducers/settings/utils.ts | Added shouldSyncSettingToLS helper and modified stringifySettingValue to handle undefined |
| src/store/reducers/settings/useSetting.ts | Completely refactored to support remote settings with fallback to localStorage |
| src/store/reducers/settings/settings.ts | Changed initial state to empty object, added conditional localStorage sync |
| src/store/reducers/settings/effects.ts | New file with handlers for remote setting results and optimistic writes |
| src/store/reducers/settings/api.ts | Major refactor with debounced remote writes, retry logic, and dual client support |
| src/store/reducers/cluster/cluster.ts | Removed localStorage access from cluster tab initialization |
| src/store/configureStore.ts | Added metaSettingsBaseUrl configuration from UIFactory |
| src/services/api/metaSettings.ts | Added baseUrl override support for custom settings endpoints |
| src/services/api/index.ts | Added settingsService API instance for custom backend |
| src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx | Removed localStorage writes, parameter now unused |
| src/containers/Tenant/Tenant.tsx | Migrated pane collapse state to useSetting hook |
| src/containers/Tenant/ObjectSummary/ObjectSummary.tsx | Migrated pane collapse state to useSetting hook |
| src/containers/Cluster/Cluster.tsx | Migrated default cluster tab to useSetting hook with validation |
| src/containers/App/SettingsBootstrap.tsx | New component to preload settings when remote backend is available |
| src/containers/App/Content.tsx | Wrapped children in SettingsBootstrap component |
| src/components/SplitPane/SplitPane.tsx | Migrated pane sizes to useSetting hook with debounced saves |
| src/components/Drawer/Drawer.tsx | Migrated drawer width to useSetting hook with debounced saves |
|
Skipping Bugbot: Bugbot is disabled for this repository |
|
@greptile review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
22 files reviewed, 7 comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 12 comments.
Comments suppressed due to low confidence (2)
src/containers/Tenant/ObjectSummary/ObjectSummary.tsx:81
- Superfluous argument passed to function paneVisibilityToggleReducerCreator.
paneVisibilityToggleReducerCreator(DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED),
src/containers/Tenant/Tenant.tsx:47
- Superfluous argument passed to function paneVisibilityToggleReducerCreator.
paneVisibilityToggleReducerCreator(DEFAULT_IS_TENANT_SUMMARY_COLLAPSED),
| return { | ||
| ...state, | ||
| userSettings: { | ||
| ...state.userSettings, | ||
| [action.payload.name]: action.payload.value, | ||
| }, | ||
| }; |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reducer is manually returning a new state object with spread operators to maintain immutability. However, Redux Toolkit's create.reducer uses Immer internally, which allows direct mutation of the draft state. You can simplify this to state.userSettings[action.payload.name] = action.payload.value; without the return statement, which is more idiomatic for RTK and potentially more performant.
| const status = data?.status; | ||
|
|
||
| if (status && status !== 'SUCCESS') { | ||
| throw new Error('Cannot set setting - status is not SUCCESS'); | ||
| } |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the status is not 'SUCCESS', an error is thrown with a generic message. However, the actual status value (which might provide useful debugging information) is not included in the error message. Consider including the actual status in the error: new Error(\Cannot set setting - status is ${status}, expected SUCCESS`)` to aid debugging.
| false, | ||
| ); | ||
| const [commonInfoVisibilityState, dispatchCommonInfoVisibilityState] = React.useReducer( | ||
| paneVisibilityToggleReducerCreator(DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED), |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function paneVisibilityToggleReducerCreator is called with an argument DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED, but the updated function signature no longer accepts any parameters. The parameter was removed in the refactoring, but the call site was not updated.
| false, | ||
| ); | ||
| const [summaryVisibilityState, dispatchSummaryVisibilityAction] = React.useReducer( | ||
| paneVisibilityToggleReducerCreator(DEFAULT_IS_TENANT_SUMMARY_COLLAPSED), |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function paneVisibilityToggleReducerCreator is called with an argument DEFAULT_IS_TENANT_SUMMARY_COLLAPSED, but the updated function signature no longer accepts any parameters. The parameter was removed in the refactoring, but the call site was not updated.
| function joinBaseUrlAndPath(baseUrl: string, path: string) { | ||
| const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; | ||
| const normalizedPath = path.startsWith('/') ? path : `/${path}`; | ||
| return `${normalizedBaseUrl}${normalizedPath}`; | ||
| } |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The joinBaseUrlAndPath helper is duplicating logic that likely exists elsewhere in the codebase. Consider extracting this to a shared utility module to avoid code duplication and ensure consistent URL handling across the application.
| const defaultSizePane = React.useMemo(() => { | ||
| if (initialSizes) { | ||
| return initialSizes; | ||
| } | ||
| const sizes = localStorage.getItem(defaultSizePaneKey)?.split(',').map(Number); | ||
| return sizes || defaultSizes; | ||
| }; | ||
| const setDefaultSizePane = (sizes: number[]) => { | ||
| const {defaultSizePaneKey} = props; | ||
| localStorage.setItem(defaultSizePaneKey, sizes.join(',')); | ||
| }; | ||
| const sizes = savedSizesString?.split(',').map(Number); | ||
| return sizes || defaultSizesProp || sizesDefaultInner; | ||
| }, [defaultSizesProp, initialSizes, savedSizesString]); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The defaultSizePane is computed in a useMemo from savedSizesString, which means it will update when savedSizesString loads asynchronously. However, if the component has already set innerSizes via user interaction before the remote setting loads, the remote value might override the user's choice. Consider tracking whether the user has manually adjusted sizes and preserving that over late-loading remote settings.
| async function withRetries<T>(fn: () => Promise<T>, attempts: number, delay: number) { | ||
| let lastError: unknown; | ||
| for (let attempt = 0; attempt < attempts; attempt++) { | ||
| try { | ||
| return await fn(); | ||
| } catch (error) { | ||
| lastError = error; | ||
| if (attempt < attempts - 1) { | ||
| await delayMs(delay); | ||
| } | ||
| } | ||
| } | ||
| throw lastError; | ||
| } |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The retry logic is hardcoded with 3 attempts and 200ms delay. Consider extracting these as named constants (e.g., REMOTE_WRITE_RETRY_ATTEMPTS, REMOTE_WRITE_RETRY_DELAY_MS) at the top of the file for easier configuration and consistency with REMOTE_WRITE_DEBOUNCE_MS.
| const [savedWidthString, setSavedWidthString] = useSetting<string | undefined>(storageKey); | ||
| const [drawerWidth, setDrawerWidth] = React.useState<number | undefined>(() => { | ||
| return isNumeric(savedWidthString) ? Number(savedWidthString) : defaultWidth; | ||
| }); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The drawer width is initialized from savedWidthString in the useState initializer, but when savedWidthString is loaded asynchronously (e.g., from remote settings), the component won't update its drawerWidth state. This means the drawer will always use the default width until the user manually resizes it. Consider adding a useEffect that updates setDrawerWidth when savedWidthString changes and the current width is still undefined or the default.
CI Results
Test Status: ❌ FAILED
📊 Full Report
Test Changes Summary ⏭️2
⏭️ Skipped Tests (2)
Bundle Size: 🔺
Current: 62.56 MB | Main: 62.54 MB
Diff: +0.03 MB (0.04%)
ℹ️ CI Information