Skip to content

Conversation

@artemmufazalov
Copy link
Member

@artemmufazalov artemmufazalov commented Nov 19, 2025

Part of #2892

Currently it can be checked with table columns width.
Stand (without meta): https://nda.ya.ru/t/BEIjuuLI7N8Qd7

Sync all values within api onQueryStarted instead of useEffects.

This approach proved itself good with and without meta api. Without meta api there is only 1 failed test (against 100-200 before): #3054

greptile-review

CI Results

Test Status: ⚠️ FLAKY

📊 Full Report

Total Passed Failed Flaky Skipped
378 374 0 2 2
Test Changes Summary ⏭️2

⏭️ Skipped Tests (2)

  1. Scroll to row, get shareable link, navigate to URL and verify row is scrolled into view (tenant/diagnostics/tabs/queries.test.ts)
  2. Copy result button copies to clipboard (tenant/queryEditor/queryEditor.test.ts)

Bundle Size: ✅

Current: 62.48 MB | Main: 62.49 MB
Diff: 1.98 KB (-0.00%)

✅ Bundle size unchanged.

ℹ️ CI Information
  • Test recordings for failed tests are available in the full report.
  • Bundle size is measured for the entire 'dist' directory.
  • 📊 indicates links to detailed reports.
  • 🔺 indicates increase, 🔽 decrease, and ✅ no change in bundle size.

@artemmufazalov artemmufazalov changed the title feat(useSetting); sync values with LS and store in api feat(useSetting): sync values with LS and store in api Nov 19, 2025
Copy link
Contributor

Copilot AI left a 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 refactors the settings synchronization architecture by moving the sync logic from useEffect hooks in useSetting.ts to RTK Query's onQueryStarted lifecycle hooks in the API layer. The main goal is to centralize settings synchronization between localStorage, Redux store, and the meta settings API.

Key Changes:

  • Consolidates all settings sync logic into the settingsApi endpoints' onQueryStarted hooks
  • Simplifies useSetting hook by removing multiple useEffect dependencies
  • Handles localStorage and API settings synchronization at the API layer instead of component layer

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/store/reducers/settings/utils.ts Adds helper functions getSettingDefault and shouldSyncSettingToLS to support centralized settings sync logic
src/store/reducers/settings/useSetting.ts Removes all useEffect-based sync logic and simplifies hook to only trigger API queries and mutations
src/store/reducers/settings/constants.ts Updates SETTINGS_OPTIONS type to explicitly allow undefined values for better type safety
src/store/reducers/settings/api.ts Implements comprehensive sync logic in onQueryStarted hooks for both query and mutation endpoints, handling localStorage and store updates
src/services/api/metaSettings.ts Updates return type to allow undefined for settings that don't exist in the backend

@artemmufazalov artemmufazalov marked this pull request as ready for review November 19, 2025 20:11
@Raubzeug
Copy link
Contributor

@greptile-review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile


return {data};
} catch (error) {
console.error('Cannot get settings:', error);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can it be that error is "Cannot get setting" - but the real situation is that one of patches failed ?

@astandrik
Copy link
Collaborator

astandrik commented Nov 20, 2025

Now as I see we have three sources of truth:

  • LS
  • Backend
  • Redux Store

And we struggle to keep all of them in sync
Is my assumption correct?

@artemmufazalov
Copy link
Member Author

Now as I see we have three sources of truth:

  • LS
  • Backend
  • Redux Store

And we struggle to keep all of them in sync Is my assumption correct?

Yes, everything is to keep them in sync

@astandrik
Copy link
Collaborator

Now as I see we have three sources of truth:

  • LS
  • Backend
  • Redux Store

And we struggle to keep all of them in sync Is my assumption correct?

Yes, everything is to keep them in sync

Is it a goal or temporary state?

@artemmufazalov
Copy link
Member Author

Is it a goal or temporary state?

It's a goal

const newValue = data ?? {name, user};

// Try to sync local value if there is no backend value
syncLocalValueToMetaIfNoData(newValue, dispatch);
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it possible that null/undefined on backend is intentional?
i mean we removed some setting for example

but LS has some old value (in another browser for example) and pushes this setting to backend?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a current limitation, yes. We cannot have null or undefined as backend value, there should be at least something, otherwise defined localStorage value will be loaded to backend. Without this it's unclear, when we should sync local value to backend

return;
}

