Skip to content

Commit bc7c1d2

Browse files
committed
feat(ui): add runtime config loading from API (Phase 3)
Enable the UI to fetch configuration from the API at startup, allowing environment-specific values to be loaded at runtime instead of being baked in at build time. Changes: - Add ui-config slice to fetch config from /api/info/ui-config - Update getConfig() to merge runtime config with build-time defaults - Add setRuntimeConfig() and getRuntimeConfig() exports - Fetch UI config during app initialization The runtime config is fetched early in the init flow and merged with the build-time defaults. If the fetch fails, the app continues with build-time defaults (graceful degradation).
1 parent 24844a9 commit bc7c1d2

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed
Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
import { merge, cloneDeep } from 'lodash'
2-
import { Config } from 'uiSrc/config/default'
2+
import { Config, PartialConfig } from 'uiSrc/config/default'
33
import { domainConfig } from './domain'
44

55
let config: Config
6+
let runtimeConfig: PartialConfig | null = null
67

8+
/**
9+
* Set runtime configuration from API
10+
* Call this after fetching UI config from /api/info/ui-config
11+
*/
12+
export const setRuntimeConfig = (apiConfig: PartialConfig): void => {
13+
runtimeConfig = apiConfig
14+
// Reset cached config to force re-merge with runtime config
15+
config = null as unknown as Config
16+
}
17+
18+
/**
19+
* Get runtime configuration (if available)
20+
*/
21+
export const getRuntimeConfig = (): PartialConfig | null => runtimeConfig
22+
23+
/**
24+
* Get merged configuration
25+
* Priority: riConfig (build-time) -> domainConfig -> runtimeConfig (API)
26+
*/
727
export const getConfig = (): Config => {
828
if (config) {
929
return config
@@ -12,5 +32,10 @@ export const getConfig = (): Config => {
1232
config = cloneDeep(riConfig)
1333
merge(config, domainConfig)
1434

35+
// Merge runtime config if available (highest priority)
36+
if (runtimeConfig) {
37+
merge(config, runtimeConfig)
38+
}
39+
1540
return config
1641
}

redisinsight/ui/src/slices/app/init.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createSlice } from '@reduxjs/toolkit'
22
import { fetchCsrfTokenAction } from 'uiSrc/slices/app/csrf'
33
import { fetchFeatureFlags } from 'uiSrc/slices/app/features'
4+
import { fetchUiConfigAction } from 'uiSrc/slices/app/ui-config'
45
import { FeatureFlags } from 'uiSrc/constants'
56
import { fetchCloudUserProfile } from 'uiSrc/slices/user/cloud-user-profile'
67
import { AppDispatch, RootState } from '../store'
@@ -78,6 +79,11 @@ export function initializeAppAction(
7879
return async (dispatch: AppDispatch) => {
7980
try {
8081
dispatch(initializeAppState())
82+
83+
// Fetch UI runtime config first (non-blocking on failure)
84+
// This enables environment switching without rebuilding
85+
await dispatch(fetchUiConfigAction())
86+
8187
await dispatch(
8288
fetchCsrfTokenAction(undefined, () => {
8389
throw new Error(FAILED_TO_FETCH_CSRF_TOKEN_ERROR)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2+
import { apiService } from 'uiSrc/services'
3+
import { ApiEndpoints } from 'uiSrc/constants'
4+
import { setRuntimeConfig } from 'uiSrc/config'
5+
import { PartialConfig } from 'uiSrc/config/default'
6+
import { AppDispatch, RootState } from '../store'
7+
8+
/**
9+
* UI Runtime Configuration
10+
*
11+
* This slice manages runtime configuration fetched from the API,
12+
* enabling environment switching without rebuilding the UI.
13+
*/
14+
15+
export interface UiConfig {
16+
env?: string
17+
segmentWriteKey?: string
18+
cloudCapiUrl?: string
19+
aiConvAiApiUrl?: string
20+
aiQuerySocketUrl?: string
21+
features?: {
22+
cloudAds?: boolean
23+
envDependent?: boolean
24+
}
25+
appFolderName?: string
26+
}
27+
28+
export interface UiConfigState {
29+
data: UiConfig | null
30+
loading: boolean
31+
error: string | null
32+
}
33+
34+
export const initialState: UiConfigState = {
35+
data: null,
36+
loading: false,
37+
error: null,
38+
}
39+
40+
const uiConfigSlice = createSlice({
41+
name: 'uiConfig',
42+
initialState,
43+
reducers: {
44+
fetchUiConfigStart: (state) => {
45+
state.loading = true
46+
state.error = null
47+
},
48+
fetchUiConfigSuccess: (state, action: PayloadAction<UiConfig>) => {
49+
state.loading = false
50+
state.data = action.payload
51+
},
52+
fetchUiConfigFailure: (state, action: PayloadAction<string>) => {
53+
state.loading = false
54+
state.error = action.payload
55+
// On failure, we continue with build-time defaults
56+
},
57+
},
58+
})
59+
60+
export const {
61+
fetchUiConfigStart,
62+
fetchUiConfigSuccess,
63+
fetchUiConfigFailure,
64+
} = uiConfigSlice.actions
65+
66+
export const uiConfigSelector = (state: RootState) => state.app.uiConfig
67+
68+
export default uiConfigSlice.reducer
69+
70+
/**
71+
* Convert API response to PartialConfig format for merging
72+
*/
73+
function mapApiConfigToPartialConfig(apiConfig: UiConfig): PartialConfig {
74+
return {
75+
app: {
76+
env: apiConfig.env,
77+
},
78+
features: apiConfig.features
79+
? {
80+
cloudAds: {
81+
defaultFlag: apiConfig.features.cloudAds ?? true,
82+
},
83+
envDependent: {
84+
defaultFlag: apiConfig.features.envDependent ?? true,
85+
},
86+
}
87+
: undefined,
88+
}
89+
}
90+
91+
/**
92+
* Fetch UI configuration from the API
93+
* This is called during app initialization to get runtime config
94+
*/
95+
export function fetchUiConfigAction(
96+
onSuccess?: (config: UiConfig) => void,
97+
onFail?: (error: string) => void,
98+
) {
99+
return async (dispatch: AppDispatch) => {
100+
dispatch(fetchUiConfigStart())
101+
102+
try {
103+
const { data } = await apiService.get<UiConfig>(
104+
`${ApiEndpoints.INFO}/ui-config`,
105+
)
106+
107+
// Apply runtime config to the config module
108+
const partialConfig = mapApiConfigToPartialConfig(data)
109+
setRuntimeConfig(partialConfig)
110+
111+
dispatch(fetchUiConfigSuccess(data))
112+
onSuccess?.(data)
113+
} catch (error: any) {
114+
const errorMessage = error?.message || 'Failed to fetch UI config'
115+
dispatch(fetchUiConfigFailure(errorMessage))
116+
// Don't throw - we can continue with build-time defaults
117+
onFail?.(errorMessage)
118+
}
119+
}
120+
}

redisinsight/ui/src/slices/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import appRedisCommandsReducer from './app/redis-commands'
3131
import appPluginsReducer from './app/plugins'
3232
import appsSocketConnectionReducer from './app/socket-connection'
3333
import appFeaturesReducer from './app/features'
34+
import appUiConfigReducer from './app/ui-config'
3435
import appUrlHandlingReducer from './app/url-handling'
3536
import appOauthReducer from './oauth/cloud'
3637
import workbenchResultsReducer from './workbench/wb-results'
@@ -67,6 +68,7 @@ export const rootReducer = combineReducers({
6768
plugins: appPluginsReducer,
6869
socketConnection: appsSocketConnectionReducer,
6970
features: appFeaturesReducer,
71+
uiConfig: appUiConfigReducer,
7072
urlHandling: appUrlHandlingReducer,
7173
csrf: appCsrfReducer,
7274
init: appInitReducer,

0 commit comments

Comments
 (0)