@plantops/web-kit 0.1.0
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 +88 -0
- package/dist/claims.d.ts +35 -0
- package/dist/claims.d.ts.map +1 -0
- package/dist/claims.js +76 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +52 -0
- package/dist/grants-provider.d.ts +51 -0
- package/dist/grants-provider.d.ts.map +1 -0
- package/dist/grants-provider.js +46 -0
- package/dist/iam-provider.d.ts +95 -0
- package/dist/iam-provider.d.ts.map +1 -0
- package/dist/iam-provider.js +188 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/plantops-provider.d.ts +48 -0
- package/dist/plantops-provider.d.ts.map +1 -0
- package/dist/plantops-provider.js +8 -0
- package/dist/require-auth.d.ts +45 -0
- package/dist/require-auth.d.ts.map +1 -0
- package/dist/require-auth.js +58 -0
- package/dist/scope-coverage.d.ts +54 -0
- package/dist/scope-coverage.d.ts.map +1 -0
- package/dist/scope-coverage.js +41 -0
- package/dist/token-store.d.ts +92 -0
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +144 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/use-async.d.ts +43 -0
- package/dist/use-async.d.ts.map +1 -0
- package/dist/use-async.js +76 -0
- package/dist/use-navigation.d.ts +32 -0
- package/dist/use-navigation.d.ts.map +1 -0
- package/dist/use-navigation.js +24 -0
- package/dist/use-notices.d.ts +47 -0
- package/dist/use-notices.d.ts.map +1 -0
- package/dist/use-notices.js +60 -0
- package/dist/use-permission.d.ts +91 -0
- package/dist/use-permission.d.ts.map +1 -0
- package/dist/use-permission.js +48 -0
- package/package.json +49 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* "Fetch this when the screen opens, and tell me how it went."
|
|
4
|
+
*
|
|
5
|
+
* Every screen in every console does the same four things: call one endpoint,
|
|
6
|
+
* show a skeleton while it is in flight, show the failure if it fails, and
|
|
7
|
+
* offer a reload. Written per screen, it is four `useState`s and a race
|
|
8
|
+
* condition — the classic one where a slow first request resolves after a fast
|
|
9
|
+
* second and overwrites it.
|
|
10
|
+
*
|
|
11
|
+
* This is deliberately small. It is not a data-fetching library: no cache, no
|
|
12
|
+
* deduplication, no background revalidation. Those matter when many components
|
|
13
|
+
* request the same thing; an admin console's screens each own their data, and
|
|
14
|
+
* the two things that genuinely are shared — grants and navigation — have their
|
|
15
|
+
* own providers precisely so they are fetched once.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
/**
|
|
19
|
+
* Runs `request` when `deps` change, and again on `reload()`.
|
|
20
|
+
*
|
|
21
|
+
* `request` is called with an `AbortSignal`; passing it on is optional but
|
|
22
|
+
* turns an abandoned request into a cancelled one. Results from a superseded
|
|
23
|
+
* call are dropped either way — the generation counter below is what makes the
|
|
24
|
+
* race impossible rather than merely unlikely.
|
|
25
|
+
*/
|
|
26
|
+
export function useAsync(request, deps, options = {}) {
|
|
27
|
+
const { enabled = true } = options;
|
|
28
|
+
const [data, setData] = React.useState(undefined);
|
|
29
|
+
const [error, setError] = React.useState(null);
|
|
30
|
+
const [loading, setLoading] = React.useState(enabled);
|
|
31
|
+
const [nonce, setNonce] = React.useState(0);
|
|
32
|
+
// The latest `request` without making it a dependency: a caller writing an
|
|
33
|
+
// inline arrow — which is every caller — would otherwise re-fetch on every
|
|
34
|
+
// render. `deps` is the caller's explicit statement of what the request
|
|
35
|
+
// depends on, and it is the only thing that should trigger one.
|
|
36
|
+
const requestRef = React.useRef(request);
|
|
37
|
+
requestRef.current = request;
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (!enabled) {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
let current = true;
|
|
45
|
+
setLoading(true);
|
|
46
|
+
requestRef
|
|
47
|
+
.current(controller.signal)
|
|
48
|
+
.then((value) => {
|
|
49
|
+
if (!current)
|
|
50
|
+
return;
|
|
51
|
+
setData(value);
|
|
52
|
+
setError(null);
|
|
53
|
+
})
|
|
54
|
+
.catch((cause) => {
|
|
55
|
+
if (!current || controller.signal.aborted)
|
|
56
|
+
return;
|
|
57
|
+
setError(cause);
|
|
58
|
+
})
|
|
59
|
+
.finally(() => {
|
|
60
|
+
if (current)
|
|
61
|
+
setLoading(false);
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
current = false;
|
|
65
|
+
controller.abort();
|
|
66
|
+
};
|
|
67
|
+
// `deps` is the caller's dependency list by design, and `request` is read
|
|
68
|
+
// through a ref so an inline arrow does not re-fetch on every render. The
|
|
69
|
+
// spread is what the rule cannot verify statically, and is the point.
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, [enabled, nonce, ...deps]);
|
|
72
|
+
const reload = React.useCallback(() => {
|
|
73
|
+
setNonce((value) => value + 1);
|
|
74
|
+
}, []);
|
|
75
|
+
return { data, error, loading, reload };
|
|
76
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The menu, from `GET /iam/navigation` (Doc 05 §4).
|
|
3
|
+
*
|
|
4
|
+
* A hook rather than a provider because a console fetches it once, in the shell
|
|
5
|
+
* layout, and passes the tree down as a prop to whatever renders it — the
|
|
6
|
+
* sidebar, a mobile drawer, a command palette. Wrapping it in a context would
|
|
7
|
+
* add an indirection for a single consumer.
|
|
8
|
+
*
|
|
9
|
+
* It is not cached here, for the reason `libs/iam-client/src/endpoints/
|
|
10
|
+
* navigation.ts` gives: the menu changes when the *catalog* changes as well as
|
|
11
|
+
* when grants change, it is fetched once per shell load rather than once per
|
|
12
|
+
* request, and there is no burst for a cache to absorb — only staleness to
|
|
13
|
+
* introduce. {@link UseNavigationResult.reload} exists for the platform admin
|
|
14
|
+
* who has just edited the catalog and wants to see it (Doc 09 §2.1).
|
|
15
|
+
*/
|
|
16
|
+
import type { NavigationResponse, NavNodeDTO } from '@plantops/contracts';
|
|
17
|
+
export interface UseNavigationResult {
|
|
18
|
+
/** The response, once it lands. */
|
|
19
|
+
navigation: NavigationResponse | undefined;
|
|
20
|
+
/** The tree alone — `[]` while loading, and for a subject who may see nothing. */
|
|
21
|
+
tree: NavNodeDTO[];
|
|
22
|
+
loading: boolean;
|
|
23
|
+
error: unknown;
|
|
24
|
+
reload: () => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* @param applicationId Narrows to one application's menu. Omit for the
|
|
28
|
+
* cross-application shell — one top-level node per enabled application
|
|
29
|
+
* (Doc 05 §4), which is what the admin console renders.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useNavigation(applicationId?: string): UseNavigationResult;
|
|
32
|
+
//# sourceMappingURL=use-navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-navigation.d.ts","sourceRoot":"","sources":["../src/use-navigation.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAK1E,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,UAAU,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC3C,kFAAkF;IAClF,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAoBzE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useAuth, useIam } from './iam-provider';
|
|
3
|
+
import { useAsync } from './use-async';
|
|
4
|
+
/**
|
|
5
|
+
* @param applicationId Narrows to one application's menu. Omit for the
|
|
6
|
+
* cross-application shell — one top-level node per enabled application
|
|
7
|
+
* (Doc 05 §4), which is what the admin console renders.
|
|
8
|
+
*/
|
|
9
|
+
export function useNavigation(applicationId) {
|
|
10
|
+
const client = useIam();
|
|
11
|
+
const { status, subject } = useAuth();
|
|
12
|
+
const state = useAsync(() => client.navigation.tree(applicationId === undefined ? {} : { applicationId }),
|
|
13
|
+
// Re-fetched when the subject changes: the menu is a projection of *their*
|
|
14
|
+
// grants, so serving the previous user's menu would be both wrong and
|
|
15
|
+
// alarming.
|
|
16
|
+
[client, subject?.id, subject?.sessionId, applicationId], { enabled: status === 'authenticated' });
|
|
17
|
+
return {
|
|
18
|
+
navigation: state.data,
|
|
19
|
+
tree: state.data?.tree ?? [],
|
|
20
|
+
loading: state.loading,
|
|
21
|
+
error: state.error,
|
|
22
|
+
reload: state.reload,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type DescribedError } from './errors';
|
|
3
|
+
export interface Notices {
|
|
4
|
+
/**
|
|
5
|
+
* Reports a failure and hands back what it was, so a caller that also wants
|
|
6
|
+
* to render it inline does not describe it twice.
|
|
7
|
+
*/
|
|
8
|
+
error: (error: unknown, options?: {
|
|
9
|
+
title?: string;
|
|
10
|
+
}) => DescribedError;
|
|
11
|
+
success: (text: string) => void;
|
|
12
|
+
info: (text: string) => void;
|
|
13
|
+
/**
|
|
14
|
+
* The Doc 09 §4 notice: "after a role/binding change, surface that access
|
|
15
|
+
* updates may take a few seconds (cache invalidation, Doc 04 §7)".
|
|
16
|
+
*
|
|
17
|
+
* Every screen that grants, revokes, or changes what a role carries owes the
|
|
18
|
+
* admin this sentence. Without it, the admin checks immediately, sees the old
|
|
19
|
+
* access, and concludes the change did not save — the single most predictable
|
|
20
|
+
* misreading of a correctly-working cache.
|
|
21
|
+
*/
|
|
22
|
+
accessChanged: (text?: string) => void;
|
|
23
|
+
/**
|
|
24
|
+
* "Are you sure?", resolving to what they chose.
|
|
25
|
+
*
|
|
26
|
+
* Here for the same reason `error` is: `Modal.confirm(…)` called statically
|
|
27
|
+
* renders outside `ConfigProvider`, so it arrives unthemed and, in dark mode,
|
|
28
|
+
* close to unreadable. `App.useApp()` gives the hooked version, and
|
|
29
|
+
* `PlantOpsThemeProvider` mounts the `App` it needs.
|
|
30
|
+
*
|
|
31
|
+
* A promise rather than an `onOk` callback because the thing a caller does
|
|
32
|
+
* next is almost always `await` — deactivate, then reload — and threading
|
|
33
|
+
* that through a callback turns one linear function into two.
|
|
34
|
+
*/
|
|
35
|
+
confirm: (options: ConfirmOptions) => Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
export interface ConfirmOptions {
|
|
38
|
+
title: string;
|
|
39
|
+
/** What the action does, and what it does *not* do. Worth writing. */
|
|
40
|
+
content?: React.ReactNode;
|
|
41
|
+
okText?: string;
|
|
42
|
+
cancelText?: string;
|
|
43
|
+
/** Renders the confirm button in the danger tone. */
|
|
44
|
+
danger?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function useNotices(): Notices;
|
|
47
|
+
//# sourceMappingURL=use-notices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-notices.d.ts","sourceRoot":"","sources":["../src/use-notices.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAE9D,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,cAAc,CAAC;IACxE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B;;;;;;;;OAQG;IACH,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;;;;;;;;;;OAWG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,UAAU,IAAI,OAAO,CAkFpC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Feedback: what the console says after something succeeded, failed, or will
|
|
5
|
+
* take a moment to take effect.
|
|
6
|
+
*
|
|
7
|
+
* Goes through antd's `App.useApp()` hooks rather than the static
|
|
8
|
+
* `message.error(…)` functions, because the static ones render outside the
|
|
9
|
+
* React tree and therefore outside `ConfigProvider` — they come out unthemed,
|
|
10
|
+
* and in dark mode nearly unreadable. `PlantOpsThemeProvider` mounts the `App`
|
|
11
|
+
* these hooks need.
|
|
12
|
+
*
|
|
13
|
+
* ## Why an error toast is a *notification* and a success is a *message*
|
|
14
|
+
*
|
|
15
|
+
* A success is an acknowledgement: it needs a second of the user's attention
|
|
16
|
+
* and then should get out of the way. A failure needs to be read, may carry a
|
|
17
|
+
* request id worth copying, and must not vanish while the user is still working
|
|
18
|
+
* out what happened — so it stays until dismissed.
|
|
19
|
+
*/
|
|
20
|
+
import { App } from 'antd';
|
|
21
|
+
import * as React from 'react';
|
|
22
|
+
import { describeError } from './errors';
|
|
23
|
+
export function useNotices() {
|
|
24
|
+
const { message, modal, notification } = App.useApp();
|
|
25
|
+
return React.useMemo(() => ({
|
|
26
|
+
error: (error, options) => {
|
|
27
|
+
const described = describeError(error);
|
|
28
|
+
notification.error({
|
|
29
|
+
message: options?.title ?? described.copy.title,
|
|
30
|
+
description: (_jsxs(_Fragment, { children: [_jsx("div", { children: described.copy.description }), described.details.length > 0 && (_jsx("ul", { style: { margin: '8px 0 0', paddingInlineStart: 20 }, children: described.details.map((detail) => (_jsxs("li", { children: [_jsx("strong", { children: detail.field }), " \u2014 ", detail.message] }, `${detail.field}:${detail.message}`))) })), described.requestId !== null && (_jsxs("div", { style: { marginBlockStart: 8, fontSize: 12, opacity: 0.75 }, children: ["Request ", described.requestId] }))] })),
|
|
31
|
+
duration: 0,
|
|
32
|
+
});
|
|
33
|
+
return described;
|
|
34
|
+
},
|
|
35
|
+
success: (text) => {
|
|
36
|
+
void message.success(text);
|
|
37
|
+
},
|
|
38
|
+
info: (text) => {
|
|
39
|
+
void message.info(text);
|
|
40
|
+
},
|
|
41
|
+
accessChanged: (text) => {
|
|
42
|
+
void message.info(text ??
|
|
43
|
+
'Saved. Access changes can take a few seconds to reach every screen.', 4);
|
|
44
|
+
},
|
|
45
|
+
confirm: (options) => new Promise((resolve) => {
|
|
46
|
+
modal.confirm({
|
|
47
|
+
title: options.title,
|
|
48
|
+
content: options.content,
|
|
49
|
+
okText: options.okText ?? 'Confirm',
|
|
50
|
+
cancelText: options.cancelText ?? 'Cancel',
|
|
51
|
+
okButtonProps: options.danger === true ? { danger: true } : undefined,
|
|
52
|
+
onOk: () => resolve(true),
|
|
53
|
+
// Fires for the cancel button, the close icon and the mask click
|
|
54
|
+
// alike, so every way out of the dialog resolves rather than
|
|
55
|
+
// leaving the caller's `await` pending forever.
|
|
56
|
+
onCancel: () => resolve(false),
|
|
57
|
+
});
|
|
58
|
+
}),
|
|
59
|
+
}), [message, modal, notification]);
|
|
60
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission-aware controls (Doc 09 §4).
|
|
3
|
+
*
|
|
4
|
+
* "Buttons/actions the subject lacks permission for are hidden/disabled (UX
|
|
5
|
+
* only; server enforces)." Every screen session from Session 28 onward uses
|
|
6
|
+
* this hook, which is why it is here rather than in `admin-web`: gatepass and
|
|
7
|
+
* visitor hide their buttons with the same code.
|
|
8
|
+
*
|
|
9
|
+
* ## Hide or disable?
|
|
10
|
+
*
|
|
11
|
+
* Hide by default. A disabled button is a promise that something exists and
|
|
12
|
+
* could be reached, which invites a support ticket asking how; an absent one
|
|
13
|
+
* says nothing. Disable when the control's absence would make a layout
|
|
14
|
+
* incoherent — a toolbar with one button left, a table column of actions where
|
|
15
|
+
* some rows are actionable and some are not — and give it a `title` saying
|
|
16
|
+
* which permission is missing, because "greyed out and unexplained" is the
|
|
17
|
+
* worst of both.
|
|
18
|
+
*
|
|
19
|
+
* ## What this is not
|
|
20
|
+
*
|
|
21
|
+
* It is not authorisation. `usePermission` reads the resolved grants the server
|
|
22
|
+
* sent this browser; anything the browser can read, the browser can lie about.
|
|
23
|
+
* The server checks again on every call (`PermissionGuard`, Session 23) and a
|
|
24
|
+
* deep link into a hidden screen still 403s (Doc 09 §4). This exists so a user
|
|
25
|
+
* is not shown doors they cannot open — not so the doors are locked.
|
|
26
|
+
*
|
|
27
|
+
* ## While grants are loading
|
|
28
|
+
*
|
|
29
|
+
* `usePermission` answers `false` until they arrive. Showing a control and then
|
|
30
|
+
* removing it is worse than showing it a moment late, and a screen that wants
|
|
31
|
+
* to hold the whole thing back reads `loading` and renders a skeleton.
|
|
32
|
+
*/
|
|
33
|
+
import type { PermissionKey, ScopePath } from '@plantops/contracts';
|
|
34
|
+
import * as React from 'react';
|
|
35
|
+
export interface PermissionQuery {
|
|
36
|
+
/**
|
|
37
|
+
* Restrict the question to a place in the org tree — "may I do this *here*".
|
|
38
|
+
*
|
|
39
|
+
* Omit for the broad question ("may I do this anywhere"), which is what a nav
|
|
40
|
+
* item or a list screen asks: Doc 05 §3 is explicit that visibility is
|
|
41
|
+
* permission-based, not scope-based, and that scope filters data *within* a
|
|
42
|
+
* screen.
|
|
43
|
+
*/
|
|
44
|
+
scopePath?: ScopePath;
|
|
45
|
+
}
|
|
46
|
+
export interface PermissionApi {
|
|
47
|
+
/** Does the subject hold this permission (optionally, at this node)? */
|
|
48
|
+
can: (permission: PermissionKey, query?: PermissionQuery) => boolean;
|
|
49
|
+
/** Any of them — the OR a nav node's `menu_permission` rows express. */
|
|
50
|
+
canAny: (permissions: readonly PermissionKey[], query?: PermissionQuery) => boolean;
|
|
51
|
+
/** All of them — for a screen that genuinely needs two. */
|
|
52
|
+
canAll: (permissions: readonly PermissionKey[], query?: PermissionQuery) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The minimal covering paths for a permission (Doc 04 §4.1).
|
|
55
|
+
*
|
|
56
|
+
* What a screen narrows its query by: "show me the gates I may check in at"
|
|
57
|
+
* rather than "show me every gate and let the server refuse most of them".
|
|
58
|
+
*/
|
|
59
|
+
scopesFor: (permission: PermissionKey) => readonly ScopePath[];
|
|
60
|
+
/** True until the first resolve lands; every answer above is `false` meanwhile. */
|
|
61
|
+
loading: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** The whole permission surface, for a screen asking several questions. */
|
|
64
|
+
export declare function usePermissions(): PermissionApi;
|
|
65
|
+
/**
|
|
66
|
+
* One permission, as a boolean — the common case.
|
|
67
|
+
*
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const canCreate = usePermission('iam.client.user.create');
|
|
70
|
+
* {canCreate && <Button type="primary">Add user</Button>}
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare function usePermission(permission: PermissionKey, query?: PermissionQuery): boolean;
|
|
74
|
+
export interface PermittedProps {
|
|
75
|
+
/** Held (at `scopePath`, if given) to render `children`. */
|
|
76
|
+
permission: PermissionKey | readonly PermissionKey[];
|
|
77
|
+
scopePath?: ScopePath;
|
|
78
|
+
children: React.ReactNode;
|
|
79
|
+
/** Rendered instead when the subject lacks it. Usually nothing. */
|
|
80
|
+
fallback?: React.ReactNode;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Renders `children` only if the subject holds the permission.
|
|
84
|
+
*
|
|
85
|
+
* The JSX form of {@link usePermission}, for a block of markup that would
|
|
86
|
+
* otherwise need a variable and a conditional. An array means "any of these",
|
|
87
|
+
* matching the OR semantics `menu_permission` uses for nav visibility
|
|
88
|
+
* (Doc 05 §3).
|
|
89
|
+
*/
|
|
90
|
+
export declare function Permitted({ permission, scopePath, children, fallback, }: PermittedProps): React.ReactNode;
|
|
91
|
+
//# sourceMappingURL=use-permission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-permission.d.ts","sourceRoot":"","sources":["../src/use-permission.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,GAAG,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC;IACrE,wEAAwE;IACxE,MAAM,EAAE,CAAC,WAAW,EAAE,SAAS,aAAa,EAAE,EAAE,KAAK,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC;IACpF,2DAA2D;IAC3D,MAAM,EAAE,CAAC,WAAW,EAAE,SAAS,aAAa,EAAE,EAAE,KAAK,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC;IACpF;;;;;OAKG;IACH,SAAS,EAAE,CAAC,UAAU,EAAE,aAAa,KAAK,SAAS,SAAS,EAAE,CAAC;IAC/D,mFAAmF;IACnF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,IAAI,aAAa,CAkB9C;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,aAAa,EACzB,KAAK,CAAC,EAAE,eAAe,GACtB,OAAO,CAGT;AAED,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,UAAU,EAAE,aAAa,GAAG,SAAS,aAAa,EAAE,CAAC;IACrD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EACxB,UAAU,EACV,SAAS,EACT,QAAQ,EACR,QAAe,GAChB,EAAE,cAAc,GAAG,KAAK,CAAC,SAAS,CAQlC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useGrants } from './grants-provider';
|
|
4
|
+
import { holdsPermission, holdsPermissionAt, permissionScopes, } from './scope-coverage';
|
|
5
|
+
/** The whole permission surface, for a screen asking several questions. */
|
|
6
|
+
export function usePermissions() {
|
|
7
|
+
const { grants, loading } = useGrants();
|
|
8
|
+
return React.useMemo(() => {
|
|
9
|
+
const can = (permission, query) => query?.scopePath === undefined
|
|
10
|
+
? holdsPermission(grants, permission)
|
|
11
|
+
: holdsPermissionAt(grants, permission, query.scopePath);
|
|
12
|
+
return {
|
|
13
|
+
can,
|
|
14
|
+
canAny: (permissions, query) => permissions.some((key) => can(key, query)),
|
|
15
|
+
canAll: (permissions, query) => permissions.length > 0 && permissions.every((key) => can(key, query)),
|
|
16
|
+
scopesFor: (permission) => permissionScopes(grants, permission),
|
|
17
|
+
loading,
|
|
18
|
+
};
|
|
19
|
+
}, [grants, loading]);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One permission, as a boolean — the common case.
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* const canCreate = usePermission('iam.client.user.create');
|
|
26
|
+
* {canCreate && <Button type="primary">Add user</Button>}
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function usePermission(permission, query) {
|
|
30
|
+
const { can } = usePermissions();
|
|
31
|
+
return can(permission, query);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Renders `children` only if the subject holds the permission.
|
|
35
|
+
*
|
|
36
|
+
* The JSX form of {@link usePermission}, for a block of markup that would
|
|
37
|
+
* otherwise need a variable and a conditional. An array means "any of these",
|
|
38
|
+
* matching the OR semantics `menu_permission` uses for nav visibility
|
|
39
|
+
* (Doc 05 §3).
|
|
40
|
+
*/
|
|
41
|
+
export function Permitted({ permission, scopePath, children, fallback = null, }) {
|
|
42
|
+
const { can, canAny } = usePermissions();
|
|
43
|
+
const query = scopePath === undefined ? undefined : { scopePath };
|
|
44
|
+
const allowed = Array.isArray(permission)
|
|
45
|
+
? canAny(permission, query)
|
|
46
|
+
: can(permission, query);
|
|
47
|
+
return allowed ? children : fallback;
|
|
48
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@plantops/web-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"@plantops/source": "./src/index.ts",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"nx": {
|
|
18
|
+
"tags": [
|
|
19
|
+
"type:lib",
|
|
20
|
+
"scope:web"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": "^19.0.0",
|
|
25
|
+
"react-dom": "^19.0.0",
|
|
26
|
+
"antd": "^6.6.1"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@plantops/contracts": "^0.1.0",
|
|
30
|
+
"@plantops/iam-client": "^0.1.0",
|
|
31
|
+
"@plantops/ui": "^0.1.0",
|
|
32
|
+
"tslib": "^2.3.0"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/dileepraghumajji/plant-ops.git",
|
|
38
|
+
"directory": "libs/web-kit"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"antd": "^6.6.1"
|
|
48
|
+
}
|
|
49
|
+
}
|