@pablozaiden/webapp 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/auth.md +10 -2
- package/docs/getting-started.md +104 -1
- package/docs/release.md +1 -1
- package/docs/server.md +6 -0
- package/docs/settings.md +17 -0
- package/docs/ui-guidelines.md +34 -1
- package/package.json +4 -2
- package/src/server/create-web-app-server.ts +2 -2
- package/src/server/framework-endpoints.ts +6 -3
- package/src/server/index.ts +1 -0
- package/src/server/runtime-config.ts +44 -4
- package/src/web/WebAppRoot.tsx +40 -58
- package/src/web/index.ts +6 -0
- package/src/web/log-level.tsx +22 -0
- package/src/web/render.tsx +6 -1
- package/src/web/settings/account-section.tsx +60 -17
- package/src/web/settings/settings-view.tsx +3 -8
- package/src/web/styles.css +136 -0
- package/src/web/theme.tsx +147 -0
- package/src/web/toast.tsx +218 -0
- package/src/web/webapp-config.tsx +117 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import type { WebAppConfigResponse } from "../contracts";
|
|
3
|
+
import { appJson } from "./api-client";
|
|
4
|
+
|
|
5
|
+
export interface WebAppConfigState {
|
|
6
|
+
config?: WebAppConfigResponse;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error?: Error;
|
|
9
|
+
refresh: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const WebAppConfigContext = createContext<WebAppConfigState | null>(null);
|
|
13
|
+
|
|
14
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isLogLevelName(value: unknown): boolean {
|
|
19
|
+
return value === "trace" || value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isCurrentUser(value: unknown): boolean {
|
|
23
|
+
return isRecord(value)
|
|
24
|
+
&& typeof value.id === "string"
|
|
25
|
+
&& typeof value.username === "string"
|
|
26
|
+
&& (value.role === "owner" || value.role === "admin" || value.role === "user")
|
|
27
|
+
&& typeof value.isOwner === "boolean"
|
|
28
|
+
&& typeof value.isAdmin === "boolean";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasBooleanFields(value: unknown, fields: readonly string[]): boolean {
|
|
32
|
+
return isRecord(value) && fields.every((field) => typeof value[field] === "boolean");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isWebAppConfigResponse(value: unknown): value is WebAppConfigResponse {
|
|
36
|
+
return isRecord(value)
|
|
37
|
+
&& typeof value.appName === "string"
|
|
38
|
+
&& typeof value.version === "string"
|
|
39
|
+
&& (value.currentUser === undefined || isCurrentUser(value.currentUser))
|
|
40
|
+
&& hasBooleanFields(value.passkeyAuth, [
|
|
41
|
+
"enabled",
|
|
42
|
+
"passkeyConfigured",
|
|
43
|
+
"passkeyDisabled",
|
|
44
|
+
"passkeyRequired",
|
|
45
|
+
"authenticated",
|
|
46
|
+
"bootstrapRequired",
|
|
47
|
+
"ownerPasskeySetupRequired",
|
|
48
|
+
])
|
|
49
|
+
&& hasBooleanFields(value.userManagement, ["enabled", "canManageUsers"])
|
|
50
|
+
&& isRecord(value.logLevel)
|
|
51
|
+
&& isLogLevelName(value.logLevel.level)
|
|
52
|
+
&& typeof value.logLevel.fromEnv === "boolean"
|
|
53
|
+
&& hasBooleanFields(value.deviceAuth, ["enabled"])
|
|
54
|
+
&& hasBooleanFields(value.apiKeys, ["enabled"]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseWebAppConfigResponse(value: unknown): WebAppConfigResponse {
|
|
58
|
+
if (!isWebAppConfigResponse(value)) {
|
|
59
|
+
throw new Error("Web app configuration response was invalid.");
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toError(value: unknown): Error {
|
|
65
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function useWebAppConfig(consumer = "useWebAppConfig"): WebAppConfigState {
|
|
69
|
+
const context = useContext(WebAppConfigContext);
|
|
70
|
+
if (!context) {
|
|
71
|
+
throw new Error(`${consumer} must be used within the framework WebAppRoot.`);
|
|
72
|
+
}
|
|
73
|
+
return context;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function WebAppConfigProvider({ children }: { children: ReactNode }) {
|
|
77
|
+
const [config, setConfig] = useState<WebAppConfigResponse>();
|
|
78
|
+
const [loading, setLoading] = useState(true);
|
|
79
|
+
const [error, setError] = useState<Error>();
|
|
80
|
+
const requestIdRef = useRef(0);
|
|
81
|
+
|
|
82
|
+
const refresh = useCallback(async () => {
|
|
83
|
+
const requestId = requestIdRef.current + 1;
|
|
84
|
+
requestIdRef.current = requestId;
|
|
85
|
+
setLoading(true);
|
|
86
|
+
setError(undefined);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const response = parseWebAppConfigResponse(await appJson<unknown>("/api/config"));
|
|
90
|
+
if (requestId !== requestIdRef.current) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
setConfig(response);
|
|
94
|
+
} catch (value) {
|
|
95
|
+
if (requestId === requestIdRef.current) {
|
|
96
|
+
setError(toError(value));
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
if (requestId === requestIdRef.current) {
|
|
100
|
+
setLoading(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
void refresh();
|
|
107
|
+
}, [refresh]);
|
|
108
|
+
|
|
109
|
+
const state = useMemo<WebAppConfigState>(() => ({
|
|
110
|
+
config,
|
|
111
|
+
loading,
|
|
112
|
+
error,
|
|
113
|
+
refresh,
|
|
114
|
+
}), [config, error, loading, refresh]);
|
|
115
|
+
|
|
116
|
+
return <WebAppConfigContext.Provider value={state}>{children}</WebAppConfigContext.Provider>;
|
|
117
|
+
}
|