@protobase/ui 0.3.4 → 0.4.1
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/dist/account/account-pages.js +20 -0
- package/dist/account/account-section.js +31 -0
- package/dist/account/account-security-page.js +59 -0
- package/dist/account/backup-codes.js +44 -0
- package/dist/account/passkeys-section.js +141 -0
- package/dist/account/password-dialog.js +58 -0
- package/dist/account/sign-in-policy-form.js +108 -0
- package/dist/account/sign-in-policy-labels.js +76 -0
- package/dist/account/sign-in-policy-page.js +72 -0
- package/dist/account/staff-sign-in-log.js +62 -0
- package/dist/account/totp-qr.js +37 -0
- package/dist/account/two-factor-section.js +123 -0
- package/dist/account/two-factor-setup.js +188 -0
- package/dist/activity-chart/activity-chart.js +1 -1
- package/dist/activity-chart/activity-tooltip.js +1 -1
- package/dist/app/app.d.ts +3 -1
- package/dist/app/app.js +50 -18
- package/dist/app/assistant-shell.js +1 -1
- package/dist/app/create/create-page.js +1 -1
- package/dist/app/delete/use-delete-flow.js +1 -1
- package/dist/app/error-banner.js +1 -1
- package/dist/app/list-page.js +1 -1
- package/dist/app/list-search.js +1 -1
- package/dist/app/meta-gate.js +1 -1
- package/dist/app/pages/blocks/modal-form.js +3 -3
- package/dist/app/pages/record-table.js +1 -1
- package/dist/app/record-page.js +5 -5
- package/dist/app/runtime/runtime-status.js +25 -0
- package/dist/app/runtime/runtime-update.js +77 -0
- package/dist/app/runtime/use-runtime-status.js +59 -0
- package/dist/app/sensitive-field.js +1 -1
- package/dist/app/staff-banner.js +67 -0
- package/dist/app/use-record-editor.js +1 -1
- package/dist/app-shell/breadcrumb.js +1 -1
- package/dist/app-shell/global-search.js +1 -1
- package/dist/app-shell/profile-menu.js +1 -1
- package/dist/app-shell/sidebar-group.js +1 -1
- package/dist/app-shell/top-bar.js +1 -1
- package/dist/assistant-dock/assistant-dock.js +1 -1
- package/dist/auth/auth-gate.js +42 -4
- package/dist/auth/auth-messages.js +48 -0
- package/dist/auth/auth-provider.js +40 -14
- package/dist/auth/code-input.js +17 -0
- package/dist/auth/email-code-form.js +81 -0
- package/dist/auth/email-code-page.js +40 -0
- package/dist/auth/first-run-page.js +1 -1
- package/dist/auth/forgot-password-page.js +1 -1
- package/dist/auth/reset-password-page.js +1 -1
- package/dist/auth/setup-required-form.js +43 -0
- package/dist/auth/setup-required-page.js +51 -0
- package/dist/auth/sign-in-form.js +53 -20
- package/dist/auth/sign-in-page.js +14 -13
- package/dist/auth/staff-link.js +30 -0
- package/dist/auth/staff-sign-in-form.js +93 -0
- package/dist/auth/staff-sign-in-page.js +41 -0
- package/dist/auth/two-factor-form.js +137 -0
- package/dist/auth/two-factor-page.js +39 -0
- package/dist/auth/user-to-shell.js +5 -2
- package/dist/chat/composer.js +1 -1
- package/dist/data/api-provider.d.ts +1 -1
- package/dist/data/api-provider.js +1 -1
- package/dist/data/meta-data.d.ts +4 -0
- package/dist/data/use-aggregates.js +1 -1
- package/dist/data/use-list.js +1 -1
- package/dist/data/use-meta.js +2 -1
- package/dist/data/use-update-record.js +1 -1
- package/dist/data-grid/cells.js +3 -3
- package/dist/data-grid/grid-header.js +2 -2
- package/dist/data-grid/grid-row.js +1 -1
- package/dist/data-grid/header-menu.js +1 -1
- package/dist/data-grid/row-menu.js +1 -1
- package/dist/diff-view/diff-view.js +1 -1
- package/dist/filter-panel/active-filter-chips.js +1 -1
- package/dist/filter-panel/facet-group.js +1 -1
- package/dist/filter-panel/filter-menu.js +1 -1
- package/dist/inline-child-table/inline-child-table.js +1 -1
- package/dist/inline-child-table/line-menu.js +1 -1
- package/dist/inline-child-table/line-row.js +2 -2
- package/dist/inline-child-table/totals-footer.js +1 -1
- package/dist/lib/use-visual-viewport.js +27 -0
- package/dist/live/use-filter-config.js +1 -1
- package/dist/primitives/checkbox.js +1 -1
- package/dist/primitives/sheet.js +1 -1
- package/dist/primitives/spinner.js +1 -1
- package/dist/record-view/field.js +1 -1
- package/dist/record-view/record-sidebar.js +1 -1
- package/dist/side-panel/side-panel.d.ts +5 -1
- package/dist/side-panel/side-panel.js +49 -35
- package/dist/step-list/step-list.js +1 -1
- package/dist/toasts/toast.js +1 -1
- package/package.json +10 -8
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { encode } from "uqr";
|
|
4
|
+
//#region src/account/totp-qr.tsx
|
|
5
|
+
/** The `otpauth://` URI as a QR code for an authenticator app to scan: one square per dark module, with a quiet border. */
|
|
6
|
+
const TotpQr = ({ uri, className }) => {
|
|
7
|
+
const { size, path } = useMemo(() => {
|
|
8
|
+
const qr = encode(uri, {
|
|
9
|
+
ecc: "M",
|
|
10
|
+
border: 2
|
|
11
|
+
});
|
|
12
|
+
const squares = qr.data.flatMap((row, y) => row.flatMap((dark, x) => dark ? [`M${x} ${y}h1v1h-1z`] : []));
|
|
13
|
+
return {
|
|
14
|
+
size: qr.size,
|
|
15
|
+
path: squares.join("")
|
|
16
|
+
};
|
|
17
|
+
}, [uri]);
|
|
18
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
19
|
+
viewBox: `0 0 ${size} ${size}`,
|
|
20
|
+
role: "img",
|
|
21
|
+
"aria-label": "QR code for your authenticator app",
|
|
22
|
+
shapeRendering: "crispEdges",
|
|
23
|
+
className,
|
|
24
|
+
children: [/* @__PURE__ */ jsx("rect", {
|
|
25
|
+
width: size,
|
|
26
|
+
height: size,
|
|
27
|
+
fill: "#fff"
|
|
28
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
29
|
+
d: path,
|
|
30
|
+
fill: "#000"
|
|
31
|
+
})]
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
/** The secret in an `otpauth://` URI, in groups of four, for typing into an app that cannot scan. */
|
|
35
|
+
const totpKey = (uri) => (new URL(uri).searchParams.get("secret") ?? "").replace(/(.{4})(?=.)/g, "$1 ");
|
|
36
|
+
//#endregion
|
|
37
|
+
export { TotpQr, totpKey };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Badge } from "../primitives/badge.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { Dialog } from "../primitives/dialog.js";
|
|
4
|
+
import { AccountSection } from "./account-section.js";
|
|
5
|
+
import { BackupCodes } from "./backup-codes.js";
|
|
6
|
+
import { PasswordDialog } from "./password-dialog.js";
|
|
7
|
+
import { TwoFactorSetup } from "./two-factor-setup.js";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/account/two-factor-section.tsx
|
|
11
|
+
const offDescriptions = {
|
|
12
|
+
allowed: "Ask for a second step after your password or an emailed sign-in code: a code from an authenticator app, or one emailed to you. A passkey needs no second step.",
|
|
13
|
+
required: "This app requires two-factor authentication for every account. Turn it on to keep using the app.",
|
|
14
|
+
forbidden: "Your admin turned two-factor authentication off for this app."
|
|
15
|
+
};
|
|
16
|
+
const TwoFactorSection = (props) => {
|
|
17
|
+
const { rule, enabled, authenticatorApp, mail, needsPassword, onTurnOff, onNewBackupCodes, onChanged } = props;
|
|
18
|
+
const [open, setOpen] = useState();
|
|
19
|
+
const close = () => setOpen(void 0);
|
|
20
|
+
const status = enabled ? /* @__PURE__ */ jsx(Badge, {
|
|
21
|
+
tone: "green",
|
|
22
|
+
children: "On"
|
|
23
|
+
}) : rule === "required" ? /* @__PURE__ */ jsx(Badge, {
|
|
24
|
+
tone: "amber",
|
|
25
|
+
children: "Required"
|
|
26
|
+
}) : /* @__PURE__ */ jsx(Badge, { children: "Off" });
|
|
27
|
+
const description = !enabled ? offDescriptions[rule] : authenticatorApp ? `Signing in with a password or an emailed sign-in code also asks for a code from your authenticator app${mail ? ", or one emailed to you" : ""}. Backup codes work when you do not have the app.` : "Signing in with a password or an emailed sign-in code also asks for a code emailed to you.";
|
|
28
|
+
if (open?.kind === "setup") return /* @__PURE__ */ jsx(AccountSection, {
|
|
29
|
+
title: "Two-factor authentication",
|
|
30
|
+
status,
|
|
31
|
+
description,
|
|
32
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
33
|
+
className: "max-w-sm",
|
|
34
|
+
children: /* @__PURE__ */ jsx(TwoFactorSetup, {
|
|
35
|
+
offerEmailedCodes: mail && !open.appOnly,
|
|
36
|
+
needsPassword,
|
|
37
|
+
onStartApp: props.onStartApp,
|
|
38
|
+
onConfirmApp: props.onConfirmApp,
|
|
39
|
+
onEmailedCodes: props.onEmailedCodes,
|
|
40
|
+
onCancel: close,
|
|
41
|
+
onDone: () => {
|
|
42
|
+
close();
|
|
43
|
+
onChanged();
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
return /* @__PURE__ */ jsxs(AccountSection, {
|
|
49
|
+
title: "Two-factor authentication",
|
|
50
|
+
status,
|
|
51
|
+
description,
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsxs("div", {
|
|
54
|
+
className: "flex flex-wrap gap-2",
|
|
55
|
+
children: [
|
|
56
|
+
!enabled && rule !== "forbidden" && /* @__PURE__ */ jsx(Button, {
|
|
57
|
+
variant: "primary",
|
|
58
|
+
onClick: () => setOpen({
|
|
59
|
+
kind: "setup",
|
|
60
|
+
appOnly: false
|
|
61
|
+
}),
|
|
62
|
+
children: "Turn on"
|
|
63
|
+
}),
|
|
64
|
+
enabled && !authenticatorApp && rule !== "forbidden" && /* @__PURE__ */ jsx(Button, {
|
|
65
|
+
onClick: () => setOpen({
|
|
66
|
+
kind: "setup",
|
|
67
|
+
appOnly: true
|
|
68
|
+
}),
|
|
69
|
+
children: "Add an authenticator app"
|
|
70
|
+
}),
|
|
71
|
+
enabled && authenticatorApp && /* @__PURE__ */ jsx(Button, {
|
|
72
|
+
onClick: () => setOpen({ kind: "new-codes" }),
|
|
73
|
+
children: "New backup codes"
|
|
74
|
+
}),
|
|
75
|
+
enabled && /* @__PURE__ */ jsx(Button, {
|
|
76
|
+
variant: "danger",
|
|
77
|
+
disabled: rule === "required",
|
|
78
|
+
title: rule === "required" ? "This app requires two-factor authentication." : void 0,
|
|
79
|
+
onClick: () => setOpen({ kind: "turn-off" }),
|
|
80
|
+
children: "Turn off"
|
|
81
|
+
})
|
|
82
|
+
]
|
|
83
|
+
}),
|
|
84
|
+
open?.kind === "turn-off" && /* @__PURE__ */ jsx(PasswordDialog, {
|
|
85
|
+
title: "Turn off two-factor authentication?",
|
|
86
|
+
description: "Signing in then needs only your password or an emailed code. Your backup codes stop working.",
|
|
87
|
+
confirmLabel: "Turn off",
|
|
88
|
+
danger: true,
|
|
89
|
+
needsPassword,
|
|
90
|
+
onClose: close,
|
|
91
|
+
onConfirm: async (password) => {
|
|
92
|
+
await onTurnOff(password);
|
|
93
|
+
close();
|
|
94
|
+
onChanged();
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
open?.kind === "new-codes" && /* @__PURE__ */ jsx(PasswordDialog, {
|
|
98
|
+
title: "Make new backup codes?",
|
|
99
|
+
description: "Your current backup codes stop working, and you get ten new ones.",
|
|
100
|
+
confirmLabel: "Make new codes",
|
|
101
|
+
needsPassword,
|
|
102
|
+
onClose: close,
|
|
103
|
+
onConfirm: async (password) => setOpen({
|
|
104
|
+
kind: "codes",
|
|
105
|
+
codes: await onNewBackupCodes(password)
|
|
106
|
+
})
|
|
107
|
+
}),
|
|
108
|
+
open?.kind === "codes" && /* @__PURE__ */ jsx(Dialog, {
|
|
109
|
+
title: "Your new backup codes",
|
|
110
|
+
description: "Keep them somewhere safe: each one signs you in once if you lose your phone. They are not shown again.",
|
|
111
|
+
onClose: close,
|
|
112
|
+
actions: /* @__PURE__ */ jsx(Button, {
|
|
113
|
+
variant: "primary",
|
|
114
|
+
onClick: close,
|
|
115
|
+
children: "I saved my backup codes"
|
|
116
|
+
}),
|
|
117
|
+
children: /* @__PURE__ */ jsx(BackupCodes, { codes: open.codes })
|
|
118
|
+
})
|
|
119
|
+
]
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
//#endregion
|
|
123
|
+
export { TwoFactorSection };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Input } from "../primitives/input.js";
|
|
3
|
+
import { authMessage } from "../auth/auth-messages.js";
|
|
4
|
+
import { Button } from "../primitives/button.js";
|
|
5
|
+
import { BackupCodes } from "./backup-codes.js";
|
|
6
|
+
import { CodeInput } from "../auth/code-input.js";
|
|
7
|
+
import { TotpQr, totpKey } from "./totp-qr.js";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { AuthError } from "@protobase/client";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/account/two-factor-setup.tsx
|
|
12
|
+
const choices = [{
|
|
13
|
+
value: "app",
|
|
14
|
+
label: "Authenticator app",
|
|
15
|
+
description: "A code from an app such as Google Authenticator, Microsoft Authenticator or 1Password. Comes with backup codes."
|
|
16
|
+
}, {
|
|
17
|
+
value: "email",
|
|
18
|
+
label: "Emailed code",
|
|
19
|
+
description: "A code mailed to your address each time you sign in with a password or an emailed sign-in code."
|
|
20
|
+
}];
|
|
21
|
+
/** Turns on two-factor authentication: choose the app or emailed codes, confirm the password, scan, and keep the backup codes. */
|
|
22
|
+
const TwoFactorSetup = ({ offerEmailedCodes, needsPassword, onStartApp, onConfirmApp, onEmailedCodes, onDone, onCancel }) => {
|
|
23
|
+
const [step, setStep] = useState({ kind: "choose" });
|
|
24
|
+
const [choice, setChoice] = useState("app");
|
|
25
|
+
const [password, setPassword] = useState("");
|
|
26
|
+
const [code, setCode] = useState("");
|
|
27
|
+
const [busy, setBusy] = useState(false);
|
|
28
|
+
const [error, setError] = useState();
|
|
29
|
+
const offered = offerEmailedCodes ? choices : choices.filter((option) => option.value === "app");
|
|
30
|
+
const attempt = async (action) => {
|
|
31
|
+
setBusy(true);
|
|
32
|
+
setError(void 0);
|
|
33
|
+
try {
|
|
34
|
+
await action();
|
|
35
|
+
} catch (failure) {
|
|
36
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
37
|
+
setError(authMessage(failure));
|
|
38
|
+
}
|
|
39
|
+
setBusy(false);
|
|
40
|
+
};
|
|
41
|
+
const start = () => attempt(async () => {
|
|
42
|
+
const given = needsPassword ? password : void 0;
|
|
43
|
+
if (choice === "email") {
|
|
44
|
+
await onEmailedCodes(given);
|
|
45
|
+
return onDone();
|
|
46
|
+
}
|
|
47
|
+
setStep({
|
|
48
|
+
kind: "scan",
|
|
49
|
+
setup: await onStartApp(given)
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
const confirm = (setup) => attempt(async () => {
|
|
53
|
+
await onConfirmApp(code.trim());
|
|
54
|
+
setStep({
|
|
55
|
+
kind: "backup-codes",
|
|
56
|
+
codes: setup.backupCodes
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
const errorLine = error && /* @__PURE__ */ jsx("p", {
|
|
60
|
+
role: "alert",
|
|
61
|
+
className: "text-xs font-medium text-danger-text",
|
|
62
|
+
children: error
|
|
63
|
+
});
|
|
64
|
+
if (step.kind === "backup-codes") return /* @__PURE__ */ jsxs("div", {
|
|
65
|
+
className: "flex flex-col gap-4",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ jsx("p", {
|
|
68
|
+
className: "text-[13px] text-muted-foreground",
|
|
69
|
+
children: "Two-factor authentication is on. Keep these backup codes somewhere safe: each one signs you in once if you lose your phone. They are not shown again."
|
|
70
|
+
}),
|
|
71
|
+
/* @__PURE__ */ jsx(BackupCodes, { codes: step.codes }),
|
|
72
|
+
/* @__PURE__ */ jsx(Button, {
|
|
73
|
+
variant: "primary",
|
|
74
|
+
onClick: onDone,
|
|
75
|
+
className: "min-h-10",
|
|
76
|
+
children: "I saved my backup codes"
|
|
77
|
+
})
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
if (step.kind === "scan") return /* @__PURE__ */ jsxs("form", {
|
|
81
|
+
className: "flex flex-col gap-4",
|
|
82
|
+
onSubmit: (event) => {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
confirm(step.setup);
|
|
85
|
+
},
|
|
86
|
+
children: [
|
|
87
|
+
/* @__PURE__ */ jsx("p", {
|
|
88
|
+
className: "text-[13px] text-muted-foreground",
|
|
89
|
+
children: "Scan this code with your authenticator app, then enter the 6-digit code it shows."
|
|
90
|
+
}),
|
|
91
|
+
/* @__PURE__ */ jsxs("div", {
|
|
92
|
+
className: "flex flex-col items-center gap-2",
|
|
93
|
+
children: [/* @__PURE__ */ jsx(TotpQr, {
|
|
94
|
+
uri: step.setup.totpURI,
|
|
95
|
+
className: "size-44 rounded-md border"
|
|
96
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
97
|
+
className: "text-center text-xs text-muted-foreground",
|
|
98
|
+
children: ["Can't scan it? Enter this key in the app:", /* @__PURE__ */ jsx("span", {
|
|
99
|
+
className: "mt-1 block break-all font-mono text-[13px] text-foreground",
|
|
100
|
+
children: totpKey(step.setup.totpURI)
|
|
101
|
+
})]
|
|
102
|
+
})]
|
|
103
|
+
}),
|
|
104
|
+
/* @__PURE__ */ jsxs("label", {
|
|
105
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
106
|
+
children: ["Code from the app", /* @__PURE__ */ jsx(CodeInput, {
|
|
107
|
+
name: "code",
|
|
108
|
+
required: true,
|
|
109
|
+
autoFocus: true,
|
|
110
|
+
value: code,
|
|
111
|
+
invalid: Boolean(error),
|
|
112
|
+
onChange: (event) => setCode(event.target.value)
|
|
113
|
+
})]
|
|
114
|
+
}),
|
|
115
|
+
errorLine,
|
|
116
|
+
/* @__PURE__ */ jsx(Button, {
|
|
117
|
+
variant: "primary",
|
|
118
|
+
type: "submit",
|
|
119
|
+
loading: busy,
|
|
120
|
+
className: "min-h-10",
|
|
121
|
+
children: "Turn on"
|
|
122
|
+
}),
|
|
123
|
+
onCancel && /* @__PURE__ */ jsx(Button, {
|
|
124
|
+
variant: "ghost",
|
|
125
|
+
onClick: onCancel,
|
|
126
|
+
className: "min-h-10",
|
|
127
|
+
children: "Cancel"
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
132
|
+
className: "flex flex-col gap-4",
|
|
133
|
+
onSubmit: (event) => {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
start();
|
|
136
|
+
},
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ jsx("div", {
|
|
139
|
+
role: "radiogroup",
|
|
140
|
+
"aria-label": "Second step",
|
|
141
|
+
className: "flex flex-col gap-2",
|
|
142
|
+
children: offered.map((option) => /* @__PURE__ */ jsxs("button", {
|
|
143
|
+
type: "button",
|
|
144
|
+
role: "radio",
|
|
145
|
+
"aria-checked": choice === option.value,
|
|
146
|
+
onClick: () => setChoice(option.value),
|
|
147
|
+
className: cn("rounded-md border p-3 text-left transition-colors", choice === option.value ? "border-primary bg-primary-soft" : "border-border-strong hover:bg-muted"),
|
|
148
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
149
|
+
className: "block text-[13px] font-medium",
|
|
150
|
+
children: option.label
|
|
151
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
152
|
+
className: "mt-0.5 block text-xs text-muted-foreground",
|
|
153
|
+
children: option.description
|
|
154
|
+
})]
|
|
155
|
+
}, option.value))
|
|
156
|
+
}),
|
|
157
|
+
needsPassword && /* @__PURE__ */ jsxs("label", {
|
|
158
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
159
|
+
children: ["Your password", /* @__PURE__ */ jsx(Input, {
|
|
160
|
+
type: "password",
|
|
161
|
+
name: "password",
|
|
162
|
+
autoComplete: "current-password",
|
|
163
|
+
required: true,
|
|
164
|
+
value: password,
|
|
165
|
+
invalid: Boolean(error),
|
|
166
|
+
onChange: (event) => setPassword(event.target.value),
|
|
167
|
+
className: "min-h-6"
|
|
168
|
+
})]
|
|
169
|
+
}),
|
|
170
|
+
errorLine,
|
|
171
|
+
/* @__PURE__ */ jsx(Button, {
|
|
172
|
+
variant: "primary",
|
|
173
|
+
type: "submit",
|
|
174
|
+
loading: busy,
|
|
175
|
+
className: "min-h-10",
|
|
176
|
+
children: "Continue"
|
|
177
|
+
}),
|
|
178
|
+
onCancel && /* @__PURE__ */ jsx(Button, {
|
|
179
|
+
variant: "ghost",
|
|
180
|
+
onClick: onCancel,
|
|
181
|
+
className: "min-h-10",
|
|
182
|
+
children: "Cancel"
|
|
183
|
+
})
|
|
184
|
+
]
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
//#endregion
|
|
188
|
+
export { TwoFactorSetup };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { useMediaQuery } from "../lib/use-media-query.js";
|
|
3
|
+
import { formatInt } from "../format/number.js";
|
|
3
4
|
import { formatDayShort } from "../format/date.js";
|
|
4
5
|
import { activitySeries } from "../mocks/activity.js";
|
|
5
|
-
import { formatInt } from "../format/number.js";
|
|
6
6
|
import { Segmented } from "../primitives/segmented.js";
|
|
7
7
|
import { halfOverHalfDelta, totalCount } from "./activity-math.js";
|
|
8
8
|
import { ActivityTooltip } from "./activity-tooltip.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { formatDate } from "../format/date.js";
|
|
2
1
|
import { formatInt } from "../format/number.js";
|
|
2
|
+
import { formatDate } from "../format/date.js";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/activity-chart/activity-tooltip.tsx
|
|
5
5
|
const ActivityTooltip = ({ active, payload, noun }) => {
|
package/dist/app/app.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SidebarMode } from "../app-shell/sidebar-mode.js";
|
|
2
2
|
import "../app-shell/index.js";
|
|
3
3
|
import { ProjectUi } from "./pages/project-ui.js";
|
|
4
|
-
import { AssistantClient, AuthSession, Client } from "@protobase/client";
|
|
4
|
+
import { AssistantClient, AuthSession, Client, RuntimeClient } from "@protobase/client";
|
|
5
5
|
//#region src/app/app.d.ts
|
|
6
6
|
export type AppProps = {
|
|
7
7
|
/** Where `/api/v1` lives. Defaults to the current origin. */
|
|
@@ -20,6 +20,8 @@ export type AppProps = {
|
|
|
20
20
|
ui?: ProjectUi;
|
|
21
21
|
/** A ready-made client of the assistant backend, for stories and tests; used only when `/meta` names an assistant. */
|
|
22
22
|
assistant?: AssistantClient;
|
|
23
|
+
/** A ready-made client of the runtime endpoint, for stories and tests; used only when `/meta` names one. */
|
|
24
|
+
runtime?: RuntimeClient;
|
|
23
25
|
};
|
|
24
26
|
/** The admin: sign-in first, then a sidebar from `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its parent. */
|
|
25
27
|
export declare const App: ({ auth, workspace, ...rest }: AppProps) => import("react").JSX.Element;
|
package/dist/app/app.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { accountMenuItems } from "../account/account-pages.js";
|
|
1
2
|
import { AppShell } from "../app-shell/app-shell.js";
|
|
2
|
-
import { ApiProvider } from "../data/api-provider.js";
|
|
3
3
|
import { AuthProvider, useAuth } from "../auth/auth-provider.js";
|
|
4
|
+
import { AccountSecurityPage } from "../account/account-security-page.js";
|
|
5
|
+
import { SignInPolicyPage } from "../account/sign-in-policy-page.js";
|
|
6
|
+
import { ApiProvider } from "../data/api-provider.js";
|
|
4
7
|
import { AuthGate } from "../auth/auth-gate.js";
|
|
5
|
-
import { userToShell } from "../auth/user-to-shell.js";
|
|
8
|
+
import { isAdmin, userToShell } from "../auth/user-to-shell.js";
|
|
6
9
|
import { useRecord } from "../data/use-record.js";
|
|
7
10
|
import { recordTitle } from "../live/model-helpers.js";
|
|
8
11
|
import { humanize } from "../live/naming.js";
|
|
@@ -19,15 +22,17 @@ import { ProjectUiProvider, useProjectUi } from "./pages/project-ui.js";
|
|
|
19
22
|
import { ComposedPage } from "./pages/composed-page.js";
|
|
20
23
|
import { RecordPage } from "./record-page.js";
|
|
21
24
|
import { useGlobalSearch } from "./search/use-global-search.js";
|
|
25
|
+
import { RuntimeUpdateButton } from "./runtime/runtime-update.js";
|
|
26
|
+
import { StaffBanner } from "./staff-banner.js";
|
|
22
27
|
import { useNavRecent } from "./use-nav-recent.js";
|
|
23
28
|
import { useEffect, useState } from "react";
|
|
24
29
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
25
30
|
//#region src/app/app.tsx
|
|
26
|
-
const Routes = ({ workspace, sidebarMode, assistant }) => {
|
|
31
|
+
const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
27
32
|
const meta = useAdminMeta();
|
|
28
|
-
const { state, signOut } = useAuth();
|
|
33
|
+
const { state, signOut, stopStaffSession } = useAuth();
|
|
29
34
|
if (state.kind !== "signed-in") throw new Error("The shell is only rendered for a signed-in user");
|
|
30
|
-
const { user } = state;
|
|
35
|
+
const { user, staff } = state;
|
|
31
36
|
const { path, params, basePath, navigate } = useRouter();
|
|
32
37
|
const route = matchRoute(path);
|
|
33
38
|
const search = useGlobalSearch();
|
|
@@ -63,7 +68,14 @@ const Routes = ({ workspace, sidebarMode, assistant }) => {
|
|
|
63
68
|
page: params.size > 0 ? `${path}?${params}` : path,
|
|
64
69
|
onClose: () => setAssistantOpen(false)
|
|
65
70
|
});
|
|
66
|
-
const
|
|
71
|
+
const runtimeUrl = meta.runtime?.url;
|
|
72
|
+
const runtimeButton = runtimeUrl && /* @__PURE__ */ jsx(RuntimeUpdateButton, {
|
|
73
|
+
url: runtimeUrl,
|
|
74
|
+
client: runtime
|
|
75
|
+
});
|
|
76
|
+
const admin = isAdmin(user);
|
|
77
|
+
const accountPage = path === "/-/account" ? "Sign-in & security" : path === "/-/sign-in-policy" ? "Sign-in policy" : void 0;
|
|
78
|
+
const breadcrumb = accountPage ? [accountPage] : breadcrumbFor({
|
|
67
79
|
basePath,
|
|
68
80
|
route,
|
|
69
81
|
group: group?.label,
|
|
@@ -71,7 +83,17 @@ const Routes = ({ workspace, sidebarMode, assistant }) => {
|
|
|
71
83
|
resourceLabel: view?.names?.plural ?? (route.resource && humanize(route.resource)),
|
|
72
84
|
recordTitle: title
|
|
73
85
|
});
|
|
74
|
-
|
|
86
|
+
const content = path === "/-/account" ? /* @__PURE__ */ jsx(AccountSecurityPage, { email: user.email }) : path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
|
|
87
|
+
title: "Admins only",
|
|
88
|
+
children: "Only an admin can change how people sign in."
|
|
89
|
+
}) : !route.resource ? /* @__PURE__ */ jsx(Notice, {
|
|
90
|
+
title: "Choose a resource",
|
|
91
|
+
children: "Pick one from the sidebar."
|
|
92
|
+
}) : page ? /* @__PURE__ */ jsx(ComposedPage, { name: page.name }, page.name) : route.key === "new" ? /* @__PURE__ */ jsx(CreatePage, { resource: route.resource }, `${route.resource}/new`) : route.key ? /* @__PURE__ */ jsx(RecordPage, {
|
|
93
|
+
resource: route.resource,
|
|
94
|
+
recordKey: route.key
|
|
95
|
+
}, `${route.resource}/${route.key}`) : /* @__PURE__ */ jsx(ListPage, { resource: route.resource }, route.resource);
|
|
96
|
+
const appShell = /* @__PURE__ */ jsx(AppShell, {
|
|
75
97
|
sidebarMode: sidebarMode ?? "text-small",
|
|
76
98
|
activeItem: route.resource ?? "",
|
|
77
99
|
breadcrumb,
|
|
@@ -79,7 +101,7 @@ const Routes = ({ workspace, sidebarMode, assistant }) => {
|
|
|
79
101
|
onSignOut: () => void signOut(),
|
|
80
102
|
workspace,
|
|
81
103
|
navGroups: groups,
|
|
82
|
-
userMenu: userMenuFromMeta(meta, basePath, route.resource),
|
|
104
|
+
userMenu: [...userMenuFromMeta(meta, basePath, route.resource), ...accountMenuItems(basePath, path, admin)],
|
|
83
105
|
onNavigate: open,
|
|
84
106
|
search: search.available ? {
|
|
85
107
|
placeholder: search.placeholder,
|
|
@@ -90,18 +112,27 @@ const Routes = ({ workspace, sidebarMode, assistant }) => {
|
|
|
90
112
|
groups: search.groups,
|
|
91
113
|
onSelect: open
|
|
92
114
|
} : void 0,
|
|
93
|
-
actions: (Actions || assistantButton) && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
115
|
+
actions: (Actions || runtimeButton || assistantButton) && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
116
|
+
Actions && /* @__PURE__ */ jsx(Actions, {}),
|
|
117
|
+
runtimeButton,
|
|
118
|
+
assistantButton
|
|
119
|
+
] }),
|
|
94
120
|
rightPanel: (RightPanel || assistantPanel) && /* @__PURE__ */ jsxs(Fragment$1, { children: [RightPanel && /* @__PURE__ */ jsx(RightPanel, {}), assistantPanel] }),
|
|
95
|
-
children:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
children: content
|
|
122
|
+
});
|
|
123
|
+
if (!staff) return appShell;
|
|
124
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
125
|
+
className: "flex h-full min-h-0 flex-col",
|
|
126
|
+
children: [/* @__PURE__ */ jsx(StaffBanner, {
|
|
127
|
+
signIn: staff,
|
|
128
|
+
onStop: stopStaffSession
|
|
129
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
130
|
+
className: "min-h-0 flex-1",
|
|
131
|
+
children: appShell
|
|
132
|
+
})]
|
|
102
133
|
});
|
|
103
134
|
};
|
|
104
|
-
const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sidebarMode, ui, assistant }) => {
|
|
135
|
+
const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sidebarMode, ui, assistant, runtime }) => {
|
|
105
136
|
const { session, markSignedOut } = useAuth();
|
|
106
137
|
return /* @__PURE__ */ jsx(ApiProvider, {
|
|
107
138
|
baseUrl,
|
|
@@ -116,7 +147,8 @@ const Authenticated = ({ baseUrl, client, basePath, initialUrl, workspace, sideb
|
|
|
116
147
|
children: /* @__PURE__ */ jsx(Routes, {
|
|
117
148
|
workspace,
|
|
118
149
|
sidebarMode,
|
|
119
|
-
assistant
|
|
150
|
+
assistant,
|
|
151
|
+
runtime
|
|
120
152
|
})
|
|
121
153
|
}) }) })
|
|
122
154
|
})
|
|
@@ -2,8 +2,8 @@ import { Button } from "../primitives/button.js";
|
|
|
2
2
|
import { AssistantDock } from "../assistant-dock/assistant-dock.js";
|
|
3
3
|
import { useAssistant } from "../assistant-dock/use-assistant.js";
|
|
4
4
|
import { useAssistantClient } from "../assistant-dock/use-assistant-client.js";
|
|
5
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
5
|
import { Sparkles } from "lucide-react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
//#region src/app/assistant-shell.tsx
|
|
8
8
|
/** Below `md`, where search is a button too, only the icon shows; the hidden text still names it. */
|
|
9
9
|
const AssistantButton = ({ open, onToggle }) => /* @__PURE__ */ jsxs(Button, {
|
|
@@ -9,9 +9,9 @@ import { useLiveSave } from "../use-live-save.js";
|
|
|
9
9
|
import { CreateRecordForm } from "./create-record-form.js";
|
|
10
10
|
import { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isRequired } from "./create-values.js";
|
|
11
11
|
import { useState } from "react";
|
|
12
|
+
import { isApiError } from "@protobase/client";
|
|
12
13
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
14
|
import { useQueryClient } from "@tanstack/react-query";
|
|
14
|
-
import { isApiError } from "@protobase/client";
|
|
15
15
|
import { encodeKey } from "@protobase/schema";
|
|
16
16
|
//#region src/app/create/create-page.tsx
|
|
17
17
|
const without = (source, name) => Object.fromEntries(Object.entries(source).filter(([key]) => key !== name));
|
|
@@ -4,9 +4,9 @@ import { useToast } from "../../toasts/toast-provider.js";
|
|
|
4
4
|
import { BulkDeleteDialog, ConfirmDeleteDialog, ConflictDialog } from "./dialogs.js";
|
|
5
5
|
import { diffRecords } from "./record-diff.js";
|
|
6
6
|
import { useState } from "react";
|
|
7
|
+
import { PreconditionFailedError, isApiError } from "@protobase/client";
|
|
7
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
import { useQueryClient } from "@tanstack/react-query";
|
|
9
|
-
import { PreconditionFailedError, isApiError } from "@protobase/client";
|
|
10
10
|
//#region src/app/delete/use-delete-flow.tsx
|
|
11
11
|
const concurrency = 4;
|
|
12
12
|
/**
|
package/dist/app/error-banner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
1
|
import { TriangleAlert } from "lucide-react";
|
|
3
2
|
import { isApiError } from "@protobase/client";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/app/error-banner.tsx
|
|
5
5
|
const describe = (error) => {
|
|
6
6
|
if (!isApiError(error)) return [{ message: error instanceof Error ? error.message : "Something went wrong" }];
|
package/dist/app/list-page.js
CHANGED
|
@@ -25,8 +25,8 @@ import { ListChart } from "./list-chart.js";
|
|
|
25
25
|
import { defaultOrderBy, parseSort, sortToOrderBy } from "./list-order.js";
|
|
26
26
|
import { ListSearch } from "./list-search.js";
|
|
27
27
|
import { useMemo, useState } from "react";
|
|
28
|
-
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
29
28
|
import { Plus, SlidersHorizontal } from "lucide-react";
|
|
29
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
30
30
|
import { createWhere, encodeKey } from "@protobase/schema";
|
|
31
31
|
//#region src/app/list-page.tsx
|
|
32
32
|
const where = createWhere();
|
package/dist/app/list-search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Input } from "../primitives/input.js";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import { jsx } from "react/jsx-runtime";
|
|
4
3
|
import { Search } from "lucide-react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/app/list-search.tsx
|
|
6
6
|
/** A search box that commits after a short pause or on Enter. */
|
|
7
7
|
const ListSearch = ({ value, onCommit, placeholder }) => {
|
package/dist/app/meta-gate.js
CHANGED
|
@@ -2,8 +2,8 @@ import { Spinner } from "../primitives/spinner.js";
|
|
|
2
2
|
import { allowEverything } from "../data/meta-data.js";
|
|
3
3
|
import { useMeta } from "../data/use-meta.js";
|
|
4
4
|
import { createContext, useContext } from "react";
|
|
5
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
5
|
import { isApiError } from "@protobase/client";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
//#region src/app/meta-gate.tsx
|
|
8
8
|
const MetaContext = createContext(null);
|
|
9
9
|
const useAdminMeta = () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useClient } from "../../../data/api-provider.js";
|
|
2
1
|
import { Button } from "../../../primitives/button.js";
|
|
2
|
+
import { Dialog } from "../../../primitives/dialog.js";
|
|
3
|
+
import { useClient } from "../../../data/api-provider.js";
|
|
3
4
|
import { humanize } from "../../../live/naming.js";
|
|
4
5
|
import { useToast } from "../../../toasts/toast-provider.js";
|
|
5
|
-
import { Dialog } from "../../../primitives/dialog.js";
|
|
6
6
|
import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
|
|
7
7
|
import { ErrorBanner } from "../../error-banner.js";
|
|
8
8
|
import { FieldEditor } from "../../field-editor.js";
|
|
@@ -14,8 +14,8 @@ import { useLayoutRecord } from "../record-scope.js";
|
|
|
14
14
|
import { useFirstRecord, useRefreshResource } from "../use-layout-data.js";
|
|
15
15
|
import { propsOf } from "./block-props.js";
|
|
16
16
|
import { useState } from "react";
|
|
17
|
-
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
18
17
|
import { isApiError } from "@protobase/client";
|
|
18
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
19
19
|
//#region src/app/pages/blocks/modal-form.tsx
|
|
20
20
|
/** The literal defaults of new records, or the record's values when editing; relations as `{ key, label }` once chosen. */
|
|
21
21
|
const startingDraft = (model, fields, record) => Object.fromEntries(fields.map((name) => {
|
|
@@ -6,8 +6,8 @@ import { scopeOf } from "./use-layout-data.js";
|
|
|
6
6
|
import { FieldValue } from "./field-value.js";
|
|
7
7
|
import { usePathValue } from "./use-path-value.js";
|
|
8
8
|
import { useRelationLabels } from "./use-relation-labels.js";
|
|
9
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
9
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
11
|
import { resolveFieldPath } from "@protobase/layout";
|
|
12
12
|
//#region src/app/pages/record-table.tsx
|
|
13
13
|
const isPath = (column) => column.includes(".");
|
package/dist/app/record-page.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Badge } from "../primitives/badge.js";
|
|
2
|
-
import { keys } from "../data/query-keys.js";
|
|
3
|
-
import { useClient } from "../data/api-provider.js";
|
|
4
2
|
import { useAuth } from "../auth/auth-provider.js";
|
|
3
|
+
import { formatRelativeTime } from "../format/date.js";
|
|
5
4
|
import { Button } from "../primitives/button.js";
|
|
5
|
+
import { keys } from "../data/query-keys.js";
|
|
6
|
+
import { useClient } from "../data/api-provider.js";
|
|
6
7
|
import { useRecord } from "../data/use-record.js";
|
|
7
8
|
import { recordTitle } from "../live/model-helpers.js";
|
|
8
9
|
import { humanize } from "../live/naming.js";
|
|
9
10
|
import { useNavigationGuard, useRouter } from "./router.js";
|
|
10
|
-
import { formatRelativeTime } from "../format/date.js";
|
|
11
11
|
import { displayValue } from "../live/display-value.js";
|
|
12
12
|
import { ErrorBanner } from "./error-banner.js";
|
|
13
13
|
import { FieldEditor } from "./field-editor.js";
|
|
@@ -29,10 +29,10 @@ import { useRecordEditor } from "./use-record-editor.js";
|
|
|
29
29
|
import { useRelatedRecords } from "./use-related-records.js";
|
|
30
30
|
import { RelatedSection } from "./related-section.js";
|
|
31
31
|
import { useMemo } from "react";
|
|
32
|
-
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
33
32
|
import { Check } from "lucide-react";
|
|
34
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
35
33
|
import { isApiError } from "@protobase/client";
|
|
34
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
35
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
36
36
|
import { enumLabel } from "@protobase/schema";
|
|
37
37
|
//#region src/app/record-page.tsx
|
|
38
38
|
const hidden = (model, name) => name === model.tenant || model.primaryKey.includes(name);
|