@protobase/ui 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/dist/activity-chart/activity-chart.js +125 -0
- package/dist/activity-chart/activity-math.js +11 -0
- package/dist/activity-chart/activity-tooltip.js +24 -0
- package/dist/app/action-icon.js +10 -0
- package/dist/app/app.d.ts +24 -0
- package/dist/app/app.js +123 -0
- package/dist/app/batch-failure.js +21 -0
- package/dist/app/breadcrumb-for.js +27 -0
- package/dist/app/create/create-page.js +118 -0
- package/dist/app/create/create-record-form.js +68 -0
- package/dist/app/create/create-values.js +52 -0
- package/dist/app/delete/dialogs.js +109 -0
- package/dist/app/delete/record-diff.js +17 -0
- package/dist/app/delete/use-delete-flow.js +193 -0
- package/dist/app/delete/use-restore.js +9 -0
- package/dist/app/draft-storage.js +18 -0
- package/dist/app/error-banner.js +28 -0
- package/dist/app/field-editor.js +88 -0
- package/dist/app/icons.js +27 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.html +12 -0
- package/dist/app/list-chart.js +36 -0
- package/dist/app/list-order.js +15 -0
- package/dist/app/list-page.js +213 -0
- package/dist/app/list-search.js +30 -0
- package/dist/app/main.d.ts +1 -0
- package/dist/app/main.js +14 -0
- package/dist/app/menu-icons.js +21 -0
- package/dist/app/meta-gate.js +46 -0
- package/dist/app/nav-from-meta.js +65 -0
- package/dist/app/nav-recent.js +33 -0
- package/dist/app/pages/action-href.js +7 -0
- package/dist/app/pages/blocks/action.js +38 -0
- package/dist/app/pages/blocks/block-props.js +5 -0
- package/dist/app/pages/blocks/card-frame.js +34 -0
- package/dist/app/pages/blocks/card-row.js +55 -0
- package/dist/app/pages/blocks/card.js +19 -0
- package/dist/app/pages/blocks/field.js +45 -0
- package/dist/app/pages/blocks/grid.js +34 -0
- package/dist/app/pages/blocks/link.js +27 -0
- package/dist/app/pages/blocks/modal-form.js +149 -0
- package/dist/app/pages/blocks/page-block.js +20 -0
- package/dist/app/pages/blocks/progress.js +54 -0
- package/dist/app/pages/blocks/record-card.js +56 -0
- package/dist/app/pages/blocks/show.js +43 -0
- package/dist/app/pages/blocks/stat.js +78 -0
- package/dist/app/pages/blocks/table.js +65 -0
- package/dist/app/pages/composed-page.js +19 -0
- package/dist/app/pages/confirm-action.js +20 -0
- package/dist/app/pages/field-value.js +58 -0
- package/dist/app/pages/project-ui.d.ts +30 -0
- package/dist/app/pages/project-ui.js +13 -0
- package/dist/app/pages/record-actions.js +40 -0
- package/dist/app/pages/record-scope.d.ts +16 -0
- package/dist/app/pages/record-scope.js +12 -0
- package/dist/app/pages/record-table.js +110 -0
- package/dist/app/pages/render-node.js +84 -0
- package/dist/app/pages/use-layout-data.js +73 -0
- package/dist/app/pages/use-path-value.js +65 -0
- package/dist/app/pages/use-relation-labels.js +30 -0
- package/dist/app/pages/use-run-action.js +88 -0
- package/dist/app/permissions.js +15 -0
- package/dist/app/record-lines.js +39 -0
- package/dist/app/record-page.js +318 -0
- package/dist/app/related-section.js +126 -0
- package/dist/app/relation-picker.js +69 -0
- package/dist/app/router.js +155 -0
- package/dist/app/search/search-model.js +70 -0
- package/dist/app/search/use-global-search.js +68 -0
- package/dist/app/sensitive-field.js +65 -0
- package/dist/app/use-lines-draft.js +45 -0
- package/dist/app/use-lines.js +19 -0
- package/dist/app/use-live-save.js +54 -0
- package/dist/app/use-nav-recent.js +41 -0
- package/dist/app/use-record-editor.js +154 -0
- package/dist/app/use-related-records.js +31 -0
- package/dist/app/user-menu-from-meta.js +36 -0
- package/dist/app-shell/app-shell.d.ts +4 -0
- package/dist/app-shell/app-shell.js +57 -0
- package/dist/app-shell/breadcrumb.js +37 -0
- package/dist/app-shell/drawer.js +30 -0
- package/dist/app-shell/global-search.d.ts +1 -0
- package/dist/app-shell/global-search.js +145 -0
- package/dist/app-shell/index.d.ts +9 -0
- package/dist/app-shell/logo.js +46 -0
- package/dist/app-shell/nav-open-storage.js +10 -0
- package/dist/app-shell/nav.d.ts +2 -0
- package/dist/app-shell/nav.js +85 -0
- package/dist/app-shell/page-header.d.ts +1 -0
- package/dist/app-shell/page-header.js +17 -0
- package/dist/app-shell/profile-menu.d.ts +2 -0
- package/dist/app-shell/profile-menu.js +115 -0
- package/dist/app-shell/sidebar-group.js +94 -0
- package/dist/app-shell/sidebar-item.js +46 -0
- package/dist/app-shell/sidebar-mode.d.ts +4 -0
- package/dist/app-shell/sidebar-mode.js +13 -0
- package/dist/app-shell/sidebar.d.ts +2 -0
- package/dist/app-shell/sidebar.js +64 -0
- package/dist/app-shell/status-dot.d.ts +1 -0
- package/dist/app-shell/status-dot.js +20 -0
- package/dist/app-shell/top-bar.js +82 -0
- package/dist/assistant-dock/assistant-dock.d.ts +15 -0
- package/dist/assistant-dock/assistant-dock.js +81 -0
- package/dist/assistant-dock/chat-message.js +9 -0
- package/dist/assistant-dock/composer.js +39 -0
- package/dist/assistant-dock/dock-item.d.ts +4 -0
- package/dist/assistant-dock/dock-item.js +37 -0
- package/dist/assistant-dock/model.d.ts +84 -0
- package/dist/assistant-dock/model.js +26 -0
- package/dist/assistant-dock/plan-card.d.ts +13 -0
- package/dist/assistant-dock/plan-card.js +133 -0
- package/dist/assistant-dock/progress-card.js +18 -0
- package/dist/assistant-dock/query-card.js +52 -0
- package/dist/assistant-dock/queue-card.js +31 -0
- package/dist/assistant-dock/result-card.js +77 -0
- package/dist/assistant-panel/content.d.ts +7 -0
- package/dist/assistant-panel/step-list.js +30 -0
- package/dist/auth/auth-gate.js +70 -0
- package/dist/auth/auth-layout.js +29 -0
- package/dist/auth/auth-provider.js +71 -0
- package/dist/auth/first-run-page.js +51 -0
- package/dist/auth/forgot-password-form.js +75 -0
- package/dist/auth/forgot-password-page.js +35 -0
- package/dist/auth/reset-link.js +29 -0
- package/dist/auth/reset-password-form.js +88 -0
- package/dist/auth/reset-password-page.js +43 -0
- package/dist/auth/sign-in-form.js +90 -0
- package/dist/auth/sign-in-page.js +39 -0
- package/dist/auth/user-to-shell.js +18 -0
- package/dist/data/api-provider.d.ts +54 -0
- package/dist/data/api-provider.js +53 -0
- package/dist/data/meta-data.d.ts +14 -0
- package/dist/data/meta-data.js +9 -0
- package/dist/data/query-keys.js +95 -0
- package/dist/data/use-aggregates.d.ts +8 -0
- package/dist/data/use-aggregates.js +43 -0
- package/dist/data/use-list.d.ts +231 -0
- package/dist/data/use-list.js +31 -0
- package/dist/data/use-meta.d.ts +8 -0
- package/dist/data/use-meta.js +35 -0
- package/dist/data/use-record.d.ts +6 -0
- package/dist/data/use-record.js +16 -0
- package/dist/data/use-update-record.d.ts +15 -0
- package/dist/data/use-update-record.js +26 -0
- package/dist/data-grid/cells.js +85 -0
- package/dist/data-grid/column-spec.js +4 -0
- package/dist/data-grid/columns.js +16 -0
- package/dist/data-grid/data-grid.js +238 -0
- package/dist/data-grid/geometry.js +32 -0
- package/dist/data-grid/grid-footer.js +33 -0
- package/dist/data-grid/grid-header.js +69 -0
- package/dist/data-grid/grid-row.js +83 -0
- package/dist/data-grid/grid-template.js +12 -0
- package/dist/data-grid/header-menu.js +52 -0
- package/dist/data-grid/page-cache.js +27 -0
- package/dist/data-grid/pinned-storage.js +13 -0
- package/dist/data-grid/row-menu.js +88 -0
- package/dist/data-grid/use-grid-scroll.js +80 -0
- package/dist/data-grid/use-horizontal-scroll.js +38 -0
- package/dist/data-grid/use-paged-rows.js +50 -0
- package/dist/data-grid/use-pinned-columns.js +16 -0
- package/dist/filter-panel/active-filter-chips.js +34 -0
- package/dist/filter-panel/date-presets.js +20 -0
- package/dist/filter-panel/facet-group.js +48 -0
- package/dist/filter-panel/filter-config.js +18 -0
- package/dist/filter-panel/filter-menu.js +41 -0
- package/dist/filter-panel/filter-panel.js +112 -0
- package/dist/filter-panel/filter-section.js +11 -0
- package/dist/filter-panel/filter-state.js +63 -0
- package/dist/filter-panel/range-filter.js +54 -0
- package/dist/filter-panel/range-math.js +11 -0
- package/dist/filter-panel/range-slider.css +32 -0
- package/dist/filter-panel/toggle-row.js +13 -0
- package/dist/filter-panel/use-filter-state.js +15 -0
- package/dist/format/date.js +37 -0
- package/dist/format/money.js +15 -0
- package/dist/format/number.js +12 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/inline-child-table/columns.js +4 -0
- package/dist/inline-child-table/inline-child-table.js +120 -0
- package/dist/inline-child-table/line-menu.js +54 -0
- package/dist/inline-child-table/line-row.js +91 -0
- package/dist/inline-child-table/number-cell.js +25 -0
- package/dist/inline-child-table/reorder.js +10 -0
- package/dist/inline-child-table/totals-footer.js +42 -0
- package/dist/lib/cn.js +4 -0
- package/dist/lib/guarded.js +12 -0
- package/dist/lib/use-debounced.js +13 -0
- package/dist/lib/use-focus-trap.js +46 -0
- package/dist/lib/use-media-query.js +12 -0
- package/dist/live/columns-from-model.js +93 -0
- package/dist/live/display-value.js +24 -0
- package/dist/live/field-values.js +32 -0
- package/dist/live/filter-string.js +135 -0
- package/dist/live/grid-loader.js +63 -0
- package/dist/live/model-helpers.js +30 -0
- package/dist/live/naming.js +9 -0
- package/dist/live/relation-labels.js +53 -0
- package/dist/live/use-filter-config.js +88 -0
- package/dist/live/use-live-rows.js +45 -0
- package/dist/live/view-fields.js +35 -0
- package/dist/mocks/activity.js +24 -0
- package/dist/mocks/invoices.js +13 -0
- package/dist/mocks/products.js +167 -0
- package/dist/mocks/rng.js +20 -0
- package/dist/mocks/users.js +63 -0
- package/dist/primitives/avatar.js +22 -0
- package/dist/primitives/badge.d.ts +1 -0
- package/dist/primitives/badge.js +25 -0
- package/dist/primitives/button.js +23 -0
- package/dist/primitives/checkbox.js +21 -0
- package/dist/primitives/dialog.js +41 -0
- package/dist/primitives/input.js +16 -0
- package/dist/primitives/kbd.js +8 -0
- package/dist/primitives/segmented.js +14 -0
- package/dist/primitives/sheet.js +44 -0
- package/dist/primitives/skeleton.js +6 -0
- package/dist/primitives/spinner.js +10 -0
- package/dist/primitives/switch.js +14 -0
- package/dist/primitives/tooltip.js +13 -0
- package/dist/record-view/action-rail.js +28 -0
- package/dist/record-view/field.js +62 -0
- package/dist/record-view/record-header.js +50 -0
- package/dist/record-view/record-sidebar.js +58 -0
- package/dist/record-view/section.js +21 -0
- package/dist/styles.css +132 -0
- package/dist/toasts/toast-provider.js +69 -0
- package/dist/toasts/toast.js +45 -0
- package/package.json +45 -3
- package/README.md +0 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useAuth } from "./auth-provider.js";
|
|
2
|
+
import { ForgotPasswordForm } from "./forgot-password-form.js";
|
|
3
|
+
import { resetLinkTarget } from "./reset-link.js";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { AuthError } from "@protobase/client";
|
|
7
|
+
//#region src/auth/forgot-password-page.tsx
|
|
8
|
+
/** Asks the server to email a reset link that leads back to this page. */
|
|
9
|
+
const ForgotPasswordPage = ({ onBack }) => {
|
|
10
|
+
const { session } = useAuth();
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const [error, setError] = useState();
|
|
13
|
+
const [sentTo, setSentTo] = useState();
|
|
14
|
+
const submit = async (email) => {
|
|
15
|
+
setBusy(true);
|
|
16
|
+
setError(void 0);
|
|
17
|
+
try {
|
|
18
|
+
await session.requestPasswordReset(email, resetLinkTarget(window.location.href));
|
|
19
|
+
setSentTo(email);
|
|
20
|
+
} catch (failure) {
|
|
21
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
22
|
+
setError(failure.status === 429 ? "Too many requests. Wait a minute and try again." : failure.message);
|
|
23
|
+
}
|
|
24
|
+
setBusy(false);
|
|
25
|
+
};
|
|
26
|
+
return /* @__PURE__ */ jsx(ForgotPasswordForm, {
|
|
27
|
+
busy,
|
|
28
|
+
error,
|
|
29
|
+
sentTo,
|
|
30
|
+
onBack,
|
|
31
|
+
onSubmit: (email) => void submit(email)
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ForgotPasswordPage };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/auth/reset-link.ts
|
|
2
|
+
/** Marks the address an emailed reset link leads back to, so the app opens the set-password page there. */
|
|
3
|
+
const resetMarker = "password-reset";
|
|
4
|
+
/** Where the emailed link leads back to: this page, marked. */
|
|
5
|
+
const resetLinkTarget = (href) => {
|
|
6
|
+
const url = new URL(href);
|
|
7
|
+
return `${url.origin}${url.pathname}?${resetMarker}`;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The reset link this page was opened from, or `undefined`. Better Auth adds the `token`, or `error=INVALID_TOKEN`
|
|
11
|
+
* instead for a used or expired link; then `token` is `undefined`.
|
|
12
|
+
*/
|
|
13
|
+
const readResetLink = (href) => {
|
|
14
|
+
const params = new URL(href).searchParams;
|
|
15
|
+
if (!params.has("password-reset")) return void 0;
|
|
16
|
+
return { token: params.get("token") || void 0 };
|
|
17
|
+
};
|
|
18
|
+
/** The address without the reset link's parameters, for the address bar once the link is used. */
|
|
19
|
+
const withoutResetLink = (href) => {
|
|
20
|
+
const url = new URL(href);
|
|
21
|
+
for (const name of [
|
|
22
|
+
resetMarker,
|
|
23
|
+
"token",
|
|
24
|
+
"error"
|
|
25
|
+
]) url.searchParams.delete(name);
|
|
26
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { readResetLink, resetLinkTarget, resetMarker, withoutResetLink };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Input } from "../primitives/input.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { AuthLayout } from "./auth-layout.js";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
const ResetPasswordForm = ({ onSubmit, invalidLink, onRequestNewLink, onBack, busy, error }) => {
|
|
7
|
+
const [password, setPassword] = useState("");
|
|
8
|
+
const [repeat, setRepeat] = useState("");
|
|
9
|
+
const [mismatch, setMismatch] = useState(false);
|
|
10
|
+
if (invalidLink) return /* @__PURE__ */ jsxs(AuthLayout, {
|
|
11
|
+
title: "This link no longer works",
|
|
12
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
13
|
+
className: "text-[13px] text-muted-foreground",
|
|
14
|
+
children: "A reset link works once, for a limited time. Ask for a new one to choose your password."
|
|
15
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
className: "mt-4 flex flex-col gap-2",
|
|
17
|
+
children: [onRequestNewLink && /* @__PURE__ */ jsx(Button, {
|
|
18
|
+
variant: "primary",
|
|
19
|
+
className: "min-h-10",
|
|
20
|
+
onClick: onRequestNewLink,
|
|
21
|
+
children: "Send a new link"
|
|
22
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
23
|
+
variant: onRequestNewLink ? "ghost" : "primary",
|
|
24
|
+
className: "min-h-10",
|
|
25
|
+
onClick: onBack,
|
|
26
|
+
children: "Back to sign in"
|
|
27
|
+
})]
|
|
28
|
+
})]
|
|
29
|
+
});
|
|
30
|
+
const shown = mismatch ? "The passwords are not the same." : error;
|
|
31
|
+
return /* @__PURE__ */ jsx(AuthLayout, {
|
|
32
|
+
title: "Choose a new password",
|
|
33
|
+
description: `At least 12 characters.`,
|
|
34
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
35
|
+
className: "flex flex-col gap-4",
|
|
36
|
+
onSubmit: (event) => {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
setMismatch(password !== repeat);
|
|
39
|
+
if (password === repeat) onSubmit(password);
|
|
40
|
+
},
|
|
41
|
+
children: [
|
|
42
|
+
/* @__PURE__ */ jsxs("label", {
|
|
43
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
44
|
+
children: ["New password", /* @__PURE__ */ jsx(Input, {
|
|
45
|
+
type: "password",
|
|
46
|
+
name: "new-password",
|
|
47
|
+
autoComplete: "new-password",
|
|
48
|
+
required: true,
|
|
49
|
+
minLength: 12,
|
|
50
|
+
autoFocus: true,
|
|
51
|
+
value: password,
|
|
52
|
+
invalid: Boolean(shown),
|
|
53
|
+
onChange: (event) => setPassword(event.target.value),
|
|
54
|
+
className: "min-h-6"
|
|
55
|
+
})]
|
|
56
|
+
}),
|
|
57
|
+
/* @__PURE__ */ jsxs("label", {
|
|
58
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
59
|
+
children: ["Repeat new password", /* @__PURE__ */ jsx(Input, {
|
|
60
|
+
type: "password",
|
|
61
|
+
name: "repeat-password",
|
|
62
|
+
autoComplete: "new-password",
|
|
63
|
+
required: true,
|
|
64
|
+
minLength: 12,
|
|
65
|
+
value: repeat,
|
|
66
|
+
invalid: Boolean(shown),
|
|
67
|
+
onChange: (event) => setRepeat(event.target.value),
|
|
68
|
+
className: "min-h-6"
|
|
69
|
+
})]
|
|
70
|
+
}),
|
|
71
|
+
shown && /* @__PURE__ */ jsx("p", {
|
|
72
|
+
role: "alert",
|
|
73
|
+
className: "text-xs font-medium text-danger-text",
|
|
74
|
+
children: shown
|
|
75
|
+
}),
|
|
76
|
+
/* @__PURE__ */ jsx(Button, {
|
|
77
|
+
variant: "primary",
|
|
78
|
+
type: "submit",
|
|
79
|
+
loading: busy,
|
|
80
|
+
className: "min-h-10",
|
|
81
|
+
children: "Set new password"
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { ResetPasswordForm };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useAuth } from "./auth-provider.js";
|
|
2
|
+
import { ResetPasswordForm } from "./reset-password-form.js";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { AuthError } from "@protobase/client";
|
|
6
|
+
//#region src/auth/reset-password-page.tsx
|
|
7
|
+
/** Better Auth's answers to a new password, in words for the person at the keyboard. */
|
|
8
|
+
const resetPasswordMessage = (error) => {
|
|
9
|
+
if (error.status === 429) return "Too many attempts. Wait a minute and try again.";
|
|
10
|
+
if (error.code === "PASSWORD_TOO_SHORT") return `Use at least 12 characters.`;
|
|
11
|
+
if (error.code === "PASSWORD_TOO_LONG") return "That password is too long.";
|
|
12
|
+
return error.message;
|
|
13
|
+
};
|
|
14
|
+
/** Sets a new password with the token of an emailed reset link. */
|
|
15
|
+
const ResetPasswordPage = ({ token, onDone, onRequestNewLink, onBack }) => {
|
|
16
|
+
const { session } = useAuth();
|
|
17
|
+
const [busy, setBusy] = useState(false);
|
|
18
|
+
const [error, setError] = useState();
|
|
19
|
+
const [invalid, setInvalid] = useState(!token);
|
|
20
|
+
const submit = async (password) => {
|
|
21
|
+
setBusy(true);
|
|
22
|
+
setError(void 0);
|
|
23
|
+
try {
|
|
24
|
+
await session.resetPassword(token, password);
|
|
25
|
+
onDone();
|
|
26
|
+
} catch (failure) {
|
|
27
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
28
|
+
if (failure.code === "INVALID_TOKEN") setInvalid(true);
|
|
29
|
+
else setError(resetPasswordMessage(failure));
|
|
30
|
+
setBusy(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return /* @__PURE__ */ jsx(ResetPasswordForm, {
|
|
34
|
+
busy,
|
|
35
|
+
error,
|
|
36
|
+
invalidLink: invalid,
|
|
37
|
+
onRequestNewLink,
|
|
38
|
+
onBack,
|
|
39
|
+
onSubmit: (password) => void submit(password)
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ResetPasswordPage, resetPasswordMessage };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Input } from "../primitives/input.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { AuthLayout } from "./auth-layout.js";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/auth/sign-in-form.tsx
|
|
7
|
+
const SignInForm = ({ onSubmit, busy, error, workspace, notice, onForgotPassword, onContinueWithGitHub }) => {
|
|
8
|
+
const [email, setEmail] = useState("");
|
|
9
|
+
const [password, setPassword] = useState("");
|
|
10
|
+
return /* @__PURE__ */ jsxs(AuthLayout, {
|
|
11
|
+
title: "Sign in to Protobase",
|
|
12
|
+
description: workspace,
|
|
13
|
+
children: [
|
|
14
|
+
notice && /* @__PURE__ */ jsx("p", {
|
|
15
|
+
role: "status",
|
|
16
|
+
className: "mb-4 rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
|
|
17
|
+
children: notice
|
|
18
|
+
}),
|
|
19
|
+
onContinueWithGitHub && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
20
|
+
onClick: onContinueWithGitHub,
|
|
21
|
+
disabled: busy,
|
|
22
|
+
className: "min-h-10 w-full",
|
|
23
|
+
children: "Continue with GitHub"
|
|
24
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
25
|
+
className: "my-4 flex items-center gap-3 text-xs text-muted-foreground",
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" }),
|
|
28
|
+
"or",
|
|
29
|
+
/* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
|
|
30
|
+
]
|
|
31
|
+
})] }),
|
|
32
|
+
/* @__PURE__ */ jsxs("form", {
|
|
33
|
+
className: "flex flex-col gap-4",
|
|
34
|
+
onSubmit: (event) => {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
onSubmit(email.trim(), password);
|
|
37
|
+
},
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ jsxs("label", {
|
|
40
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
41
|
+
children: ["Email", /* @__PURE__ */ jsx(Input, {
|
|
42
|
+
type: "email",
|
|
43
|
+
name: "email",
|
|
44
|
+
autoComplete: "username",
|
|
45
|
+
required: true,
|
|
46
|
+
autoFocus: true,
|
|
47
|
+
value: email,
|
|
48
|
+
invalid: Boolean(error),
|
|
49
|
+
onChange: (event) => setEmail(event.target.value),
|
|
50
|
+
className: "min-h-6"
|
|
51
|
+
})]
|
|
52
|
+
}),
|
|
53
|
+
/* @__PURE__ */ jsxs("label", {
|
|
54
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
55
|
+
children: ["Password", /* @__PURE__ */ jsx(Input, {
|
|
56
|
+
type: "password",
|
|
57
|
+
name: "password",
|
|
58
|
+
autoComplete: "current-password",
|
|
59
|
+
required: true,
|
|
60
|
+
value: password,
|
|
61
|
+
invalid: Boolean(error),
|
|
62
|
+
onChange: (event) => setPassword(event.target.value),
|
|
63
|
+
className: "min-h-6"
|
|
64
|
+
})]
|
|
65
|
+
}),
|
|
66
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
67
|
+
role: "alert",
|
|
68
|
+
className: "text-xs font-medium text-danger-text",
|
|
69
|
+
children: error
|
|
70
|
+
}),
|
|
71
|
+
/* @__PURE__ */ jsx(Button, {
|
|
72
|
+
variant: "primary",
|
|
73
|
+
type: "submit",
|
|
74
|
+
loading: busy,
|
|
75
|
+
className: "min-h-10",
|
|
76
|
+
children: "Sign in"
|
|
77
|
+
}),
|
|
78
|
+
onForgotPassword && /* @__PURE__ */ jsx("button", {
|
|
79
|
+
type: "button",
|
|
80
|
+
onClick: onForgotPassword,
|
|
81
|
+
className: "self-center text-[13px] font-medium text-primary-text hover:underline",
|
|
82
|
+
children: "Forgot password?"
|
|
83
|
+
})
|
|
84
|
+
]
|
|
85
|
+
})
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
export { SignInForm };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useAuth } from "./auth-provider.js";
|
|
2
|
+
import { SignInForm } from "./sign-in-form.js";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { AuthError } from "@protobase/client";
|
|
6
|
+
//#region src/auth/sign-in-page.tsx
|
|
7
|
+
/** Better Auth's messages, in words for the person at the keyboard. */
|
|
8
|
+
const signInMessage = (error) => {
|
|
9
|
+
if (error.status === 429) return "Too many attempts. Wait a minute and try again.";
|
|
10
|
+
if (error.status === 401 || error.code === "INVALID_EMAIL_OR_PASSWORD") return "The email or password is not right.";
|
|
11
|
+
return error.message;
|
|
12
|
+
};
|
|
13
|
+
const SignInPage = ({ workspace, notice, onForgotPassword }) => {
|
|
14
|
+
const { signIn, signInSocial, socialProviders } = useAuth();
|
|
15
|
+
const [busy, setBusy] = useState(false);
|
|
16
|
+
const [error, setError] = useState();
|
|
17
|
+
const attempt = async (start) => {
|
|
18
|
+
setBusy(true);
|
|
19
|
+
setError(void 0);
|
|
20
|
+
try {
|
|
21
|
+
await start();
|
|
22
|
+
} catch (failure) {
|
|
23
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
24
|
+
setError(signInMessage(failure));
|
|
25
|
+
setBusy(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return /* @__PURE__ */ jsx(SignInForm, {
|
|
29
|
+
busy,
|
|
30
|
+
error,
|
|
31
|
+
workspace,
|
|
32
|
+
notice,
|
|
33
|
+
onForgotPassword,
|
|
34
|
+
onContinueWithGitHub: socialProviders.includes("github") ? () => void attempt(() => signInSocial("github")) : void 0,
|
|
35
|
+
onSubmit: (email, password) => void attempt(() => signIn(email, password))
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { SignInPage, signInMessage };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/auth/user-to-shell.ts
|
|
2
|
+
const hueOf = (text) => [...text].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) % 360, 7);
|
|
3
|
+
const initialsOf = (name) => name.split(/[\s@.]+/).filter(Boolean).slice(0, 2).map((part) => part[0].toUpperCase()).join("");
|
|
4
|
+
/** What the sidebar's profile menu shows for a signed-in user. */
|
|
5
|
+
const userToShell = (user) => {
|
|
6
|
+
const roles = (user.role ?? "user").split(",").map((role) => role.trim());
|
|
7
|
+
const admin = roles.includes("admin");
|
|
8
|
+
return {
|
|
9
|
+
name: user.name || user.email,
|
|
10
|
+
email: user.email,
|
|
11
|
+
initials: initialsOf(user.name || user.email) || "?",
|
|
12
|
+
hue: hueOf(user.id || user.email),
|
|
13
|
+
role: admin ? "Admin" : (roles[0] ?? "user").replace(/^./, (char) => char.toUpperCase()),
|
|
14
|
+
roleTone: admin ? "violet" : "blue"
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { userToShell };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { Client, ClientOptions } from "@protobase/client";
|
|
4
|
+
//#region src/data/api-provider.d.ts
|
|
5
|
+
export declare const useClient: () => {
|
|
6
|
+
list: <R extends import("@protobase/client").ResourceRef>(resource: R, params?: import("@protobase/client").ListParams) => Promise<import("@protobase/client").ListPage<import("@protobase/client").RecordOf<R>>>;
|
|
7
|
+
search: <R extends import("@protobase/client").ResourceRef>(resource: R, params?: import("@protobase/client").ListParams) => Promise<import("@protobase/client").ListPage<import("@protobase/client").RecordOf<R>>>;
|
|
8
|
+
get: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
9
|
+
create: <R extends import("@protobase/client").ResourceRef>(resource: R, body: Partial<import("@protobase/client").RecordOf<R>>) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>> & {
|
|
10
|
+
location: string;
|
|
11
|
+
}>;
|
|
12
|
+
update: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], patch: Partial<import("@protobase/client").RecordOf<R>>, etag: string) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
13
|
+
remove: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { etag }?: {
|
|
14
|
+
etag?: string;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
undelete: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
17
|
+
reveal: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], field: string) => Promise<unknown>;
|
|
18
|
+
batchWrite: (ops: import("@protobase/client").BatchOp[]) => Promise<import("@protobase/client").BatchResult>;
|
|
19
|
+
facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
|
|
20
|
+
filter?: import("@protobase/client").FilterInput;
|
|
21
|
+
limit?: number;
|
|
22
|
+
}) => Promise<import("@protobase/client").Facet[]>;
|
|
23
|
+
series: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").SeriesParams & {
|
|
24
|
+
filter?: import("@protobase/client").FilterInput;
|
|
25
|
+
}) => Promise<import("@protobase/client").Series>;
|
|
26
|
+
histogram: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
|
|
27
|
+
filter?: import("@protobase/client").FilterInput;
|
|
28
|
+
buckets?: number;
|
|
29
|
+
}) => Promise<import("@protobase/client").Histogram>;
|
|
30
|
+
seek: <R extends import("@protobase/client").ResourceRef>(resource: R, params: import("@protobase/client").ListParams & {
|
|
31
|
+
position: number;
|
|
32
|
+
}) => Promise<import("@protobase/client").ListPage<import("@protobase/client").RecordOf<R>>>;
|
|
33
|
+
meta: (etag?: string) => Promise<import("@protobase/client").MetaResult>;
|
|
34
|
+
};
|
|
35
|
+
export type ApiProviderProps = {
|
|
36
|
+
/** Defaults to `/api/v1` on the current origin. */
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
/** A ready-made client, for tests; skips the meta-version watcher. */
|
|
39
|
+
client?: Client;
|
|
40
|
+
/** The `fetch` the built client uses, for tests that keep the meta-version watcher. */
|
|
41
|
+
fetch?: ClientOptions['fetch'];
|
|
42
|
+
queryClient?: QueryClient;
|
|
43
|
+
/** The bearer token source, and what to do when the session is over; see `createAuthSession`. */
|
|
44
|
+
token?: ClientOptions['token'];
|
|
45
|
+
onUnauthenticated?: () => void;
|
|
46
|
+
children: ReactNode;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Owns the query cache and the API client; refetches `/meta` whenever `X-Meta-Version` changes. The version is
|
|
50
|
+
* compared only with the last one seen: it hashes the models and roles, while the `/meta` ETag hashes the whole
|
|
51
|
+
* per-caller body, so the two never match.
|
|
52
|
+
*/
|
|
53
|
+
export declare const ApiProvider: ({ baseUrl, client: given, fetch, queryClient: givenQueryClient, token, onUnauthenticated, children }: ApiProviderProps) => import("react").JSX.Element;
|
|
54
|
+
//#endregion
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { keys } from "./query-keys.js";
|
|
2
|
+
import { createContext, useContext, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
+
import { createClient } from "@protobase/client";
|
|
6
|
+
//#region src/data/api-provider.tsx
|
|
7
|
+
const ApiContext = createContext(null);
|
|
8
|
+
const useClient = () => {
|
|
9
|
+
const client = useContext(ApiContext);
|
|
10
|
+
if (!client) throw new Error("useClient must be used inside ApiProvider");
|
|
11
|
+
return client;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Owns the query cache and the API client; refetches `/meta` whenever `X-Meta-Version` changes. The version is
|
|
15
|
+
* compared only with the last one seen: it hashes the models and roles, while the `/meta` ETag hashes the whole
|
|
16
|
+
* per-caller body, so the two never match.
|
|
17
|
+
*/
|
|
18
|
+
const ApiProvider = ({ baseUrl, client: given, fetch, queryClient: givenQueryClient, token, onUnauthenticated, children }) => {
|
|
19
|
+
const [queryClient] = useState(() => givenQueryClient ?? new QueryClient({ defaultOptions: { queries: {
|
|
20
|
+
staleTime: 3e4,
|
|
21
|
+
retry: false,
|
|
22
|
+
refetchOnWindowFocus: false
|
|
23
|
+
} } }));
|
|
24
|
+
const metaVersion = useRef(void 0);
|
|
25
|
+
const client = useMemo(() => given ?? createClient({
|
|
26
|
+
baseUrl,
|
|
27
|
+
fetch,
|
|
28
|
+
token,
|
|
29
|
+
onUnauthenticated,
|
|
30
|
+
onMetaVersion: (version) => {
|
|
31
|
+
const previous = metaVersion.current;
|
|
32
|
+
metaVersion.current = version;
|
|
33
|
+
if (previous === void 0 || previous === version) return;
|
|
34
|
+
queryClient.invalidateQueries({ queryKey: keys.meta }, { cancelRefetch: false });
|
|
35
|
+
}
|
|
36
|
+
}), [
|
|
37
|
+
given,
|
|
38
|
+
baseUrl,
|
|
39
|
+
fetch,
|
|
40
|
+
queryClient,
|
|
41
|
+
token,
|
|
42
|
+
onUnauthenticated
|
|
43
|
+
]);
|
|
44
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
45
|
+
client: queryClient,
|
|
46
|
+
children: /* @__PURE__ */ jsx(ApiContext.Provider, {
|
|
47
|
+
value: client,
|
|
48
|
+
children
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { ApiProvider, useClient };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ResourcePermissions } from "@protobase/client";
|
|
2
|
+
import { ResourceModel, UserMenuModel, ViewModel } from "@protobase/schema";
|
|
3
|
+
import { PageModel } from "@protobase/layout";
|
|
4
|
+
//#region src/data/meta-data.d.ts
|
|
5
|
+
export type MetaData = {
|
|
6
|
+
etag: string;
|
|
7
|
+
resources: Record<string, ResourceModel>;
|
|
8
|
+
views: Record<string, ViewModel>;
|
|
9
|
+
/** Composed pages by name. */
|
|
10
|
+
pages: Record<string, PageModel>;
|
|
11
|
+
permissions: Record<string, ResourcePermissions>;
|
|
12
|
+
userMenu?: UserMenuModel;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/data/query-keys.ts
|
|
2
|
+
const keys = {
|
|
3
|
+
meta: ["meta"],
|
|
4
|
+
list: (resource, filter, orderBy, pageSize, startToken) => [
|
|
5
|
+
"list",
|
|
6
|
+
resource,
|
|
7
|
+
filter,
|
|
8
|
+
orderBy,
|
|
9
|
+
pageSize,
|
|
10
|
+
startToken
|
|
11
|
+
],
|
|
12
|
+
page: (resource, filter, orderBy, pageSize, page) => [
|
|
13
|
+
"page",
|
|
14
|
+
resource,
|
|
15
|
+
filter,
|
|
16
|
+
orderBy,
|
|
17
|
+
pageSize,
|
|
18
|
+
page
|
|
19
|
+
],
|
|
20
|
+
/** A sidebar group's records; starts with `list`, so invalidating a resource's lists refreshes it too. */
|
|
21
|
+
navRecent: (resource, filter, orderBy, pageSize) => [
|
|
22
|
+
"list",
|
|
23
|
+
resource,
|
|
24
|
+
filter,
|
|
25
|
+
orderBy,
|
|
26
|
+
pageSize,
|
|
27
|
+
"nav-recent"
|
|
28
|
+
],
|
|
29
|
+
first: (resource, filter, orderBy) => [
|
|
30
|
+
"first-page",
|
|
31
|
+
resource,
|
|
32
|
+
filter,
|
|
33
|
+
orderBy
|
|
34
|
+
],
|
|
35
|
+
record: (resource, key) => [
|
|
36
|
+
"record",
|
|
37
|
+
resource,
|
|
38
|
+
key
|
|
39
|
+
],
|
|
40
|
+
facets: (resource, field, filter) => [
|
|
41
|
+
"facets",
|
|
42
|
+
resource,
|
|
43
|
+
field,
|
|
44
|
+
filter
|
|
45
|
+
],
|
|
46
|
+
series: (resource, field, range, granularity, filter) => [
|
|
47
|
+
"series",
|
|
48
|
+
resource,
|
|
49
|
+
field,
|
|
50
|
+
range,
|
|
51
|
+
granularity,
|
|
52
|
+
filter
|
|
53
|
+
],
|
|
54
|
+
histogram: (resource, field, buckets, filter) => [
|
|
55
|
+
"histogram",
|
|
56
|
+
resource,
|
|
57
|
+
field,
|
|
58
|
+
buckets,
|
|
59
|
+
filter
|
|
60
|
+
],
|
|
61
|
+
relatedLabel: (resource, id) => [
|
|
62
|
+
"related-label",
|
|
63
|
+
resource,
|
|
64
|
+
id
|
|
65
|
+
],
|
|
66
|
+
labels: (resource, ids) => [
|
|
67
|
+
"labels",
|
|
68
|
+
resource,
|
|
69
|
+
ids
|
|
70
|
+
],
|
|
71
|
+
count: (resource, filter) => [
|
|
72
|
+
"count",
|
|
73
|
+
resource,
|
|
74
|
+
filter
|
|
75
|
+
],
|
|
76
|
+
firstRecord: (resource, filter, orderBy) => [
|
|
77
|
+
"first-record",
|
|
78
|
+
resource,
|
|
79
|
+
filter,
|
|
80
|
+
orderBy
|
|
81
|
+
],
|
|
82
|
+
pathValue: (resource, key, path) => [
|
|
83
|
+
"path-value",
|
|
84
|
+
resource,
|
|
85
|
+
key,
|
|
86
|
+
path
|
|
87
|
+
],
|
|
88
|
+
labelMap: (resource, ids) => [
|
|
89
|
+
"label-map",
|
|
90
|
+
resource,
|
|
91
|
+
ids
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
//#endregion
|
|
95
|
+
export { keys };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FilterInput, SeriesParams } from "@protobase/client";
|
|
2
|
+
//#region src/data/use-aggregates.d.ts
|
|
3
|
+
export declare const useFacets: (resource: string, field: string, filter?: FilterInput, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<import("@protobase/client").Facet[], Error>;
|
|
4
|
+
export declare const useSeries: (resource: string, params: SeriesParams & {
|
|
5
|
+
filter?: FilterInput;
|
|
6
|
+
}, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<import("@protobase/client").Series, Error>;
|
|
7
|
+
export declare const useHistogram: (resource: string, field: string, filter?: FilterInput, buckets?: number, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<import("@protobase/client").Histogram, Error>;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { keys } from "./query-keys.js";
|
|
2
|
+
import { useClient } from "./api-provider.js";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { filterText } from "@protobase/client";
|
|
5
|
+
//#region src/data/use-aggregates.ts
|
|
6
|
+
const useFacets = (resource, field, filter, enabled = true) => {
|
|
7
|
+
const client = useClient();
|
|
8
|
+
const text = filterText(filter) ?? "";
|
|
9
|
+
return useQuery({
|
|
10
|
+
queryKey: keys.facets(resource, field, text),
|
|
11
|
+
enabled,
|
|
12
|
+
queryFn: () => client.facets(resource, field, {
|
|
13
|
+
filter: text,
|
|
14
|
+
limit: 50
|
|
15
|
+
})
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
const useSeries = (resource, params, enabled = true) => {
|
|
19
|
+
const client = useClient();
|
|
20
|
+
const text = filterText(params.filter) ?? "";
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: keys.series(resource, params.field, `${params.range ?? ""}${params.from ?? ""}${params.to ?? ""}`, params.granularity ?? "", text),
|
|
23
|
+
enabled,
|
|
24
|
+
queryFn: () => client.series(resource, {
|
|
25
|
+
...params,
|
|
26
|
+
filter: text
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const useHistogram = (resource, field, filter, buckets = 20, enabled = true) => {
|
|
31
|
+
const client = useClient();
|
|
32
|
+
const text = filterText(filter) ?? "";
|
|
33
|
+
return useQuery({
|
|
34
|
+
queryKey: keys.histogram(resource, field, buckets, text),
|
|
35
|
+
enabled,
|
|
36
|
+
queryFn: () => client.histogram(resource, field, {
|
|
37
|
+
filter: text,
|
|
38
|
+
buckets
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { useFacets, useHistogram, useSeries };
|