@pyric/ui 0.1.0-alpha.11 → 0.1.0-alpha.13
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 +5 -4
- package/src/agents/ContextWindowUsage.tsx +514 -0
- package/src/agents/EmptyState.tsx +27 -0
- package/src/agents/Fold.tsx +64 -0
- package/src/agents/Modal.tsx +65 -0
- package/src/agents/PulsingDot.tsx +28 -0
- package/src/agents/inbrowser-agent-usage.d.ts +141 -0
- package/src/agents/index.ts +28 -0
- package/src/auth/authApi.ts +72 -0
- package/src/auth/claims.ts +63 -0
- package/src/auth/components/AuthProviderToggles.tsx +147 -0
- package/src/auth/components/AuthSignInHelper.tsx +197 -0
- package/src/auth/components/AuthUserForm.tsx +328 -0
- package/src/auth/components/AuthUserList.tsx +219 -0
- package/src/auth/components/ClaimsField.tsx +50 -0
- package/src/auth/components/confirmActions.tsx +114 -0
- package/src/auth/controller.ts +173 -0
- package/src/auth/hooks/index.ts +36 -0
- package/src/auth/hooks/useAuthFlowHelper.ts +55 -0
- package/src/auth/hooks/useAuthProviderConfig.ts +139 -0
- package/src/auth/hooks/useAuthUserEditor.ts +76 -0
- package/src/auth/hooks/useAuthUsers.ts +154 -0
- package/src/auth/index.ts +42 -0
- package/src/auth/providers.ts +28 -0
- package/src/auth/reducers/userEditor.ts +186 -0
- package/src/events/components/ActivityActionItems.tsx +136 -0
- package/src/events/components/ActivityGrid.tsx +197 -0
- package/src/events/components/ActivityGridRow.tsx +65 -0
- package/src/events/components/ProposedChangeDiff.tsx +175 -0
- package/src/events/components/format.ts +21 -0
- package/src/events/components/index.ts +17 -0
- package/src/events/digest.ts +630 -0
- package/src/events/hooks/index.ts +9 -0
- package/src/events/hooks/useActivityDigest.ts +42 -0
- package/src/events/hooks/useActivityStream.ts +86 -0
- package/src/events/index.ts +39 -0
- package/src/events/types.ts +152 -0
- package/src/firestore/components/CollectionList.tsx +78 -0
- package/src/firestore/components/DeleteWithConfirm.tsx +88 -0
- package/src/firestore/components/DocumentEditor.tsx +350 -0
- package/src/firestore/components/DocumentList.tsx +217 -0
- package/src/firestore/components/DocumentPreview.tsx +265 -0
- package/src/firestore/components/FieldRenderer.tsx +25 -0
- package/src/firestore/components/QueryBuilder.tsx +181 -0
- package/src/firestore/components/ReferencePicker.tsx +212 -0
- package/src/firestore/components/TreeEntry.tsx +58 -0
- package/src/firestore/components/context.ts +25 -0
- package/src/firestore/fieldEditors/array.tsx +30 -0
- package/src/firestore/fieldEditors/boolean.tsx +39 -0
- package/src/firestore/fieldEditors/bytes.tsx +53 -0
- package/src/firestore/fieldEditors/geopoint.tsx +71 -0
- package/src/firestore/fieldEditors/map.tsx +33 -0
- package/src/firestore/fieldEditors/null.tsx +27 -0
- package/src/firestore/fieldEditors/number.tsx +43 -0
- package/src/firestore/fieldEditors/reference.tsx +87 -0
- package/src/firestore/fieldEditors/registry.ts +45 -0
- package/src/firestore/fieldEditors/string.tsx +33 -0
- package/src/firestore/fieldEditors/timestamp.tsx +75 -0
- package/src/firestore/fieldEditors/types.ts +68 -0
- package/src/firestore/fieldEditors/vector.tsx +142 -0
- package/src/firestore/firestoreApi.ts +86 -0
- package/src/firestore/hooks/coerceError.ts +34 -0
- package/src/firestore/hooks/index.ts +46 -0
- package/src/firestore/hooks/useCollectionList.ts +102 -0
- package/src/firestore/hooks/useDocumentEditor.ts +161 -0
- package/src/firestore/hooks/useDocumentList.ts +242 -0
- package/src/firestore/hooks/useDocumentSubcollections.ts +86 -0
- package/src/firestore/hooks/useFirestoreCollection.ts +51 -0
- package/src/firestore/hooks/useFirestoreDoc.ts +55 -0
- package/src/firestore/hooks/useQueryBuilder.ts +188 -0
- package/src/firestore/hooks/useRecursiveDelete.ts +77 -0
- package/src/firestore/hooks/useReferencePicker.ts +228 -0
- package/src/firestore/import/parseImport.ts +137 -0
- package/src/firestore/index.ts +87 -0
- package/src/firestore/reducers/defaults.ts +38 -0
- package/src/firestore/reducers/documentEditor.ts +239 -0
- package/src/firestore/reducers/tree.ts +140 -0
- package/src/firestore/reducers/types.ts +92 -0
- package/src/firestore/reducers/validation.ts +129 -0
- package/src/firestore/types.ts +231 -0
- package/src/firestore/validation/ids.ts +45 -0
- package/src/firestore/valueEquality.ts +43 -0
- package/src/index.ts +10 -0
- package/src/primitives/Badge.tsx +40 -0
- package/src/primitives/ConfirmDialog.tsx +137 -0
- package/src/primitives/CopyButton.tsx +62 -0
- package/src/primitives/JsonView.tsx +151 -0
- package/src/primitives/SegmentedControl.tsx +72 -0
- package/src/primitives/Toast.tsx +161 -0
- package/src/primitives/VirtualList.tsx +104 -0
- package/src/primitives/hooks/useContainerSize.ts +53 -0
- package/src/primitives/hooks/useUpdateHighlights.ts +109 -0
- package/src/primitives/index.ts +39 -0
- package/src/primitives/useConfirm.tsx +113 -0
- package/src/rtdb/components/RtdbPathBar.tsx +135 -0
- package/src/rtdb/components/RtdbTree.tsx +409 -0
- package/src/rtdb/editor.ts +79 -0
- package/src/rtdb/hooks/useRtdbTree.ts +137 -0
- package/src/rtdb/index.ts +66 -0
- package/src/rtdb/pathInput.ts +47 -0
- package/src/rtdb/reducers/tree.ts +191 -0
- package/src/rtdb/rtdbApi.ts +23 -0
- package/src/rtdb/values.ts +188 -0
- package/src/rules/components/DenialInspector.tsx +227 -0
- package/src/rules/components/format.ts +171 -0
- package/src/rules/components/index.ts +12 -0
- package/src/rules/components/scope.ts +75 -0
- package/src/rules/hooks/index.ts +5 -0
- package/src/rules/hooks/useDenialTrace.ts +100 -0
- package/src/rules/index.ts +24 -0
- package/src/rules/types.ts +91 -0
- package/src/storage/collisionRename.ts +114 -0
- package/src/storage/components/DeleteSelectionWithConfirm.tsx +193 -0
- package/src/storage/components/ObjectBrowser.tsx +219 -0
- package/src/storage/components/ObjectInspector.tsx +169 -0
- package/src/storage/components/PathBreadcrumb.tsx +84 -0
- package/src/storage/components/UploadDropzone.tsx +182 -0
- package/src/storage/folderPlaceholder.ts +40 -0
- package/src/storage/hooks/index.ts +59 -0
- package/src/storage/hooks/useMetadataEditor.ts +329 -0
- package/src/storage/hooks/useObjectUpload.ts +262 -0
- package/src/storage/hooks/usePathState.ts +94 -0
- package/src/storage/hooks/useStorageDelete.ts +195 -0
- package/src/storage/hooks/useStorageList.ts +261 -0
- package/src/storage/hooks/useStorageObject.ts +162 -0
- package/src/storage/hooks/useStorageRulesGate.ts +270 -0
- package/src/storage/hooks/useStorageSelection.ts +90 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/pendingPrefixes.ts +125 -0
- package/src/storage/previews.tsx +120 -0
- package/src/storage/storageApi.ts +54 -0
- package/src/traffic/components/RuleHeatmap.tsx +97 -0
- package/src/traffic/components/TrafficDetail.tsx +160 -0
- package/src/traffic/components/TrafficGroupRow.tsx +91 -0
- package/src/traffic/components/TrafficLineChart.tsx +139 -0
- package/src/traffic/components/TrafficLog.tsx +175 -0
- package/src/traffic/components/TrafficMetricCards.tsx +77 -0
- package/src/traffic/components/TrafficRow.tsx +69 -0
- package/src/traffic/components/TrafficStats.tsx +73 -0
- package/src/traffic/components/TrafficTimeline.tsx +289 -0
- package/src/traffic/components/format.ts +22 -0
- package/src/traffic/components/index.ts +22 -0
- package/src/traffic/hooks/index.ts +60 -0
- package/src/traffic/hooks/useRuleHeatmap.ts +92 -0
- package/src/traffic/hooks/useTrafficBuckets.ts +146 -0
- package/src/traffic/hooks/useTrafficFilter.ts +74 -0
- package/src/traffic/hooks/useTrafficGroups.ts +126 -0
- package/src/traffic/hooks/useTrafficMetrics.ts +250 -0
- package/src/traffic/hooks/useTrafficMonitor.ts +111 -0
- package/src/traffic/hooks/useTrafficStats.ts +77 -0
- package/src/traffic/index.ts +13 -0
- package/src/traffic/types.ts +85 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { Fragment } from 'react';
|
|
2
|
+
import type { FormEvent, ReactNode } from 'react';
|
|
3
|
+
import { FEDERATED_PROVIDER_IDS } from 'pyric/auth';
|
|
4
|
+
import type { AuthUserRecord, CreateUserRequest, UpdateUserRequest } from 'pyric/auth';
|
|
5
|
+
import { useAuthUserEditor } from '../hooks/useAuthUserEditor.js';
|
|
6
|
+
import { providerLabel } from '../providers.js';
|
|
7
|
+
import { ClaimsField } from './ClaimsField.js';
|
|
8
|
+
|
|
9
|
+
/** Create-mode provider multi-select: one checkbox per federated provider
|
|
10
|
+
* id the sandbox supports (the canonical `FEDERATED_PROVIDER_IDS` set from
|
|
11
|
+
* `pyric/auth` — derived from the shipped provider classes, not a UI-side
|
|
12
|
+
* copy). A user can link one or several. Headless: style
|
|
13
|
+
* `[data-pyric-provider-checklist]` / `[data-pyric-provider-option]`. */
|
|
14
|
+
function ProviderChecklist({
|
|
15
|
+
selected,
|
|
16
|
+
onChange,
|
|
17
|
+
}: {
|
|
18
|
+
selected: string[];
|
|
19
|
+
onChange: (ids: string[]) => void;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div data-pyric-provider-checklist>
|
|
23
|
+
{FEDERATED_PROVIDER_IDS.map((id) => (
|
|
24
|
+
<label key={id} data-pyric-provider-option={id}>
|
|
25
|
+
<input
|
|
26
|
+
type="checkbox"
|
|
27
|
+
checked={selected.includes(id)}
|
|
28
|
+
onChange={(e) =>
|
|
29
|
+
onChange(
|
|
30
|
+
e.target.checked ? [...selected, id] : selected.filter((p) => p !== id),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
/>
|
|
34
|
+
<span data-pyric-provider-option-label>{providerLabel(id)}</span>
|
|
35
|
+
<span data-pyric-provider-option-id>{id}</span>
|
|
36
|
+
</label>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What `onSubmit` receives — discriminated on the form's mode. */
|
|
43
|
+
export type AuthUserFormSubmit =
|
|
44
|
+
| { mode: 'create'; request: CreateUserRequest }
|
|
45
|
+
| { mode: 'edit'; uid: string; request: UpdateUserRequest };
|
|
46
|
+
|
|
47
|
+
/** Field names the {@link AuthUserFormProps.renderField} slot receives,
|
|
48
|
+
* in render order. Claims is NOT a slot field — it stays the standalone
|
|
49
|
+
* `<ClaimsField>` (override it by composing `useAuthUserEditor`). */
|
|
50
|
+
export type AuthUserFormFieldName =
|
|
51
|
+
| 'email'
|
|
52
|
+
| 'password'
|
|
53
|
+
| 'display-name'
|
|
54
|
+
| 'phone-number'
|
|
55
|
+
| 'photo-url'
|
|
56
|
+
| 'providers'
|
|
57
|
+
| 'email-verified'
|
|
58
|
+
| 'disabled';
|
|
59
|
+
|
|
60
|
+
/** What the {@link AuthUserFormProps.renderField} slot receives per field. */
|
|
61
|
+
export interface AuthUserFormField {
|
|
62
|
+
name: AuthUserFormFieldName;
|
|
63
|
+
/** The visible label text the default rendering uses. */
|
|
64
|
+
label: string;
|
|
65
|
+
/** The wired, controlled input element (carries `data-pyric-field`).
|
|
66
|
+
* Place it anywhere; state/validation stay connected. */
|
|
67
|
+
input: ReactNode;
|
|
68
|
+
/** Current validation message for this field, or null. */
|
|
69
|
+
error: string | null;
|
|
70
|
+
/** `'text' | 'checkbox' | 'group'` — lets one slot impl branch on
|
|
71
|
+
* layout (`group` = a multi-control block, e.g. the provider
|
|
72
|
+
* checklist; create mode only). */
|
|
73
|
+
kind: 'text' | 'checkbox' | 'group';
|
|
74
|
+
/** The default rendering (label wrapper + label text + input + error).
|
|
75
|
+
* Call it to keep the stock layout for fields you don't customize. */
|
|
76
|
+
defaultRender: () => ReactNode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface AuthUserFormProps {
|
|
80
|
+
/** Existing record → edit mode (delta payloads); omit → create mode. */
|
|
81
|
+
initial?: AuthUserRecord;
|
|
82
|
+
/** Receives the validated payload. Wire `create` to
|
|
83
|
+
* `useAuthUsers().createUser` and `edit` to `updateUser`. */
|
|
84
|
+
onSubmit: (submit: AuthUserFormSubmit) => void;
|
|
85
|
+
onCancel?: () => void;
|
|
86
|
+
submitLabel?: string;
|
|
87
|
+
cancelLabel?: string;
|
|
88
|
+
/** Extra content rendered before the action row (e.g. an error from a
|
|
89
|
+
* failed `createUser` call). */
|
|
90
|
+
children?: ReactNode;
|
|
91
|
+
className?: string;
|
|
92
|
+
/** Per-field layout override. Called for each field (see
|
|
93
|
+
* {@link AuthUserFormFieldName} order); return your own markup around
|
|
94
|
+
* `field.input`, or `field.defaultRender()` to keep the stock label
|
|
95
|
+
* wrapper for that field. Omit the prop for the default layout. */
|
|
96
|
+
renderField?: (field: AuthUserFormField) => ReactNode;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Headless add/edit-user form over `useAuthUserEditor` — the emulator
|
|
101
|
+
* UI's user dialog fields (email, password, display name, phone, photo
|
|
102
|
+
* URL, verified/disabled toggles, custom claims) with its validation
|
|
103
|
+
* messages. Zero CSS; structure addressable via `data-pyric-*`:
|
|
104
|
+
*
|
|
105
|
+
* - root `form[data-pyric-ui="auth-user-form"]` with `data-pyric-mode`,
|
|
106
|
+
* `data-pyric-is-dirty`, `data-pyric-is-valid` state attrs
|
|
107
|
+
* - every field (text inputs AND checkboxes) is wrapped in a
|
|
108
|
+
* `label[data-pyric-field-label="<name>"]` carrying a visible
|
|
109
|
+
* `span[data-pyric-label-text]` — labeled grid layouts are pure CSS
|
|
110
|
+
* (`display: grid` on the wrappers); label-less designs hide
|
|
111
|
+
* `[data-pyric-label-text]` and lean on the placeholders
|
|
112
|
+
* - inputs `[data-pyric-field="email" | "password" | "display-name" |
|
|
113
|
+
* "phone-number" | "photo-url" | "email-verified" | "disabled"]`
|
|
114
|
+
* - CREATE mode only: a "Sign-in providers" group
|
|
115
|
+
* (`fieldset[data-pyric-field-label="providers"]` wrapping
|
|
116
|
+
* `[data-pyric-provider-checklist]`) — one checkbox per federated
|
|
117
|
+
* provider the sandbox supports (`FEDERATED_PROVIDER_IDS` from
|
|
118
|
+
* `pyric/auth`; multiple selectable, entries land on
|
|
119
|
+
* `CreateUserRequest.providerUserInfo`)
|
|
120
|
+
* - claims via the standalone `<ClaimsField>`
|
|
121
|
+
* - per-field messages `[data-pyric-field-error="email" | "password"]`
|
|
122
|
+
* render INSIDE the field's label wrapper, after the input
|
|
123
|
+
* - `button[data-pyric-cancel]` / `button[data-pyric-submit]` (submit is
|
|
124
|
+
* disabled while invalid, or pristine in edit mode)
|
|
125
|
+
*
|
|
126
|
+
* Submit emits payloads only — no sandbox calls — so the same form works
|
|
127
|
+
* for create and edit and the consumer owns error handling.
|
|
128
|
+
*/
|
|
129
|
+
export function AuthUserForm({
|
|
130
|
+
initial,
|
|
131
|
+
onSubmit,
|
|
132
|
+
onCancel,
|
|
133
|
+
submitLabel = 'Save',
|
|
134
|
+
cancelLabel = 'Cancel',
|
|
135
|
+
children,
|
|
136
|
+
className,
|
|
137
|
+
renderField,
|
|
138
|
+
}: AuthUserFormProps) {
|
|
139
|
+
const editor = useAuthUserEditor({ initial });
|
|
140
|
+
const mode = initial ? 'edit' : 'create';
|
|
141
|
+
const submittable = editor.isValid && (mode === 'create' || editor.isDirty);
|
|
142
|
+
|
|
143
|
+
/** Build one field descriptor: the wired input + the stock label-wrapper
|
|
144
|
+
* rendering. `renderField` consumers place `input` themselves. */
|
|
145
|
+
const field = (
|
|
146
|
+
name: AuthUserFormFieldName,
|
|
147
|
+
label: string,
|
|
148
|
+
kind: 'text' | 'checkbox' | 'group',
|
|
149
|
+
input: ReactNode,
|
|
150
|
+
error: string | null = null,
|
|
151
|
+
): AuthUserFormField => ({
|
|
152
|
+
name,
|
|
153
|
+
label,
|
|
154
|
+
input,
|
|
155
|
+
error,
|
|
156
|
+
kind,
|
|
157
|
+
defaultRender: () =>
|
|
158
|
+
kind === 'checkbox' ? (
|
|
159
|
+
<label data-pyric-field-label={name} key={name}>
|
|
160
|
+
{input}
|
|
161
|
+
<span data-pyric-label-text>{label}</span>
|
|
162
|
+
</label>
|
|
163
|
+
) : kind === 'group' ? (
|
|
164
|
+
<fieldset data-pyric-field-label={name} key={name}>
|
|
165
|
+
<legend data-pyric-label-text>{label}</legend>
|
|
166
|
+
{input}
|
|
167
|
+
</fieldset>
|
|
168
|
+
) : (
|
|
169
|
+
<label data-pyric-field-label={name} key={name}>
|
|
170
|
+
<span data-pyric-label-text>{label}</span>
|
|
171
|
+
{input}
|
|
172
|
+
{error != null && (
|
|
173
|
+
<p role="alert" data-pyric-field-error={name}>
|
|
174
|
+
{error}
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
</label>
|
|
178
|
+
),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const fields: AuthUserFormField[] = [
|
|
182
|
+
field(
|
|
183
|
+
'email',
|
|
184
|
+
'Email',
|
|
185
|
+
'text',
|
|
186
|
+
<input
|
|
187
|
+
type="email"
|
|
188
|
+
data-pyric-field="email"
|
|
189
|
+
placeholder="email@example.com"
|
|
190
|
+
value={editor.fields.email}
|
|
191
|
+
onChange={(e) => editor.setField('email', e.target.value)}
|
|
192
|
+
/>,
|
|
193
|
+
editor.errors.email ?? null,
|
|
194
|
+
),
|
|
195
|
+
field(
|
|
196
|
+
'password',
|
|
197
|
+
'Password',
|
|
198
|
+
'text',
|
|
199
|
+
<input
|
|
200
|
+
type="password"
|
|
201
|
+
data-pyric-field="password"
|
|
202
|
+
placeholder={mode === 'edit' ? 'New password (unchanged if empty)' : 'Password'}
|
|
203
|
+
value={editor.fields.password}
|
|
204
|
+
onChange={(e) => editor.setField('password', e.target.value)}
|
|
205
|
+
/>,
|
|
206
|
+
editor.errors.password ?? null,
|
|
207
|
+
),
|
|
208
|
+
field(
|
|
209
|
+
'display-name',
|
|
210
|
+
'Display name',
|
|
211
|
+
'text',
|
|
212
|
+
<input
|
|
213
|
+
type="text"
|
|
214
|
+
data-pyric-field="display-name"
|
|
215
|
+
placeholder="Display name (optional)"
|
|
216
|
+
value={editor.fields.displayName}
|
|
217
|
+
onChange={(e) => editor.setField('displayName', e.target.value)}
|
|
218
|
+
/>,
|
|
219
|
+
),
|
|
220
|
+
field(
|
|
221
|
+
'phone-number',
|
|
222
|
+
'Phone number',
|
|
223
|
+
'text',
|
|
224
|
+
<input
|
|
225
|
+
type="tel"
|
|
226
|
+
data-pyric-field="phone-number"
|
|
227
|
+
placeholder="+1 555 555 0100"
|
|
228
|
+
value={editor.fields.phoneNumber}
|
|
229
|
+
onChange={(e) => editor.setField('phoneNumber', e.target.value)}
|
|
230
|
+
/>,
|
|
231
|
+
),
|
|
232
|
+
field(
|
|
233
|
+
'photo-url',
|
|
234
|
+
'Photo URL',
|
|
235
|
+
'text',
|
|
236
|
+
<input
|
|
237
|
+
type="url"
|
|
238
|
+
data-pyric-field="photo-url"
|
|
239
|
+
placeholder="https://example.com/avatar.png"
|
|
240
|
+
value={editor.fields.photoUrl}
|
|
241
|
+
onChange={(e) => editor.setField('photoUrl', e.target.value)}
|
|
242
|
+
/>,
|
|
243
|
+
),
|
|
244
|
+
// Provider selection is CREATE-only: in edit mode linked providers are
|
|
245
|
+
// managed by the consumer's own affordance over `updateUser` (Studio's
|
|
246
|
+
// per-user ProvidersEditor), not the delta form.
|
|
247
|
+
...(mode === 'create'
|
|
248
|
+
? [
|
|
249
|
+
field(
|
|
250
|
+
'providers',
|
|
251
|
+
'Sign-in providers',
|
|
252
|
+
'group',
|
|
253
|
+
<ProviderChecklist
|
|
254
|
+
selected={editor.fields.providerIds}
|
|
255
|
+
onChange={(ids) => editor.setField('providerIds', ids)}
|
|
256
|
+
/>,
|
|
257
|
+
),
|
|
258
|
+
]
|
|
259
|
+
: []),
|
|
260
|
+
field(
|
|
261
|
+
'email-verified',
|
|
262
|
+
'Verified email',
|
|
263
|
+
'checkbox',
|
|
264
|
+
<input
|
|
265
|
+
type="checkbox"
|
|
266
|
+
data-pyric-field="email-verified"
|
|
267
|
+
checked={editor.fields.emailVerified}
|
|
268
|
+
onChange={(e) => editor.setField('emailVerified', e.target.checked)}
|
|
269
|
+
/>,
|
|
270
|
+
),
|
|
271
|
+
field(
|
|
272
|
+
'disabled',
|
|
273
|
+
'Disabled (sign-in attempts are rejected)',
|
|
274
|
+
'checkbox',
|
|
275
|
+
<input
|
|
276
|
+
type="checkbox"
|
|
277
|
+
data-pyric-field="disabled"
|
|
278
|
+
checked={editor.fields.disabled}
|
|
279
|
+
onChange={(e) => editor.setField('disabled', e.target.checked)}
|
|
280
|
+
/>,
|
|
281
|
+
),
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const submit = (e: FormEvent) => {
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
if (!submittable) return;
|
|
287
|
+
if (mode === 'edit') {
|
|
288
|
+
onSubmit({ mode, uid: initial!.uid, request: editor.toUpdateRequest() });
|
|
289
|
+
} else {
|
|
290
|
+
onSubmit({ mode, request: editor.toCreateRequest() });
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<form
|
|
296
|
+
className={className}
|
|
297
|
+
data-pyric-ui="auth-user-form"
|
|
298
|
+
data-pyric-mode={mode}
|
|
299
|
+
data-pyric-is-dirty={editor.isDirty ? '' : undefined}
|
|
300
|
+
data-pyric-is-valid={editor.isValid ? '' : undefined}
|
|
301
|
+
onSubmit={submit}
|
|
302
|
+
>
|
|
303
|
+
{fields.map((f) =>
|
|
304
|
+
renderField ? (
|
|
305
|
+
<Fragment key={f.name}>{renderField(f)}</Fragment>
|
|
306
|
+
) : (
|
|
307
|
+
f.defaultRender()
|
|
308
|
+
),
|
|
309
|
+
)}
|
|
310
|
+
<ClaimsField
|
|
311
|
+
value={editor.fields.claimsText}
|
|
312
|
+
onChange={(text) => editor.setField('claimsText', text)}
|
|
313
|
+
error={editor.errors.claims}
|
|
314
|
+
/>
|
|
315
|
+
{children}
|
|
316
|
+
<footer data-pyric-form-actions>
|
|
317
|
+
{onCancel ? (
|
|
318
|
+
<button type="button" data-pyric-cancel onClick={onCancel}>
|
|
319
|
+
{cancelLabel}
|
|
320
|
+
</button>
|
|
321
|
+
) : null}
|
|
322
|
+
<button type="submit" data-pyric-submit disabled={!submittable}>
|
|
323
|
+
{submitLabel}
|
|
324
|
+
</button>
|
|
325
|
+
</footer>
|
|
326
|
+
</form>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AuthUserRecord } from 'pyric/auth';
|
|
3
|
+
import { VirtualList } from '../../primitives/VirtualList.js';
|
|
4
|
+
import { providerLabel } from '../providers.js';
|
|
5
|
+
|
|
6
|
+
export interface AuthUserListProps {
|
|
7
|
+
/** Rows to render — usually `useAuthUsers().users`. */
|
|
8
|
+
users: AuthUserRecord[];
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
error?: Error;
|
|
11
|
+
/** The active filter text. Distinguishes the "no users yet" zero state
|
|
12
|
+
* (empty filter) from "no results" (non-empty). */
|
|
13
|
+
filter?: string;
|
|
14
|
+
/** Fired when the identifier cell is clicked. When omitted, the
|
|
15
|
+
* identifier renders as plain text. */
|
|
16
|
+
onSelect?: (user: AuthUserRecord) => void;
|
|
17
|
+
/** Identifier-cell override. Default: email, else phone, else
|
|
18
|
+
* `anonymous`, else the uid. */
|
|
19
|
+
renderIdentifier?: (user: AuthUserRecord) => ReactNode;
|
|
20
|
+
/** Providers-cell override. Default: one `<span data-pyric-provider-id>`
|
|
21
|
+
* per linked provider with its text label (`anonymous` for anonymous
|
|
22
|
+
* users) — hook icons off the attribute. */
|
|
23
|
+
renderProviders?: (user: AuthUserRecord) => ReactNode;
|
|
24
|
+
/** Per-row selection control. Rendered in the leading cell so bulk
|
|
25
|
+
* selection stays visually separate from trailing row actions. */
|
|
26
|
+
renderSelection?: (user: AuthUserRecord) => ReactNode;
|
|
27
|
+
/** Optional content for the leading selection column header (for example,
|
|
28
|
+
* a select-all checkbox). Only rendered with `renderSelection`. */
|
|
29
|
+
renderSelectionHeader?: ReactNode;
|
|
30
|
+
/** Per-row action slot (edit / disable / delete menu). Rendered in a
|
|
31
|
+
* trailing cell; column header is added when this is provided. */
|
|
32
|
+
renderActions?: (user: AuthUserRecord) => ReactNode;
|
|
33
|
+
/** Optional content for the trailing actions column header (for example,
|
|
34
|
+
* a select-all checkbox). Only rendered with `renderActions`. */
|
|
35
|
+
renderActionsHeader?: ReactNode;
|
|
36
|
+
/** Timestamp formatter for Created. Default: locale date, em dash for null. */
|
|
37
|
+
formatCreatedAt?: (iso: string | null) => ReactNode;
|
|
38
|
+
/** Timestamp formatter for Signed In. Kept separate because a missing login
|
|
39
|
+
* means "never", while a missing/invalid creation time is malformed data. */
|
|
40
|
+
formatLastLoginAt?: (iso: string | null) => ReactNode;
|
|
41
|
+
/** Zero state when the project has no users at all. */
|
|
42
|
+
emptyState?: ReactNode;
|
|
43
|
+
/** Zero state when the filter matches nothing. */
|
|
44
|
+
noResultsState?: ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
/** Above this row count, rows render through `<VirtualList>`.
|
|
47
|
+
* Default 100. `Infinity` disables. */
|
|
48
|
+
virtualizeThreshold?: number;
|
|
49
|
+
/** Estimated row height when virtualizing. Default 44. */
|
|
50
|
+
rowHeight?: number | ((index: number) => number);
|
|
51
|
+
/** Scroll-container height when virtualized. Default `'60vh'`. */
|
|
52
|
+
virtualizedHeight?: number | string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultIdentifier(user: AuthUserRecord): ReactNode {
|
|
56
|
+
return user.email ?? user.phoneNumber ?? (user.isAnonymous ? 'anonymous' : user.uid);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function defaultProviders(user: AuthUserRecord): ReactNode {
|
|
60
|
+
if (user.isAnonymous && user.providerUserInfo.length === 0) {
|
|
61
|
+
return <span data-pyric-provider-id="anonymous">{providerLabel('anonymous')}</span>;
|
|
62
|
+
}
|
|
63
|
+
return user.providerUserInfo.map((p) => (
|
|
64
|
+
<span key={p.providerId} data-pyric-provider-id={p.providerId}>
|
|
65
|
+
{providerLabel(p.providerId)}
|
|
66
|
+
</span>
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function defaultFormatDate(iso: string | null): ReactNode {
|
|
71
|
+
if (!iso) return '—';
|
|
72
|
+
const d = new Date(iso);
|
|
73
|
+
return Number.isNaN(d.getTime()) ? '—' : d.toLocaleDateString();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Headless users table — the emulator UI's columns (Identifier,
|
|
78
|
+
* Provider, Created, Signed In, User UID, actions) over the
|
|
79
|
+
* `data-pyric-*` styling contract, with role-based table semantics so
|
|
80
|
+
* rows can virtualize (a real `<table>` can't wrap a scroll container).
|
|
81
|
+
*
|
|
82
|
+
* The hook (`useAuthUsers`) owns data + filter state; this component
|
|
83
|
+
* just renders. Disabled accounts carry `data-pyric-user-disabled` for
|
|
84
|
+
* dimmed styling.
|
|
85
|
+
*/
|
|
86
|
+
export function AuthUserList({
|
|
87
|
+
users,
|
|
88
|
+
isLoading,
|
|
89
|
+
error,
|
|
90
|
+
filter,
|
|
91
|
+
onSelect,
|
|
92
|
+
renderIdentifier,
|
|
93
|
+
renderProviders,
|
|
94
|
+
renderSelection,
|
|
95
|
+
renderSelectionHeader,
|
|
96
|
+
renderActions,
|
|
97
|
+
renderActionsHeader,
|
|
98
|
+
formatCreatedAt = defaultFormatDate,
|
|
99
|
+
formatLastLoginAt = defaultFormatDate,
|
|
100
|
+
emptyState,
|
|
101
|
+
noResultsState,
|
|
102
|
+
className,
|
|
103
|
+
virtualizeThreshold = 100,
|
|
104
|
+
rowHeight = 44,
|
|
105
|
+
virtualizedHeight = '60vh',
|
|
106
|
+
}: AuthUserListProps) {
|
|
107
|
+
if (error) {
|
|
108
|
+
return (
|
|
109
|
+
<div className={className} data-pyric-ui="auth-user-list" data-pyric-error="" role="alert">
|
|
110
|
+
{error.message}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (isLoading && users.length === 0) {
|
|
115
|
+
return <div className={className} data-pyric-ui="auth-user-list" data-pyric-loading="" />;
|
|
116
|
+
}
|
|
117
|
+
if (users.length === 0) {
|
|
118
|
+
const filtered = Boolean(filter?.trim());
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
className={className}
|
|
122
|
+
data-pyric-ui="auth-user-list"
|
|
123
|
+
data-pyric-empty=""
|
|
124
|
+
data-pyric-no-results={filtered ? '' : undefined}
|
|
125
|
+
>
|
|
126
|
+
{filtered
|
|
127
|
+
? (noResultsState ?? 'No results')
|
|
128
|
+
: (emptyState ?? 'No users for this project yet')}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const virtualized = users.length > virtualizeThreshold;
|
|
134
|
+
|
|
135
|
+
const row = (user: AuthUserRecord) => (
|
|
136
|
+
<div
|
|
137
|
+
key={user.uid}
|
|
138
|
+
role="row"
|
|
139
|
+
data-pyric-user-entry
|
|
140
|
+
data-pyric-user-uid={user.uid}
|
|
141
|
+
data-pyric-user-disabled={user.disabled ? '' : undefined}
|
|
142
|
+
>
|
|
143
|
+
{renderSelection ? (
|
|
144
|
+
<span role="cell" data-pyric-user-cell="selection">
|
|
145
|
+
{renderSelection(user)}
|
|
146
|
+
</span>
|
|
147
|
+
) : null}
|
|
148
|
+
<span role="cell" data-pyric-user-cell="identifier">
|
|
149
|
+
{onSelect ? (
|
|
150
|
+
<button type="button" data-pyric-user-select onClick={() => onSelect(user)}>
|
|
151
|
+
{renderIdentifier ? renderIdentifier(user) : defaultIdentifier(user)}
|
|
152
|
+
</button>
|
|
153
|
+
) : (
|
|
154
|
+
(renderIdentifier ?? defaultIdentifier)(user)
|
|
155
|
+
)}
|
|
156
|
+
</span>
|
|
157
|
+
<span role="cell" data-pyric-user-cell="providers">
|
|
158
|
+
{(renderProviders ?? defaultProviders)(user)}
|
|
159
|
+
</span>
|
|
160
|
+
<span role="cell" data-pyric-user-cell="created">
|
|
161
|
+
{formatCreatedAt(user.createdAt)}
|
|
162
|
+
</span>
|
|
163
|
+
<span role="cell" data-pyric-user-cell="signed-in">
|
|
164
|
+
{formatLastLoginAt(user.lastLoginAt)}
|
|
165
|
+
</span>
|
|
166
|
+
<span role="cell" data-pyric-user-cell="uid">
|
|
167
|
+
{user.uid}
|
|
168
|
+
</span>
|
|
169
|
+
{renderActions ? (
|
|
170
|
+
<span role="cell" data-pyric-user-cell="actions">
|
|
171
|
+
{renderActions(user)}
|
|
172
|
+
</span>
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className={className}
|
|
180
|
+
role="table"
|
|
181
|
+
aria-label="Authentication users"
|
|
182
|
+
data-pyric-ui="auth-user-list"
|
|
183
|
+
data-pyric-virtualized={virtualized ? '' : undefined}
|
|
184
|
+
data-pyric-selection={renderSelection ? '' : undefined}
|
|
185
|
+
data-pyric-actions={renderActions ? '' : undefined}
|
|
186
|
+
>
|
|
187
|
+
<div role="row" data-pyric-user-header>
|
|
188
|
+
{renderSelection ? (
|
|
189
|
+
<span role="columnheader" data-pyric-user-cell="selection" aria-label="Selection">
|
|
190
|
+
{renderSelectionHeader}
|
|
191
|
+
</span>
|
|
192
|
+
) : null}
|
|
193
|
+
<span role="columnheader" data-pyric-user-cell="identifier">Identifier</span>
|
|
194
|
+
<span role="columnheader" data-pyric-user-cell="providers">Provider</span>
|
|
195
|
+
<span role="columnheader" data-pyric-user-cell="created">Created</span>
|
|
196
|
+
<span role="columnheader" data-pyric-user-cell="signed-in">Signed In</span>
|
|
197
|
+
<span role="columnheader" data-pyric-user-cell="uid">User UID</span>
|
|
198
|
+
{renderActions ? (
|
|
199
|
+
<span role="columnheader" data-pyric-user-cell="actions" aria-label="Actions">
|
|
200
|
+
{renderActionsHeader}
|
|
201
|
+
</span>
|
|
202
|
+
) : null}
|
|
203
|
+
</div>
|
|
204
|
+
{virtualized ? (
|
|
205
|
+
<VirtualList
|
|
206
|
+
items={users}
|
|
207
|
+
estimateSize={rowHeight}
|
|
208
|
+
height={virtualizedHeight}
|
|
209
|
+
getItemKey={(user) => user.uid}
|
|
210
|
+
renderItem={row}
|
|
211
|
+
/>
|
|
212
|
+
) : (
|
|
213
|
+
<div role="rowgroup" data-pyric-user-rows>
|
|
214
|
+
{users.map(row)}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ClaimsFieldProps {
|
|
4
|
+
/** Raw claims JSON text. */
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (text: string) => void;
|
|
7
|
+
/** Validation message (from `validateSerializedClaims` /
|
|
8
|
+
* `useAuthUserEditor().errors.claims`). Renders a `role="alert"`
|
|
9
|
+
* paragraph and marks the textarea invalid. */
|
|
10
|
+
error?: string;
|
|
11
|
+
/** Helper text under the field (rules-usage hint by default). */
|
|
12
|
+
hint?: ReactNode;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Headless custom-claims textarea — the emulator UI's
|
|
19
|
+
* `customAttributes` control. Standalone so custom forms can reuse the
|
|
20
|
+
* exact field (the playground's sign-in helper and the user form both
|
|
21
|
+
* render one); validation itself lives in `validateSerializedClaims`.
|
|
22
|
+
*/
|
|
23
|
+
export function ClaimsField({
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
error,
|
|
27
|
+
hint,
|
|
28
|
+
placeholder = '{"role":"admin"}',
|
|
29
|
+
className,
|
|
30
|
+
}: ClaimsFieldProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div className={className} data-pyric-ui="claims-field">
|
|
33
|
+
<textarea
|
|
34
|
+
data-pyric-field="claims"
|
|
35
|
+
data-pyric-claims-invalid={error != null ? '' : undefined}
|
|
36
|
+
aria-label="Custom claims (optional)"
|
|
37
|
+
aria-invalid={error != null || undefined}
|
|
38
|
+
placeholder={placeholder}
|
|
39
|
+
value={value}
|
|
40
|
+
onChange={(e) => onChange(e.target.value)}
|
|
41
|
+
/>
|
|
42
|
+
{error != null && (
|
|
43
|
+
<p role="alert" data-pyric-claims-error>
|
|
44
|
+
{error}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
{hint != null && <p data-pyric-claims-hint>{hint}</p>}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|