@valentinkolb/cloud 0.3.1 → 0.5.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 +18 -8
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +119 -47
- package/src/_internal/runtime-context.ts +1 -0
- package/src/api/accounts-entities.ts +4 -0
- package/src/api/admin-core-settings.ts +98 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/auth/schemas.ts +24 -0
- package/src/api/auth.ts +113 -10
- package/src/api/index.ts +15 -25
- package/src/api/me.ts +203 -14
- package/src/api/search/schemas.ts +1 -0
- package/src/api/search.ts +62 -8
- package/src/config/ssr.ts +2 -9
- package/src/contracts/announcements.test.ts +37 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app.ts +4 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +4 -0
- package/src/contracts/shared.ts +108 -1
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/server/api/index.ts +1 -1
- package/src/server/api/respond.ts +50 -10
- package/src/server/index.ts +44 -38
- package/src/server/middleware/auth.ts +98 -9
- package/src/server/middleware/index.ts +2 -1
- package/src/server/middleware/settings.ts +26 -0
- package/src/server/services/access.test.ts +197 -0
- package/src/server/services/access.ts +254 -6
- package/src/server/services/index.ts +14 -11
- package/src/server/services/pagination.ts +22 -0
- package/src/server/time.ts +45 -0
- package/src/services/account-lifecycle/index.ts +142 -18
- package/src/services/accounts/app.ts +658 -170
- package/src/services/accounts/authz.test.ts +77 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/entities.ts +84 -5
- package/src/services/accounts/groups.ts +30 -24
- package/src/services/accounts/model.test.ts +30 -0
- package/src/services/accounts/switching.test.ts +14 -0
- package/src/services/accounts/switching.ts +15 -6
- package/src/services/accounts/users.ts +75 -52
- package/src/services/announcements/index.test.ts +32 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/audit/index.test.ts +84 -0
- package/src/services/audit/index.ts +431 -0
- package/src/services/auth-flows/index.ts +9 -2
- package/src/services/auth-flows/ipa.ts +0 -2
- package/src/services/auth-flows/magic-link.ts +3 -2
- package/src/services/auth-flows/password-reset.ts +284 -0
- package/src/services/auth-flows/proxy-return.test.ts +24 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/gateway.ts +162 -0
- package/src/services/index.ts +44 -2
- package/src/services/ipa/effective-groups.test.ts +33 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/profile.ts +45 -3
- package/src/services/ipa/search.ts +3 -5
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.test.ts +32 -0
- package/src/services/ipa/sync-planning.ts +22 -0
- package/src/services/ipa/sync.ts +110 -38
- package/src/services/oauth-tokens.ts +104 -0
- package/src/services/postgres.ts +21 -6
- package/src/services/providers/local/auth.test.ts +22 -0
- package/src/services/providers/local/auth.ts +46 -3
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.test.ts +210 -0
- package/src/services/service-account-credentials.ts +715 -0
- package/src/services/service-accounts.ts +188 -0
- package/src/services/session/index.ts +7 -8
- package/src/services/settings/app.ts +4 -20
- package/src/services/settings/defaults.ts +64 -22
- package/src/services/settings/store.ts +47 -0
- package/src/services/weather/forecast.ts +40 -7
- package/src/services/webauthn.test.ts +36 -0
- package/src/services/webauthn.ts +384 -0
- package/src/shared/icons.ts +391 -100
- package/src/shared/index.ts +7 -0
- package/src/shared/markdown/extensions/code.ts +38 -1
- package/src/shared/markdown/extensions/images.ts +39 -3
- package/src/shared/markdown/extensions/info-blocks.ts +5 -5
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +79 -58
- package/src/shared/markdown/formula.test.ts +1089 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +76 -2
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/redirect.test.ts +49 -0
- package/src/shared/redirect.ts +52 -0
- package/src/shared/theme.test.ts +24 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +7 -3
- package/src/ssr/AdminSidebar.tsx +115 -49
- package/src/ssr/AppLaunchpad.island.tsx +176 -0
- package/src/ssr/Footer.island.tsx +3 -8
- package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
- package/src/ssr/GlobalSearchDialog.tsx +545 -117
- package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
- package/src/ssr/Layout.tsx +74 -66
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutHelp.tsx +266 -0
- package/src/ssr/NavMenu.island.tsx +0 -39
- package/src/ssr/ThemeToggleRail.island.tsx +3 -3
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/islands/index.ts +13 -0
- package/src/styles/base-popover.css +5 -2
- package/src/styles/effects.css +87 -6
- package/src/styles/global.css +146 -9
- package/src/styles/input.css +3 -1
- package/src/styles/utilities-buttons.css +133 -27
- package/src/styles/utilities-code-display.css +67 -0
- package/src/styles/utilities-completion.css +223 -0
- package/src/styles/utilities-detail.css +73 -0
- package/src/styles/utilities-feedback.css +16 -15
- package/src/styles/utilities-layout.css +42 -2
- package/src/styles/utilities-markdown-editor.css +472 -0
- package/src/styles/utilities-navigation.css +63 -8
- package/src/styles/utilities-script.css +84 -0
- package/src/styles/utilities-table-tile.css +229 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/ui/completion/behaviors.test.ts +95 -0
- package/src/ui/completion/behaviors.ts +205 -0
- package/src/ui/completion/engine.ts +368 -0
- package/src/ui/completion/index.ts +40 -0
- package/src/ui/completion/overlay.ts +92 -0
- package/src/ui/dialog-core.ts +173 -45
- package/src/ui/filter/FilterChip.tsx +42 -40
- package/src/ui/index.ts +11 -12
- package/src/ui/input/AutocompleteEditor.tsx +656 -0
- package/src/ui/input/CheckboxCard.tsx +91 -0
- package/src/ui/input/Combobox.tsx +375 -0
- package/src/ui/input/DatePicker.tsx +846 -0
- package/src/ui/input/DateTimeInput.tsx +29 -4
- package/src/ui/input/FileDropzone.tsx +116 -0
- package/src/ui/input/IconInput.tsx +116 -0
- package/src/ui/input/ImageInput.tsx +19 -2
- package/src/ui/input/MultiSelectInput.tsx +448 -0
- package/src/ui/input/NumberInput.tsx +417 -61
- package/src/ui/input/SegmentedControl.tsx +2 -2
- package/src/ui/input/Select.tsx +172 -10
- package/src/ui/input/Slider.tsx +3 -4
- package/src/ui/input/Switch.tsx +3 -2
- package/src/ui/input/TemplateEditor.tsx +212 -0
- package/src/ui/input/TextInput.tsx +144 -13
- package/src/ui/input/index.ts +53 -8
- package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
- package/src/ui/input/markdown/Toolbar.tsx +90 -0
- package/src/ui/input/markdown/actions.ts +233 -0
- package/src/ui/input/markdown/active-formats.ts +94 -0
- package/src/ui/input/markdown/behaviors.ts +193 -0
- package/src/ui/input/markdown/code-zone.ts +23 -0
- package/src/ui/input/markdown/highlight.ts +316 -0
- package/src/ui/layout.ts +22 -0
- package/src/ui/misc/AppOverview.tsx +105 -0
- package/src/ui/misc/AppWorkspace.tsx +607 -0
- package/src/ui/misc/Calendar.tsx +1291 -0
- package/src/ui/misc/Chart.tsx +162 -0
- package/src/ui/misc/CodeDisplay.tsx +54 -0
- package/src/ui/misc/ContextMenu.tsx +2 -2
- package/src/ui/misc/DataTable.tsx +269 -0
- package/src/ui/misc/DockWorkspace.tsx +425 -0
- package/src/ui/misc/Docs.tsx +153 -0
- package/src/ui/misc/Dropdown.tsx +2 -2
- package/src/ui/misc/EntitySearch.tsx +260 -129
- package/src/ui/misc/LinkCard.tsx +14 -2
- package/src/ui/misc/LogEntriesTable.tsx +34 -31
- package/src/ui/misc/Pagination.tsx +31 -12
- package/src/ui/misc/PanelDialog.tsx +109 -0
- package/src/ui/misc/Panes.tsx +873 -0
- package/src/ui/misc/PermissionEditor.tsx +358 -262
- package/src/ui/misc/Placeholder.tsx +40 -0
- package/src/ui/misc/ProgressBar.tsx +1 -1
- package/src/ui/misc/ResourceApiKeys.tsx +260 -0
- package/src/ui/misc/SettingsModal.tsx +150 -0
- package/src/ui/misc/StatCell.tsx +182 -40
- package/src/ui/misc/StatGrid.tsx +149 -0
- package/src/ui/misc/StructuredDataPreview.tsx +107 -0
- package/src/ui/misc/code-highlight.ts +213 -0
- package/src/ui/misc/index.ts +93 -12
- package/src/ui/prompts.tsx +362 -312
- package/src/ui/toast.ts +384 -0
- package/src/ui/widgets/Widget.tsx +12 -4
- package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
- package/src/ui/ipa/GroupView.tsx +0 -36
- package/src/ui/ipa/LoginBtn.tsx +0 -16
- package/src/ui/ipa/UserView.tsx +0 -58
- package/src/ui/ipa/index.ts +0 -4
- package/src/ui/navigation.ts +0 -32
- package/src/ui/sidebar.tsx +0 -468
- /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
|
@@ -1,79 +1,197 @@
|
|
|
1
|
-
import { createSignal, For, Show } from "solid-js";
|
|
2
|
-
|
|
3
|
-
import { prompts } from "../prompts";
|
|
4
1
|
import { mutation } from "@valentinkolb/stdlib/solid";
|
|
5
|
-
import
|
|
6
|
-
import EntitySearch, { type EntitySearchResult } from "./EntitySearch";
|
|
2
|
+
import { createSignal, For, Show } from "solid-js";
|
|
7
3
|
import type { AccessEntry, PermissionLevel, Principal } from "../../contracts/shared";
|
|
4
|
+
import Combobox, { type ComboboxOption } from "../input/Combobox";
|
|
5
|
+
import { prompts } from "../prompts";
|
|
6
|
+
import Dropdown from "./Dropdown";
|
|
7
|
+
import Placeholder from "./Placeholder";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Public API
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** The three grantable permission levels — `"none"` exists in the
|
|
14
|
+
* contract for resolution semantics but is never directly granted. */
|
|
15
|
+
export type GrantableLevel = Exclude<PermissionLevel, "none">;
|
|
16
|
+
|
|
17
|
+
/** Either a bare level (uses the default View / Edit / Manage label and
|
|
18
|
+
* icon) or an object with per-context overrides. */
|
|
19
|
+
export type AllowedLevel = GrantableLevel | { level: GrantableLevel; label?: string; icon?: string };
|
|
18
20
|
|
|
19
21
|
type PermissionEditorProps = {
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/** Initial access entries — caller stays the source of truth for
|
|
23
|
+
* what's stored on the resource; the editor only mutates locally
|
|
24
|
+
* on optimistic update. */
|
|
23
25
|
initialEntries: AccessEntry[];
|
|
24
|
-
|
|
26
|
+
|
|
27
|
+
/** Whether the current user can edit permissions. When `false`, the
|
|
28
|
+
* editor renders the entries read-only — no row dropdowns, no
|
|
29
|
+
* delete buttons, no add form. */
|
|
25
30
|
canEdit?: boolean;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
/** Grant access. The caller closes over the resource id. */
|
|
33
|
+
grantAccess: (principal: Principal, permission: GrantableLevel) => Promise<AccessEntry>;
|
|
34
|
+
|
|
35
|
+
/** Update an existing entry's permission level. */
|
|
36
|
+
updateAccess: (accessId: string, permission: GrantableLevel) => Promise<void>;
|
|
37
|
+
|
|
38
|
+
/** Revoke an existing entry. The last entry IS deletable — for
|
|
39
|
+
* hierarchical resources the parent ACL still applies. */
|
|
40
|
+
revokeAccess: (accessId: string) => Promise<void>;
|
|
41
|
+
|
|
42
|
+
/** Allow granting `public` access from this editor. The
|
|
43
|
+
* authenticated principal is always allowed (no flag needed). */
|
|
33
44
|
allowPublic?: boolean;
|
|
45
|
+
|
|
46
|
+
/** Allow granting service accounts. Off by default so ordinary
|
|
47
|
+
* permission pickers stay user/group focused. */
|
|
48
|
+
allowServiceAccounts?: boolean;
|
|
49
|
+
|
|
50
|
+
/** Which levels the UI offers — and what they're called. Bare strings
|
|
51
|
+
* use the default labels (View / Edit / Manage). Objects override
|
|
52
|
+
* label and/or icon for per-context vocabulary (e.g. forms call
|
|
53
|
+
* write "Use", views call read "View"). When undefined, all three
|
|
54
|
+
* are offered with default labels. New entries are granted
|
|
55
|
+
* `allowedLevels[0]` on pick — the user upgrades via the row pill
|
|
56
|
+
* afterwards. */
|
|
57
|
+
allowedLevels?: AllowedLevel[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// Defaults & helpers
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const DEFAULT_LABELS: Record<PermissionLevel, { label: string; icon: string }> = {
|
|
65
|
+
read: { label: "View", icon: "ti-eye" },
|
|
66
|
+
write: { label: "Edit", icon: "ti-pencil" },
|
|
67
|
+
admin: { label: "Manage", icon: "ti-shield" },
|
|
68
|
+
// Defensive — never granted by this editor, but renders correctly if
|
|
69
|
+
// a legacy entry has permission === "none".
|
|
70
|
+
none: { label: "No access", icon: "ti-ban" },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type ResolvedLevel = {
|
|
74
|
+
level: GrantableLevel;
|
|
75
|
+
label: string;
|
|
76
|
+
icon: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Resolve the AllowedLevel union into a flat shape the renderer can
|
|
80
|
+
* loop over. Falls back to the default View / Edit / Manage list when
|
|
81
|
+
* no override is given. */
|
|
82
|
+
const resolveAllowedLevels = (allowed: AllowedLevel[] | undefined): ResolvedLevel[] => {
|
|
83
|
+
const list = allowed && allowed.length > 0 ? allowed : (["read", "write", "admin"] as GrantableLevel[]);
|
|
84
|
+
return list.map((entry) => {
|
|
85
|
+
const level = typeof entry === "string" ? entry : entry.level;
|
|
86
|
+
const override = typeof entry === "string" ? null : entry;
|
|
87
|
+
const def = DEFAULT_LABELS[level];
|
|
88
|
+
return {
|
|
89
|
+
level,
|
|
90
|
+
label: override?.label ?? def.label,
|
|
91
|
+
icon: override?.icon ?? def.icon,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Resolve a stored entry's permission to a renderable {label,icon},
|
|
97
|
+
* preferring the caller's allowedLevels override and falling back to
|
|
98
|
+
* the platform defaults. Tolerates "none" / unknown legacy values. */
|
|
99
|
+
const resolveEntryDisplay = (permission: PermissionLevel, allowed: ResolvedLevel[]): { label: string; icon: string } => {
|
|
100
|
+
const fromAllowed = allowed.find((a) => a.level === permission);
|
|
101
|
+
if (fromAllowed) return fromAllowed;
|
|
102
|
+
return DEFAULT_LABELS[permission] ?? DEFAULT_LABELS.none;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getEntryDisplayName = (entry: AccessEntry): string => {
|
|
106
|
+
if (entry.displayName) return entry.displayName;
|
|
107
|
+
if (entry.principal.type === "authenticated") return "All users (incl. guests)";
|
|
108
|
+
if (entry.principal.type === "public") return "Public";
|
|
109
|
+
if (entry.principal.type === "user") return entry.principal.userId;
|
|
110
|
+
if (entry.principal.type === "service_account") return entry.principal.serviceAccountId;
|
|
111
|
+
return entry.principal.groupId;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const getPrincipalIcon = (principal: Principal): string => {
|
|
115
|
+
switch (principal.type) {
|
|
116
|
+
case "user":
|
|
117
|
+
return "ti-user";
|
|
118
|
+
case "group":
|
|
119
|
+
return "ti-users-group";
|
|
120
|
+
case "service_account":
|
|
121
|
+
return "ti-key";
|
|
122
|
+
case "authenticated":
|
|
123
|
+
return "ti-lock-open-2";
|
|
124
|
+
case "public":
|
|
125
|
+
return "ti-world";
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const getPermissionColor = (level: PermissionLevel): string => {
|
|
130
|
+
switch (level) {
|
|
131
|
+
case "read":
|
|
132
|
+
return "text-blue-500";
|
|
133
|
+
case "write":
|
|
134
|
+
return "text-amber-500";
|
|
135
|
+
case "admin":
|
|
136
|
+
return "text-purple-500";
|
|
137
|
+
default:
|
|
138
|
+
return "text-zinc-500";
|
|
139
|
+
}
|
|
34
140
|
};
|
|
35
141
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
142
|
+
// Backend `/api/accounts/entities` shape (the subset we consume).
|
|
143
|
+
type ApiEntity =
|
|
144
|
+
| { kind: "user"; user: { id: string; uid: string; displayName: string; mail: string | null } }
|
|
145
|
+
| { kind: "group"; group: { id: string; name: string; description: string | null } }
|
|
146
|
+
| { kind: "service_account"; serviceAccount: { id: string; name: string; kind: "user_delegated" | "resource_bound"; appId: string | null; resourceType: string | null; resourceId: string | null } };
|
|
147
|
+
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
149
|
+
// PermissionEditor
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
40
152
|
export default function PermissionEditor(props: PermissionEditorProps) {
|
|
41
153
|
const [entries, setEntries] = createSignal<AccessEntry[]>([...props.initialEntries]);
|
|
42
|
-
const [showAddForm, setShowAddForm] = createSignal(false);
|
|
43
154
|
const canEdit = () => props.canEdit !== false;
|
|
44
155
|
const allowPublic = () => props.allowPublic === true;
|
|
156
|
+
const allowed = () => resolveAllowedLevels(props.allowedLevels);
|
|
157
|
+
const isSinglePicker = () => allowed().length === 1;
|
|
158
|
+
|
|
159
|
+
// Defensive dev-warning: an empty allowedLevels array makes the editor
|
|
160
|
+
// unable to grant anything.
|
|
161
|
+
if (props.allowedLevels && props.allowedLevels.length === 0) {
|
|
162
|
+
if (typeof console !== "undefined") {
|
|
163
|
+
console.warn(
|
|
164
|
+
"[PermissionEditor] `allowedLevels=[]` — the editor cannot grant any permission. Pass at least one level or omit the prop for the default View / Edit / Manage set.",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
45
168
|
|
|
46
|
-
// Get existing user IDs and group IDs to exclude from search
|
|
47
169
|
const existingUserIds = () =>
|
|
48
170
|
entries()
|
|
49
171
|
.filter((e) => e.principal.type === "user")
|
|
50
172
|
.map((e) => (e.principal as { type: "user"; userId: string }).userId);
|
|
51
|
-
|
|
52
173
|
const existingGroupIds = () =>
|
|
53
174
|
entries()
|
|
54
175
|
.filter((e) => e.principal.type === "group")
|
|
55
176
|
.map((e) => (e.principal as { type: "group"; groupId: string }).groupId);
|
|
56
|
-
|
|
177
|
+
const existingServiceAccountIds = () =>
|
|
178
|
+
entries()
|
|
179
|
+
.filter((e) => e.principal.type === "service_account")
|
|
180
|
+
.map((e) => (e.principal as { type: "service_account"; serviceAccountId: string }).serviceAccountId);
|
|
57
181
|
const hasAuthenticatedEntry = () => entries().some((entry) => entry.principal.type === "authenticated");
|
|
58
182
|
const hasPublicEntry = () => entries().some((entry) => entry.principal.type === "public");
|
|
59
183
|
|
|
60
|
-
// Grant access mutation
|
|
61
184
|
const grantMut = mutation.create({
|
|
62
|
-
mutation: async (data: { principal: Principal; permission:
|
|
63
|
-
return props.grantAccess(props.resourceId, data.principal, data.permission);
|
|
64
|
-
},
|
|
185
|
+
mutation: async (data: { principal: Principal; permission: GrantableLevel }) => props.grantAccess(data.principal, data.permission),
|
|
65
186
|
onSuccess: (newEntry) => {
|
|
66
187
|
setEntries([...entries(), newEntry as AccessEntry]);
|
|
67
|
-
setShowAddForm(false);
|
|
68
188
|
},
|
|
69
189
|
onError: (err) => prompts.error(err.message),
|
|
70
190
|
});
|
|
71
191
|
|
|
72
|
-
|
|
73
|
-
const updateMut = mutation.create<{ accessId: string; permission: PermissionLevel }, { accessId: string; permission: PermissionLevel }>({
|
|
192
|
+
const updateMut = mutation.create<{ accessId: string; permission: GrantableLevel }, { accessId: string; permission: GrantableLevel }>({
|
|
74
193
|
mutation: async (data) => {
|
|
75
|
-
await props.updateAccess(
|
|
76
|
-
// Return the data so we can use it in onSuccess
|
|
194
|
+
await props.updateAccess(data.accessId, data.permission);
|
|
77
195
|
return data;
|
|
78
196
|
},
|
|
79
197
|
onSuccess: (result) => {
|
|
@@ -84,20 +202,14 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
84
202
|
onError: (err) => prompts.error(err.message),
|
|
85
203
|
});
|
|
86
204
|
|
|
87
|
-
// Revoke access mutation
|
|
88
205
|
const revokeMut = mutation.create<void, string>({
|
|
89
206
|
mutation: async (accessId: string) => {
|
|
90
|
-
await props.revokeAccess(
|
|
207
|
+
await props.revokeAccess(accessId);
|
|
91
208
|
},
|
|
92
209
|
onError: (err) => prompts.error(err.message),
|
|
93
210
|
});
|
|
94
211
|
|
|
95
212
|
const handleRevoke = async (entry: AccessEntry) => {
|
|
96
|
-
if (entries().length <= 1) {
|
|
97
|
-
prompts.error("Cannot remove the last access entry");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
213
|
const displayName = getEntryDisplayName(entry);
|
|
102
214
|
const confirmed = await prompts.confirm(`Remove access for ${displayName}?`, { title: "Remove Access", variant: "danger" });
|
|
103
215
|
if (confirmed) {
|
|
@@ -106,186 +218,254 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
106
218
|
}
|
|
107
219
|
};
|
|
108
220
|
|
|
109
|
-
|
|
110
|
-
|
|
221
|
+
// ── Combobox add-flow ─────────────────────────────────────────────────
|
|
222
|
+
// The Combobox is a fire-and-forget input: type → pick → granted at the
|
|
223
|
+
// lowest allowed level. The `principalsByOptId` map carries the original
|
|
224
|
+
// discriminated principal across the ComboboxOption boundary so onSelect
|
|
225
|
+
// can route it to grantAccess without re-parsing prefixed ids.
|
|
226
|
+
let principalsByOptId = new Map<string, Principal>();
|
|
227
|
+
|
|
228
|
+
const fetchPrincipals = async (q: string, signal: AbortSignal): Promise<ComboboxOption[]> => {
|
|
229
|
+
const map = new Map<string, Principal>();
|
|
230
|
+
const opts: ComboboxOption[] = [];
|
|
231
|
+
|
|
232
|
+
// Synthetic principals — only when allowed AND not already granted.
|
|
233
|
+
// Placed first so they're visible immediately on focus, before any
|
|
234
|
+
// typing kicks off a backend request.
|
|
235
|
+
if (!hasAuthenticatedEntry()) {
|
|
236
|
+
map.set("auth", { type: "authenticated" });
|
|
237
|
+
opts.push({
|
|
238
|
+
id: "auth",
|
|
239
|
+
label: "All users (incl. guests)",
|
|
240
|
+
description: "Anyone signed in to the cloud",
|
|
241
|
+
icon: "ti-lock-open-2",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (allowPublic() && !hasPublicEntry()) {
|
|
245
|
+
map.set("public", { type: "public" });
|
|
246
|
+
opts.push({
|
|
247
|
+
id: "public",
|
|
248
|
+
label: "Public",
|
|
249
|
+
description: "Anyone with the link, even unauthenticated",
|
|
250
|
+
icon: "ti-world",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
111
253
|
|
|
112
|
-
|
|
254
|
+
// Real entities require a query — avoid a wide listing on every focus.
|
|
255
|
+
if (q.length >= 2) {
|
|
256
|
+
const url = new URL("/api/accounts/entities", window.location.origin);
|
|
257
|
+
url.searchParams.set("search", q);
|
|
258
|
+
url.searchParams.set("kinds", props.allowServiceAccounts ? "user,group,service_account" : "user,group");
|
|
259
|
+
url.searchParams.set("per_page", "10");
|
|
260
|
+
const userIds = existingUserIds();
|
|
261
|
+
if (userIds.length) url.searchParams.set("exclude_user_ids", userIds.join(","));
|
|
262
|
+
const groupIds = existingGroupIds();
|
|
263
|
+
if (groupIds.length) url.searchParams.set("exclude_group_ids", groupIds.join(","));
|
|
264
|
+
const serviceAccountIds = existingServiceAccountIds();
|
|
265
|
+
if (serviceAccountIds.length) url.searchParams.set("exclude_service_account_ids", serviceAccountIds.join(","));
|
|
266
|
+
|
|
267
|
+
const res = await fetch(url.toString(), { credentials: "same-origin", signal });
|
|
268
|
+
if (res.ok) {
|
|
269
|
+
const data = (await res.json()) as { items?: ApiEntity[] };
|
|
270
|
+
for (const item of data.items ?? []) {
|
|
271
|
+
if (item.kind === "user") {
|
|
272
|
+
const id = `u:${item.user.id}`;
|
|
273
|
+
map.set(id, { type: "user", userId: item.user.id });
|
|
274
|
+
opts.push({
|
|
275
|
+
id,
|
|
276
|
+
label: item.user.displayName,
|
|
277
|
+
description: item.user.mail ?? item.user.uid,
|
|
278
|
+
icon: "ti-user",
|
|
279
|
+
});
|
|
280
|
+
} else if (item.kind === "group") {
|
|
281
|
+
const id = `g:${item.group.id}`;
|
|
282
|
+
map.set(id, { type: "group", groupId: item.group.id });
|
|
283
|
+
opts.push({
|
|
284
|
+
id,
|
|
285
|
+
label: item.group.name,
|
|
286
|
+
description: item.group.description ?? undefined,
|
|
287
|
+
icon: "ti-users-group",
|
|
288
|
+
});
|
|
289
|
+
} else if (item.kind === "service_account") {
|
|
290
|
+
const id = `sa:${item.serviceAccount.id}`;
|
|
291
|
+
map.set(id, { type: "service_account", serviceAccountId: item.serviceAccount.id });
|
|
292
|
+
opts.push({
|
|
293
|
+
id,
|
|
294
|
+
label: item.serviceAccount.name,
|
|
295
|
+
description:
|
|
296
|
+
item.serviceAccount.kind === "user_delegated"
|
|
297
|
+
? "User-bound service account"
|
|
298
|
+
: [item.serviceAccount.appId, item.serviceAccount.resourceType, item.serviceAccount.resourceId].filter(Boolean).join(" · "),
|
|
299
|
+
icon: "ti-key",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
principalsByOptId = map;
|
|
307
|
+
return opts;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const handleSelect = (option: ComboboxOption) => {
|
|
311
|
+
const principal = principalsByOptId.get(option.id);
|
|
312
|
+
if (!principal) return;
|
|
313
|
+
const firstLevel = allowed()[0]?.level;
|
|
314
|
+
if (!firstLevel) return; // dev-warned above; bail silently
|
|
315
|
+
grantMut.mutate({ principal, permission: firstLevel });
|
|
113
316
|
};
|
|
114
317
|
|
|
115
318
|
return (
|
|
116
319
|
<div class="flex flex-col gap-3">
|
|
117
320
|
{/* Existing entries */}
|
|
118
|
-
<div class="flex flex-col
|
|
321
|
+
<div class="flex flex-col gap-1">
|
|
119
322
|
<For each={entries()}>
|
|
120
323
|
{(entry) => (
|
|
121
324
|
<AccessEntryRow
|
|
122
325
|
entry={entry}
|
|
123
326
|
canEdit={canEdit()}
|
|
124
|
-
|
|
327
|
+
allowed={allowed()}
|
|
328
|
+
singlePicker={isSinglePicker()}
|
|
125
329
|
onUpdatePermission={(permission) => updateMut.mutate({ accessId: entry.id, permission })}
|
|
126
330
|
onRevoke={() => handleRevoke(entry)}
|
|
127
|
-
updating={updateMut.loading()}
|
|
128
331
|
/>
|
|
129
332
|
)}
|
|
130
333
|
</For>
|
|
334
|
+
<Show when={entries().length === 0}>
|
|
335
|
+
<Placeholder align="left" class="px-1 py-2">
|
|
336
|
+
No direct grants yet.
|
|
337
|
+
</Placeholder>
|
|
338
|
+
</Show>
|
|
131
339
|
</div>
|
|
132
340
|
|
|
133
|
-
{/* Add access
|
|
341
|
+
{/* Add access — single Combobox, granted at the lowest allowed
|
|
342
|
+
level on pick. The user upgrades via the row pill if they want
|
|
343
|
+
a higher level. KISS: one decision per step. */}
|
|
134
344
|
<Show when={canEdit()}>
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class="flex items-center gap-2 text-sm text-dimmed hover:text-primary transition-colors"
|
|
142
|
-
>
|
|
143
|
-
<i class="ti ti-plus" />
|
|
144
|
-
<span>Add access</span>
|
|
145
|
-
</button>
|
|
146
|
-
}
|
|
147
|
-
>
|
|
148
|
-
<AddAccessForm
|
|
149
|
-
existingUserIds={existingUserIds()}
|
|
150
|
-
existingGroupIds={existingGroupIds()}
|
|
151
|
-
onSelectEntity={handleEntitySelect}
|
|
152
|
-
onSelectPrincipal={(principal, permission) => grantMut.mutate({ principal, permission })}
|
|
153
|
-
onCancel={() => setShowAddForm(false)}
|
|
154
|
-
loading={grantMut.loading()}
|
|
155
|
-
showAuthenticated={!hasAuthenticatedEntry()}
|
|
156
|
-
showPublic={allowPublic() && !hasPublicEntry()}
|
|
157
|
-
/>
|
|
158
|
-
</Show>
|
|
345
|
+
<Combobox
|
|
346
|
+
placeholder={props.allowServiceAccounts ? "Add user, group, service account or audience..." : "Add user, group or audience..."}
|
|
347
|
+
fetchData={fetchPrincipals}
|
|
348
|
+
onSelect={handleSelect}
|
|
349
|
+
disabled={grantMut.loading()}
|
|
350
|
+
/>
|
|
159
351
|
</Show>
|
|
160
352
|
</div>
|
|
161
353
|
);
|
|
162
354
|
}
|
|
163
355
|
|
|
164
|
-
//
|
|
165
|
-
// Helper Functions
|
|
166
|
-
// =============================================================================
|
|
167
|
-
|
|
168
|
-
function getEntryDisplayName(entry: AccessEntry): string {
|
|
169
|
-
if (entry.displayName) return entry.displayName;
|
|
170
|
-
if (entry.principal.type === "authenticated") return "All users (incl. guests)";
|
|
171
|
-
if (entry.principal.type === "public") return "Public";
|
|
172
|
-
if (entry.principal.type === "user") return entry.principal.userId;
|
|
173
|
-
return entry.principal.groupId;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function getPrincipalIcon(principal: Principal): string {
|
|
177
|
-
switch (principal.type) {
|
|
178
|
-
case "user":
|
|
179
|
-
return "ti-user";
|
|
180
|
-
case "group":
|
|
181
|
-
return "ti-users-group";
|
|
182
|
-
case "authenticated":
|
|
183
|
-
return "ti-lock-open-2";
|
|
184
|
-
case "public":
|
|
185
|
-
return "ti-world";
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function getPermissionColor(level: PermissionLevel): string {
|
|
190
|
-
switch (level) {
|
|
191
|
-
case "read":
|
|
192
|
-
return "text-blue-500";
|
|
193
|
-
case "write":
|
|
194
|
-
return "text-amber-500";
|
|
195
|
-
case "admin":
|
|
196
|
-
return "text-purple-500";
|
|
197
|
-
default:
|
|
198
|
-
return "text-zinc-500";
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// =============================================================================
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
203
357
|
// Access Entry Row
|
|
204
|
-
//
|
|
358
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
205
359
|
|
|
206
360
|
function AccessEntryRow(props: {
|
|
207
361
|
entry: AccessEntry;
|
|
208
362
|
canEdit: boolean;
|
|
209
|
-
|
|
210
|
-
|
|
363
|
+
allowed: ResolvedLevel[];
|
|
364
|
+
/** When true the per-row picker collapses to a non-interactive badge
|
|
365
|
+
* (single-level mode — there's nothing to switch to). */
|
|
366
|
+
singlePicker: boolean;
|
|
367
|
+
onUpdatePermission: (permission: GrantableLevel) => void;
|
|
211
368
|
onRevoke: () => void;
|
|
212
|
-
updating: boolean;
|
|
213
369
|
}) {
|
|
214
|
-
const
|
|
370
|
+
const display = () => resolveEntryDisplay(props.entry.permission, props.allowed);
|
|
371
|
+
const isInteractive = () => props.canEdit && !props.singlePicker;
|
|
372
|
+
|
|
373
|
+
const badgeClass = () => `flex items-center gap-1 px-2 py-0.5 text-xs rounded-full border ${getPermissionColor(props.entry.permission)}`;
|
|
374
|
+
const badgeBorderList = () => ({
|
|
375
|
+
"border-blue-200 dark:border-blue-900": props.entry.permission === "read",
|
|
376
|
+
"border-amber-200 dark:border-amber-900": props.entry.permission === "write",
|
|
377
|
+
"border-purple-200 dark:border-purple-900": props.entry.permission === "admin",
|
|
378
|
+
"border-zinc-200 dark:border-zinc-700":
|
|
379
|
+
props.entry.permission !== "read" && props.entry.permission !== "write" && props.entry.permission !== "admin",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const badgeContent = (
|
|
383
|
+
<>
|
|
384
|
+
<i class={`ti ${display().icon}`} />
|
|
385
|
+
<span>{display().label}</span>
|
|
386
|
+
<Show when={isInteractive()}>
|
|
387
|
+
<i class="ti ti-chevron-down text-[10px]" />
|
|
388
|
+
</Show>
|
|
389
|
+
</>
|
|
390
|
+
);
|
|
215
391
|
|
|
216
392
|
return (
|
|
217
|
-
<div class="
|
|
218
|
-
{/*
|
|
219
|
-
<div class="flex shrink-0 items-center justify-center rounded-full bg-zinc-200 dark:bg-zinc-700
|
|
393
|
+
<div class="flex items-center gap-2 py-1.5">
|
|
394
|
+
{/* Principal icon */}
|
|
395
|
+
<div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300">
|
|
220
396
|
<i class={`ti ${getPrincipalIcon(props.entry.principal)} text-sm`} />
|
|
221
397
|
</div>
|
|
222
398
|
|
|
223
|
-
{/*
|
|
224
|
-
<div class="
|
|
225
|
-
<span class="text-sm
|
|
399
|
+
{/* Display name */}
|
|
400
|
+
<div class="min-w-0 flex-1">
|
|
401
|
+
<span class="truncate text-sm">{getEntryDisplayName(props.entry)}</span>
|
|
226
402
|
<Show when={props.entry.principal.type === "public"}>
|
|
227
|
-
<span class="text-xs text-dimmed
|
|
403
|
+
<span class="ml-1 text-xs text-dimmed">(Anyone with the link)</span>
|
|
228
404
|
</Show>
|
|
229
405
|
</div>
|
|
230
406
|
|
|
231
|
-
{/* Permission badge
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
407
|
+
{/* Permission badge — interactive Dropdown when editable, plain
|
|
408
|
+
span otherwise. */}
|
|
409
|
+
<Show
|
|
410
|
+
when={isInteractive()}
|
|
411
|
+
fallback={
|
|
412
|
+
<span class={`${badgeClass()} cursor-default`} classList={badgeBorderList()}>
|
|
413
|
+
{badgeContent}
|
|
414
|
+
</span>
|
|
415
|
+
}
|
|
416
|
+
>
|
|
417
|
+
<Dropdown
|
|
418
|
+
trigger={
|
|
419
|
+
<button
|
|
420
|
+
type="button"
|
|
421
|
+
class={`${badgeClass()} cursor-pointer transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800`}
|
|
422
|
+
classList={badgeBorderList()}
|
|
423
|
+
>
|
|
424
|
+
{badgeContent}
|
|
425
|
+
</button>
|
|
426
|
+
}
|
|
427
|
+
position="bottom-left"
|
|
428
|
+
width="10rem"
|
|
429
|
+
// Custom `element` items rather than action items — lets us
|
|
430
|
+
// color-tint each row by its level (matching the row pill
|
|
431
|
+
// colors), prefix icons correctly with the `ti ` base class
|
|
432
|
+
// (Dropdown's action.icon expected the full class but we
|
|
433
|
+
// store just `ti-eye` etc.), and mark the currently-active
|
|
434
|
+
// level with a checkmark.
|
|
435
|
+
elements={props.allowed.map((option) => ({
|
|
436
|
+
element: (close) => {
|
|
437
|
+
const isCurrent = () => option.level === props.entry.permission;
|
|
438
|
+
return (
|
|
258
439
|
<button
|
|
259
440
|
type="button"
|
|
260
441
|
onClick={() => {
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
setShowPermissionMenu(false);
|
|
265
|
-
}}
|
|
266
|
-
class="w-full px-3 py-1.5 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
|
|
267
|
-
classList={{
|
|
268
|
-
"bg-zinc-50 dark:bg-zinc-700/50": option.value === props.entry.permission,
|
|
442
|
+
if (!isCurrent()) props.onUpdatePermission(option.level);
|
|
443
|
+
close();
|
|
269
444
|
}}
|
|
445
|
+
class="flex w-full items-center gap-2 px-3 py-1.5 text-sm transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
|
446
|
+
classList={{ "bg-zinc-50 dark:bg-zinc-700/50": isCurrent() }}
|
|
270
447
|
>
|
|
271
|
-
<i class={`ti ${option.icon} ${getPermissionColor(option.
|
|
272
|
-
<span>{option.label}</span>
|
|
273
|
-
<Show when={
|
|
274
|
-
<i class="ti ti-check
|
|
448
|
+
<i class={`ti ${option.icon} ${getPermissionColor(option.level)}`} />
|
|
449
|
+
<span class="flex-1 text-left">{option.label}</span>
|
|
450
|
+
<Show when={isCurrent()}>
|
|
451
|
+
<i class="ti ti-check text-emerald-500" />
|
|
275
452
|
</Show>
|
|
276
453
|
</button>
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
</
|
|
454
|
+
);
|
|
455
|
+
},
|
|
456
|
+
}))}
|
|
457
|
+
/>
|
|
458
|
+
</Show>
|
|
282
459
|
|
|
283
|
-
{/* Delete button
|
|
284
|
-
|
|
460
|
+
{/* Delete button — always visible when editable, dimmed by default
|
|
461
|
+
(no longer hover-only). The last entry IS deletable; parent ACL
|
|
462
|
+
covers the gap. */}
|
|
463
|
+
<Show when={props.canEdit}>
|
|
285
464
|
<button
|
|
286
465
|
type="button"
|
|
287
466
|
onClick={props.onRevoke}
|
|
288
|
-
|
|
467
|
+
aria-label={`Remove ${getEntryDisplayName(props.entry)}`}
|
|
468
|
+
class="flex h-6 w-6 items-center justify-center text-zinc-400 transition-colors hover:text-red-500"
|
|
289
469
|
>
|
|
290
470
|
<i class="ti ti-x text-sm" />
|
|
291
471
|
</button>
|
|
@@ -293,87 +473,3 @@ function AccessEntryRow(props: {
|
|
|
293
473
|
</div>
|
|
294
474
|
);
|
|
295
475
|
}
|
|
296
|
-
|
|
297
|
-
// =============================================================================
|
|
298
|
-
// Add Access Form
|
|
299
|
-
// =============================================================================
|
|
300
|
-
|
|
301
|
-
function AddAccessForm(props: {
|
|
302
|
-
existingUserIds: string[];
|
|
303
|
-
existingGroupIds: string[];
|
|
304
|
-
onSelectEntity: (result: EntitySearchResult, permission: PermissionLevel) => void;
|
|
305
|
-
onSelectPrincipal: (principal: Principal, permission: PermissionLevel) => void;
|
|
306
|
-
onCancel: () => void;
|
|
307
|
-
loading: boolean;
|
|
308
|
-
showAuthenticated: boolean;
|
|
309
|
-
showPublic: boolean;
|
|
310
|
-
}) {
|
|
311
|
-
const [permission, setPermission] = createSignal<PermissionLevel>("read");
|
|
312
|
-
const hasTwoPrincipalButtons = () => props.showAuthenticated && props.showPublic;
|
|
313
|
-
const permissionOptions = PERMISSION_OPTIONS.map((option) => ({
|
|
314
|
-
value: option.value,
|
|
315
|
-
label: option.label,
|
|
316
|
-
icon: `ti ${option.icon}`,
|
|
317
|
-
}));
|
|
318
|
-
|
|
319
|
-
return (
|
|
320
|
-
<div class="paper p-3 flex flex-col gap-2">
|
|
321
|
-
{/* Permission level selector */}
|
|
322
|
-
<div class="flex flex-col gap-1">
|
|
323
|
-
<p class="text-xs text-secondary">Permission Level</p>
|
|
324
|
-
<SegmentedControl options={permissionOptions} value={permission} onChange={setPermission} disabled={props.loading} />
|
|
325
|
-
</div>
|
|
326
|
-
|
|
327
|
-
{/* User/Group search */}
|
|
328
|
-
<EntitySearch
|
|
329
|
-
apiBaseUrl="/api/accounts"
|
|
330
|
-
searchUsers
|
|
331
|
-
searchGroups
|
|
332
|
-
excludeUserIds={props.existingUserIds}
|
|
333
|
-
excludeGroupIds={props.existingGroupIds}
|
|
334
|
-
onSelect={(result) => props.onSelectEntity(result, permission())}
|
|
335
|
-
placeholder="Search users or groups..."
|
|
336
|
-
adding={props.loading}
|
|
337
|
-
resultsHeightClass="max-h-36 min-h-20"
|
|
338
|
-
/>
|
|
339
|
-
|
|
340
|
-
<Show when={props.showAuthenticated || props.showPublic}>
|
|
341
|
-
<div
|
|
342
|
-
class="grid gap-2"
|
|
343
|
-
classList={{
|
|
344
|
-
"grid-cols-2": hasTwoPrincipalButtons(),
|
|
345
|
-
"grid-cols-1": !hasTwoPrincipalButtons(),
|
|
346
|
-
}}
|
|
347
|
-
>
|
|
348
|
-
<Show when={props.showAuthenticated}>
|
|
349
|
-
<button
|
|
350
|
-
type="button"
|
|
351
|
-
onClick={() => props.onSelectPrincipal({ type: "authenticated" }, permission())}
|
|
352
|
-
disabled={props.loading}
|
|
353
|
-
class="btn-simple btn-sm w-full justify-center"
|
|
354
|
-
>
|
|
355
|
-
<i class="ti ti-lock-open-2" />
|
|
356
|
-
All users (incl. guests)
|
|
357
|
-
</button>
|
|
358
|
-
</Show>
|
|
359
|
-
<Show when={props.showPublic}>
|
|
360
|
-
<button
|
|
361
|
-
type="button"
|
|
362
|
-
onClick={() => props.onSelectPrincipal({ type: "public" }, permission())}
|
|
363
|
-
disabled={props.loading}
|
|
364
|
-
class="btn-secondary btn-sm w-full justify-center"
|
|
365
|
-
>
|
|
366
|
-
<i class="ti ti-world" />
|
|
367
|
-
Allow public
|
|
368
|
-
</button>
|
|
369
|
-
</Show>
|
|
370
|
-
</div>
|
|
371
|
-
</Show>
|
|
372
|
-
|
|
373
|
-
{/* Cancel button */}
|
|
374
|
-
<button type="button" onClick={props.onCancel} class="text-xs text-dimmed hover:text-primary self-end">
|
|
375
|
-
Cancel
|
|
376
|
-
</button>
|
|
377
|
-
</div>
|
|
378
|
-
);
|
|
379
|
-
}
|