feat(appConfig): add tenant-level defaultAppMode override via yaml - #31099
feat(appConfig): add tenant-level defaultAppMode override via yaml#31099chirag-madlani wants to merge 5 commits into
Conversation
…v1/system/config/appConfig
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
|
| Count | Rule |
|---|---|
| 9 | react-hooks/exhaustive-deps |
| 8 | @typescript-eslint/no-explicit-any |
| 3 | sonarjs/no-nested-functions |
| 1 | sonarjs/cyclomatic-complexity |
| 1 | sonarjs/expression-complexity |
All findings
| Location | Rule | Message | |
|---|---|---|---|
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:193:9 |
react-hooks/exhaustive-deps |
The 'onLoginHandler' function makes the dependencies of useMemo Hook (at line 838) change on every render. Move it inside the useMemo callback. Alternatively, w |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:265:6 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'navigate', 'setApplicationLoading', 'setCurrentUser', and 'setIsAuthenticated'. Either include them or remove |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:323:9 |
react-hooks/exhaustive-deps |
The 'resetUserDetails' function makes the dependencies of useMemo Hook (at line 838) change on every render. To fix this, wrap the definition of 'resetUserDetai |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:408:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'startTokenExpiryTimer'. Either include it or remove the dependency array. |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:451:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'startTokenExpiryTimer'. Either include it or remove the dependency array. |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:461:9 |
react-hooks/exhaustive-deps |
The 'handleFailedLogin' function makes the dependencies of useMemo Hook (at line 838) change on every render. Move it inside the useMemo callback. Alternatively |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:522:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has missing dependencies: 'authConfig?.provider', 'handledVerifiedUser', 'navigate', 'resetUserDetails', and 'startTokenExpiryTimer'. Eit |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:567:9 |
react-hooks/exhaustive-deps |
The 'initializeAxiosInterceptors' function makes the dependencies of useMemo Hook (at line 838) change on every render. To fix this, wrap the definition of 'ini |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:643:65 |
sonarjs/no-nested-functions |
Refactor this code to not nest functions more than 4 levels deep. |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:653:39 |
sonarjs/no-nested-functions |
Refactor this code to not nest functions more than 4 levels deep. |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:668:41 |
sonarjs/no-nested-functions |
Refactor this code to not nest functions more than 4 levels deep. |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:738:30 |
sonarjs/cyclomatic-complexity |
{"message":"Function has a complexity of 17 which is greater than 10 authorized.","cost":7,"secondaryLocations":[{"line":738,"column":29,"endLine":738,"endColum |
| 🟡 | src/components/Auth/AuthProviders/AuthProvider.tsx:827:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has missing dependencies: 'cleanup', 'fetchAuthConfig', 'initializeAxiosInterceptors', and 'startTokenExpiryTimer'. Either include them or |
| 🟡 | src/hooks/currentUserStore/useCurrentUserStore.test.ts:47:12 |
@typescript-eslint/no-explicit-any |
Unexpected any. Specify a different type. |
| 🟡 | src/hooks/currentUserStore/useCurrentUserStore.test.ts:90:14 |
@typescript-eslint/no-explicit-any |
Unexpected any. Specify a different type. |
| 🟡 | src/hooks/currentUserStore/useCurrentUserStore.test.ts:116:14 |
@typescript-eslint/no-explicit-any |
Unexpected any. Specify a different type. |
| 🟡 | src/hooks/currentUserStore/useCurrentUserStore.test.ts:129:16 |
@typescript-eslint/no-explicit-any |
Unexpected any. Specify a different type. |
| 🟡 | src/hooks/currentUserStore/useCurrentUserStore.test.ts:153:14 |
@typescript-eslint/no-explicit-any |
Unexpected any. Specify a different type. |
| 🟡 | src/hooks/currentUserStore/useCurrentUserStore.test.ts:197:14 |
@typescript-eslint/no-explicit-any |
Unexpected any. Specify a different type. |
| 🟡 | src/hooks/currentUserStore/useCurrentUserStore.test.ts:225:16 |
@typescript-eslint/no-explicit-any |
Unexpected any. Specify a different type. |
| 🟡 | src/hooks/currentUserStore/useCurrentUserStore.test.ts:248:16 |
@typescript-eslint/no-explicit-any |
Unexpected any. Specify a different type. |
| 🟡 | src/hooks/useAppMode.ts:137:7 |
sonarjs/expression-complexity |
Reduce the number of conditional operators (5) used in the expression (maximum allowed 3). |
Fix locally (fast - only checks files changed in this branch):
make ui-checkstyle-changed
✅ TypeScript Types Auto-UpdatedThe generated TypeScript types have been automatically updated based on JSON schema changes in this PR. |
…ation works The store is module-level and survives an SPA logout→login on the same tab. If a prior session left `isForced=true`, `writeAppMode`'s guard would no-op the initial pin in `hydrateAppModeConfig` and the new session would be stuck on the previous runtime mode — for example, user A gets 'ai' pinned, logs out, user B logs in on the same tab, and B's initial pin gets silently dropped even though the tenant force is unchanged. Fix: `setForced(null)` first, then `writeAppMode(runtimeMode)`, then `setForced(wireMode)`. The initial pin always lands regardless of prior state; every subsequent user-initiated write is still blocked. Added regression test. Reported by Gitar review on PR #31099. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ Playwright Results — workflow succeededValidated commit ✅ 1004 passed · ❌ 0 failed · 🟡 1 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky PerformanceBlocking targets: ✅ met · Optimization targets: 🟡 in progress Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting. 🕒 Full workflow signal wall (to summary) 57m 23s ⏱️ Max setup 3m 19s · max shard execution 20m 9s · max shard-job elapsed before upload 23m 34s · reporting 7s 🌐 208.46 requests/attempt · 2.47 app boots/UI scenario · 12.08% common-shard skew Optimization targets still in progress:
🟡 1 flaky test(s) (passed on retry)
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
…ched test The generated `DefaultAppMode` enum exports members as `AI` (all caps) and `Classic`. The hand-written `CONFIG_MODE_TO_RUNTIME` map used `DefaultAppMode.Ai` (title case), which is `undefined` at runtime — `[undefined]: AI_APP_MODE` becomes the key `"undefined"`, so a wire value of `"ai"` looked up as `CONFIG_MODE_TO_RUNTIME["ai"]` returned `undefined` and `writeAppMode` was never called. Only the `Classic` case worked. Fix the reference to `DefaultAppMode.AI`. Also re-format `useCurrentUserStore.test.ts` — CI's prettier check flagged it after the last commit even though the file passed prettier locally at authoring time (line-length drift from an unrelated re-flow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
| dataset: ${RDF_DATASET:-"openmetadata"} | ||
| inferenceEnabled: ${RDF_INFERENCE_ENABLED:-false} | ||
|
|
||
| # App Configuration |
There was a problem hiding this comment.
let's remove it from here , we can keep this only in the database
mohityadav766
left a comment
There was a problem hiding this comment.
Needs some work. Use SettingsCache to load the setting into the database if not existing, else don't .
We don't need to store the settings in opemetadata.yaml.
|
Closing in favor of the v2 architecture per reviewer feedback (Pablo + Mohit). New combined PR forthcoming — will link when opened. Branch |
Code Review ✅ Approved 1 resolved / 1 findingsAdds YAML-configured tenant-level app mode override supporting bootstrap hydration and user write guards, addressing the stale isForced blocks force re-pin on same-tab user switch finding. No issues found. ✅ 1 resolved✅ Bug: Stale isForced blocks force re-pin on same-tab user switch
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |



Summary
Add a yaml-configured tenant-level force for the app mode. Operators can pin every user in a deployment to a specific mode (
aiorclassic) viaopenmetadata.yaml— overriding the per-user preference. When unset, the current per-user behavior wins.Complements #31030 (per-user preference persistence). Independent of it — this PR branches off
mainand can land in either order.Fallback chain at boot
appConfiguration.defaultAppMode(tenant force from yaml)user.preferences.appMode(per-user preference, from feat(user): persist per-user UI preferences on the User entity (starting with appMode) #31030)DEFAULT_APP_MODEconstantWhat changed
Backend (yaml → Java → REST):
openmetadata-spec/.../api/configuration/appConfiguration.jsonwith a nullabledefaultAppModefield (enum:"ai" | "classic" | null).OpenMetadataApplicationConfig.appConfigurationfield, defaulted tonew AppConfiguration()sogetDefaultAppMode()is null-safe when yaml is absent.GET /v1/system/config/appConfiginConfigResource— mirrors the/authorizerpattern (reads fromopenMetadataApplicationConfig, notSettingsCache). StandardJwtFilterauth.conf/openmetadata.yamlgains:UI:
getAppConfig()inrest/miscAPI.ts.useAppModeConfigZustand store with{ isForced, forcedMode, setForced }.hydrateAppModeConfig(config)— called from both bootstrap paths inAuthProvider.tsx(returning-session AND fresh-login), viaPromise.all([getLoggedInUser(), getAppConfig()])to avoid a waterfall. WhendefaultAppModeis non-null, pins runtime viawriteAppModeand setsisForced=true.writeAppMode(...)(inuseAppMode.ts) andsetPreference({ appMode })(inuseCurrentUserStore.ts) short-circuit whenisForcedis true. ThesetPreferenceguard is per-key — other preference keys still write through normally.CONFIG_MODE_TO_RUNTIMEtranslation ('classic'→DEFAULT_APP_MODEwhich is'default'). Without this, forcing "classic" would have set an unrecognized runtime string, anduseIsAiMode()would incorrectly returntruefor a classic-forced tenant. Mirrors the existingAPP_MODE_ENUM_TO_RUNTIMEpattern inuseResolvedAppMode.ts.Testing
ConfigResourceTest(new, 3/3 pass) — Mockito unit test in the same style asIndexResourceTest.java. No infra needed; runs via plainmvn test. Covers: default returns null; configured value is echoed; null-safe whengetAppConfiguration()returnsnull.useAppModeConfig(new suite),useCurrentUserStore(+2 guard tests),useAppMode(+2 guard tests). Pre-existingAuthProvider.test.tsxstill 12/12 (no regression).mvn spotless:applyclean onopenmetadata-service/openmetadata-spec.Design decisions locked
SettingsCache, no admin UI page, no runtime mutation. Ops change the value by editing yaml + restart, same asauthorizerConfiguration.AppModeSwitcher(Collate-side follow-up) will hide when forced.appModekey is guarded onsetPreference; other user prefs pass through.Not in scope
AppModeSwitchervisibility guard — lives in Collate, tracked separately.Test plan for reviewers
APP_DEFAULT_MODE=ai→ every user lands in AI mode at login; user toggle attempts are no-ops.APP_DEFAULT_MODE=classic→ same in reverse (verifies the runtime translation).DEFAULT_APP_MODEif no preference).GET /api/v1/system/config/appConfigreturns{ defaultAppMode: null }on a default deployment; returns the configured value when set.🤖 Generated with Claude Code