const localValue = localStorage.getItem(params.name);
Copy link
Collaborator

Choose a reason for hiding this comment

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

we use localStorage directly here but there are already functions for this like

readSettingValueFromLS / shouldSyncSettingToLS

these functions as I may suppose were created to make work with LS somehow guarded

@astandrik
Copy link
Collaborator

astandrik commented Nov 20, 2025

Now as I see we have three sources of truth:

  • LS
  • Backend
  • Redux Store

And we struggle to keep all of them in sync Is my assumption correct?

Yes, everything is to keep them in sync

I thought this over again and came to a conclusion that actually there is one source of truth (backend) while LS and rtq storage are used as caches for interface rensponsiveness so actually its not "three sources of truth" but "one with two caches"

Does it make sense?

@artemmufazalov
Copy link
Member Author

I thought this over again and came to a conclusion that actually there is one source of truth (backend) while LS and rtq storage are used as caches for interface rensponsiveness so actually its not "three sources of truth" but "one with two caches"

Does it make sense?

In general, you are right. But it depends on app installation. There are 3 cases.

  1. No meta (embedded single cluster version). LS is used as source of truth, meta is not used at all, we read and load value to redux to use it in the app without a need to read it from LS.

  2. There is meta service with settings. In this case value from LS is used as temporary initial value while setting from meta is not loaded. When value is loaded, it is used.

So, preload data from LS to store, load actual value from meta when request is finished.

  1. There is meta service, but there is no meta setting value. It is possible case in migration period - users have all their setting in LS now. It is similar to case 2, but when we receive undefined value from backend, we load value from LS there

In all cases redux is used as sync cache for data.

@astandrik
Copy link
Collaborator

astandrik commented Nov 20, 2025

So there are two main scenarios with mainly opposite mechanics

  1. with meta
  2. without meta

It appears we try to handle both at once with one handler and therefore it looks a bit complex

I'm trying to imagine if we could explicitly split these scenarios for clarity and maintainability

its possible to postpone this and separate to some tech issue but would be great if we could handle it at once

@artemmufazalov artemmufazalov marked this pull request as draft December 5, 2025 08:30
@artemmufazalov artemmufazalov force-pushed the settings-sync-values-in-api branch from 29a9e3d to 759ba53 Compare December 5, 2025 08:30
Comment on lines +109 to +110
isFetching={isFetching}
isLoading={isLoading}
Copy link
Member Author

Choose a reason for hiding this comment

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

Table loading changes are needed to synchronize settings and data loading states, without it tests somehow fail to find table container

proxyMeta: undefined | boolean;
// this setting allows to use schema object path relative to database in api requests
useRelativePath: undefined | boolean;
useMetaSettings: undefined | boolean;
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved useMetaSettings from uiFactory to api setup. The reason is that uiFactory property requires api and uiFactory to be inited in the specific order: first we need to configureUIFactory and only after setup api. Otherwise we need two settings - for uiFactory and to api.

So this help to reduce it to only one setting without any additional requirements

isAuthenticated: true,
user: undefined,
id: undefined,
metaUser: undefined,
Copy link
Member Author

Choose a reason for hiding this comment

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

I did not figured out how to properly receive metaUser value yet, so I decided to test everything with some hardcoded user and to do all metaUser code separately - there are additional user points in issue checklist.

What are the problems with metaUser:

  1. The logic may be different in different installations
  2. It should not break current authentication cases
  3. We need somehow to tackle the case, when user do not have access to meta cluster

@artemmufazalov artemmufazalov changed the title feat(useSetting): sync values with LS and store in api feat(settings): simplify hook, split meta and LS cases Dec 5, 2025
@artemmufazalov artemmufazalov marked this pull request as ready for review December 5, 2025 09:05
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@artemmufazalov artemmufazalov force-pushed the settings-sync-values-in-api branch from 2f7f3b5 to d7dd85e Compare December 10, 2025 08:36
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

