@k2b/cloud 0.42.0 → 0.44.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/package.json +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { mutation } from "@k2b/stdlib/solid";
|
|
2
|
-
import { announce, IconButton, Placeholder, prompts, SelectChip, Tooltip, useLocale } from "@k2b/ui";
|
|
3
|
-
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
|
2
|
+
import { announce, Button, IconButton, Placeholder, prompts, Select, SelectChip, Tooltip, useLocale } from "@k2b/ui";
|
|
3
|
+
import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from "solid-js";
|
|
4
4
|
import { createStore, reconcile } from "solid-js/store";
|
|
5
5
|
import { CloudAvatar } from "../account/Avatar";
|
|
6
|
-
import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind } from "../contracts/shared";
|
|
6
|
+
import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
|
|
7
7
|
import { createGroupCoverage, type GroupCoverage, GroupCoveragePanel, GroupCoverageToggle } from "./GroupCoverage";
|
|
8
8
|
import { isManagerEntry } from "./managers";
|
|
9
9
|
import { accessMessages } from "./messages";
|
|
10
|
+
import PrincipalAvatar from "./PrincipalAvatar";
|
|
10
11
|
import PrincipalPicker, { principalKey } from "./PrincipalPicker";
|
|
11
12
|
import { principalLabel } from "./principal-label";
|
|
12
13
|
import { serviceAccountKindDisplay } from "./service-account-kind";
|
|
@@ -27,6 +28,13 @@ export type GrantableLevel = Exclude<PermissionLevel, "none">;
|
|
|
27
28
|
export type AllowedLevel = GrantableLevel | { level: GrantableLevel; scope?: string; label?: string; icon?: string; description?: string };
|
|
28
29
|
|
|
29
30
|
type PermissionEditorProps = {
|
|
31
|
+
/** Expanded form: choose a principal and level before adding; show durable inline feedback. */
|
|
32
|
+
expanded?: boolean;
|
|
33
|
+
/** Mark these entries as the viewer's own grants in the expanded form. */
|
|
34
|
+
ownEntryIds?: readonly string[];
|
|
35
|
+
/** Confirm a consequential change. When supplied, replaces the default removal confirmation. */
|
|
36
|
+
beforeChange?: (entry: AccessEntry, next: GrantableLevel | null) => Promise<boolean>;
|
|
37
|
+
|
|
30
38
|
/** The editor mounts from this list and merges later lists by entry id once no change is saving.
|
|
31
39
|
* Rows the caller changed since its last list win; unchanged rows keep the editor's confirmed result.
|
|
32
40
|
* Keep the editor mounted (no keyed Show or unmount during refresh) and pass refetched data. */
|
|
@@ -34,7 +42,10 @@ type PermissionEditorProps = {
|
|
|
34
42
|
|
|
35
43
|
/** Whether the current user can edit permissions. When `false`, the
|
|
36
44
|
* editor renders the entries read-only — no row dropdowns, no
|
|
37
|
-
* delete buttons, no add form.
|
|
45
|
+
* delete buttons, no add form. When it becomes `true` again, failed
|
|
46
|
+
* changes and their retries are cleared, so re-enable editing only
|
|
47
|
+
* after the list passed in is canonical again. In expanded mode the caller
|
|
48
|
+
* explains why editing stopped; row errors leave with editing. */
|
|
38
49
|
canEdit?: boolean;
|
|
39
50
|
|
|
40
51
|
/** Grant access. The caller closes over the resource id. The optional
|
|
@@ -70,9 +81,22 @@ type PermissionEditorProps = {
|
|
|
70
81
|
* true; resources with an explicit principal allowlist can disable it. */
|
|
71
82
|
allowAuthenticated?: boolean;
|
|
72
83
|
|
|
73
|
-
/** Allow granting service accounts.
|
|
74
|
-
*
|
|
84
|
+
/** Allow granting service accounts. Defaults to true, including agents.
|
|
85
|
+
* Disable only when the resource documents a user-backed identity requirement. */
|
|
75
86
|
allowServiceAccounts?: boolean;
|
|
87
|
+
/** Optional directory filters for the resource's accepted credential kinds. */
|
|
88
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
89
|
+
/** Grant selection defaults to active accounts; existing entries are unaffected. */
|
|
90
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
91
|
+
|
|
92
|
+
/** The principal search's placeholder, in the consumer's terms. Defaults to the principal kinds it offers. */
|
|
93
|
+
placeholder?: string;
|
|
94
|
+
|
|
95
|
+
/** The consumer's name and description for every signed-in person, in rows and in the principal search. */
|
|
96
|
+
signedIn?: { label: string; description: string };
|
|
97
|
+
|
|
98
|
+
/** Expanded form: a row's subtitle instead of its principal kind, such as a group's member count. */
|
|
99
|
+
entryDetail?: (entry: AccessEntry) => string | undefined;
|
|
76
100
|
|
|
77
101
|
/** Which levels the UI offers — and what they're called. Bare strings
|
|
78
102
|
* use the default labels (View / Edit / Manage). Objects override
|
|
@@ -172,6 +196,21 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
172
196
|
const initialEntries = props.initialEntries;
|
|
173
197
|
let appliedCallerEntries = new Map(initialEntries.map((entry) => [entry.id, JSON.stringify(entry)]));
|
|
174
198
|
const [entries, setEntries] = createStore<AccessEntry[]>(initialEntries.map((entry) => ({ ...entry })));
|
|
199
|
+
type Draft = {
|
|
200
|
+
principal: Principal;
|
|
201
|
+
display: { displayName: string; avatarHash?: string | null; serviceAccountKind?: ServiceAccountKind };
|
|
202
|
+
};
|
|
203
|
+
const [draft, setDraft] = createSignal<Draft>();
|
|
204
|
+
const [draftLevel, setDraftLevel] = createSignal("read");
|
|
205
|
+
const [feedback, setFeedback] = createStore<Record<string, { message: string; retry?: () => void } | undefined>>({});
|
|
206
|
+
const failure = (id: string, error: unknown, retry: () => void) => {
|
|
207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
+
if (props.expanded) setFeedback(id, { message, retry });
|
|
209
|
+
else prompts.error(message);
|
|
210
|
+
};
|
|
211
|
+
const saved = (id: string) => {
|
|
212
|
+
if (props.expanded) setFeedback(id, { message: t().saved });
|
|
213
|
+
};
|
|
175
214
|
const [pending, setPending] = createSignal<string | null>(null);
|
|
176
215
|
const grantPending = () => pending() !== null;
|
|
177
216
|
let disposed = false;
|
|
@@ -183,6 +222,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
183
222
|
const allowAuthenticated = () => props.allowAuthenticated !== false;
|
|
184
223
|
const allowed = (principal: Principal) =>
|
|
185
224
|
resolveAllowedLevels(typeof props.allowedLevels === "function" ? props.allowedLevels(principal) : props.allowedLevels, t());
|
|
225
|
+
const labelOf = (entry: AccessEntry) =>
|
|
226
|
+
entry.principal.type === "authenticated" && props.signedIn ? props.signedIn.label : principalLabel(entry, locale());
|
|
186
227
|
|
|
187
228
|
// Defensive dev-warning: an empty allowedLevels array makes the editor
|
|
188
229
|
// unable to grant anything.
|
|
@@ -197,14 +238,25 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
197
238
|
const grant = async (
|
|
198
239
|
principal: Principal,
|
|
199
240
|
display: { displayName: string; avatarHash?: string | null; serviceAccountKind?: ServiceAccountKind },
|
|
241
|
+
choice?: ResolvedLevel,
|
|
200
242
|
) => {
|
|
201
|
-
if (
|
|
243
|
+
if (!canStart()) return;
|
|
202
244
|
const key = principalKey(principal);
|
|
203
|
-
if (entries.some((entry) => principalKey(entry.principal) === key))
|
|
204
|
-
|
|
245
|
+
if (entries.some((entry) => principalKey(entry.principal) === key)) {
|
|
246
|
+
// A retry or selection for a principal already in the list has nothing left to add.
|
|
247
|
+
setFeedback("add", undefined);
|
|
248
|
+
if (draft() && principalKey(draft()!.principal) === key) {
|
|
249
|
+
setDraft(undefined);
|
|
250
|
+
if (props.expanded) focusPrincipalPicker();
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const first = choice ?? allowed(principal)[0];
|
|
205
255
|
if (!first) return;
|
|
206
256
|
const id = `pending:${key}`;
|
|
257
|
+
let granted = false;
|
|
207
258
|
setPending(id);
|
|
259
|
+
setFeedback("add", undefined);
|
|
208
260
|
setEntries(entries.length, {
|
|
209
261
|
id,
|
|
210
262
|
principal,
|
|
@@ -224,15 +276,19 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
224
276
|
serviceAccountKind: entry.serviceAccountKind ?? display.serviceAccountKind,
|
|
225
277
|
};
|
|
226
278
|
if (index !== -1) setEntries(index, reconcile(saved));
|
|
227
|
-
announce(t().accessGranted({ name:
|
|
279
|
+
announce(t().accessGranted({ name: labelOf(saved) }));
|
|
280
|
+
setDraft(undefined);
|
|
281
|
+
granted = true;
|
|
282
|
+
if (props.expanded) setFeedback(saved.id, { message: t().saved });
|
|
228
283
|
} catch (err) {
|
|
229
284
|
if (!disposed) setEntries((current) => current.filter((entry) => entry.id !== id));
|
|
230
285
|
// Prompts are global, so a grant that fails after its dialog closed still says so.
|
|
231
286
|
if (!(typeof err === "object" && err !== null && "name" in err && err.name === "AbortError")) {
|
|
232
|
-
|
|
287
|
+
failure("add", err, () => void grant(principal, display, first));
|
|
233
288
|
}
|
|
234
289
|
} finally {
|
|
235
290
|
if (!disposed) setPending(null);
|
|
291
|
+
if (props.expanded && granted) focusPrincipalPicker();
|
|
236
292
|
}
|
|
237
293
|
};
|
|
238
294
|
|
|
@@ -244,18 +300,28 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
244
300
|
const change = updating();
|
|
245
301
|
return change?.accessId === accessId ? change : undefined;
|
|
246
302
|
};
|
|
247
|
-
const updateMut = mutation.create<LevelChange, LevelChange>({
|
|
303
|
+
const updateMut = mutation.create<LevelChange | null, LevelChange>({
|
|
248
304
|
mutation: async (data) => {
|
|
305
|
+
const entry = entries.find((entry) => entry.id === data.accessId);
|
|
306
|
+
if (!entry || disposed || !canEdit()) return null;
|
|
307
|
+
if (props.beforeChange && !(await props.beforeChange(entry, data.permission))) return null;
|
|
308
|
+
if (disposed || !canEdit()) return null;
|
|
249
309
|
setUpdating(data);
|
|
310
|
+
setFeedback(data.accessId, undefined);
|
|
250
311
|
await props.updateAccess(data.accessId, data.permission, data.scope);
|
|
251
312
|
return data;
|
|
252
313
|
},
|
|
253
314
|
onSuccess: (result) => {
|
|
254
315
|
if (result) {
|
|
255
316
|
setEntries((entry) => entry.id === result.accessId, { permission: result.permission, scope: result.scope });
|
|
317
|
+
saved(result.accessId);
|
|
256
318
|
}
|
|
257
319
|
},
|
|
258
|
-
onError: (err) =>
|
|
320
|
+
onError: (err) => {
|
|
321
|
+
const change = updating();
|
|
322
|
+
if (change) failure(change.accessId, err, () => startUpdate(change));
|
|
323
|
+
else prompts.error(err.message);
|
|
324
|
+
},
|
|
259
325
|
onFinally: () => {
|
|
260
326
|
if (!disposed) setUpdating(null);
|
|
261
327
|
},
|
|
@@ -263,25 +329,63 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
263
329
|
|
|
264
330
|
const revokeMut = mutation.create<string | null, AccessEntry>({
|
|
265
331
|
mutation: async (entry) => {
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
332
|
+
if (disposed || !canEdit()) return null;
|
|
333
|
+
const displayName = labelOf(entry);
|
|
334
|
+
const confirmed = props.beforeChange
|
|
335
|
+
? await props.beforeChange(entry, null)
|
|
336
|
+
: await prompts.confirm(t().removeAccessConfirm({ name: displayName }), {
|
|
337
|
+
title: t().removeAccess,
|
|
338
|
+
variant: "danger",
|
|
339
|
+
});
|
|
340
|
+
if (!confirmed || disposed || !canEdit()) return null;
|
|
272
341
|
setRevoking(entry.id);
|
|
342
|
+
setFeedback(entry.id, undefined);
|
|
273
343
|
await props.revokeAccess(entry.id);
|
|
274
344
|
return entry.id;
|
|
275
345
|
},
|
|
276
346
|
onSuccess: (accessId) => {
|
|
277
|
-
if (accessId)
|
|
347
|
+
if (!accessId) return;
|
|
348
|
+
const index = entries.findIndex((entry) => entry.id === accessId);
|
|
349
|
+
setEntries((current) => current.filter((entry) => entry.id !== accessId));
|
|
350
|
+
focusAfterRemoval(index);
|
|
351
|
+
},
|
|
352
|
+
onError: (err) => {
|
|
353
|
+
const entry = entries.find((entry) => entry.id === revoking());
|
|
354
|
+
if (entry) failure(entry.id, err, () => startRevoke(entry));
|
|
355
|
+
else prompts.error(err.message);
|
|
278
356
|
},
|
|
279
|
-
onError: (err) => prompts.error(err.message),
|
|
280
357
|
onFinally: () => {
|
|
281
358
|
if (!disposed) setRevoking(null);
|
|
282
359
|
},
|
|
283
360
|
});
|
|
284
|
-
|
|
361
|
+
// Confirmation holds the operation lock, but its opener must stay focusable until the dialog returns focus.
|
|
362
|
+
const saving = () => grantPending() || updating() !== null || revoking() !== null;
|
|
363
|
+
const busy = () => saving() || updateMut.loading() || revokeMut.loading();
|
|
364
|
+
const canStart = () => !disposed && canEdit() && !busy();
|
|
365
|
+
const startUpdate = (change: LevelChange): void => {
|
|
366
|
+
if (canStart()) void updateMut.mutate(change);
|
|
367
|
+
};
|
|
368
|
+
const startRevoke = (entry: AccessEntry): void => {
|
|
369
|
+
if (canStart()) void revokeMut.mutate(entry);
|
|
370
|
+
};
|
|
371
|
+
let editorRoot: HTMLDivElement | undefined;
|
|
372
|
+
let principalPickerHost: HTMLDivElement | undefined;
|
|
373
|
+
const focusPrincipalPicker = () => {
|
|
374
|
+
queueMicrotask(() => {
|
|
375
|
+
if (!disposed && canEdit() && !busy()) principalPickerHost?.querySelector<HTMLInputElement>('input[role="combobox"]')?.focus();
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
// A removed row takes its focused button along; continue at the neighbouring row, else the principal search.
|
|
379
|
+
const focusAfterRemoval = (index: number) => {
|
|
380
|
+
queueMicrotask(() => {
|
|
381
|
+
if (disposed || busy()) return;
|
|
382
|
+
const active = document.activeElement;
|
|
383
|
+
if (active && active !== document.body && active.isConnected) return;
|
|
384
|
+
const removes = Array.from(editorRoot?.querySelectorAll<HTMLButtonElement>("button[data-access-remove]") ?? []);
|
|
385
|
+
const next = [removes[index], removes[index - 1]].find((button) => button && !button.disabled);
|
|
386
|
+
(next ?? editorRoot?.querySelector<HTMLInputElement>('input[role="combobox"]:enabled'))?.focus();
|
|
387
|
+
});
|
|
388
|
+
};
|
|
285
389
|
createEffect(() => {
|
|
286
390
|
const next = props.initialEntries;
|
|
287
391
|
const nextCallerEntries = new Map(next.map((entry) => [entry.id, JSON.stringify(entry)]));
|
|
@@ -305,6 +409,16 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
305
409
|
setEntries(reconcile(merged, { key: "id" }));
|
|
306
410
|
appliedCallerEntries = nextCallerEntries;
|
|
307
411
|
});
|
|
412
|
+
// Editing returns once the caller has the canonical list again, even an unchanged one: earlier failures and their
|
|
413
|
+
// retries no longer apply. Rows, confirmations and an unrelated selection stay.
|
|
414
|
+
createEffect(
|
|
415
|
+
on(canEdit, (editable, wasEditable) => {
|
|
416
|
+
if (!editable || wasEditable !== false) return;
|
|
417
|
+
for (const [id, state] of Object.entries(feedback)) if (state?.retry) setFeedback(id, undefined);
|
|
418
|
+
const selection = draft();
|
|
419
|
+
if (selection && entries.some((entry) => principalKey(entry.principal) === principalKey(selection.principal))) setDraft(undefined);
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
308
422
|
// The service refuses to remove the last manager; the row says so up front.
|
|
309
423
|
const lastManagerId = createMemo(() => {
|
|
310
424
|
// A grant on part of the resource never manages it.
|
|
@@ -314,31 +428,127 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
314
428
|
});
|
|
315
429
|
|
|
316
430
|
return (
|
|
317
|
-
<div
|
|
431
|
+
<div
|
|
432
|
+
ref={editorRoot}
|
|
433
|
+
class={props.expanded ? "flex flex-col gap-4" : "flex flex-col gap-3"}
|
|
434
|
+
style={
|
|
435
|
+
props.expanded
|
|
436
|
+
? { "--k2b-field-surface": "var(--k2b-surface)", "--k2b-field-border": "var(--k2b-border)", "--k2b-field-font-size": "15px" }
|
|
437
|
+
: undefined
|
|
438
|
+
}
|
|
439
|
+
>
|
|
440
|
+
<Show when={props.expanded && canEdit()}>
|
|
441
|
+
{/* Every control in the row stretches to the 36px field height, including the selection that replaces the search. */}
|
|
442
|
+
<div class="grid grid-cols-[minmax(0,1fr)_auto] gap-2 sm:grid-cols-[minmax(0,1fr)_176px_auto]">
|
|
443
|
+
<div ref={principalPickerHost} class="col-span-2 flex min-w-0 flex-col sm:col-span-1">
|
|
444
|
+
<Show
|
|
445
|
+
when={draft()}
|
|
446
|
+
fallback={
|
|
447
|
+
<PrincipalPicker
|
|
448
|
+
existing={entries.map((entry) => entry.principal)}
|
|
449
|
+
allowPublic={allowPublic()}
|
|
450
|
+
allowAuthenticated={allowAuthenticated()}
|
|
451
|
+
allowServiceAccounts={props.allowServiceAccounts}
|
|
452
|
+
placeholder={props.placeholder}
|
|
453
|
+
signedIn={props.signedIn}
|
|
454
|
+
disabled={saving()}
|
|
455
|
+
onSelect={(principal, display) => {
|
|
456
|
+
if (!canStart()) return;
|
|
457
|
+
setDraft({ principal, display });
|
|
458
|
+
setDraftLevel(allowed(principal)[0]?.key ?? "read");
|
|
459
|
+
}}
|
|
460
|
+
/>
|
|
461
|
+
}
|
|
462
|
+
>
|
|
463
|
+
{(selection) => (
|
|
464
|
+
<Button
|
|
465
|
+
variant="input"
|
|
466
|
+
style={{ "font-size": "15px" }}
|
|
467
|
+
align="start"
|
|
468
|
+
disabled={saving()}
|
|
469
|
+
class="w-full font-normal"
|
|
470
|
+
aria-label={t().clearSelection({ name: selection().display.displayName })}
|
|
471
|
+
onClick={() => {
|
|
472
|
+
if (!canStart()) return;
|
|
473
|
+
setDraft(undefined);
|
|
474
|
+
focusPrincipalPicker();
|
|
475
|
+
}}
|
|
476
|
+
>
|
|
477
|
+
<span class="min-w-0 flex-1 truncate">{selection().display.displayName}</span>
|
|
478
|
+
<i class="ti ti-x" aria-hidden="true" />
|
|
479
|
+
</Button>
|
|
480
|
+
)}
|
|
481
|
+
</Show>
|
|
482
|
+
</div>
|
|
483
|
+
<Select
|
|
484
|
+
aria-label={t().newPermission}
|
|
485
|
+
value={draftLevel}
|
|
486
|
+
disabled={saving()}
|
|
487
|
+
options={allowed(draft()?.principal ?? { type: "user", userId: "" }).map((option) => ({
|
|
488
|
+
value: option.key,
|
|
489
|
+
label: option.label,
|
|
490
|
+
}))}
|
|
491
|
+
onValueChange={(value) => {
|
|
492
|
+
if (value && canStart()) setDraftLevel(value);
|
|
493
|
+
}}
|
|
494
|
+
/>
|
|
495
|
+
<Button
|
|
496
|
+
style={{ "font-size": "15px" }}
|
|
497
|
+
disabled={!draft() || saving()}
|
|
498
|
+
loading={grantPending()}
|
|
499
|
+
onClick={() => {
|
|
500
|
+
const selection = draft();
|
|
501
|
+
if (selection)
|
|
502
|
+
void grant(
|
|
503
|
+
selection.principal,
|
|
504
|
+
selection.display,
|
|
505
|
+
allowed(selection.principal).find((option) => option.key === draftLevel()),
|
|
506
|
+
);
|
|
507
|
+
}}
|
|
508
|
+
>
|
|
509
|
+
{t().add}
|
|
510
|
+
</Button>
|
|
511
|
+
</div>
|
|
512
|
+
<Show when={feedback.add}>
|
|
513
|
+
{(state) => (
|
|
514
|
+
<div role="alert" class="text-sm text-danger">
|
|
515
|
+
{state().message}{" "}
|
|
516
|
+
<Button variant="text" disabled={saving()} onClick={() => state().retry?.()}>
|
|
517
|
+
{t().retry}
|
|
518
|
+
</Button>
|
|
519
|
+
</div>
|
|
520
|
+
)}
|
|
521
|
+
</Show>
|
|
522
|
+
</Show>
|
|
318
523
|
{/* Existing entries */}
|
|
319
|
-
<div class="flex flex-col gap-1">
|
|
524
|
+
<div class={props.expanded ? "flex flex-col" : "flex flex-col gap-1"}>
|
|
320
525
|
<For each={entries}>
|
|
321
526
|
{(entry) => {
|
|
322
|
-
// A row keeps its principal, so its group coverage is created once per row.
|
|
323
|
-
|
|
527
|
+
// A row keeps its principal, so its group coverage is created once per row. Expanded rows name the
|
|
528
|
+
// group's size in their subtitle instead.
|
|
529
|
+
const coverage = entry.principal.type === "group" && !props.expanded ? createGroupCoverage(entry.principal.groupId) : undefined;
|
|
324
530
|
return (
|
|
325
531
|
<AccessEntryRow
|
|
326
532
|
entry={entry}
|
|
533
|
+
label={labelOf(entry)}
|
|
534
|
+
detail={props.expanded ? props.entryDetail?.(entry) : undefined}
|
|
535
|
+
signedInDescription={props.signedIn?.description}
|
|
536
|
+
expanded={props.expanded}
|
|
537
|
+
ownEntry={props.ownEntryIds?.includes(entry.id)}
|
|
538
|
+
feedback={feedback[entry.id]}
|
|
327
539
|
coverage={coverage}
|
|
328
540
|
canEdit={canEdit()}
|
|
329
541
|
pending={entry.id === pending()}
|
|
330
542
|
updating={updatingRow(entry.id)}
|
|
331
543
|
revoking={entry.id === revoking()}
|
|
332
|
-
disabled={
|
|
544
|
+
disabled={saving()}
|
|
333
545
|
lastManager={entry.id === lastManagerId()}
|
|
334
546
|
allowed={allowed(entry.principal)}
|
|
335
547
|
singlePicker={allowed(entry.principal).length === 1}
|
|
336
548
|
onUpdatePermission={(option) => {
|
|
337
|
-
|
|
338
|
-
}}
|
|
339
|
-
onRevoke={() => {
|
|
340
|
-
if (!busy()) void revokeMut.mutate(entry);
|
|
549
|
+
startUpdate({ accessId: entry.id, permission: option.level, scope: option.scope });
|
|
341
550
|
}}
|
|
551
|
+
onRevoke={() => startRevoke(entry)}
|
|
342
552
|
/>
|
|
343
553
|
);
|
|
344
554
|
}}
|
|
@@ -351,14 +561,18 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
351
561
|
{/* Add access — single Combobox, granted at the lowest allowed
|
|
352
562
|
level on pick. The user upgrades via the row pill if they want
|
|
353
563
|
a higher level. KISS: one decision per step. */}
|
|
354
|
-
<Show when={canEdit()}>
|
|
564
|
+
<Show when={canEdit() && !props.expanded}>
|
|
355
565
|
<PrincipalPicker
|
|
356
566
|
existing={entries.map((e) => e.principal)}
|
|
357
567
|
allowPublic={allowPublic()}
|
|
358
568
|
allowAuthenticated={allowAuthenticated()}
|
|
359
569
|
allowServiceAccounts={props.allowServiceAccounts}
|
|
570
|
+
serviceAccountKinds={props.serviceAccountKinds}
|
|
571
|
+
serviceAccountStatus={props.serviceAccountStatus ?? "active"}
|
|
572
|
+
placeholder={props.placeholder}
|
|
573
|
+
signedIn={props.signedIn}
|
|
360
574
|
readOnly={grantPending()}
|
|
361
|
-
disabled={
|
|
575
|
+
disabled={updating() !== null || revoking() !== null}
|
|
362
576
|
onSelect={(principal, display) => void grant(principal, display)}
|
|
363
577
|
/>
|
|
364
578
|
</Show>
|
|
@@ -372,6 +586,13 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
372
586
|
|
|
373
587
|
function AccessEntryRow(props: {
|
|
374
588
|
entry: AccessEntry;
|
|
589
|
+
label: string;
|
|
590
|
+
/** Replaces the expanded subtitle's principal kind. */
|
|
591
|
+
detail?: string;
|
|
592
|
+
signedInDescription?: string;
|
|
593
|
+
expanded?: boolean;
|
|
594
|
+
ownEntry?: boolean;
|
|
595
|
+
feedback?: { message: string; retry?: () => void };
|
|
375
596
|
canEdit: boolean;
|
|
376
597
|
/** Another change is saving: the row keeps its controls, disabled. */
|
|
377
598
|
disabled: boolean;
|
|
@@ -394,7 +615,7 @@ function AccessEntryRow(props: {
|
|
|
394
615
|
}) {
|
|
395
616
|
const locale = useLocale();
|
|
396
617
|
const t = () => accessMessages.resolve([locale()]).t;
|
|
397
|
-
const displayName = () =>
|
|
618
|
+
const displayName = () => props.label;
|
|
398
619
|
const entryKey = () => levelKey(props.entry.permission, props.entry.scope);
|
|
399
620
|
const display = () => resolveEntryDisplay(props.entry.permission, props.entry.scope, props.allowed, t());
|
|
400
621
|
const manageLabel = () => resolveEntryDisplay("admin", undefined, props.allowed, t()).label;
|
|
@@ -420,37 +641,65 @@ function AccessEntryRow(props: {
|
|
|
420
641
|
|
|
421
642
|
return (
|
|
422
643
|
<div>
|
|
423
|
-
<div
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
>
|
|
432
|
-
<
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
644
|
+
<div
|
|
645
|
+
class={
|
|
646
|
+
props.expanded
|
|
647
|
+
? "group/access-row grid min-h-[52px] grid-cols-[32px_minmax(0,1fr)_36px] items-center gap-x-3 gap-y-1.5 rounded-[var(--ui-radius-control)] py-2 sm:flex sm:px-2 sm:py-1.5 sm:hover:bg-[var(--ui-surface-subtle)]"
|
|
648
|
+
: "group/access-row flex items-center gap-2 py-1.5"
|
|
649
|
+
}
|
|
650
|
+
aria-busy={saving() ? "true" : undefined}
|
|
651
|
+
>
|
|
652
|
+
<Show when={!props.expanded} fallback={<PrincipalAvatar entry={props.entry} label={displayName()} />}>
|
|
653
|
+
<Show
|
|
654
|
+
when={props.entry.principal.type === "user"}
|
|
655
|
+
fallback={
|
|
656
|
+
<div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
|
|
657
|
+
<i class={`ti ${getPrincipalIcon(props.entry, t())} text-sm`} />
|
|
658
|
+
</div>
|
|
659
|
+
}
|
|
660
|
+
>
|
|
661
|
+
<CloudAvatar
|
|
662
|
+
username={displayName()}
|
|
663
|
+
userId={props.entry.principal.type === "user" ? props.entry.principal.userId : undefined}
|
|
664
|
+
avatarHash={props.entry.avatarHash}
|
|
665
|
+
size="xs"
|
|
666
|
+
class="h-7 w-7"
|
|
667
|
+
/>
|
|
668
|
+
</Show>
|
|
439
669
|
</Show>
|
|
440
670
|
|
|
441
671
|
{/* Display name. Name and label share one line so every row keeps its height on phones:
|
|
442
672
|
when both do not fit, each gets an equal share and the shorter one stays whole. A group's
|
|
443
673
|
members toggle always stays whole; the name takes the rest. */}
|
|
444
674
|
<div
|
|
445
|
-
class=
|
|
675
|
+
class={
|
|
676
|
+
props.expanded ? "min-w-0 flex-1" : "grid min-w-0 flex-1 auto-cols-[minmax(0,max-content)] grid-flow-col items-baseline gap-1"
|
|
677
|
+
}
|
|
446
678
|
classList={{ "grid-cols-[minmax(0,max-content)_max-content]": props.coverage !== undefined }}
|
|
447
679
|
>
|
|
448
|
-
<span class="truncate text-sm">{displayName()}</span>
|
|
680
|
+
<span class={props.expanded ? "block truncate text-[15px] font-medium" : "truncate text-sm"}>{displayName()}</span>
|
|
681
|
+
<Show when={props.expanded}>
|
|
682
|
+
<span class="block text-[13px] text-dimmed">
|
|
683
|
+
{props.detail ??
|
|
684
|
+
(props.entry.principal.type === "user"
|
|
685
|
+
? t().person
|
|
686
|
+
: props.entry.principal.type === "group"
|
|
687
|
+
? t().group
|
|
688
|
+
: props.entry.principal.type === "authenticated"
|
|
689
|
+
? (props.signedInDescription ?? t().signedInDescription)
|
|
690
|
+
: props.entry.principal.type === "service_account"
|
|
691
|
+
? (props.entry.serviceAccountKind && serviceAccountKindDisplay(props.entry.serviceAccountKind, t()).label) ||
|
|
692
|
+
t().standaloneServiceAccount
|
|
693
|
+
: "")}
|
|
694
|
+
<Show when={props.ownEntry}> · {t().you}</Show>
|
|
695
|
+
</span>
|
|
696
|
+
</Show>
|
|
449
697
|
<Show when={props.entry.principal.type === "public"}>
|
|
450
698
|
<span class="truncate text-xs text-dimmed">({t().anyoneWithLink})</span>
|
|
451
699
|
</Show>
|
|
452
700
|
<Show
|
|
453
701
|
when={
|
|
702
|
+
!props.expanded &&
|
|
454
703
|
props.entry.principal.type === "service_account" &&
|
|
455
704
|
props.entry.serviceAccountKind &&
|
|
456
705
|
serviceAccountKindDisplay(props.entry.serviceAccountKind, t()).label
|
|
@@ -463,58 +712,118 @@ function AccessEntryRow(props: {
|
|
|
463
712
|
</Show>
|
|
464
713
|
</div>
|
|
465
714
|
|
|
715
|
+
<Show when={props.expanded}>
|
|
716
|
+
<span class="col-start-3 row-start-2 w-5 text-center text-sm" role="status">
|
|
717
|
+
<Show
|
|
718
|
+
when={saving()}
|
|
719
|
+
fallback={
|
|
720
|
+
<Show when={props.feedback && !props.feedback.retry}>
|
|
721
|
+
<>
|
|
722
|
+
<i class="ti ti-check text-success" aria-hidden="true" />
|
|
723
|
+
<span class="sr-only">{t().saved}</span>
|
|
724
|
+
</>
|
|
725
|
+
</Show>
|
|
726
|
+
}
|
|
727
|
+
>
|
|
728
|
+
<>
|
|
729
|
+
<i class="ti ti-loader-2 animate-spin" aria-hidden="true" />
|
|
730
|
+
<span class="sr-only">{t().saving}</span>
|
|
731
|
+
</>
|
|
732
|
+
</Show>
|
|
733
|
+
</span>
|
|
734
|
+
</Show>
|
|
735
|
+
|
|
466
736
|
{/* Permission badge — interactive single-value picker when editable,
|
|
467
737
|
plain span otherwise. */}
|
|
468
|
-
<
|
|
469
|
-
<
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
738
|
+
<div class={props.expanded ? "col-start-2 row-start-2 min-w-0 sm:w-44 sm:flex-none" : "contents"}>
|
|
739
|
+
<Show
|
|
740
|
+
when={props.expanded}
|
|
741
|
+
fallback={
|
|
742
|
+
<Show when={hasPicker()} fallback={<span class={`${badgeClass} cursor-default`}>{badgeContent}</span>}>
|
|
743
|
+
<SelectChip
|
|
744
|
+
aria-label={t().permissionFor({ name: displayName() })}
|
|
745
|
+
value={shownKey}
|
|
746
|
+
disabled={props.disabled}
|
|
747
|
+
loading={props.pending || props.updating !== undefined}
|
|
748
|
+
options={props.allowed.map((option) => {
|
|
749
|
+
const locked = props.lastManager && (option.level !== "admin" || option.scope !== undefined);
|
|
750
|
+
return {
|
|
751
|
+
value: option.key,
|
|
752
|
+
label: option.label,
|
|
753
|
+
icon: `ti ${option.icon}`,
|
|
754
|
+
disabled: locked,
|
|
755
|
+
description: locked ? t().lastManagerOption({ level: manageLabel() }) : option.description,
|
|
756
|
+
};
|
|
757
|
+
})}
|
|
758
|
+
icon={`ti ${shownDisplay().icon}`}
|
|
759
|
+
position="bottom-left"
|
|
760
|
+
onValueChange={(key) => {
|
|
761
|
+
const option = props.allowed.find((candidate) => candidate.key === key);
|
|
762
|
+
if (option && key !== entryKey()) props.onUpdatePermission(option);
|
|
763
|
+
}}
|
|
764
|
+
/>
|
|
765
|
+
</Show>
|
|
766
|
+
}
|
|
767
|
+
>
|
|
768
|
+
<Select
|
|
769
|
+
aria-label={t().permissionFor({ name: displayName() })}
|
|
770
|
+
value={shownKey}
|
|
771
|
+
disabled={props.disabled || !props.canEdit || props.singlePicker}
|
|
772
|
+
options={props.allowed.map((option) => ({
|
|
477
773
|
value: option.key,
|
|
478
774
|
label: option.label,
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (option && key !== entryKey()) props.onUpdatePermission(option);
|
|
489
|
-
}}
|
|
490
|
-
/>
|
|
491
|
-
</Show>
|
|
775
|
+
disabled: props.lastManager && option.level !== "admin",
|
|
776
|
+
}))}
|
|
777
|
+
onValueChange={(key) => {
|
|
778
|
+
const option = props.allowed.find((candidate) => candidate.key === key);
|
|
779
|
+
if (option && key !== entryKey()) props.onUpdatePermission(option);
|
|
780
|
+
}}
|
|
781
|
+
/>
|
|
782
|
+
</Show>
|
|
783
|
+
</div>
|
|
492
784
|
|
|
493
785
|
{/* Destructive row actions stay quiet until the row is engaged. They
|
|
494
786
|
remain keyboard reachable and stay visible on touch-sized layouts. */}
|
|
495
787
|
<Show when={props.canEdit}>
|
|
496
788
|
<Tooltip.Anchor
|
|
497
789
|
content={removeLabel()}
|
|
498
|
-
class=
|
|
790
|
+
class={
|
|
791
|
+
props.expanded
|
|
792
|
+
? "col-start-3 row-start-1 shrink-0"
|
|
793
|
+
: "shrink-0 opacity-100 transition-opacity sm:opacity-0 sm:group-hover/access-row:opacity-100 sm:group-focus-within/access-row:opacity-100"
|
|
794
|
+
}
|
|
499
795
|
>
|
|
500
796
|
<IconButton
|
|
501
797
|
type="button"
|
|
502
798
|
label={t().remove({ name: displayName() })}
|
|
503
799
|
variant="ghost"
|
|
504
|
-
size="xs"
|
|
800
|
+
size={props.expanded ? "md" : "xs"}
|
|
505
801
|
onClick={props.onRevoke}
|
|
506
802
|
disabled={props.disabled || props.pending || props.lastManager}
|
|
507
803
|
loading={props.revoking}
|
|
508
804
|
aria-label={t().remove({ name: displayName() })}
|
|
509
805
|
// A disabled button takes no focus and touch has no hover, so the reason also reaches assistive technology.
|
|
510
806
|
aria-description={props.lastManager ? removeLabel() : undefined}
|
|
807
|
+
data-access-remove=""
|
|
808
|
+
style={props.expanded ? { width: "2.25rem", height: "2.25rem", "min-height": "2.25rem" } : undefined}
|
|
511
809
|
class="focus-ui flex h-7 w-7 items-center justify-center rounded text-dimmed transition-colors focus:opacity-100 enabled:hover:bg-red-500/[0.08] enabled:hover:text-red-600 dark:enabled:hover:text-red-400"
|
|
512
810
|
>
|
|
513
|
-
<i class="ti ti-x text-sm" />
|
|
811
|
+
<i class={props.expanded ? "ti ti-x text-lg" : "ti ti-x text-sm"} />
|
|
514
812
|
</IconButton>
|
|
515
813
|
</Tooltip.Anchor>
|
|
516
814
|
</Show>
|
|
517
815
|
</div>
|
|
816
|
+
<Show when={props.expanded && props.lastManager && props.canEdit}>
|
|
817
|
+
<p class="pb-2 pl-11 text-[13px] text-dimmed sm:pl-13">{t().lastManager({ level: manageLabel() })}</p>
|
|
818
|
+
</Show>
|
|
819
|
+
<Show when={props.expanded && props.canEdit && props.feedback?.retry}>
|
|
820
|
+
<div role="alert" class="pl-11 text-[13px] text-danger sm:pl-13">
|
|
821
|
+
{props.feedback?.message}{" "}
|
|
822
|
+
<Button variant="text" disabled={props.disabled} onClick={() => props.feedback?.retry?.()}>
|
|
823
|
+
{t().retry}
|
|
824
|
+
</Button>
|
|
825
|
+
</div>
|
|
826
|
+
</Show>
|
|
518
827
|
{/* Expanding pushes the rows below down, only on the viewer's own request. */}
|
|
519
828
|
<Show when={props.coverage}>{(coverage) => <GroupCoveragePanel coverage={coverage()} groupName={displayName()} />}</Show>
|
|
520
829
|
</div>
|