-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix(app-settings): use-app-setting (#2615)
* fix(cli): vite-proxy for persons/me/settings with own use proxy path * fix(app-settings): improvements to flows * fix(app-module): faiilover when meta.id is not set. vite proxy handles manifest only * feat(cli): add appSettingsPlugin to handle app settings requests * feat(app): add updateSetting and updateSettingAsync methods for app settings management * fix(react-app): fix build of app settings - Add README.md to settings directory - changed functionality of `useAppSetting` from allowing dot path to only allowing root property name - removed dot-path.ts * feat(react-app): add initial setup for app-react-settings cookbook with configuration and main app component * chore(dependencies): update devDependencies for app-react-settings cookbook * Empty-Commit * refactor(app): simplify state selection for observables in App class * fix(app): improve action filtering for updateSettings in App class * fix(app): enhance AppClient to utilize IHttpClient for updating app settings `Query` does not support update actions, since the query method only execute a method, so the update must done in a separate action which mutates the query on update. * fix(app): update appSettingsPlugin to directly assign parsed request body for PUT method align the plugin with PUT, not PATCH * fix(app): update settings handling and improve action structure in AppClient * fix(app): enhance useAppSetting and useAppSettings hooks to support status handling and improve error management * fix(app): enhance useAppSetting and useAppSettings hooks to support callback functions for setting updates and improve error handling * fix(app): enhance App component to support new 'fancy' setting and loading states for theme and size updates * fix(deps): add fast-deep-equal dependency at version 3.1.3 in pnpm-lock.yaml * docs(settings): add section for Portal Settings in README.md * feat(docs): add documentation for app settings * docs(settings): update README.md with notes on global state handling and UI best practices for settings updates --------- Co-authored-by: Øyvind Eikeland <oyvind@eikeland.me>
- Loading branch information
Showing
30 changed files
with
883 additions
and
235 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@equinor/fusion-framework-cookbook-app-react-settings': major | ||
--- | ||
|
||
Created a cookbook for using settings |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
'@equinor/fusion-framework-cli': minor | ||
--- | ||
|
||
Created a plugin for handling application settings. This plugin allows retrieving and setting application settings when developing locally by intercepting the request to the settings API and returning the local settings instead. Settings are stored in memory and are not persisted, which means the CLI will always provide settings as if the user has never set them before. By restarting the CLI, the settings will be lost. This plugin is useful for testing and development purposes. | ||
|
||
Also added a utility function `parseJsonFromRequest` to parse JSON from a request body. This function is used in the plugin to parse the `PUT` request body and update the settings accordingly. | ||
|
||
The default development server has enabled this plugin by default and confiuigred it to intercept the settings API on `/apps-proxy/persons/me/apps/${CURRENT_APP_KEY}/settings` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
--- | ||
'@equinor/fusion-framework-module-app': minor | ||
--- | ||
|
||
Added `updateSetting` and `updateSettingAsync` to the `App` class. This allows updating a setting in settings without the need to handle the settings object directly. This wil ensure that the settings are mutated correctly. | ||
|
||
```ts | ||
const app = new App(); | ||
// the app class will fetch the latest settings before updating the setting | ||
app.updateSetting('property', 'value'); | ||
``` | ||
|
||
example of flux state of settings: | ||
|
||
```ts | ||
const app = new App(); | ||
const settings = app.getSettings(); | ||
|
||
setTimeout(() => { | ||
settings.foo = 'foo'; | ||
app.updateSettingsAsync(settings); | ||
}, 1000); | ||
|
||
setTimeout(() => { | ||
settings.bar = 'bar'; | ||
app.updateSettingsAsync(settings); | ||
// foo is now reset to its original value, which is not what we want | ||
}, 2000); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@equinor/fusion-framework-docs': minor | ||
--- | ||
|
||
Added doc for app settings |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# React cookbook | ||
|
||
App for cooking settings with Fusion-Framework and React |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"name": "@equinor/fusion-framework-cookbook-app-react-settings", | ||
"version": "0.0.0", | ||
"description": "", | ||
"private": true, | ||
"type": "module", | ||
"main": "src/index.ts", | ||
"scripts": { | ||
"build": "fusion-framework-cli app build", | ||
"dev": "fusion-framework-cli app dev", | ||
"docker": "cd .. && sh docker-script.sh app-react" | ||
}, | ||
"author": "", | ||
"license": "ISC", | ||
"devDependencies": { | ||
"@equinor/fusion-framework-cli": "workspace:^", | ||
"@equinor/fusion-framework-react-app": "workspace:^", | ||
"@types/react": "^18.2.50", | ||
"@types/react-dom": "^18.2.7", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"typescript": "^5.5.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { useCallback, useState } from 'react'; | ||
import { useAppSettings, useAppSetting } from '@equinor/fusion-framework-react-app/settings'; | ||
|
||
type MyAppSettings = { | ||
theme: 'none' | 'light' | 'dark'; | ||
size: 'small' | 'medium' | 'large'; | ||
fancy: boolean; | ||
}; | ||
|
||
declare module '@equinor/fusion-framework-react-app/settings' { | ||
interface AppSettings extends MyAppSettings {} | ||
} | ||
|
||
export const App = () => { | ||
const [isLoading, setIsLoading] = useState(false); | ||
const [isUpdating, setIsUpdating] = useState(false); | ||
const [settingsHooks] = useState(() => ({ | ||
onLoading: setIsLoading, | ||
onUpdating: setIsUpdating, | ||
})); | ||
|
||
const [theme, setTheme] = useAppSetting('theme', 'none', settingsHooks); | ||
const [size, setSize] = useAppSetting('size', 'medium', settingsHooks); | ||
const [fancy, setFancy] = useAppSetting('fancy', false); | ||
|
||
const onFancyChange = useCallback(() => setFancy((isFancy) => !isFancy), [setFancy]); | ||
|
||
const [settings] = useAppSettings(); | ||
|
||
return ( | ||
<div | ||
style={{ | ||
height: '100%', | ||
display: 'flex', | ||
flexDirection: 'column', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
fontFamily: fancy ? 'cursive' : 'sans-serif', | ||
fontSize: size === 'small' ? '0.6rem' : size === 'large' ? '2rem' : '1rem', | ||
background: | ||
theme === 'light' ? '#f0f0f0' : theme === 'dark' ? '#343434' : '#f97fcc', | ||
color: theme === 'dark' ? '#f0f0f0' : '#343434', | ||
}} | ||
> | ||
<div | ||
style={{ | ||
background: theme === 'dark' ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)', | ||
padding: '1em', | ||
}} | ||
> | ||
<h1>🚀 Hello Fusion Settings 🔧</h1> | ||
<section style={{ display: 'grid', gridTemplateColumns: '3em auto', gap: '1rem' }}> | ||
<span>Theme:</span> | ||
<select | ||
disabled={isLoading || isUpdating} | ||
value={theme} | ||
onChange={(e) => setTheme(e.currentTarget.value as MyAppSettings['theme'])} | ||
> | ||
<option value="none">None</option> | ||
<option value="light">Light</option> | ||
<option value="dark">Dark</option> | ||
</select> | ||
</section> | ||
<section style={{ display: 'grid', gridTemplateColumns: '3em auto', gap: '1rem' }}> | ||
<span>Size:</span> | ||
<select | ||
disabled={isLoading || isUpdating} | ||
value={size} | ||
onChange={(e) => setSize(e.currentTarget.value as MyAppSettings['size'])} | ||
> | ||
<option value="small">Small</option> | ||
<option value="medium">Medium</option> | ||
<option value="large">Large</option> | ||
</select> | ||
</section> | ||
<section style={{ display: 'grid', gridTemplateColumns: '3em auto', gap: '1rem' }}> | ||
<span>Size:</span> | ||
<input type="checkbox" checked={fancy} onChange={onFancyChange} /> | ||
</section> | ||
<div> | ||
<span>App settings:</span> | ||
<br /> | ||
<pre>{JSON.stringify(settings, null, 2)}</pre> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app'; | ||
|
||
export const configure: AppModuleInitiator = () => {}; | ||
|
||
export default configure; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { createElement } from 'react'; | ||
import { createRoot } from 'react-dom/client'; | ||
|
||
import { ComponentRenderArgs, makeComponent } from '@equinor/fusion-framework-react-app'; | ||
|
||
import configure from './config'; | ||
import App from './App'; | ||
|
||
/** create a render component */ | ||
const appComponent = createElement(App); | ||
|
||
/** create React render root component */ | ||
const createApp = (args: ComponentRenderArgs) => makeComponent(appComponent, args, configure); | ||
|
||
/** Render function */ | ||
export const renderApp = (el: HTMLElement, args: ComponentRenderArgs) => { | ||
/** make render element */ | ||
const app = createApp(args); | ||
|
||
/** create render root from provided element */ | ||
const root = createRoot(el); | ||
|
||
/** render Application */ | ||
root.render(createElement(app)); | ||
|
||
/** Teardown */ | ||
return () => root.unmount(); | ||
}; | ||
|
||
export default renderApp; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"extends": "../../tsconfig.base.json", | ||
"compilerOptions": { | ||
"rootDir": "src", | ||
"jsx": "react-jsx", | ||
}, | ||
"references": [ | ||
{ | ||
"path": "../../packages/react/app" | ||
}, | ||
{ | ||
"path": "../../packages/cli" | ||
}, | ||
], | ||
"include": [ | ||
"src/**/*" | ||
], | ||
"exclude": [ | ||
"node_modules", | ||
"lib" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { type Plugin } from 'vite'; | ||
|
||
import parseJsonFromRequest from '../../utils/parse-json-request.js'; | ||
|
||
/** | ||
* Options for configuring the AppSettingsPlugin. | ||
*/ | ||
export interface AppSettingsPluginOptions { | ||
/** | ||
* A string or regular expression to match specific settings. | ||
* If provided, only settings that match this pattern will be considered. | ||
*/ | ||
match?: string | RegExp; | ||
|
||
/** | ||
* A record of default settings to be used if no other settings are provided. | ||
* The keys are setting names and the values are the default values for those settings. | ||
*/ | ||
defaultSettings?: Record<string, unknown>; | ||
} | ||
|
||
/** | ||
* This plugin provides a simple way to manage application settings in a local development environment. | ||
* | ||
* This plugin will cache the settings in memory and respond to `PUT` requests to update the settings. | ||
* Restarting the development server will reset the settings to the default values. | ||
* | ||
* @param options - The options for configuring the app settings plugin. | ||
* @returns A Vite Plugin object that can be used to configure a server. | ||
* | ||
* The plugin provides the following functionality: | ||
* - Matches requests based on a specified path pattern. | ||
* - Handles `PUT` requests to update application settings. | ||
* - Responds with the current application settings in JSON format. | ||
*/ | ||
export function appSettingsPlugin(options: AppSettingsPluginOptions): Plugin { | ||
let appSettings = options.defaultSettings ?? {}; | ||
const pathMatch = new RegExp(options.match ?? '/persons/me/apps/.*/settings'); | ||
return { | ||
name: 'app-settings', | ||
configureServer(server) { | ||
server.middlewares.use(async (req, res, next) => { | ||
if (!req.url?.match(pathMatch)) { | ||
return next(); | ||
} | ||
|
||
if (req.method === 'PUT') { | ||
appSettings = await parseJsonFromRequest(req); | ||
} | ||
|
||
res.setHeader('content-type', 'application/json'); | ||
res.end(JSON.stringify(appSettings)); | ||
}); | ||
}, | ||
}; | ||
} | ||
|
||
export default appSettingsPlugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { IncomingMessage } from 'node:http'; | ||
|
||
/** | ||
* Extracts and parses JSON data from an incoming HTTP request. | ||
* | ||
* @param req - The incoming HTTP request object. | ||
* @returns A promise that resolves to a record containing the parsed JSON data. | ||
* @throws Will reject the promise if there is an error during data reception or JSON parsing. | ||
*/ | ||
export async function parseJsonFromRequest(req: IncomingMessage): Promise<Record<string, unknown>> { | ||
return await new Promise<Record<string, unknown>>((resolve, reject) => { | ||
let data = ''; | ||
req.on('data', (chunk) => (data += chunk.toString())); | ||
req.on('end', () => resolve(JSON.parse(data))); | ||
req.on('error', reject); | ||
}); | ||
} | ||
|
||
export default parseJsonFromRequest; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.