import {useTypedSelector} from './useTypedSelector';

export const useSetting = <T>(key: string, defaultValue?: T): [T, (value: T) => void] => {
export const useSetting = <T>(key?: string, defaultValue?: T): [T, (value: T) => void] => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@greptile why key is optional here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@greptile why key is optional here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at this useSetting hook, the key parameter is optional (marked with ?) which seems unusual and potentially problematic.

If you look at how the hook is used internally:

  • Line 13: getSettingValue(state, key) - passing potentially undefined key
  • Line 18: dispatch(setSettingValue(key, value)) - dispatching with potentially undefined key

This means the hook could be called without a key like useSetting(), which would result in trying to get/set a setting with an undefined key. This doesn't make much practical sense since you need a specific key to identify which setting you're working with.

The optional key seems like it could lead to bugs where:

  1. Someone calls useSetting() without a key by mistake
  2. The hook "works" but doesn't actually store/retrieve any meaningful setting
  3. The setting operations silently fail or behave unexpectedly

This should probably be required: key: string instead of key?: string. The only scenario where optional might make sense is if there's some default key behavior in the getSettingValue/setSettingValue functions, but that would be unusual and error-prone.

Is there a specific use case where you need to call useSetting without a key, or should this be made required?

Copy link
Member Author

Choose a reason for hiding this comment

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

Key is optional in useSetting for one specific case - table columns width. We use useSetting hook in ResizeableDataTable via useTableResize, if there is no key provided, only value from useState will be used, if there is a key - it syncs with settings


if (!shouldUseOnlyExternalSettings) {
setSettingValueToLS(name, value);
if (isNil(window.api?.metaSettings)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

upper in code we have straighforward condition but here is reverted logics - maybe needs unification

Copy link
Member Author

Choose a reason for hiding this comment

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

Unlike for settingValue, here we also check if user is present, so condition is different. Altered it a little, now it's not reversed

this.meta = new MetaAPI(axiosParams, baseApiParams);
}
if (uiFactory.useMetaSettings) {
if (useMetaSettings) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

previosly we had this in uiFactory - so this value could be overriden
Is it an intentional removal of this possibility? (probably a breaking change - major version)

Copy link
Member Author

Choose a reason for hiding this comment

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

Now it can be configured in configureStore.

In internal version we configure two entities - store and uiFactory. With setting from uiFactory there was an issue when uiFactory was configured after store

Code with bug:

const {store, history} = configureStore({
    singleClusterMode: SINGLE_CLUSTER_MODE,
    api: new YdbInternalAPI({...params}),
});

configureUIFactory<IssueType, MetricsIds>({
    ...params
});

Correct code:

configureUIFactory<IssueType, MetricsIds>({
    ...params
});

const {store, history} = configureStore({
    singleClusterMode: SINGLE_CLUSTER_MODE,
    api: new YdbInternalAPI({...params}),
});

It's not obvious at all and should not be that way. So I moved this setting to api.

I don't consider this major, because this feature is not finished yet and does not work anywhere

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile


user: string | undefined;
id: string | undefined;

Copy link
Contributor

Choose a reason for hiding this comment

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

logic: metaUser field is never set anywhere in the codebase - no reducer action populates this field. Since selectMetaUser returns metaUser ?? id, this will always fall back to id.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/authentication/types.ts
Line: 8:8

Comment:
**logic:** `metaUser` field is never set anywhere in the codebase - no reducer action populates this field. Since `selectMetaUser` returns `metaUser ?? id`, this will always fall back to `id`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's intentional, user will be added with another PR

#3111 (comment)

Comment on lines 56 to 60
if (isNil(window.api?.metaSettings)) {
saveSettingToLS(value);
} else if (user) {
setMetaSetting({user, name, value});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: when window.api?.metaSettings exists but user is undefined, settings changes are silently dropped. Since metaUser field is never populated (no reducer sets it), and selectMetaUser returns metaUser ?? id, this means settings will fail to save if id is also undefined during early app initialization

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/useSetting.ts
Line: 56:60

Comment:
**logic:** when `window.api?.metaSettings` exists but `user` is `undefined`, settings changes are silently dropped. Since `metaUser` field is never populated (no reducer sets it), and `selectMetaUser` returns `metaUser ?? id`, this means settings will fail to save if `id` is also undefined during early app initialization

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's right, if there is no id provided, it will not work as expected. However, there should be always id in whoami as fallback

Comment on lines 60 to +61
dispatch(settingsSlice.actions.setSettingValue({name, value}));
setSettingValueToLS(name, value);
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: every call to setSettingValue now writes to localStorage, even when meta settings API is available. In environments using meta settings, this duplicates storage and may cause sync issues

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/settings.ts
Line: 60:61

Comment:
**logic:** every call to `setSettingValue` now writes to localStorage, even when meta settings API is available. In environments using meta settings, this duplicates storage and may cause sync issues

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's intentional, setSettingValue is called outside new useSetting hook only for settings that are not migrated

Comment on lines +43 to 47
if (window.api?.metaSettings) {
value = settingFromMeta;
} else {
value = settingFromLS;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: when window.api?.metaSettings exists but query hasn't completed (params are skipToken or loading), settingFromMeta is undefined and meta path is chosen, causing fallback to default instead of using LS value. This breaks the intended graceful degradation during loading states

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/useSetting.ts
Line: 43:47

Comment:
**logic:** when `window.api?.metaSettings` exists but query hasn't completed (params are skipToken or loading), `settingFromMeta` is `undefined` and meta path is chosen, causing fallback to default instead of using LS value. This breaks the intended graceful degradation during loading states

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's intentional, when there is meta, LS should not be used

greptile-apps[bot]

This comment was marked as off-topic.

*
* It prevents redux console error on string error in code - `throw new Error("description")`
*/
export function serializeError(error: unknown) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpick: maybe we could enhance prepareErrorMessage in shared utils
or move this function to shared utils (with renaming to serializeReduxError)
since this may be needed elsewhere

Copy link
Member Author

Choose a reason for hiding this comment

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

prepareErrorMessage always return string error, while serializeReduxError could return an object

Moved function to utils

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile


if (!shouldUseOnlyExternalSettings) {
setSettingValueToLS(name, value);
if (window.api.metaSettings && user) {
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: missing optional chaining - will throw if window.api is undefined

Suggested change
if (window.api.metaSettings && user) {
if (window.api?.metaSettings && user) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/useSetting.ts
Line: 56:56

Comment:
**logic:** missing optional chaining - will throw if `window.api` is undefined

```suggestion
            if (window.api?.metaSettings && user) {
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +43 to 47
if (window.api?.metaSettings) {
value = settingFromMeta;
} else {
value = settingFromLS;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: the check on line 43 evaluates to true even when query hasn't started (params are skipToken) or during loading state. This causes settingFromMeta to be undefined and the hook returns default value instead of using the localStorage fallback. During initial load before meta query completes, users will see default values instead of their saved LS values.

Check the query status to ensure meta is actually available:

Suggested change
if (window.api?.metaSettings) {
value = settingFromMeta;
} else {
value = settingFromLS;
}
if (window.api?.metaSettings && settingFromMeta !== undefined) {
value = settingFromMeta;
} else {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/useSetting.ts
Line: 43:47

Comment:
**logic:** the check on line 43 evaluates to true even when query hasn't started (params are skipToken) or during loading state. This causes `settingFromMeta` to be `undefined` and the hook returns default value instead of using the localStorage fallback. During initial load before meta query completes, users will see default values instead of their saved LS values.

Check the query status to ensure meta is actually available:
```suggestion
        if (window.api?.metaSettings && settingFromMeta !== undefined) {
            value = settingFromMeta;
        } else {
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

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

It should never use local setting if there is meta

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@astandrik
Copy link
Collaborator

astandrik commented Dec 10, 2025

@artemmufazalov I had a couple of questions in comments
upd: actually you answered them - my gh page seems to have used stale data

@artemmufazalov artemmufazalov added this pull request to the merge queue Dec 10, 2025
Merged via the queue into main with commit e56ce4d Dec 10, 2025
9 checks passed
@artemmufazalov artemmufazalov deleted the settings-sync-values-in-api branch December 10, 2025 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants