diff --git a/api/src/index.ts b/api/src/index.ts index da3fab092..e356ded16 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -24,6 +24,8 @@ import { PORT, environment } from '@app/environment'; import { shutdownApiEvent } from '@app/store/actions/shutdown-api-event'; import { PingTimeoutJobs } from '@app/mothership/jobs/ping-timeout-jobs'; import { type BaseContext, type ApolloServer } from '@apollo/server'; +import { loadDynamixConfigFile } from '@app/store/modules/dynamix'; +import { setupDynamixConfigWatch } from '@app/store/watch/dynamix-config-watch'; let server: ApolloServer; @@ -56,6 +58,9 @@ void am( // Load initial registration key into store await store.dispatch(loadRegistrationKey()); + // Load my dynamix config file into store + await store.dispatch(loadDynamixConfigFile()); + // Start listening to file updates StateManager.getInstance(); @@ -65,6 +70,9 @@ void am( // Start listening to docker events setupDockerWatch(); + // Start listening to dynamix config file changes + setupDynamixConfigWatch(); + // Try and load the HTTP server logger.debug('Starting HTTP server'); diff --git a/api/src/store/index.ts b/api/src/store/index.ts new file mode 100644 index 000000000..c23166ac0 --- /dev/null +++ b/api/src/store/index.ts @@ -0,0 +1,54 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { paths } from '@app/store/modules/paths'; +import { mothership } from '@app/store/modules/minigraph'; +import { configReducer } from '@app/store/modules/config'; +import { emhttp } from '@app/store/modules/emhttp'; +import { registration } from '@app/store/modules/registration'; +import { cache } from '@app/store/modules/cache'; +import { dashboard } from '@app/store/modules/dashboard'; +import { docker } from '@app/store/modules/docker'; +import { upnp } from '@app/store/modules/upnp'; +import { listenerMiddleware } from '@app/store/listeners/listener-middleware'; +import { apiKeyReducer } from '@app/store/modules/apikey'; +import { dynamicRemoteAccessReducer } from '@app/store/modules/dynamic-remote-access'; +import { remoteGraphQLReducer } from '@app/store/modules/remote-graphql'; +import { dynamix } from '@app/store/modules/dynamix'; + +export const store = configureStore({ + reducer: { + apiKey: apiKeyReducer, + config: configReducer, + dynamicRemoteAccess: dynamicRemoteAccessReducer, + minigraph: mothership.reducer, + paths: paths.reducer, + emhttp: emhttp.reducer, + registration: registration.reducer, + remoteGraphQL: remoteGraphQLReducer, + cache: cache.reducer, + dashboard: dashboard.reducer, + docker: docker.reducer, + upnp: upnp.reducer, + dynamix: dynamix.reducer, + }, + middleware: getDefaultMiddleware => getDefaultMiddleware({ + serializableCheck: false, + }).prepend(listenerMiddleware.middleware), +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +export const getters = { + apiKey: () => store.getState().apiKey, + config: () => store.getState().config, + minigraph: () => store.getState().minigraph, + paths: () => store.getState().paths, + emhttp: () => store.getState().emhttp, + registration: () => store.getState().registration, + remoteGraphQL: () => store.getState().remoteGraphQL, + cache: () => store.getState().cache, + dashboard: () => store.getState().dashboard, + docker: () => store.getState().docker, + upnp: () => store.getState().upnp, + dynamix: () => store.getState().dynamix, +}; diff --git a/api/src/store/modules/dynamix.ts b/api/src/store/modules/dynamix.ts new file mode 100644 index 000000000..53b59babd --- /dev/null +++ b/api/src/store/modules/dynamix.ts @@ -0,0 +1,73 @@ +import { parseConfig } from '@app/core/utils/misc/parse-config'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { access } from 'fs/promises'; +import merge from 'lodash/merge'; +import { FileLoadStatus } from '@app/store/types'; +import { F_OK } from 'constants'; +import { RecursivePartial, RecursiveNullable } from '@app/types'; +import { toBoolean } from '@app/core/utils/casting'; +import { DynamixConfig } from '@app/core/types/ini'; + +export type SliceState = { + status: FileLoadStatus; +} & DynamixConfig; + +export const initialState: Partial = { + status: FileLoadStatus.UNLOADED, +}; + +/** + * Load the dynamix.cfg into the store. + * + * Note: If the file doesn't exist this will fallback to default values. + */ +export const loadDynamixConfigFile = createAsyncThunk>, string | undefined>('config/load-dynamix-config-file', async filePath => { + const store = await import('@app/store'); + const paths = store.getters.paths(); + const path = filePath ?? paths['dynamix-config']; + const fileExists = await access(path, F_OK).then(() => true).catch(() => false); + const file = fileExists ? parseConfig>({ + filePath: path, + type: 'ini', + }) : {}; + const { display } = file; + return merge(file, { + ...(display?.scale ? { scale: toBoolean(display?.scale) } : {}), + ...(display?.tabs ? { tabs: toBoolean(display?.tabs) } : {}), + ...(display?.resize ? { resize: toBoolean(display?.resize) } : {}), + ...(display?.wwn ? { wwn: toBoolean(display?.wwn) } : {}), + ...(display?.total ? { total: toBoolean(display?.total) } : {}), + ...(display?.usage ? { usage: toBoolean(display?.usage) } : {}), + ...(display?.text ? { text: toBoolean(display?.text) } : {}), + ...(display?.warning ? { warning: Number.parseInt(display?.warning, 10) } : {}), + ...(display?.critical ? { critical: Number.parseInt(display?.critical, 10) } : {}), + ...(display?.hot ? { hot: Number.parseInt(display?.hot, 10) } : {}), + ...(display?.max ? { max: Number.parseInt(display?.max, 10) } : {}), + locale: display?.locale ?? 'en_US', + }) as RecursivePartial; +}); + +export const dynamix = createSlice({ + name: 'dynamix', + initialState, + reducers: { + updateDynamixConfig(state, action: PayloadAction>) { + return merge(state, action.payload); + }, + }, + extraReducers(builder) { + builder.addCase(loadDynamixConfigFile.pending, (state, _action) => { + state.status = FileLoadStatus.LOADING; + }); + + builder.addCase(loadDynamixConfigFile.fulfilled, (state, action) => { + merge(state, action.payload, { status: FileLoadStatus.LOADED }); + }); + + builder.addCase(loadDynamixConfigFile.rejected, (state, action) => { + merge(state, action.payload, { status: FileLoadStatus.FAILED_LOADING }); + }); + }, +}); + +export const { updateDynamixConfig } = dynamix.actions; diff --git a/api/src/store/watch/dynamix-config-watch.ts b/api/src/store/watch/dynamix-config-watch.ts new file mode 100644 index 000000000..b385abc2e --- /dev/null +++ b/api/src/store/watch/dynamix-config-watch.ts @@ -0,0 +1,16 @@ +import { getters, store } from '@app/store'; +import { watch } from 'chokidar'; +import { loadDynamixConfigFile } from '@app/store/modules/dynamix'; + +export const setupDynamixConfigWatch = () => { + const configPath = getters.paths()?.['dynamix-config']; + + // Update store when cfg changes + watch(configPath, { + persistent: true, + ignoreInitial: true, + }).on('change', async () => { + // Load updated dynamix config file into store + await store.dispatch(loadDynamixConfigFile()); + }); +}; diff --git a/api/src/types/index.d.ts b/api/src/types/index.d.ts new file mode 100644 index 000000000..d71b5fce8 --- /dev/null +++ b/api/src/types/index.d.ts @@ -0,0 +1,9 @@ +declare module '*.json'; + +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + +export type RecursiveNullable = { + [P in keyof T]: RecursiveNullable | null; +};