@datalayer/core 1.1.66 → 1.2.10
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 +41 -18
- package/lib/api/constants.d.ts +1 -1
- package/lib/api/constants.js +1 -1
- package/lib/api/contents/attachments.js +3 -7
- package/lib/api/contents/datasets.d.ts +48 -0
- package/lib/api/contents/datasets.js +44 -7
- package/lib/api/contents/runtimeMounts.js +1 -1
- package/lib/api/contents/sandboxUid.d.ts +13 -45
- package/lib/api/contents/sandboxUid.js +14 -65
- package/lib/api/evals/client.d.ts +402 -0
- package/lib/api/evals/client.js +359 -0
- package/lib/api/evals/index.d.ts +10 -0
- package/lib/api/evals/index.js +18 -0
- package/lib/api/evals/request.d.ts +43 -0
- package/lib/api/evals/request.js +67 -0
- package/lib/api/evals/status.d.ts +27 -0
- package/lib/api/evals/status.js +122 -0
- package/lib/api/evals/types.d.ts +1335 -0
- package/lib/api/evals/types.js +5 -0
- package/lib/api/iam/connectedAgents.d.ts +29 -0
- package/lib/api/iam/connectedAgents.js +35 -0
- package/lib/api/iam/identityProviders.d.ts +76 -0
- package/lib/api/iam/identityProviders.js +187 -0
- package/lib/api/iam/index.d.ts +1 -0
- package/lib/api/iam/index.js +1 -0
- package/lib/api/iam/mcpPolicy.d.ts +102 -1
- package/lib/api/iam/mcpPolicy.js +38 -2
- package/lib/api/iam/profile.d.ts +17 -1
- package/lib/api/iam/profile.js +27 -0
- package/lib/api/iam/scimTokens.d.ts +79 -0
- package/lib/api/iam/scimTokens.js +141 -0
- package/lib/api/iam/secrets.js +9 -21
- package/lib/api/iam/trials.d.ts +38 -0
- package/lib/api/iam/trials.js +66 -0
- package/lib/api/index.d.ts +4 -0
- package/lib/api/index.js +4 -0
- package/lib/api/mcp/observability.d.ts +25 -9
- package/lib/api/mcp/observability.js +63 -50
- package/lib/api/orchestration/client.d.ts +53 -0
- package/lib/api/orchestration/client.js +105 -0
- package/lib/api/orchestration/events.d.ts +88 -0
- package/lib/api/orchestration/events.js +245 -0
- package/lib/api/orchestration/generated.d.ts +693 -0
- package/lib/api/orchestration/generated.js +687 -0
- package/lib/api/orchestration/index.d.ts +30 -0
- package/lib/api/orchestration/index.js +34 -0
- package/lib/api/orchestration/lifecycle.d.ts +39 -0
- package/lib/api/orchestration/lifecycle.js +57 -0
- package/lib/api/orchestration/measures.d.ts +55 -0
- package/lib/api/orchestration/measures.js +99 -0
- package/lib/api/otel/dashboards.d.ts +56 -0
- package/lib/api/otel/dashboards.js +50 -0
- package/lib/api/otel/index.d.ts +1 -0
- package/lib/api/otel/index.js +1 -0
- package/lib/api/otel/metrics.d.ts +16 -0
- package/lib/api/otel/metrics.js +15 -0
- package/lib/api/scheduler/client.d.ts +32 -0
- package/lib/api/scheduler/client.js +42 -0
- package/lib/api/scheduler/index.d.ts +8 -0
- package/lib/api/scheduler/index.js +16 -0
- package/lib/api/scheduler/request.d.ts +28 -0
- package/lib/api/scheduler/request.js +50 -0
- package/lib/api/scheduler/types.d.ts +88 -0
- package/lib/api/scheduler/types.js +5 -0
- package/lib/api/spacer/comments.d.ts +97 -0
- package/lib/api/spacer/comments.js +45 -0
- package/lib/api/spacer/index.d.ts +9 -0
- package/lib/api/spacer/index.js +17 -0
- package/lib/api/spacer/notebooks.d.ts +33 -0
- package/lib/api/spacer/notebooks.js +39 -0
- package/lib/api/spacer/request.d.ts +25 -0
- package/lib/api/spacer/request.js +46 -0
- package/lib/api/spacer/spaces.d.ts +68 -0
- package/lib/api/spacer/spaces.js +45 -0
- package/lib/api/utils/validation.d.ts +4 -4
- package/lib/api/utils/validation.js +10 -7
- package/lib/client/constants.d.ts +1 -0
- package/lib/client/constants.js +1 -0
- package/lib/components/animation/AnimatedText.d.ts +1 -44
- package/lib/components/animation/AnimatedText.js +6 -122
- package/lib/components/anonymous/AnonymousKeyExpired.d.ts +56 -0
- package/lib/components/anonymous/AnonymousKeyExpired.js +96 -0
- package/lib/components/anonymous/AnonymousKeyTimer.d.ts +63 -0
- package/lib/components/anonymous/AnonymousKeyTimer.js +140 -0
- package/lib/components/anonymous/index.d.ts +7 -0
- package/lib/components/anonymous/index.js +15 -0
- package/lib/components/avatars/UserAvatar.d.ts +7 -1
- package/lib/components/avatars/UserAvatar.js +15 -1
- package/lib/components/billing/BillingEntitySelect.d.ts +6 -0
- package/lib/components/billing/BillingEntitySelect.js +49 -10
- package/lib/components/billing/eligibility.d.ts +22 -0
- package/lib/components/billing/eligibility.js +27 -0
- package/lib/components/billing/index.d.ts +1 -0
- package/lib/components/billing/index.js +1 -0
- package/lib/components/collaboration/LiveEditorCollaborators.d.ts +5 -0
- package/lib/components/collaboration/LiveEditorCollaborators.js +3 -1
- package/lib/components/display/DatalayerBox.d.ts +2 -3
- package/lib/components/display/DatalayerBox.js +1 -1
- package/lib/components/display/NavLink.js +3 -2
- package/lib/components/labels/StatusLabels.d.ts +2 -10
- package/lib/components/labels/StatusLabels.js +2 -26
- package/lib/components/principal/PrincipalAppearance.d.ts +21 -42
- package/lib/components/principal/PrincipalAppearance.js +232 -174
- package/lib/components/principal/PrincipalAvatar.d.ts +8 -1
- package/lib/components/principal/PrincipalAvatar.js +2 -2
- package/lib/components/principal/PrincipalDetailsOverlay.d.ts +8 -1
- package/lib/components/principal/PrincipalDetailsOverlay.js +2 -2
- package/lib/components/screencapture/Screencapture.js +46 -3
- package/lib/components/sharing/ShareAccessComponent.d.ts +45 -4
- package/lib/components/sharing/ShareAccessComponent.js +110 -78
- package/lib/components/sharing/SharingEditor.d.ts +5 -1
- package/lib/components/sharing/SharingEditor.js +9 -4
- package/lib/components/sharing/index.d.ts +1 -0
- package/lib/components/sharing/index.js +1 -0
- package/lib/components/sharing/sandboxSharing.d.ts +26 -0
- package/lib/components/sharing/sandboxSharing.js +41 -0
- package/lib/components/spaces/SpaceDetailsOverlay.d.ts +39 -0
- package/lib/components/spaces/SpaceDetailsOverlay.js +80 -0
- package/lib/components/spaces/SpaceDisplay.d.ts +38 -0
- package/lib/components/spaces/SpaceDisplay.js +92 -0
- package/lib/components/spaces/index.d.ts +4 -0
- package/lib/components/spaces/index.js +7 -0
- package/lib/components/spaces/spaceDisplayModel.d.ts +93 -0
- package/lib/components/spaces/spaceDisplayModel.js +170 -0
- package/lib/hooks/cacheConverters.d.ts +0 -7
- package/lib/hooks/cacheConverters.js +0 -25
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useBillingEntityStore.js +3 -0
- package/lib/hooks/useCache.d.ts +168 -25
- package/lib/hooks/useCache.js +1424 -307
- package/lib/hooks/useContents.d.ts +22 -1
- package/lib/hooks/useContents.js +90 -21
- package/lib/hooks/useMcp.d.ts +87 -1
- package/lib/hooks/useMcp.js +247 -12
- package/lib/hooks/useNavigate.d.ts +41 -3
- package/lib/hooks/useNavigate.js +106 -52
- package/lib/hooks/usePrincipalCacheStore.js +3 -0
- package/lib/hooks/usePrincipalStore.js +3 -0
- package/lib/hooks/useSpaceCacheStore.d.ts +50 -0
- package/lib/hooks/useSpaceCacheStore.js +85 -0
- package/lib/models/CreditsDTO.d.ts +6 -5
- package/lib/models/CreditsDTO.js +7 -5
- package/lib/models/McpBinding.d.ts +6 -0
- package/lib/models/Organization.d.ts +69 -0
- package/lib/models/Organization.js +96 -0
- package/lib/models/Secret.d.ts +1 -2
- package/lib/models/Secret.js +2 -18
- package/lib/models/StartedBy.d.ts +34 -0
- package/lib/models/StartedBy.js +36 -0
- package/lib/models/Team.d.ts +2 -0
- package/lib/models/Team.js +2 -0
- package/lib/models/User.d.ts +4 -0
- package/lib/models/User.js +3 -0
- package/lib/models/UserSettings.d.ts +13 -0
- package/lib/models/UserSettings.js +9 -0
- package/lib/models/contents/__fixtures__/v1-contracts.json +158 -30
- package/lib/navigation/components.js +2 -3
- package/lib/routes/publicPaths.js +4 -0
- package/lib/state/index.d.ts +1 -0
- package/lib/state/index.js +1 -0
- package/lib/state/sessionEnd.d.ts +55 -0
- package/lib/state/sessionEnd.js +162 -0
- package/lib/state/substates/CoreState.js +3 -3
- package/lib/state/substates/IAMState.js +7 -0
- package/lib/state/substates/LayoutState.js +12 -0
- package/lib/state/substates/ProfileState.d.ts +1 -0
- package/lib/state/substates/ProfileState.js +1 -0
- package/lib/utils/Lazy.d.ts +20 -0
- package/lib/utils/Lazy.js +32 -6
- package/lib/utils/Screencapture.js +3 -0
- package/lib/utils/WithSuspense.d.ts +11 -1
- package/lib/utils/WithSuspense.js +11 -1
- package/lib/views/mcp/AdmittedClients.d.ts +44 -0
- package/lib/views/mcp/AdmittedClients.js +59 -0
- package/lib/views/mcp/ApprovalQueue.d.ts +66 -0
- package/lib/views/mcp/ApprovalQueue.js +95 -0
- package/lib/views/mcp/ConnectedAgents.js +38 -3
- package/lib/views/mcp/EnterpriseConsole.d.ts +1 -1
- package/lib/views/mcp/EnterpriseConsole.js +23 -1
- package/lib/views/mcp/IdentityProviders.d.ts +33 -0
- package/lib/views/mcp/IdentityProviders.js +299 -0
- package/lib/views/mcp/McpDashboard.js +90 -7
- package/lib/views/mcp/McpObservability.d.ts +30 -1
- package/lib/views/mcp/McpObservability.js +105 -19
- package/lib/views/mcp/NotebookRuns.d.ts +39 -0
- package/lib/views/mcp/NotebookRuns.js +44 -0
- package/lib/views/mcp/OrganizationPolicy.js +2 -1
- package/lib/views/mcp/PersonalPolicy.js +2 -1
- package/lib/views/mcp/PolicyForm.d.ts +17 -1
- package/lib/views/mcp/PolicyForm.js +25 -3
- package/lib/views/mcp/PolicyHistory.d.ts +13 -0
- package/lib/views/mcp/PolicyHistory.js +14 -1
- package/lib/views/mcp/RunDetail.d.ts +87 -0
- package/lib/views/mcp/RunDetail.js +186 -0
- package/lib/views/mcp/ScimProvisioning.d.ts +54 -0
- package/lib/views/mcp/ScimProvisioning.js +173 -0
- package/lib/views/mcp/TeamPolicies.js +66 -5
- package/lib/views/mcp/ToolAccess.d.ts +60 -0
- package/lib/views/mcp/ToolAccess.js +101 -0
- package/lib/views/mcp/TraceTimeline.d.ts +81 -0
- package/lib/views/mcp/TraceTimeline.js +132 -0
- package/lib/views/mcp/index.d.ts +5 -0
- package/lib/views/mcp/index.js +5 -0
- package/lib/views/otel/simpleAuthStore.js +3 -0
- package/lib/views/secrets/SecretEdit.js +3 -2
- package/lib/views/secrets/SecretNew.js +5 -1
- package/package.json +269 -264
- package/scripts/generate-mcp-types.py +1 -1
- package/scripts/generate-orchestration-types.py +920 -0
|
@@ -7,6 +7,9 @@ export const takeHTMLNodeScreencapture = async (node) => {
|
|
|
7
7
|
const sc = html2canvas(node, {
|
|
8
8
|
width: node.getBoundingClientRect().width,
|
|
9
9
|
height: node.getBoundingClientRect().height,
|
|
10
|
+
// Cross-origin pictures are otherwise left out and their space kept, so a
|
|
11
|
+
// cell carrying an `<img>` from the web captures as a blank rectangle.
|
|
12
|
+
useCORS: true,
|
|
10
13
|
});
|
|
11
14
|
return sc.then(canvas => {
|
|
12
15
|
const croppedCanvas = document.createElement('canvas');
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* A lazily-loaded route, with something to look at while it arrives.
|
|
4
|
+
*
|
|
5
|
+
* `skeleton` was a boolean between one grey box and **nothing at all**, and
|
|
6
|
+
* nothing was the default — so every route in the application showed a blank
|
|
7
|
+
* page for as long as its chunk took, which for an editor is seconds. It now
|
|
8
|
+
* also takes a node, so a route can hand over a drawing of the thing that is
|
|
9
|
+
* coming rather than a hole where it will be.
|
|
10
|
+
*/
|
|
11
|
+
export declare const WithSuspense: (Component: any, preload?: boolean, skeleton?: boolean | ReactNode) => (props: any) => import("react").JSX.Element;
|
|
2
12
|
export default WithSuspense;
|
|
@@ -9,10 +9,20 @@ const Loading = (props) => {
|
|
|
9
9
|
const { skeleton } = props;
|
|
10
10
|
return skeleton ? (_jsx(_Fragment, { children: _jsx(SkeletonBox, { height: "100px" }) })) : (_jsx(_Fragment, {}));
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* A lazily-loaded route, with something to look at while it arrives.
|
|
14
|
+
*
|
|
15
|
+
* `skeleton` was a boolean between one grey box and **nothing at all**, and
|
|
16
|
+
* nothing was the default — so every route in the application showed a blank
|
|
17
|
+
* page for as long as its chunk took, which for an editor is seconds. It now
|
|
18
|
+
* also takes a node, so a route can hand over a drawing of the thing that is
|
|
19
|
+
* coming rather than a hole where it will be.
|
|
20
|
+
*/
|
|
12
21
|
export const WithSuspense = (Component, preload = true, skeleton = false) => (props) => {
|
|
13
22
|
if (preload) {
|
|
14
23
|
Component.preload();
|
|
15
24
|
}
|
|
16
|
-
|
|
25
|
+
const fallback = typeof skeleton === 'boolean' ? (_jsx(Loading, { skeleton: skeleton })) : (skeleton);
|
|
26
|
+
return (_jsx(Suspense, { fallback: fallback, children: _jsx(Component, { ...props }) }));
|
|
17
27
|
};
|
|
18
28
|
export default WithSuspense;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the admitted-clients list actually admits.
|
|
3
|
+
*
|
|
4
|
+
* The field above it is a textarea of client ids, because that is what they
|
|
5
|
+
* are: a client id is a URL somebody pastes, and there is nothing to pick
|
|
6
|
+
* from. What a textarea cannot do is tell whoever typed it whether an entry
|
|
7
|
+
* resolves — and this is the one policy rule whose typo fails silently in
|
|
8
|
+
* the direction that matters. A misspelled entry does not refuse anything
|
|
9
|
+
* loudly; it never matches, and every agent from that vendor is turned away
|
|
10
|
+
* with "your organization does not admit this client", which reads to its
|
|
11
|
+
* owner as a decision somebody made on purpose.
|
|
12
|
+
*
|
|
13
|
+
* So each stored entry is shown with what it names. Three states, and the
|
|
14
|
+
* reason this is a component rather than a line of text is that collapsing
|
|
15
|
+
* any two of them misleads:
|
|
16
|
+
*
|
|
17
|
+
* * a **hostname** admits every client that host publishes. There is no
|
|
18
|
+
* document and none is expected — shown as unresolved it would send
|
|
19
|
+
* somebody chasing a fetch that was never going to happen.
|
|
20
|
+
* * a **document that resolves**, with the name and publishing host.
|
|
21
|
+
* * a **document that does not**, with the reason. The state the whole
|
|
22
|
+
* thing is for.
|
|
23
|
+
*
|
|
24
|
+
* **It describes what is stored, not the draft.** Resolving on every
|
|
25
|
+
* keystroke would make a textarea a source of outbound requests, so this
|
|
26
|
+
* goes stale while somebody edits and says so rather than pretending to
|
|
27
|
+
* describe a line that has not been saved.
|
|
28
|
+
*
|
|
29
|
+
* @module views/mcp/AdmittedClients
|
|
30
|
+
*/
|
|
31
|
+
import type { JSX } from 'react';
|
|
32
|
+
import type { McpPolicyScope } from '../../api/iam/mcpPolicy';
|
|
33
|
+
export interface AdmittedClientsProps {
|
|
34
|
+
scope: McpPolicyScope;
|
|
35
|
+
subjectUid: string;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the textarea above differs from what is stored. The entries
|
|
38
|
+
* below describe the *stored* list, and saying so is the difference
|
|
39
|
+
* between a stale panel and a wrong one.
|
|
40
|
+
*/
|
|
41
|
+
edited?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare const AdmittedClients: ({ scope, subjectUid, edited, }: AdmittedClientsProps) => JSX.Element | null;
|
|
44
|
+
export default AdmittedClients;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Label, Link, Spinner, Text } from '@primer/react';
|
|
3
|
+
import { Box } from '@datalayer/primer-addons';
|
|
4
|
+
import { AlertIcon, CheckCircleIcon, GlobeIcon, QuestionIcon, } from '@primer/octicons-react';
|
|
5
|
+
import { useAdmittedClients } from '../../hooks/useMcp.js';
|
|
6
|
+
const Row = ({ client }) => {
|
|
7
|
+
const unreadable = client.kind === 'url' && client.resolved === false;
|
|
8
|
+
const unusable = client.kind === 'unusable';
|
|
9
|
+
const icon = unusable ? (_jsx(QuestionIcon, {})) : unreadable ? (_jsx(AlertIcon, {})) : client.kind === 'hostname' ? (_jsx(GlobeIcon, {})) : (_jsx(CheckCircleIcon, {}));
|
|
10
|
+
const tone = unusable || unreadable ? 'danger.fg' : 'fg.muted';
|
|
11
|
+
return (_jsxs(Box, { sx: {
|
|
12
|
+
display: 'grid',
|
|
13
|
+
gridTemplateColumns: 'auto 1fr',
|
|
14
|
+
gap: 2,
|
|
15
|
+
alignItems: 'start',
|
|
16
|
+
py: 2,
|
|
17
|
+
borderTop: '1px solid',
|
|
18
|
+
borderColor: 'border.muted',
|
|
19
|
+
}, children: [_jsx(Box, { sx: { color: tone, pt: '2px' }, children: icon }), _jsxs(Box, { sx: { display: 'grid', gap: 1, minWidth: 0 }, children: [_jsxs(Box, { sx: {
|
|
20
|
+
display: 'flex',
|
|
21
|
+
gap: 2,
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
flexWrap: 'wrap',
|
|
24
|
+
}, children: [client.clientName && (_jsx(Text, { sx: { fontWeight: 'semibold' }, children: client.clientName })), client.kind === 'hostname' && (_jsx(Label, { variant: "accent", size: "small", children: "Whole host" })), client.localhostOnly && (
|
|
25
|
+
// Worth a label of its own: every redirect goes to the reader's
|
|
26
|
+
// own machine, and a document cannot prove which local program
|
|
27
|
+
// is listening on the port.
|
|
28
|
+
_jsx(Label, { variant: "attention", size: "small", children: "Redirects to localhost" })), unreadable && (_jsx(Label, { variant: "danger", size: "small", children: "Cannot be read" })), unusable && (_jsx(Label, { variant: "danger", size: "small", children: "Matches nothing" }))] }), _jsx(Text, { sx: {
|
|
29
|
+
fontSize: 0,
|
|
30
|
+
fontFamily: 'mono',
|
|
31
|
+
color: 'fg.muted',
|
|
32
|
+
overflowWrap: 'anywhere',
|
|
33
|
+
}, children: client.entry }), client.detail && (_jsx(Text, { sx: { fontSize: 0, color: tone }, children: client.detail })), client.clientUri && (_jsx(Link, { href: client.clientUri, target: "_blank", rel: "noreferrer noopener", sx: { fontSize: 0 }, children: client.clientHostname || client.clientUri }))] })] }));
|
|
34
|
+
};
|
|
35
|
+
export const AdmittedClients = ({ scope, subjectUid, edited = false, }) => {
|
|
36
|
+
const described = useAdmittedClients(scope, subjectUid);
|
|
37
|
+
if (described.isPending && !described.data) {
|
|
38
|
+
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 3 }, children: _jsx(Spinner, { size: "small" }) }));
|
|
39
|
+
}
|
|
40
|
+
// An error here is not worth a blankslate: the field above still works and
|
|
41
|
+
// the policy is unaffected. Saying nothing would be worse than saying it
|
|
42
|
+
// could not be checked, so it says that and stays out of the way.
|
|
43
|
+
if (described.isError) {
|
|
44
|
+
return (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0 }, children: "The admitted clients could not be described just now. The list above is still what is enforced." }));
|
|
45
|
+
}
|
|
46
|
+
const answer = described.data;
|
|
47
|
+
if (!answer) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!answer.allowlisted) {
|
|
51
|
+
// Said, not rendered as an empty list. An empty allowlist is not an
|
|
52
|
+
// allowlist, and "no clients admitted" would be the opposite of what
|
|
53
|
+
// the gateway does with a blank one.
|
|
54
|
+
return (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0 }, children: answer.detail ??
|
|
55
|
+
'This layer names no client allowlist, so every client is admitted by it.' }));
|
|
56
|
+
}
|
|
57
|
+
return (_jsxs(Box, { sx: { display: 'grid', gap: 1 }, children: [edited && (_jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'attention.fg', m: 0 }, children: ["Describing the ", _jsx("strong", { children: "saved" }), " list. Save to check what you have just typed."] })), answer.clients.map(client => (_jsx(Row, { client: client }, client.entry)))] }));
|
|
58
|
+
};
|
|
59
|
+
export default AdmittedClients;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The calls an agent cannot make without somebody saying yes.
|
|
3
|
+
*
|
|
4
|
+
* A source set to `explicit` stops every call it does not already hold an
|
|
5
|
+
* approval for. The call waits as `pending-approval`, and the person who
|
|
6
|
+
* owns the source is the only one who can move it — which means that until
|
|
7
|
+
* this page existed, "approval required" was a setting that stopped agents
|
|
8
|
+
* and gave nobody a way to unstop them.
|
|
9
|
+
*
|
|
10
|
+
* **An approval is bound to the arguments it was asked about.** Contents
|
|
11
|
+
* hashes them and re-checks the hash when the call runs, so approving is
|
|
12
|
+
* approving *those* arguments and not that tool in general. That is why the
|
|
13
|
+
* redacted arguments are drawn in full rather than summarised: a reader who
|
|
14
|
+
* cannot see what they are allowing is a reader who approves everything.
|
|
15
|
+
*
|
|
16
|
+
* **A pending approval that has expired is not decidable**, and the buttons
|
|
17
|
+
* go rather than fail. Contents expires them lazily — the record still says
|
|
18
|
+
* `pending` until something looks at it — so a row can arrive here already
|
|
19
|
+
* over, and offering an Approve button on it would be offering a decision
|
|
20
|
+
* the server will refuse.
|
|
21
|
+
*
|
|
22
|
+
* @module views/mcp/ApprovalQueue
|
|
23
|
+
*/
|
|
24
|
+
import type { JSX } from 'react';
|
|
25
|
+
import type { McpApproval } from '../../api/contents/generated';
|
|
26
|
+
import { type McpErrorStateFn } from './types';
|
|
27
|
+
/** How each state of an approval is drawn. */
|
|
28
|
+
export declare const APPROVAL_LOOK: Record<McpApproval['status'], {
|
|
29
|
+
label: string;
|
|
30
|
+
variant: 'attention' | 'success' | 'danger' | 'secondary';
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Whether this approval can still be decided.
|
|
34
|
+
*
|
|
35
|
+
* Two conditions, and the second is the one that matters: the record says
|
|
36
|
+
* `pending`, *and* its deadline has not passed. Contents expires an approval
|
|
37
|
+
* when something next reads it rather than on a timer, so a row fetched a
|
|
38
|
+
* minute ago can be `pending` and over. Trusting the status alone would draw
|
|
39
|
+
* buttons whose only outcome is an error.
|
|
40
|
+
*
|
|
41
|
+
* An unparseable deadline is treated as passed. That is the safe direction:
|
|
42
|
+
* the worst case is somebody has to ask the agent to try again, where the
|
|
43
|
+
* other way round is a decision taken on a record nobody can date.
|
|
44
|
+
*/
|
|
45
|
+
export declare const isDecidable: (approval: Pick<McpApproval, "status" | "expiresAt">, now?: number) => boolean;
|
|
46
|
+
/**
|
|
47
|
+
* What a row says about its own deadline.
|
|
48
|
+
*
|
|
49
|
+
* A pending approval that has run out is drawn as expired even though the
|
|
50
|
+
* record still says pending — the reader's question is "can I still act on
|
|
51
|
+
* this", and the record's answer to that is stale by construction.
|
|
52
|
+
*/
|
|
53
|
+
export declare const approvalLook: (approval: Pick<McpApproval, "status" | "expiresAt">, now?: number) => {
|
|
54
|
+
label: string;
|
|
55
|
+
variant: "attention" | "success" | "danger" | "secondary";
|
|
56
|
+
};
|
|
57
|
+
/** The states this page can be filtered to. `pending` is what it opens on. */
|
|
58
|
+
export declare const APPROVAL_FILTERS: McpApproval['status'][];
|
|
59
|
+
export interface ApprovalQueueProps {
|
|
60
|
+
/** The application's words for a failed request. */
|
|
61
|
+
errorState: McpErrorStateFn;
|
|
62
|
+
/** Only the approvals of one source, when the caller has one in mind. */
|
|
63
|
+
sourceUid?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare const ApprovalQueue: ({ errorState, sourceUid, }: ApprovalQueueProps) => JSX.Element;
|
|
66
|
+
export default ApprovalQueue;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Button, Heading, Label, RelativeTime, SegmentedControl, Spinner, Text, Textarea, } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { McpErrorBlankslate } from '../../components/mcp/index.js';
|
|
6
|
+
import { useDecideMcpApproval, useMcpApprovals, } from '../../hooks/useContents.js';
|
|
7
|
+
/** How each state of an approval is drawn. */
|
|
8
|
+
export const APPROVAL_LOOK = {
|
|
9
|
+
pending: { label: 'Waiting on you', variant: 'attention' },
|
|
10
|
+
approved: { label: 'Approved', variant: 'success' },
|
|
11
|
+
rejected: { label: 'Rejected', variant: 'danger' },
|
|
12
|
+
expired: { label: 'Expired', variant: 'secondary' },
|
|
13
|
+
consumed: { label: 'Used', variant: 'secondary' },
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Whether this approval can still be decided.
|
|
17
|
+
*
|
|
18
|
+
* Two conditions, and the second is the one that matters: the record says
|
|
19
|
+
* `pending`, *and* its deadline has not passed. Contents expires an approval
|
|
20
|
+
* when something next reads it rather than on a timer, so a row fetched a
|
|
21
|
+
* minute ago can be `pending` and over. Trusting the status alone would draw
|
|
22
|
+
* buttons whose only outcome is an error.
|
|
23
|
+
*
|
|
24
|
+
* An unparseable deadline is treated as passed. That is the safe direction:
|
|
25
|
+
* the worst case is somebody has to ask the agent to try again, where the
|
|
26
|
+
* other way round is a decision taken on a record nobody can date.
|
|
27
|
+
*/
|
|
28
|
+
export const isDecidable = (approval, now = Date.now()) => {
|
|
29
|
+
if (approval.status !== 'pending') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const deadline = Date.parse(approval.expiresAt);
|
|
33
|
+
return Number.isFinite(deadline) && deadline > now;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* What a row says about its own deadline.
|
|
37
|
+
*
|
|
38
|
+
* A pending approval that has run out is drawn as expired even though the
|
|
39
|
+
* record still says pending — the reader's question is "can I still act on
|
|
40
|
+
* this", and the record's answer to that is stale by construction.
|
|
41
|
+
*/
|
|
42
|
+
export const approvalLook = (approval, now = Date.now()) => approval.status === 'pending' && !isDecidable(approval, now)
|
|
43
|
+
? APPROVAL_LOOK.expired
|
|
44
|
+
: APPROVAL_LOOK[approval.status];
|
|
45
|
+
/** The states this page can be filtered to. `pending` is what it opens on. */
|
|
46
|
+
export const APPROVAL_FILTERS = [
|
|
47
|
+
'pending',
|
|
48
|
+
'approved',
|
|
49
|
+
'rejected',
|
|
50
|
+
];
|
|
51
|
+
export const ApprovalQueue = ({ errorState, sourceUid, }) => {
|
|
52
|
+
const [status, setStatus] = useState('pending');
|
|
53
|
+
const [noteFor, setNoteFor] = useState({});
|
|
54
|
+
const approvals = useMcpApprovals(status, sourceUid ? { sourceUid } : {});
|
|
55
|
+
const decide = useDecideMcpApproval();
|
|
56
|
+
const items = approvals.data?.items ?? [];
|
|
57
|
+
return (_jsxs(Box, { sx: { display: 'grid', gap: 3, minWidth: 0 }, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 1 }, children: "Approvals" }), _jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: "A source set to require approval stops each call until you allow it. An approval covers the exact arguments below and nothing else, so a second call with different arguments asks again." })] }), _jsx(SegmentedControl, { "aria-label": "Which approvals", size: "small", children: APPROVAL_FILTERS.map(state => (_jsx(SegmentedControl.Button, { selected: status === state, onClick: () => setStatus(state), children: APPROVAL_LOOK[state].label }, state))) }), approvals.isError ? (_jsx(McpErrorBlankslate, { state: errorState(approvals.error, 'The approvals'), onRetry: () => approvals.refetch() })) : approvals.isPending ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 5 }, children: _jsx(Spinner, {}) })) : items.length === 0 ? (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: status === 'pending'
|
|
58
|
+
? 'Nothing is waiting on you.'
|
|
59
|
+
: `No ${APPROVAL_LOOK[status].label.toLowerCase()} approvals.` })) : (items.map(approval => {
|
|
60
|
+
const look = approvalLook(approval);
|
|
61
|
+
const decidable = isDecidable(approval);
|
|
62
|
+
return (_jsxs(Box, { sx: {
|
|
63
|
+
border: '1px solid',
|
|
64
|
+
borderColor: decidable ? 'attention.emphasis' : 'border.default',
|
|
65
|
+
borderRadius: 2,
|
|
66
|
+
p: 3,
|
|
67
|
+
display: 'grid',
|
|
68
|
+
gap: 2,
|
|
69
|
+
minWidth: 0,
|
|
70
|
+
}, children: [_jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }, children: [_jsx(Label, { size: "small", variant: look.variant, children: look.label }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: approval.tool }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontFamily: 'mono' }, children: approval.actorUid }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["asked ", _jsx(RelativeTime, { datetime: approval.createdAt })] }), decidable && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["expires ", _jsx(RelativeTime, { datetime: approval.expiresAt })] }))] }), _jsx(Box, { as: "pre", sx: {
|
|
71
|
+
fontFamily: 'mono',
|
|
72
|
+
fontSize: 0,
|
|
73
|
+
whiteSpace: 'pre-wrap',
|
|
74
|
+
overflowX: 'auto',
|
|
75
|
+
bg: 'canvas.subtle',
|
|
76
|
+
borderRadius: 2,
|
|
77
|
+
p: 2,
|
|
78
|
+
m: 0,
|
|
79
|
+
}, children: JSON.stringify(approval.argumentsRedacted, null, 2) }), approval.destinationUri && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Writes to", ' ', _jsx(Text, { as: "span", sx: { fontFamily: 'mono' }, children: approval.destinationUri })] })), decidable ? (_jsxs(_Fragment, { children: [_jsx(Textarea, { "aria-label": `Why, for ${approval.tool}`, rows: 2, placeholder: "Why (optional; kept with the decision)", value: noteFor[approval.uid] ?? '', onChange: event => setNoteFor(current => ({
|
|
80
|
+
...current,
|
|
81
|
+
[approval.uid]: event.target.value,
|
|
82
|
+
})) }), _jsxs(Box, { sx: { display: 'flex', gap: 2 }, children: [_jsx(Button, { variant: "primary", disabled: decide.isPending, onClick: () => decide.mutate({
|
|
83
|
+
approvalUid: approval.uid,
|
|
84
|
+
decision: 'approve',
|
|
85
|
+
note: noteFor[approval.uid] || undefined,
|
|
86
|
+
}), children: "Allow this call" }), _jsx(Button, { variant: "danger", disabled: decide.isPending, onClick: () => decide.mutate({
|
|
87
|
+
approvalUid: approval.uid,
|
|
88
|
+
decision: 'reject',
|
|
89
|
+
note: noteFor[approval.uid] || undefined,
|
|
90
|
+
}), children: "Refuse" })] })] })) : (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: [approval.decidedBy
|
|
91
|
+
? `Decided by ${approval.decidedBy}`
|
|
92
|
+
: 'This request ran out before anybody decided it. The agent has to ask again.', approval.note ? ` — ${approval.note}` : ''] }))] }, approval.uid));
|
|
93
|
+
}))] }));
|
|
94
|
+
};
|
|
95
|
+
export default ApprovalQueue;
|
|
@@ -5,19 +5,37 @@ import { Blankslate, DataTable, Dialog, Table, } from '@primer/react/experimenta
|
|
|
5
5
|
import { Box } from '@datalayer/primer-addons';
|
|
6
6
|
import { KebabHorizontalIcon, PlugIcon } from '@primer/octicons-react';
|
|
7
7
|
import { ClientBadge, McpErrorBlankslate, ScopeList, } from '../../components/mcp/index.js';
|
|
8
|
-
import { useConnectedAgents, useDisconnectAgent } from '../../hooks/useMcp.js';
|
|
8
|
+
import { useConnectedAgents, useDisconnectAgent, useDisconnectEveryAgent, } from '../../hooks/useMcp.js';
|
|
9
9
|
import { useNavigate, useToast } from '../../hooks/index.js';
|
|
10
10
|
export const ConnectedAgents = ({ errorState, routes, showTitle = true, readOnly = false, }) => {
|
|
11
11
|
const navigate = useNavigate();
|
|
12
12
|
const { enqueueToast } = useToast();
|
|
13
13
|
const agents = useConnectedAgents();
|
|
14
14
|
const disconnect = useDisconnectAgent();
|
|
15
|
+
const disconnectEvery = useDisconnectEveryAgent();
|
|
15
16
|
const [disconnecting, setDisconnecting] = useState(null);
|
|
17
|
+
const [disconnectingEvery, setDisconnectingEvery] = useState(false);
|
|
16
18
|
// Primer 37 types Dialog's focus refs with React 18's non-nullable
|
|
17
19
|
// `RefObject<HTMLElement>`. React 19's `useRef(null)` is nullable, and this
|
|
18
20
|
// ref is only handed to Dialog, so narrow it once here.
|
|
19
21
|
const returnFocusRef = useRef(null);
|
|
20
22
|
const rows = useMemo(() => (agents.data ?? []).map(agent => ({ ...agent, id: agent.uid })), [agents.data]);
|
|
23
|
+
const confirmDisconnectEvery = () => {
|
|
24
|
+
disconnectEvery.mutate(undefined, {
|
|
25
|
+
onSuccess: answer => {
|
|
26
|
+
enqueueToast(answer.revoked === 1
|
|
27
|
+
? '1 agent disconnected.'
|
|
28
|
+
: `${answer.revoked} agents disconnected.`, { variant: 'success' });
|
|
29
|
+
setDisconnectingEvery(false);
|
|
30
|
+
},
|
|
31
|
+
onError: error => {
|
|
32
|
+
setDisconnectingEvery(false);
|
|
33
|
+
enqueueToast(`Could not disconnect: ${error.message}`, {
|
|
34
|
+
variant: 'error',
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
};
|
|
21
39
|
const confirmDisconnect = () => {
|
|
22
40
|
const agent = disconnecting;
|
|
23
41
|
if (!agent) {
|
|
@@ -83,7 +101,12 @@ export const ConnectedAgents = ({ errorState, routes, showTitle = true, readOnly
|
|
|
83
101
|
if (agents.isError) {
|
|
84
102
|
return (_jsx(McpErrorBlankslate, { state: errorState(agents.error, 'Connected agents'), onRetry: () => agents.refetch() }));
|
|
85
103
|
}
|
|
86
|
-
return (_jsxs(Box, { sx: { display: 'grid', gap: 3, minWidth: 0 }, children: [showTitle && (_jsxs(Box, {
|
|
104
|
+
return (_jsxs(Box, { sx: { display: 'grid', gap: 3, minWidth: 0 }, children: [showTitle && (_jsxs(Box, { sx: {
|
|
105
|
+
display: 'flex',
|
|
106
|
+
alignItems: 'start',
|
|
107
|
+
justifyContent: 'space-between',
|
|
108
|
+
gap: 3,
|
|
109
|
+
}, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Connected Agents" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, children: "Every agent you have authorized, what it may do, and when it last did it." })] }), !readOnly && rows.length > 0 && (_jsx(Button, { variant: "danger", onClick: () => setDisconnectingEvery(true), disabled: disconnectEvery.isPending, children: "Disconnect all" }))] })), agents.isPending && !agents.data ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 5 }, children: _jsx(Spinner, {}) })) : rows.length > 0 ? (_jsxs(Table.Container, { children: [_jsx(Table.Title, { as: "h3", id: "connected-agents", children: "Agents" }), _jsx(Table.Subtitle, { as: "p", id: "connected-agents-subtitle", children: "An agent acts with exactly the access of the person who authorized it, and no more." }), _jsx(DataTable, { "aria-labelledby": "connected-agents", "aria-describedby": "connected-agents-subtitle", data: rows, columns: columns })] })) : (_jsxs(Blankslate, { border: true, spacious: true, children: [_jsx(Blankslate.Visual, { children: _jsx(PlugIcon, { size: "medium" }) }), _jsx(Blankslate.Heading, { children: "No agent connected" }), _jsx(Blankslate.Description, { children: _jsx(Text, { sx: { textAlign: 'center' }, children: "Connect an MCP client and approve the scopes it asks for; the grant appears here and can be revoked from here." }) }), _jsx(Button, { size: "small", onClick: () => navigate(routes.access), children: "Connect an agent" })] })), _jsx(Text, { sx: { fontSize: 0, color: 'fg.subtle' }, children: "Narrowing a grant to fewer scopes without disconnecting arrives with the organization policy." }), disconnecting && (_jsx(Dialog, { title: "Disconnect this agent?", onClose: () => setDisconnecting(null), returnFocusRef: returnFocusRef, footerButtons: [
|
|
87
110
|
{
|
|
88
111
|
buttonType: 'default',
|
|
89
112
|
content: 'Keep it',
|
|
@@ -95,6 +118,18 @@ export const ConnectedAgents = ({ errorState, routes, showTitle = true, readOnly
|
|
|
95
118
|
onClick: confirmDisconnect,
|
|
96
119
|
disabled: disconnect.isPending,
|
|
97
120
|
},
|
|
98
|
-
], children: _jsxs(Text, { sx: { fontSize: 1 }, children: [disconnecting.clientName || disconnecting.clientId, " loses its grant at once and its next call is refused. An access token it still holds dies with its own short expiry. The other agents are untouched, and nothing this one already did is undone."] }) }))
|
|
121
|
+
], children: _jsxs(Text, { sx: { fontSize: 1 }, children: [disconnecting.clientName || disconnecting.clientId, " loses its grant at once and its next call is refused. An access token it still holds dies with its own short expiry. The other agents are untouched, and nothing this one already did is undone."] }) })), disconnectingEvery && (_jsxs(Dialog, { title: "Disconnect every agent?", onClose: () => setDisconnectingEvery(false), returnFocusRef: returnFocusRef, footerButtons: [
|
|
122
|
+
{
|
|
123
|
+
buttonType: 'default',
|
|
124
|
+
content: 'Keep them',
|
|
125
|
+
onClick: () => setDisconnectingEvery(false),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
buttonType: 'danger',
|
|
129
|
+
content: 'Disconnect all',
|
|
130
|
+
onClick: confirmDisconnectEvery,
|
|
131
|
+
disabled: disconnectEvery.isPending,
|
|
132
|
+
},
|
|
133
|
+
], children: [_jsx(Text, { sx: { fontSize: 1 }, children: "Every agent you have authorized loses its grant at once \u2014 signing out everywhere. Each will ask again the next time you connect it, and this time it goes through the approval screen rather than being answered from what you agreed before, so this is also how you force a fresh approval." }), _jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', mt: 2, mb: 0 }, children: "Yours only. Nobody else\u2019s sessions end, in this organization or any other, and nothing any agent already did is undone." })] }))] }));
|
|
99
134
|
};
|
|
100
135
|
export default ConnectedAgents;
|
|
@@ -34,7 +34,7 @@ import type { McpAuditFilters } from '../../api/mcp';
|
|
|
34
34
|
import { type McpObservabilityPane } from './McpObservability';
|
|
35
35
|
import { type McpErrorStateFn, type McpRoutes } from './types';
|
|
36
36
|
/** The pages milestone 1 carries. */
|
|
37
|
-
export type EnterpriseConsolePage = 'overview' | 'usage' | 'agents' | 'service-agents' | 'policy' | 'teams' | 'alerts' | 'audit' | 'observability';
|
|
37
|
+
export type EnterpriseConsolePage = 'overview' | 'usage' | 'agents' | 'service-agents' | 'policy' | 'teams' | 'identity-providers' | 'alerts' | 'approvals' | 'audit' | 'observability';
|
|
38
38
|
export declare const ENTERPRISE_CONSOLE_PAGES: {
|
|
39
39
|
id: EnterpriseConsolePage;
|
|
40
40
|
label: string;
|
|
@@ -9,11 +9,14 @@ import { useNavigate, useToast } from '../../hooks/index.js';
|
|
|
9
9
|
import { AuditLog } from './AuditLog.js';
|
|
10
10
|
import { McpObservability, } from './McpObservability.js';
|
|
11
11
|
import { AlertRules } from './AlertRules.js';
|
|
12
|
+
import { IdentityProviders } from './IdentityProviders.js';
|
|
13
|
+
import { ScimProvisioning } from './ScimProvisioning.js';
|
|
12
14
|
import { TeamPolicies } from './TeamPolicies.js';
|
|
13
15
|
import { OrganizationUsage } from './OrganizationUsage.js';
|
|
14
16
|
import { OrganizationPolicy } from './OrganizationPolicy.js';
|
|
15
17
|
import { ServiceAgents } from './ServiceAgents.js';
|
|
16
18
|
import { clientStatusOf, plural, timeAgo } from './format.js';
|
|
19
|
+
import { ApprovalQueue } from './ApprovalQueue.js';
|
|
17
20
|
export const ENTERPRISE_CONSOLE_PAGES = [
|
|
18
21
|
{ id: 'overview', label: 'Overview' },
|
|
19
22
|
{ id: 'usage', label: 'Usage' },
|
|
@@ -21,7 +24,9 @@ export const ENTERPRISE_CONSOLE_PAGES = [
|
|
|
21
24
|
{ id: 'service-agents', label: 'Service Agents' },
|
|
22
25
|
{ id: 'policy', label: 'Policy' },
|
|
23
26
|
{ id: 'teams', label: 'Teams' },
|
|
27
|
+
{ id: 'identity-providers', label: 'Identity' },
|
|
24
28
|
{ id: 'alerts', label: 'Alerts' },
|
|
29
|
+
{ id: 'approvals', label: 'Approvals' },
|
|
25
30
|
{ id: 'audit', label: 'Audit' },
|
|
26
31
|
{ id: 'observability', label: 'Observability' },
|
|
27
32
|
];
|
|
@@ -35,7 +40,12 @@ export const pagesForRoles = (roles) => {
|
|
|
35
40
|
'service-agents',
|
|
36
41
|
'policy',
|
|
37
42
|
'teams',
|
|
43
|
+
// Who may sign in here at all, and as what, is the organization's own
|
|
44
|
+
// authority — the same reason a provider needs an owner to register,
|
|
45
|
+
// never a security auditor or a usage reviewer.
|
|
46
|
+
'identity-providers',
|
|
38
47
|
'alerts',
|
|
48
|
+
'approvals',
|
|
39
49
|
'audit',
|
|
40
50
|
'observability',
|
|
41
51
|
];
|
|
@@ -53,9 +63,21 @@ export const pagesForRoles = (roles) => {
|
|
|
53
63
|
// The team layers too: an auditor asked why one team's agents are
|
|
54
64
|
// treated differently needs the layer that treats them so.
|
|
55
65
|
'teams',
|
|
66
|
+
// Identity providers too, read-only: which directory an organization
|
|
67
|
+
// trusts and what its group mapping grants is exactly the posture an
|
|
68
|
+
// auditor is there to read, and the page carries no secret — the
|
|
69
|
+
// client secret is a reference, never the value.
|
|
70
|
+
'identity-providers',
|
|
56
71
|
// The alerts too: what an organization watches for is part of the
|
|
57
72
|
// security posture an auditor is there to read.
|
|
58
73
|
'alerts',
|
|
74
|
+
// The approvals are deliberately NOT here. Contents scopes
|
|
75
|
+
// `/mcp-approvals` to the caller — the actor, or the person who owns
|
|
76
|
+
// the source and must decide — so an auditor would open a queue of
|
|
77
|
+
// their own approvals under an organization's console and read it as
|
|
78
|
+
// that organization having none. An empty table that looks like an
|
|
79
|
+
// answer is worse than no tab. What an auditor needs is the decision
|
|
80
|
+
// *after* it was taken, and that is an audit row.
|
|
59
81
|
'audit',
|
|
60
82
|
'observability',
|
|
61
83
|
];
|
|
@@ -167,7 +189,7 @@ export const EnterpriseConsole = ({ errorState, routes, organization, roles, pag
|
|
|
167
189
|
: 'neutral' })] }), (overview.data?.refusals ?? []).length > 0 && (_jsxs(Box, { sx: { display: 'grid', gap: 2 }, children: [_jsx(Heading, { as: "h4", sx: { fontSize: 1 }, children: "Why calls were refused" }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: (overview.data?.refusals ?? []).map(row => (_jsxs(Label, { size: "small", variant: "danger", children: [row.reason, " \u00B7 ", row.count] }, row.reason))) })] })), _jsxs(Box, { sx: { display: 'grid', gap: 2 }, children: [_jsx(Heading, { as: "h4", sx: { fontSize: 1 }, children: "Compliance" }), (overview.data?.compliance ?? []).length === 0 ? (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Nothing to check yet: the strip fills as the organization's sign-in, admitted clients and audit export are configured." })) : (_jsx(Box, { sx: { display: 'grid', gap: 1 }, children: (overview.data?.compliance ?? []).map(check => (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Box, { sx: {
|
|
168
190
|
color: check.ok ? 'success.fg' : 'attention.fg',
|
|
169
191
|
display: 'flex',
|
|
170
|
-
}, children: check.ok ? (_jsx(CheckCircleIcon, { size: 14 })) : (_jsx(AlertIcon, { size: 14 })) }), _jsx(Text, { sx: { fontSize: 1 }, children: check.name }), !check.ok && check.detail && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: check.detail }))] }, check.name))) }))] }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsx(Metric, { label: "Approvals waiting", value: overview.data?.approvalsWaiting ?? '—', note: "The queue arrives with durable execution" }), _jsx(Metric, { label: "Alerts firing", value: overview.data?.alertsFiring ?? '—', note: "Alert rules arrive with quotas" })] })] }))), current === 'usage' && (_jsx(OrganizationUsage, { errorState: errorState, orgUid: organization.uid, showTitle: false })), current === 'teams' && (_jsx(TeamPolicies, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })), current === 'alerts' && (_jsx(AlertRules, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })), current === 'policy' && (_jsx(OrganizationPolicy, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })), current === 'service-agents' && (_jsx(ServiceAgents, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })), current === 'agents' &&
|
|
192
|
+
}, children: check.ok ? (_jsx(CheckCircleIcon, { size: 14 })) : (_jsx(AlertIcon, { size: 14 })) }), _jsx(Text, { sx: { fontSize: 1 }, children: check.name }), !check.ok && check.detail && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: check.detail }))] }, check.name))) }))] }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsx(Metric, { label: "Approvals waiting", value: overview.data?.approvalsWaiting ?? '—', note: "The queue arrives with durable execution" }), _jsx(Metric, { label: "Alerts firing", value: overview.data?.alertsFiring ?? '—', note: "Alert rules arrive with quotas" })] })] }))), current === 'usage' && (_jsx(OrganizationUsage, { errorState: errorState, orgUid: organization.uid, showTitle: false })), current === 'teams' && (_jsx(TeamPolicies, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })), current === 'identity-providers' && (_jsxs(Box, { sx: { display: 'grid', gap: 5, minWidth: 0 }, children: [_jsx(IdentityProviders, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') }), _jsx(ScimProvisioning, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })] })), current === 'alerts' && (_jsx(AlertRules, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })), current === 'approvals' && _jsx(ApprovalQueue, { errorState: errorState }), current === 'policy' && (_jsx(OrganizationPolicy, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })), current === 'service-agents' && (_jsx(ServiceAgents, { errorState: errorState, orgUid: organization.uid, readOnly: !roles.includes('organization_owner') })), current === 'agents' &&
|
|
171
193
|
(activity.isError ? (_jsx(McpErrorBlankslate, { state: errorState(activity.error, "The organization's agents"), onRetry: () => activity.refetch() })) : activity.isPending && !activity.data ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 6 }, children: _jsx(Spinner, {}) })) : agentRows.length === 0 ? (_jsxs(Blankslate, { border: true, spacious: true, children: [_jsx(Blankslate.Visual, { children: _jsx(PlugIcon, { size: "medium" }) }), _jsx(Blankslate.Heading, { children: "No agent acts for this organization" }), _jsx(Blankslate.Description, { children: _jsxs(Text, { sx: { textAlign: 'center' }, children: ["A member who authorizes a client while acting for", ' ', organization.name || organization.handle, " appears here with the scopes the grant carries and what it last did."] }) })] })) : (_jsxs(Table.Container, { children: [_jsx(Table.Title, { as: "h4", id: "org-agents", children: "Agents" }), _jsxs(Table.Subtitle, { as: "p", id: "org-agents-subtitle", children: [plural(agentRows.length, 'principal'), " acting through MCP for this organization. A member's own grant is revoked by that member; revoking on their behalf arrives with the organization's agent directory."] }), _jsx(DataTable, { "aria-labelledby": "org-agents", "aria-describedby": "org-agents-subtitle", data: agentRows, columns: agentColumns })] }))), current === 'audit' && (_jsx(AuditLog, { errorState: errorState, routes: routes, showTitle: false, subject: organization.name || organization.handle, filters: { ...auditFilters, org: organization.uid }, onFiltersChange: filters => onAuditFiltersChange({ ...filters, org: undefined }) })), current === 'observability' && (_jsx(McpObservability, { errorState: errorState, routes: routes, showTitle: false, org: organization.uid, pane: observabilityPane, onPaneChange: onObservabilityPaneChange, taskUid: observabilityTask, traceId: observabilityTrace, onSelect: onObservabilitySelect })), _jsx(Text, { sx: { fontSize: 0, color: 'fg.subtle' }, children: "Teams, Policies, Quotas, Alerts, Approvals and Runs join this console as the layers behind them ship. Identity \u2014 providers, group mapping, SCIM and session controls \u2014 comes last, with enterprise sign-in." })] }));
|
|
172
194
|
};
|
|
173
195
|
export default EnterpriseConsole;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An organization's own identity provider.
|
|
3
|
+
*
|
|
4
|
+
* Enterprise sign-in used to be one Okta for the whole deployment. Here an
|
|
5
|
+
* organization registers the directory it already has — issuer, client id,
|
|
6
|
+
* the email domains it claims — and a sign-in with an address at one of
|
|
7
|
+
* those domains is routed to it.
|
|
8
|
+
*
|
|
9
|
+
* **A claim is not proof, and this page is careful to say so.** A domain
|
|
10
|
+
* just typed in and one proved by DNS are shown differently on purpose: a
|
|
11
|
+
* grey "claimed" label beside a domain that routes nothing yet, a green
|
|
12
|
+
* "verified" one beside a domain that does. Collapsing the two into one list
|
|
13
|
+
* would let an owner believe a claim is already live when it is not — and
|
|
14
|
+
* until it is verified, nobody signs in through it at all.
|
|
15
|
+
*
|
|
16
|
+
* **The client secret never appears here**, only its name in the
|
|
17
|
+
* organization's own secret store. This page has no field that could render
|
|
18
|
+
* one, by construction rather than by care taken while writing it.
|
|
19
|
+
*
|
|
20
|
+
* @module views/mcp/IdentityProviders
|
|
21
|
+
*/
|
|
22
|
+
import type { JSX } from 'react';
|
|
23
|
+
import type { McpErrorStateFn } from './types';
|
|
24
|
+
export interface IdentityProvidersProps {
|
|
25
|
+
errorState: McpErrorStateFn;
|
|
26
|
+
orgUid: string;
|
|
27
|
+
/** Read-only for anybody who is not an owner: registering a provider
|
|
28
|
+
* decides who may get into the organization at all. */
|
|
29
|
+
readOnly?: boolean;
|
|
30
|
+
showTitle?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare const IdentityProviders: ({ errorState, orgUid, readOnly, showTitle, }: IdentityProvidersProps) => JSX.Element;
|
|
33
|
+
export default IdentityProviders;
|