@protobase/ui 0.12.2 → 0.12.4
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/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +6 -2
- package/dist/app/meta-gate.js +47 -6
- package/dist/app/nav-from-meta.js +20 -1
- package/dist/app/pages/project-ui.d.ts +5 -1
- package/dist/app/pages/use-run-action.js +3 -2
- package/dist/app/record-page.js +115 -111
- package/dist/app/source-notice.js +47 -0
- package/dist/app-shell/sidebar-item.js +7 -1
- package/dist/data/api-provider.d.ts +46 -3
- package/dist/data/api-provider.js +57 -3
- package/dist/data/meta-context.js +6 -0
- package/dist/data/meta-data.d.ts +15 -1
- package/dist/data/query-keys.js +8 -0
- package/dist/data/use-meta.d.ts +4 -2
- package/dist/data/use-meta.js +15 -3
- package/dist/data/use-source-metas.js +61 -0
- package/dist/data/with-sources.js +38 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-C1uXjaoy.js +65 -0
- package/dist/shell/assets/{index-DheSSTn2.css → index-BVOqIEYd.css} +1 -1
- package/dist/shell/assets/{index-ACvBBjQJ.js → index-uIZKnNIY.js} +1 -1
- package/dist/shell/assets/shared-protobase-ui-DDq8y6_Q.js +1 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-BpWT2V8v.js +0 -65
- package/dist/shell/assets/shared-protobase-ui-DLJ-Jjub.js +0 -1
|
@@ -5,17 +5,42 @@ import { jsx } from "react/jsx-runtime";
|
|
|
5
5
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
6
|
//#region src/data/api-provider.tsx
|
|
7
7
|
const ApiContext = createContext(null);
|
|
8
|
+
const SourceClientsContext = createContext(null);
|
|
9
|
+
const ResourceClientContext = createContext(null);
|
|
10
|
+
/** The API client: the app's own, or below `MetaGate` one that sends each resource's calls to the API that serves it. */
|
|
8
11
|
const useClient = () => {
|
|
9
12
|
const client = useContext(ApiContext);
|
|
10
13
|
if (!client) throw new Error("useClient must be used inside ApiProvider");
|
|
11
14
|
return client;
|
|
12
15
|
};
|
|
13
16
|
/**
|
|
17
|
+
* The client of the API that serves `resource`: a source's for a resource the app draws from one, the app's own
|
|
18
|
+
* otherwise. A source's client calls that API's functions with `invoke`, where `useClient()` calls the app's own.
|
|
19
|
+
*/
|
|
20
|
+
const useResourceClient = (resource) => {
|
|
21
|
+
const own = useClient();
|
|
22
|
+
return useContext(ResourceClientContext)?.(resource) ?? own;
|
|
23
|
+
};
|
|
24
|
+
/** The client of a source's API, one per URL, with the signed-in user's token; for `MetaGate`. */
|
|
25
|
+
const useSourceClients = () => {
|
|
26
|
+
const clientOf = useContext(SourceClientsContext);
|
|
27
|
+
if (!clientOf) throw new Error("useSourceClients must be used inside ApiProvider");
|
|
28
|
+
return clientOf;
|
|
29
|
+
};
|
|
30
|
+
/** Gives what is below it `client` from `useClient()`, and the client of a resource's API from `useResourceClient()`. */
|
|
31
|
+
const ClientProvider = ({ client, resourceClient, children }) => /* @__PURE__ */ jsx(ApiContext.Provider, {
|
|
32
|
+
value: client,
|
|
33
|
+
children: /* @__PURE__ */ jsx(ResourceClientContext.Provider, {
|
|
34
|
+
value: resourceClient,
|
|
35
|
+
children
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
14
39
|
* Owns the query cache and the API client; refetches `/meta` whenever `X-Meta-Version` changes. The version is
|
|
15
40
|
* compared only with the last one seen: it hashes the models and roles, while the `/meta` ETag hashes the whole
|
|
16
41
|
* per-caller body, so the two never match.
|
|
17
42
|
*/
|
|
18
|
-
const ApiProvider = ({ baseUrl, client: given, fetch, queryClient: givenQueryClient, token, onUnauthenticated, children }) => {
|
|
43
|
+
const ApiProvider = ({ baseUrl, client: given, sources, fetch, queryClient: givenQueryClient, token, onUnauthenticated, children }) => {
|
|
19
44
|
const [queryClient] = useState(() => givenQueryClient ?? new QueryClient({ defaultOptions: { queries: {
|
|
20
45
|
staleTime: 3e4,
|
|
21
46
|
retry: false,
|
|
@@ -41,13 +66,42 @@ const ApiProvider = ({ baseUrl, client: given, fetch, queryClient: givenQueryCli
|
|
|
41
66
|
token,
|
|
42
67
|
onUnauthenticated
|
|
43
68
|
]);
|
|
69
|
+
const sourceClient = useMemo(() => {
|
|
70
|
+
const made = /* @__PURE__ */ new Map();
|
|
71
|
+
return (source) => {
|
|
72
|
+
const ready = sources?.[source.url] ?? made.get(source.url);
|
|
73
|
+
if (ready) return ready;
|
|
74
|
+
let seen;
|
|
75
|
+
const client = createClient({
|
|
76
|
+
baseUrl: source.url,
|
|
77
|
+
fetch,
|
|
78
|
+
token,
|
|
79
|
+
onMetaVersion: (version) => {
|
|
80
|
+
const previous = seen;
|
|
81
|
+
seen = version;
|
|
82
|
+
if (previous === void 0 || previous === version) return;
|
|
83
|
+
queryClient.invalidateQueries({ queryKey: keys.sourceMetas(source.url) }, { cancelRefetch: false });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
made.set(source.url, client);
|
|
87
|
+
return client;
|
|
88
|
+
};
|
|
89
|
+
}, [
|
|
90
|
+
sources,
|
|
91
|
+
fetch,
|
|
92
|
+
token,
|
|
93
|
+
queryClient
|
|
94
|
+
]);
|
|
44
95
|
return /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
45
96
|
client: queryClient,
|
|
46
97
|
children: /* @__PURE__ */ jsx(ApiContext.Provider, {
|
|
47
98
|
value: client,
|
|
48
|
-
children
|
|
99
|
+
children: /* @__PURE__ */ jsx(SourceClientsContext.Provider, {
|
|
100
|
+
value: sourceClient,
|
|
101
|
+
children
|
|
102
|
+
})
|
|
49
103
|
})
|
|
50
104
|
});
|
|
51
105
|
};
|
|
52
106
|
//#endregion
|
|
53
|
-
export { ApiProvider, useClient };
|
|
107
|
+
export { ApiProvider, ClientProvider, useClient, useResourceClient, useSourceClients };
|
package/dist/data/meta-data.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginRef, ResourcePermissions } from "@protobase/client";
|
|
1
|
+
import { PluginRef, ResourcePermissions, SourceRef } from "@protobase/client";
|
|
2
2
|
import { BottomBarModel, ResourceModel, UserMenuModel, ViewModel } from "@protobase/schema";
|
|
3
3
|
import { PageModel } from "@protobase/layout";
|
|
4
4
|
//#region src/data/meta-data.d.ts
|
|
@@ -22,5 +22,19 @@ export type MetaData = {
|
|
|
22
22
|
};
|
|
23
23
|
/** The plugins the app loads for the user (see `PluginGate`). */
|
|
24
24
|
plugins?: PluginRef[];
|
|
25
|
+
/** The other APIs the app draws resources from for the user, and how far their `/meta` has loaded (see `withSources`). */
|
|
26
|
+
sources?: SourceStatus[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A source of resources and its `/meta`: `ready` once its resources are among the app's, `error` when it could not be
|
|
30
|
+
* read, the reason in `error`.
|
|
31
|
+
*/
|
|
32
|
+
export type SourceStatus = {
|
|
33
|
+
source: SourceRef;
|
|
34
|
+
state: 'loading' | 'ready';
|
|
35
|
+
} | {
|
|
36
|
+
source: SourceRef;
|
|
37
|
+
state: 'error';
|
|
38
|
+
error: Error;
|
|
25
39
|
};
|
|
26
40
|
//#endregion
|
package/dist/data/query-keys.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
//#region src/data/query-keys.ts
|
|
2
2
|
const keys = {
|
|
3
3
|
meta: ["meta"],
|
|
4
|
+
/** A source's `/meta`, as far as the app draws it: its URL, and the resources the app lists for it. */
|
|
5
|
+
sourceMeta: (url, resources) => [
|
|
6
|
+
"source-meta",
|
|
7
|
+
url,
|
|
8
|
+
resources.join(",")
|
|
9
|
+
],
|
|
10
|
+
/** Every `/meta` query of the source at `url`. */
|
|
11
|
+
sourceMetas: (url) => ["source-meta", url],
|
|
4
12
|
list: (resource, filter, orderBy, pageSize, startToken) => [
|
|
5
13
|
"list",
|
|
6
14
|
resource,
|
package/dist/data/use-meta.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { MetaData } from "./meta-data.js";
|
|
2
|
+
import { UseQueryResult } from "@tanstack/react-query";
|
|
2
3
|
//#region src/data/use-meta.d.ts
|
|
3
4
|
/**
|
|
4
5
|
* The resource and view models. Refetches on window focus with `If-None-Match`, so an unchanged
|
|
5
|
-
* `/meta` costs a 304, and the cached object keeps its identity.
|
|
6
|
+
* `/meta` costs a 304, and the cached object keeps its identity. Below the app's `MetaGate`, `data` holds the resources
|
|
7
|
+
* of its sources too, as every page of the app reads them.
|
|
6
8
|
*/
|
|
7
|
-
export declare const useMeta: () =>
|
|
9
|
+
export declare const useMeta: () => UseQueryResult<MetaData>;
|
|
8
10
|
//#endregion
|
package/dist/data/use-meta.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
|
+
import { MetaContext } from "./meta-context.js";
|
|
4
|
+
import { useContext, useMemo } from "react";
|
|
3
5
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
6
|
//#region src/data/use-meta.ts
|
|
5
7
|
/**
|
|
6
8
|
* The resource and view models. Refetches on window focus with `If-None-Match`, so an unchanged
|
|
7
|
-
* `/meta` costs a 304, and the cached object keeps its identity.
|
|
9
|
+
* `/meta` costs a 304, and the cached object keeps its identity. Below the app's `MetaGate`, `data` holds the resources
|
|
10
|
+
* of its sources too, as every page of the app reads them.
|
|
8
11
|
*/
|
|
9
12
|
const useMeta = () => {
|
|
10
13
|
const client = useClient();
|
|
11
14
|
const queryClient = useQueryClient();
|
|
12
|
-
|
|
15
|
+
const merged = useContext(MetaContext);
|
|
16
|
+
const query = useQuery({
|
|
13
17
|
queryKey: keys.meta,
|
|
14
18
|
staleTime: 0,
|
|
15
19
|
refetchOnWindowFocus: "always",
|
|
@@ -30,10 +34,18 @@ const useMeta = () => {
|
|
|
30
34
|
...result.meta.bottomBar && { bottomBar: result.meta.bottomBar },
|
|
31
35
|
...result.meta.assistant && { assistant: result.meta.assistant },
|
|
32
36
|
...result.meta.runtime && { runtime: result.meta.runtime },
|
|
33
|
-
...result.meta.plugins && { plugins: result.meta.plugins }
|
|
37
|
+
...result.meta.plugins && { plugins: result.meta.plugins },
|
|
38
|
+
...result.meta.sources && { sources: result.meta.sources.map((source) => ({
|
|
39
|
+
source,
|
|
40
|
+
state: "loading"
|
|
41
|
+
})) }
|
|
34
42
|
};
|
|
35
43
|
}
|
|
36
44
|
});
|
|
45
|
+
return useMemo(() => merged && query.data ? {
|
|
46
|
+
...query,
|
|
47
|
+
data: merged
|
|
48
|
+
} : query, [merged, query]);
|
|
37
49
|
};
|
|
38
50
|
//#endregion
|
|
39
51
|
export { useMeta };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { keys } from "./query-keys.js";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { sourceMeta } from "@protobase/client";
|
|
4
|
+
import { useQueries, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
//#region src/data/use-source-metas.ts
|
|
6
|
+
/** How long the app waits for a source's `/meta` before it shows the source's resources as unavailable. */
|
|
7
|
+
const sourceTimeoutMs = 1e4;
|
|
8
|
+
const combine = (results) => results.map((result) => ({
|
|
9
|
+
data: result.data?.meta,
|
|
10
|
+
error: result.error
|
|
11
|
+
}));
|
|
12
|
+
/**
|
|
13
|
+
* Each source's `/meta`, read with the user's token from that API and scoped to the resources the app draws from it
|
|
14
|
+
* (`sourceMeta`). Like the app's own, it is read again on window focus with `If-None-Match`, and a source that cannot be
|
|
15
|
+
* read is an `error` without holding up the others or the app.
|
|
16
|
+
*/
|
|
17
|
+
const useSourceMetas = (sources, clientOf) => {
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
const results = useQueries({
|
|
20
|
+
queries: sources.map((source) => {
|
|
21
|
+
const queryKey = keys.sourceMeta(source.url, source.resources);
|
|
22
|
+
return {
|
|
23
|
+
queryKey,
|
|
24
|
+
staleTime: 0,
|
|
25
|
+
refetchOnWindowFocus: "always",
|
|
26
|
+
queryFn: async () => {
|
|
27
|
+
const previous = queryClient.getQueryData(queryKey);
|
|
28
|
+
const result = await clientOf(source).meta(previous?.etag, { signal: AbortSignal.timeout(sourceTimeoutMs) });
|
|
29
|
+
if (result.status === "unchanged") {
|
|
30
|
+
if (!previous) throw new Error(`${source.label} answered 304 to a request without If-None-Match`);
|
|
31
|
+
return previous;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
etag: result.etag,
|
|
35
|
+
meta: sourceMeta(result.meta, source)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}),
|
|
40
|
+
combine
|
|
41
|
+
});
|
|
42
|
+
return useMemo(() => sources.map((source, index) => {
|
|
43
|
+
const { data, error } = results[index];
|
|
44
|
+
if (data) return {
|
|
45
|
+
source,
|
|
46
|
+
state: "ready",
|
|
47
|
+
meta: data
|
|
48
|
+
};
|
|
49
|
+
if (error) return {
|
|
50
|
+
source,
|
|
51
|
+
state: "error",
|
|
52
|
+
error
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
source,
|
|
56
|
+
state: "loading"
|
|
57
|
+
};
|
|
58
|
+
}), [sources, results]);
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { sourceTimeoutMs, useSourceMetas };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/data/with-sources.ts
|
|
2
|
+
/**
|
|
3
|
+
* The app's `/meta` with the resources it draws from its sources: those of every source that is ready, with their views
|
|
4
|
+
* and permissions, as if they were the app's own, and how far each source has loaded in `sources`. A source that is
|
|
5
|
+
* loading or cannot be read adds no resources; the app shows its resources as unavailable (see `sourceStatusOf`).
|
|
6
|
+
*/
|
|
7
|
+
const withSources = (meta, results) => {
|
|
8
|
+
if (results.length === 0) return meta;
|
|
9
|
+
const ready = results.flatMap((result) => result.state === "ready" ? [result.meta] : []);
|
|
10
|
+
const sources = results.map((result) => result.state === "error" ? {
|
|
11
|
+
source: result.source,
|
|
12
|
+
state: "error",
|
|
13
|
+
error: result.error
|
|
14
|
+
} : {
|
|
15
|
+
source: result.source,
|
|
16
|
+
state: result.state
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
...meta,
|
|
20
|
+
resources: {
|
|
21
|
+
...meta.resources,
|
|
22
|
+
...Object.fromEntries(ready.flatMap((part) => part.resources.map((model) => [model.name, model])))
|
|
23
|
+
},
|
|
24
|
+
views: {
|
|
25
|
+
...meta.views,
|
|
26
|
+
...Object.fromEntries(ready.flatMap((part) => part.views.map((view) => [view.resource, view])))
|
|
27
|
+
},
|
|
28
|
+
permissions: {
|
|
29
|
+
...meta.permissions,
|
|
30
|
+
...Object.assign({}, ...ready.map((part) => part.permissions))
|
|
31
|
+
},
|
|
32
|
+
sources
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/** The source the app draws `resource` from, with how far it has loaded; `undefined` for a resource of the app's own. */
|
|
36
|
+
const sourceStatusOf = (meta, resource) => meta.sources?.find((status) => status.source.resources.includes(resource));
|
|
37
|
+
//#endregion
|
|
38
|
+
export { sourceStatusOf, withSources };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ActionContext, ActionHandler, ProjectUi, definePlugin, defineUi } from
|
|
|
3
3
|
import { PluginInfo, usePlugin } from "./plugins/plugin-context.js";
|
|
4
4
|
import { App, AppProps } from "./app/app.js";
|
|
5
5
|
import "./app/index.js";
|
|
6
|
-
import { ApiProvider, ApiProviderProps, useClient } from "./data/api-provider.js";
|
|
6
|
+
import { ApiProvider, ApiProviderProps, useClient, useResourceClient } from "./data/api-provider.js";
|
|
7
7
|
import { MetaData } from "./data/meta-data.js";
|
|
8
8
|
import { useMeta } from "./data/use-meta.js";
|
|
9
9
|
import { useList } from "./data/use-list.js";
|
|
@@ -39,4 +39,4 @@ import { Step, StepList, StepState } from "./step-list/step-list.js";
|
|
|
39
39
|
import { DiffView, DiffViewProps } from "./diff-view/diff-view.js";
|
|
40
40
|
import { DiffLine, diffStats, parseDiff } from "./diff-view/diff.js";
|
|
41
41
|
import { CompactTable, CompactTableProps } from "./compact-table/compact-table.js";
|
|
42
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, SidePanel, type SidePanelProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useSeries, useUpdateRecord };
|
|
42
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, SidePanel, type SidePanelProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiProvider, useClient } from "./data/api-provider.js";
|
|
1
|
+
import { ApiProvider, useClient, useResourceClient } from "./data/api-provider.js";
|
|
2
2
|
import { useApiToken } from "./auth/use-api-token.js";
|
|
3
3
|
import { useRecord } from "./data/use-record.js";
|
|
4
4
|
import { ChatMessage } from "./chat/chat-message.js";
|
|
@@ -30,4 +30,4 @@ import { useList } from "./data/use-list.js";
|
|
|
30
30
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
31
31
|
import { App } from "./app/app.js";
|
|
32
32
|
import { RecordLink } from "./app/displays/record-link.js";
|
|
33
|
-
export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, PartView, RecordLink, SidePanel, StepList, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useSeries, useUpdateRecord };
|
|
33
|
+
export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, PartView, RecordLink, SidePanel, StepList, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
|
package/dist/package.js
CHANGED