@withpotter/pages-admin 0.13.0-next.74 → 0.13.0-next.77
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/admin/_shell/nav-fold.js +1 -1
- package/dist/admin/_shell/nav-fold.js.map +1 -1
- package/dist/admin/_shell/nav-group.js +2 -2
- package/dist/admin/_shell/nav-group.js.map +1 -1
- package/dist/admin/_shell/sidebar.js +6 -3
- package/dist/admin/_shell/sidebar.js.map +1 -1
- package/dist/admin/_shell/use-open-groups.js +20 -0
- package/dist/admin/_shell/use-open-groups.js.map +1 -0
- package/dist/admin/accounts/account-panel.js +15 -4
- package/dist/admin/accounts/account-panel.js.map +1 -1
- package/dist/admin/accounts/account-panel.module.css +21 -0
- package/dist/admin/accounts/account-row.js +11 -2
- package/dist/admin/accounts/account-row.js.map +1 -1
- package/dist/admin/accounts/account-sessions.js +51 -0
- package/dist/admin/accounts/account-sessions.js.map +1 -0
- package/dist/admin/accounts/account-sessions.module.css +53 -0
- package/dist/admin/accounts/account-stage-filter.js +29 -0
- package/dist/admin/accounts/account-stage-filter.js.map +1 -0
- package/dist/admin/accounts/account-stage.js +47 -0
- package/dist/admin/accounts/account-stage.js.map +1 -0
- package/dist/admin/accounts/accounts-table.js +39 -8
- package/dist/admin/accounts/accounts-table.js.map +1 -1
- package/dist/admin/accounts/hint.js +12 -0
- package/dist/admin/accounts/hint.js.map +1 -0
- package/dist/admin/accounts/rows.module.css +17 -0
- package/dist/admin/hooks.js +8 -0
- package/dist/admin/hooks.js.map +1 -1
- package/dist/admin/settings/account-tab.js +1 -25
- package/dist/admin/settings/account-tab.js.map +1 -1
- package/dist/admin/tenants/tenant-list-row.js +2 -1
- package/dist/admin/tenants/tenant-list-row.js.map +1 -1
- package/dist/admin/tenants/tenants-table.js +2 -1
- package/dist/admin/tenants/tenants-table.js.map +1 -1
- package/package.json +5 -5
- package/dist/admin/_shell/use-open-group.js +0 -16
- package/dist/admin/_shell/use-open-group.js.map +0 -1
- package/dist/admin/settings/system-status-card.js +0 -30
- package/dist/admin/settings/system-status-card.js.map +0 -1
|
@@ -5,7 +5,7 @@ const FOLDABLE_GROUPS = /* @__PURE__ */ new Set([
|
|
|
5
5
|
"Platform"
|
|
6
6
|
]);
|
|
7
7
|
const foldsGroup = (label, rows, collapsed) => label !== null && FOLDABLE_GROUPS.has(label) && rows > 1 && !collapsed;
|
|
8
|
-
const isGroupOpen = (
|
|
8
|
+
const isGroupOpen = (choices, label, containsActive) => choices[label] ?? containsActive;
|
|
9
9
|
export {
|
|
10
10
|
FOLDABLE_GROUPS,
|
|
11
11
|
foldsGroup,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_shell/nav-fold.ts"],"sourcesContent":["/**\n * Which sidebar groups fold, and which one is open. Pure, so it is tested\n * without a browser. The merchant rail's hubs follow the same rules.\n */\n\n
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_shell/nav-fold.ts"],"sourcesContent":["/**\n * Which sidebar groups fold, and which one is open. Pure, so it is tested\n * without a browser. The merchant rail's hubs follow the same rules.\n */\n\n/**\n * The groups the admin opened or closed by hand, by label. A group not listed\n * has no choice yet, and each group is chosen on its own.\n */\nexport type GroupChoices = Record<string, boolean>;\n\n/** The groups that fold; every other group keeps its plain heading. */\nexport const FOLDABLE_GROUPS: ReadonlySet<string> = new Set([\n 'Workspaces',\n 'Operations',\n 'Inbox',\n 'Platform',\n]);\n\n/**\n * A group folds only when it is one of those, has more than one row to hide,\n * and the rail is wide: the icon-only rail has no headings to press.\n */\nexport const foldsGroup = (\n label: string | null,\n rows: number,\n collapsed: boolean,\n): label is string =>\n label !== null && FOLDABLE_GROUPS.has(label) && rows > 1 && !collapsed;\n\n/**\n * A group the admin chose is as they left it, whatever the others are doing.\n * With no choice yet it is open when it owns the current page, so the page\n * you are on always has a lit row.\n */\nexport const isGroupOpen = (\n choices: GroupChoices,\n label: string,\n containsActive: boolean,\n) => choices[label] ?? containsActive;\n"],"mappings":"AAYO,MAAM,kBAAuC,oBAAI,IAAI;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMM,MAAM,aAAa,CACxB,OACA,MACA,cAEA,UAAU,QAAQ,gBAAgB,IAAI,KAAK,KAAK,OAAO,KAAK,CAAC;AAOxD,MAAM,cAAc,CACzB,SACA,OACA,mBACG,QAAQ,KAAK,KAAK;","names":[]}
|
|
@@ -12,7 +12,7 @@ function NavGroupView({
|
|
|
12
12
|
pathname,
|
|
13
13
|
collapsed,
|
|
14
14
|
canSupport,
|
|
15
|
-
|
|
15
|
+
choices,
|
|
16
16
|
onToggle
|
|
17
17
|
}) {
|
|
18
18
|
const reduced = useReducedMotion();
|
|
@@ -33,7 +33,7 @@ function NavGroupView({
|
|
|
33
33
|
label && /* @__PURE__ */ jsx("div", { className: styles.groupLabel, children: label }),
|
|
34
34
|
rows
|
|
35
35
|
] });
|
|
36
|
-
const open = isGroupOpen(
|
|
36
|
+
const open = isGroupOpen(choices, label, containsActive);
|
|
37
37
|
return /* @__PURE__ */ jsxs("div", { className: styles.group, children: [
|
|
38
38
|
/* @__PURE__ */ jsxs(
|
|
39
39
|
"button",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_shell/nav-group.tsx"],"sourcesContent":["'use client';\n\nimport { ChevronDown } from 'lucide-react';\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react';\nimport { eases } from '@withpotter/lib/motion';\nimport type { NavGroup } from './nav-config';\nimport { isActive } from './nav-filter';\nimport { foldsGroup, isGroupOpen, type
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_shell/nav-group.tsx"],"sourcesContent":["'use client';\n\nimport { ChevronDown } from 'lucide-react';\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react';\nimport { eases } from '@withpotter/lib/motion';\nimport type { NavGroup } from './nav-config';\nimport { isActive } from './nav-filter';\nimport { foldsGroup, isGroupOpen, type GroupChoices } from './nav-fold';\nimport { NavRow } from './nav-row';\nimport styles from './sidebar.module.css';\n\n/**\n * One sidebar group. With a heading and more than one row, the heading is a\n * button that opens and closes the rows, on its own: no other group moves.\n * The group holding the current page starts open, and the icon-only rail shows\n * every row.\n */\nexport function NavGroupView({\n group,\n pathname,\n collapsed,\n canSupport,\n choices,\n onToggle,\n}: {\n group: NavGroup;\n pathname: string;\n collapsed: boolean;\n canSupport: boolean;\n choices: GroupChoices;\n onToggle: (label: string, next: boolean) => void;\n}) {\n const reduced = useReducedMotion();\n const { label, items } = group;\n const containsActive = items.some((item) => isActive(item, pathname));\n\n const rows = items.map((item) => (\n <NavRow\n key={item.href}\n item={item}\n active={isActive(item, pathname)}\n collapsed={collapsed}\n canSupport={canSupport}\n />\n ));\n\n if (!foldsGroup(label, items.length, collapsed))\n return (\n <div className={styles.group}>\n {label && <div className={styles.groupLabel}>{label}</div>}\n {rows}\n </div>\n );\n\n const open = isGroupOpen(choices, label, containsActive);\n\n return (\n <div className={styles.group}>\n <button\n type=\"button\"\n className={`${styles.groupLabel} ${styles.groupToggle}`}\n aria-expanded={open}\n onClick={() => onToggle(label, !open)}\n >\n <span>{label}</span>\n <ChevronDown size={14} className={styles.groupChevron} aria-hidden />\n </button>\n {/* The rows grow and collapse rather than blink, as the merchant rail's\n hubs do: height so the groups below move with them, opacity finishing\n early on the way in. Clipped, or the rows spill past the closing box. */}\n <AnimatePresence initial={false}>\n {open && (\n <motion.div\n className={styles.groupRows}\n initial={{ height: 0, opacity: 0 }}\n animate={{ height: 'auto', opacity: 1 }}\n exit={{ height: 0, opacity: 0 }}\n transition={\n reduced\n ? { duration: 0 }\n : {\n height: { duration: 0.24, ease: eases.settle },\n opacity: { duration: 0.16, ease: eases.out },\n }\n }\n style={{ overflow: 'hidden' }}\n >\n {rows}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n );\n}\n"],"mappings":";AAqCI,cAWE,YAXF;AAnCJ,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB,QAAQ,wBAAwB;AAC1D,SAAS,aAAa;AAEtB,SAAS,gBAAgB;AACzB,SAAS,YAAY,mBAAsC;AAC3D,SAAS,cAAc;AACvB,OAAO,YAAY;AAQZ,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,UAAU,iBAAiB;AACjC,QAAM,EAAE,OAAO,MAAM,IAAI;AACzB,QAAM,iBAAiB,MAAM,KAAK,CAAC,SAAS,SAAS,MAAM,QAAQ,CAAC;AAEpE,QAAM,OAAO,MAAM,IAAI,CAAC,SACtB;AAAA,IAAC;AAAA;AAAA,MAEC;AAAA,MACA,QAAQ,SAAS,MAAM,QAAQ;AAAA,MAC/B;AAAA,MACA;AAAA;AAAA,IAJK,KAAK;AAAA,EAKZ,CACD;AAED,MAAI,CAAC,WAAW,OAAO,MAAM,QAAQ,SAAS;AAC5C,WACE,qBAAC,SAAI,WAAW,OAAO,OACpB;AAAA,eAAS,oBAAC,SAAI,WAAW,OAAO,YAAa,iBAAM;AAAA,MACnD;AAAA,OACH;AAGJ,QAAM,OAAO,YAAY,SAAS,OAAO,cAAc;AAEvD,SACE,qBAAC,SAAI,WAAW,OAAO,OACrB;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW,GAAG,OAAO,UAAU,IAAI,OAAO,WAAW;AAAA,QACrD,iBAAe;AAAA,QACf,SAAS,MAAM,SAAS,OAAO,CAAC,IAAI;AAAA,QAEpC;AAAA,8BAAC,UAAM,iBAAM;AAAA,UACb,oBAAC,eAAY,MAAM,IAAI,WAAW,OAAO,cAAc,eAAW,MAAC;AAAA;AAAA;AAAA,IACrE;AAAA,IAIA,oBAAC,mBAAgB,SAAS,OACvB,kBACC;AAAA,MAAC,OAAO;AAAA,MAAP;AAAA,QACC,WAAW,OAAO;AAAA,QAClB,SAAS,EAAE,QAAQ,GAAG,SAAS,EAAE;AAAA,QACjC,SAAS,EAAE,QAAQ,QAAQ,SAAS,EAAE;AAAA,QACtC,MAAM,EAAE,QAAQ,GAAG,SAAS,EAAE;AAAA,QAC9B,YACE,UACI,EAAE,UAAU,EAAE,IACd;AAAA,UACE,QAAQ,EAAE,UAAU,MAAM,MAAM,MAAM,OAAO;AAAA,UAC7C,SAAS,EAAE,UAAU,MAAM,MAAM,MAAM,IAAI;AAAA,QAC7C;AAAA,QAEN,OAAO,EAAE,UAAU,SAAS;AAAA,QAE3B;AAAA;AAAA,IACH,GAEJ;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { isActive } from "./nav-filter";
|
|
4
4
|
import { NavGroupView } from "./nav-group";
|
|
5
|
-
import {
|
|
5
|
+
import { useOpenGroups } from "./use-open-groups";
|
|
6
6
|
import { SidebarFooter } from "./sidebar-footer";
|
|
7
7
|
import { useFluidHover } from "./use-fluid-hover";
|
|
8
8
|
import { UserMenu } from "./user-menu";
|
|
@@ -18,12 +18,15 @@ function Sidebar({
|
|
|
18
18
|
onSearch
|
|
19
19
|
}) {
|
|
20
20
|
const hover = useFluidHover();
|
|
21
|
-
const { choice, setGroupOpen } = useOpenGroup(pathname);
|
|
22
21
|
const settings = groups.flatMap((group) => group.items).find((item) => item.href === FOOT_HREF);
|
|
23
22
|
const navGroups = groups.map((group) => ({
|
|
24
23
|
...group,
|
|
25
24
|
items: group.items.filter((item) => item.href !== FOOT_HREF)
|
|
26
25
|
})).filter((group) => group.items.length);
|
|
26
|
+
const activeGroup = navGroups.find(
|
|
27
|
+
(group) => group.items.some((item) => isActive(item, pathname))
|
|
28
|
+
)?.label ?? null;
|
|
29
|
+
const { choices, setGroupOpen } = useOpenGroups(pathname, activeGroup);
|
|
27
30
|
return /* @__PURE__ */ jsxs("aside", { className: styles.sidebar, "data-collapsed": collapsed, children: [
|
|
28
31
|
/* @__PURE__ */ jsx(UserMenu, { email, role, collapsed }),
|
|
29
32
|
/* @__PURE__ */ jsxs(
|
|
@@ -53,7 +56,7 @@ function Sidebar({
|
|
|
53
56
|
pathname,
|
|
54
57
|
collapsed,
|
|
55
58
|
canSupport,
|
|
56
|
-
|
|
59
|
+
choices,
|
|
57
60
|
onToggle: setGroupOpen
|
|
58
61
|
},
|
|
59
62
|
group.label ?? index
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/_shell/sidebar.tsx"],"sourcesContent":["'use client';\n\nimport type { NavGroup } from './nav-config';\nimport { isActive } from './nav-filter';\nimport { NavGroupView } from './nav-group';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_shell/sidebar.tsx"],"sourcesContent":["'use client';\n\nimport type { NavGroup } from './nav-config';\nimport { isActive } from './nav-filter';\nimport { NavGroupView } from './nav-group';\nimport { useOpenGroups } from './use-open-groups';\nimport { SidebarFooter } from './sidebar-footer';\nimport { useFluidHover } from './use-fluid-hover';\nimport { UserMenu } from './user-menu';\nimport styles from './sidebar.module.css';\n\n/** Settings lives in the pinned foot, beside the theme switch. */\nconst FOOT_HREF = '/admin/settings';\n\nexport function Sidebar({\n groups,\n pathname,\n canSupport,\n collapsed,\n email,\n role,\n onSearch,\n}: {\n groups: NavGroup[];\n pathname: string;\n canSupport: boolean;\n collapsed: boolean;\n email: string;\n role: string;\n onSearch: () => void;\n}) {\n const hover = useFluidHover();\n const settings = groups\n .flatMap((group) => group.items)\n .find((item) => item.href === FOOT_HREF);\n const navGroups = groups\n .map((group) => ({\n ...group,\n items: group.items.filter((item) => item.href !== FOOT_HREF),\n }))\n .filter((group) => group.items.length);\n const activeGroup =\n navGroups.find((group) =>\n group.items.some((item) => isActive(item, pathname)),\n )?.label ?? null;\n const { choices, setGroupOpen } = useOpenGroups(pathname, activeGroup);\n\n return (\n <aside className={styles.sidebar} data-collapsed={collapsed}>\n <UserMenu email={email} role={role} collapsed={collapsed} />\n\n <nav\n className={styles.nav}\n aria-label=\"Admin\"\n onPointerMove={hover.onPointerMove}\n onPointerLeave={hover.onPointerLeave}\n >\n <span\n aria-hidden\n className={styles.glide}\n data-visible={Boolean(hover.rect)}\n style={\n hover.rect\n ? {\n transform: `translateY(${hover.rect.top}px)`,\n height: hover.rect.height,\n }\n : undefined\n }\n />\n {navGroups.map((group, index) => (\n <NavGroupView\n key={group.label ?? index}\n group={group}\n pathname={pathname}\n collapsed={collapsed}\n canSupport={canSupport}\n choices={choices}\n onToggle={setGroupOpen}\n />\n ))}\n </nav>\n\n <SidebarFooter\n collapsed={collapsed}\n showSettings={Boolean(settings)}\n settingsActive={settings ? isActive(settings, pathname) : false}\n onSearch={onSearch}\n />\n </aside>\n );\n}\n"],"mappings":";AAiDM,cAEA,YAFA;AA9CN,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,OAAO,YAAY;AAGnB,MAAM,YAAY;AAEX,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,QAAQ,cAAc;AAC5B,QAAM,WAAW,OACd,QAAQ,CAAC,UAAU,MAAM,KAAK,EAC9B,KAAK,CAAC,SAAS,KAAK,SAAS,SAAS;AACzC,QAAM,YAAY,OACf,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,OAAO,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,SAAS;AAAA,EAC7D,EAAE,EACD,OAAO,CAAC,UAAU,MAAM,MAAM,MAAM;AACvC,QAAM,cACJ,UAAU;AAAA,IAAK,CAAC,UACd,MAAM,MAAM,KAAK,CAAC,SAAS,SAAS,MAAM,QAAQ,CAAC;AAAA,EACrD,GAAG,SAAS;AACd,QAAM,EAAE,SAAS,aAAa,IAAI,cAAc,UAAU,WAAW;AAErE,SACE,qBAAC,WAAM,WAAW,OAAO,SAAS,kBAAgB,WAChD;AAAA,wBAAC,YAAS,OAAc,MAAY,WAAsB;AAAA,IAE1D;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,OAAO;AAAA,QAClB,cAAW;AAAA,QACX,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QAEtB;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAW,OAAO;AAAA,cAClB,gBAAc,QAAQ,MAAM,IAAI;AAAA,cAChC,OACE,MAAM,OACF;AAAA,gBACE,WAAW,cAAc,MAAM,KAAK,GAAG;AAAA,gBACvC,QAAQ,MAAM,KAAK;AAAA,cACrB,IACA;AAAA;AAAA,UAER;AAAA,UACC,UAAU,IAAI,CAAC,OAAO,UACrB;AAAA,YAAC;AAAA;AAAA,cAEC;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA,UAAU;AAAA;AAAA,YANL,MAAM,SAAS;AAAA,UAOtB,CACD;AAAA;AAAA;AAAA,IACH;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,cAAc,QAAQ,QAAQ;AAAA,QAC9B,gBAAgB,WAAW,SAAS,UAAU,QAAQ,IAAI;AAAA,QAC1D;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
function useOpenGroups(pathname, activeGroup) {
|
|
4
|
+
const [choices, setChoices] = useState({});
|
|
5
|
+
const setGroupOpen = (label, next) => setChoices((current) => ({ ...current, [label]: next }));
|
|
6
|
+
const [choiceRoute, setChoiceRoute] = useState(pathname);
|
|
7
|
+
if (choiceRoute !== pathname) {
|
|
8
|
+
setChoiceRoute(pathname);
|
|
9
|
+
if (activeGroup && activeGroup in choices) {
|
|
10
|
+
const next = { ...choices };
|
|
11
|
+
delete next[activeGroup];
|
|
12
|
+
setChoices(next);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return { choices, setGroupOpen };
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
useOpenGroups
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=use-open-groups.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/_shell/use-open-groups.ts"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport type { GroupChoices } from './nav-fold';\n\n/**\n * Which sidebar groups the admin has opened or closed by hand, each on its\n * own: pressing one never moves another. `activeGroup` is the group holding the\n * current page, or null.\n */\nexport function useOpenGroups(pathname: string, activeGroup: string | null) {\n const [choices, setChoices] = useState<GroupChoices>({});\n const setGroupOpen = (label: string, next: boolean) =>\n setChoices((current) => ({ ...current, [label]: next }));\n // Arriving on a page retires the choice for its own group only, or a group\n // closed by hand would hide the row you just landed on. The other groups\n // stay as they were. Adjusted during render, not in an effect, so no stale\n // frame paints first.\n const [choiceRoute, setChoiceRoute] = useState(pathname);\n\n if (choiceRoute !== pathname) {\n setChoiceRoute(pathname);\n\n if (activeGroup && activeGroup in choices) {\n const next = { ...choices };\n\n delete next[activeGroup];\n setChoices(next);\n }\n }\n\n return { choices, setGroupOpen };\n}\n"],"mappings":";AAEA,SAAS,gBAAgB;AAQlB,SAAS,cAAc,UAAkB,aAA4B;AAC1E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAuB,CAAC,CAAC;AACvD,QAAM,eAAe,CAAC,OAAe,SACnC,WAAW,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,KAAK,GAAG,KAAK,EAAE;AAKzD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,QAAQ;AAEvD,MAAI,gBAAgB,UAAU;AAC5B,mBAAe,QAAQ;AAEvB,QAAI,eAAe,eAAe,SAAS;AACzC,YAAM,OAAO,EAAE,GAAG,QAAQ;AAE1B,aAAO,KAAK,WAAW;AACvB,iBAAW,IAAI;AAAA,IACjB;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,aAAa;AACjC;","names":[]}
|
|
@@ -7,6 +7,9 @@ import { StatusPill } from "../audit/status-pill";
|
|
|
7
7
|
import { useAccountTenants } from "../hooks";
|
|
8
8
|
import { CopyText } from "../tenants/[id]/copy-text";
|
|
9
9
|
import { AccountActivity } from "./account-activity";
|
|
10
|
+
import { ACTIVE_HINT, STAGE_LABELS, stoppedAt } from "./account-stage";
|
|
11
|
+
import { AccountSessions } from "./account-sessions";
|
|
12
|
+
import { Hint } from "./hint";
|
|
10
13
|
import { AccountVitals } from "./account-vitals";
|
|
11
14
|
import { useAccountActions } from "./use-account-actions";
|
|
12
15
|
import { useModuleOn } from "../_shell/modules-context";
|
|
@@ -56,11 +59,11 @@ function AccountPanel({
|
|
|
56
59
|
label: account.emailVerified ? "Email verified" : "Email unverified"
|
|
57
60
|
}
|
|
58
61
|
),
|
|
59
|
-
account.
|
|
62
|
+
account.stage && /* @__PURE__ */ jsx(
|
|
60
63
|
StatusPill,
|
|
61
64
|
{
|
|
62
|
-
status: account.
|
|
63
|
-
label: account.
|
|
65
|
+
status: account.stage === "active" ? "active" : "pending",
|
|
66
|
+
label: STAGE_LABELS[account.stage]
|
|
64
67
|
}
|
|
65
68
|
),
|
|
66
69
|
account.authProvider && /* @__PURE__ */ jsx("span", { className: styles.muted, children: account.authProvider })
|
|
@@ -106,6 +109,13 @@ function AccountPanel({
|
|
|
106
109
|
(action, i) => action.isError && /* @__PURE__ */ jsx(Banner, { tone: "error", title: "That didn\u2019t work", children: action.error.message }, i)
|
|
107
110
|
),
|
|
108
111
|
/* @__PURE__ */ jsx(AccountVitals, { account, workspaces: liveWorkspaces }),
|
|
112
|
+
account.stage && /* @__PURE__ */ jsxs("p", { className: styles.stopped, children: [
|
|
113
|
+
/* @__PURE__ */ jsxs("span", { className: styles.stoppedLabel, children: [
|
|
114
|
+
"Where this account stopped",
|
|
115
|
+
/* @__PURE__ */ jsx(Hint, { label: ACTIVE_HINT })
|
|
116
|
+
] }),
|
|
117
|
+
/* @__PURE__ */ jsx("span", { children: stoppedAt(account) ?? "Active" })
|
|
118
|
+
] }),
|
|
109
119
|
/* @__PURE__ */ jsx(
|
|
110
120
|
Section,
|
|
111
121
|
{
|
|
@@ -123,7 +133,8 @@ function AccountPanel({
|
|
|
123
133
|
}
|
|
124
134
|
),
|
|
125
135
|
/* @__PURE__ */ jsx(Section, { title: "Workspaces", index: 1, count: liveWorkspaces, children: /* @__PURE__ */ jsx(Workspaces, { accountId: account.id }) }),
|
|
126
|
-
/* @__PURE__ */ jsx(Section, { title: "
|
|
136
|
+
/* @__PURE__ */ jsx(Section, { title: "Sessions", index: 2, children: /* @__PURE__ */ jsx(AccountSessions, { accountId: account.id }) }),
|
|
137
|
+
/* @__PURE__ */ jsx(Section, { title: "Account ID", index: 3, children: /* @__PURE__ */ jsx(CopyText, { value: account.id }) })
|
|
127
138
|
] })
|
|
128
139
|
}
|
|
129
140
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/accounts/account-panel.tsx"],"sourcesContent":["'use client';\n\nimport { Banner, Button } from '@withpotter/ui-kit';\nimport type { AccountRow } from '@withpotter/lib/admin-api';\nimport { Reveal } from '../_fx';\nimport { Drawer } from '../_ui';\nimport { StatusPill } from '../audit/status-pill';\nimport { useAccountTenants } from '../hooks';\nimport { CopyText } from '../tenants/[id]/copy-text';\nimport { AccountActivity } from './account-activity';\nimport { AccountVitals } from './account-vitals';\nimport { useAccountActions } from './use-account-actions';\nimport { useModuleOn } from '../_shell/modules-context';\nimport { useAccountActivity } from './use-account-activity';\nimport { Workspaces } from './account-workspaces';\nimport styles from './account-panel.module.css';\n\n/** Enough to open the panel from anywhere; the table row adds the rest. */\nexport type AccountSummary = Pick<AccountRow, 'id' | 'email' | 'name'> &\n Partial<AccountRow>;\n\nfunction Section({\n title,\n index,\n count,\n children,\n}: {\n title: string;\n index: number;\n count?: number;\n children: React.ReactNode;\n}) {\n return (\n <Reveal as=\"section\" index={index} className={styles.section}>\n <h3 className={styles.heading}>\n {title}\n {count !== undefined && <span className={styles.count}>{count}</span>}\n </h3>\n {children}\n </Reveal>\n );\n}\n\nexport function AccountPanel({\n account,\n onClose,\n}: {\n account: AccountSummary | null;\n onClose: () => void;\n}) {\n const { setStatus, verify, impersonate } = useAccountActions();\n const ownerView = useModuleOn('ownerView');\n const tenants = useAccountTenants(account?.id ?? null);\n const activity = useAccountActivity(account?.id ?? null, tenants.data);\n const suspended = account?.status === 'suspended';\n const liveWorkspaces = tenants.data?.filter(\n (w) => w.status !== 'deleted' && w.memberStatus !== 'removed',\n ).length;\n\n return (\n <Drawer\n open={Boolean(account)}\n onClose={onClose}\n width={560}\n title={account?.name || account?.email || 'Account'}\n subtitle={account?.name ? account.email : undefined}\n meta={\n account && (\n <>\n {account.status && <StatusPill status={account.status} />}\n {account.emailVerified !== undefined && (\n <StatusPill\n status={account.emailVerified ? 'verified' : 'unverified'}\n label={\n account.emailVerified ? 'Email verified' : 'Email unverified'\n }\n />\n )}\n {account.
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/accounts/account-panel.tsx"],"sourcesContent":["'use client';\n\nimport { Banner, Button } from '@withpotter/ui-kit';\nimport type { AccountRow } from '@withpotter/lib/admin-api';\nimport { Reveal } from '../_fx';\nimport { Drawer } from '../_ui';\nimport { StatusPill } from '../audit/status-pill';\nimport { useAccountTenants } from '../hooks';\nimport { CopyText } from '../tenants/[id]/copy-text';\nimport { AccountActivity } from './account-activity';\nimport { ACTIVE_HINT, STAGE_LABELS, stoppedAt } from './account-stage';\nimport { AccountSessions } from './account-sessions';\nimport { Hint } from './hint';\nimport { AccountVitals } from './account-vitals';\nimport { useAccountActions } from './use-account-actions';\nimport { useModuleOn } from '../_shell/modules-context';\nimport { useAccountActivity } from './use-account-activity';\nimport { Workspaces } from './account-workspaces';\nimport styles from './account-panel.module.css';\n\n/** Enough to open the panel from anywhere; the table row adds the rest. */\nexport type AccountSummary = Pick<AccountRow, 'id' | 'email' | 'name'> &\n Partial<AccountRow>;\n\nfunction Section({\n title,\n index,\n count,\n children,\n}: {\n title: string;\n index: number;\n count?: number;\n children: React.ReactNode;\n}) {\n return (\n <Reveal as=\"section\" index={index} className={styles.section}>\n <h3 className={styles.heading}>\n {title}\n {count !== undefined && <span className={styles.count}>{count}</span>}\n </h3>\n {children}\n </Reveal>\n );\n}\n\nexport function AccountPanel({\n account,\n onClose,\n}: {\n account: AccountSummary | null;\n onClose: () => void;\n}) {\n const { setStatus, verify, impersonate } = useAccountActions();\n const ownerView = useModuleOn('ownerView');\n const tenants = useAccountTenants(account?.id ?? null);\n const activity = useAccountActivity(account?.id ?? null, tenants.data);\n const suspended = account?.status === 'suspended';\n const liveWorkspaces = tenants.data?.filter(\n (w) => w.status !== 'deleted' && w.memberStatus !== 'removed',\n ).length;\n\n return (\n <Drawer\n open={Boolean(account)}\n onClose={onClose}\n width={560}\n title={account?.name || account?.email || 'Account'}\n subtitle={account?.name ? account.email : undefined}\n meta={\n account && (\n <>\n {account.status && <StatusPill status={account.status} />}\n {account.emailVerified !== undefined && (\n <StatusPill\n status={account.emailVerified ? 'verified' : 'unverified'}\n label={\n account.emailVerified ? 'Email verified' : 'Email unverified'\n }\n />\n )}\n {account.stage && (\n <StatusPill\n status={account.stage === 'active' ? 'active' : 'pending'}\n label={STAGE_LABELS[account.stage]}\n />\n )}\n {account.authProvider && (\n <span className={styles.muted}>{account.authProvider}</span>\n )}\n </>\n )\n }\n footer={\n account && (\n <>\n {account.status && (\n <Button\n size=\"sm\"\n variant={suspended ? 'secondary' : 'ghostDanger'}\n loading={setStatus.isPending}\n onClick={() =>\n setStatus.mutate({\n id: account.id,\n status: suspended ? 'active' : 'suspended',\n })\n }\n >\n {suspended ? 'Restore' : 'Suspend'}\n </Button>\n )}\n <span className={styles.spacer} />\n {account.emailVerified === false && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n loading={verify.isPending}\n onClick={() => verify.mutate(account.id)}\n >\n Verify email\n </Button>\n )}\n {ownerView && (\n <Button\n size=\"sm\"\n variant=\"primary\"\n loading={impersonate.isPending}\n onClick={() => impersonate.mutate(account.id)}\n >\n View as user\n </Button>\n )}\n </>\n )\n }\n >\n {account && (\n <div className={styles.panel}>\n {[verify, impersonate, setStatus].map(\n (action, i) =>\n action.isError && (\n <Banner key={i} tone=\"error\" title=\"That didn’t work\">\n {action.error.message}\n </Banner>\n ),\n )}\n\n <AccountVitals account={account} workspaces={liveWorkspaces} />\n\n {account.stage && (\n <p className={styles.stopped}>\n <span className={styles.stoppedLabel}>\n Where this account stopped\n <Hint label={ACTIVE_HINT} />\n </span>\n <span>{stoppedAt(account) ?? 'Active'}</span>\n </p>\n )}\n\n <Section\n title=\"Recent activity\"\n index={0}\n count={activity.entries.length}\n >\n <AccountActivity\n entries={activity.entries}\n isLoading={activity.isLoading}\n skipped={activity.skipped}\n />\n </Section>\n\n <Section title=\"Workspaces\" index={1} count={liveWorkspaces}>\n <Workspaces accountId={account.id} />\n </Section>\n\n <Section title=\"Sessions\" index={2}>\n <AccountSessions accountId={account.id} />\n </Section>\n\n <Section title=\"Account ID\" index={3}>\n <CopyText value={account.id} />\n </Section>\n </div>\n )}\n </Drawer>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -138,3 +138,24 @@
|
|
|
138
138
|
line-height: 16px;
|
|
139
139
|
color: var(--text-tertiary);
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
.stopped {
|
|
143
|
+
display: grid;
|
|
144
|
+
gap: 4px;
|
|
145
|
+
margin: 0;
|
|
146
|
+
padding: 12px 16px;
|
|
147
|
+
border-radius: var(--adm-card-radius);
|
|
148
|
+
border: 1px solid var(--adm-line);
|
|
149
|
+
background: var(--bg-base);
|
|
150
|
+
font-size: 14px;
|
|
151
|
+
font-weight: 500;
|
|
152
|
+
line-height: 20px;
|
|
153
|
+
color: var(--text-primary);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.stoppedLabel {
|
|
157
|
+
font-size: 12px;
|
|
158
|
+
font-weight: 400;
|
|
159
|
+
line-height: 16px;
|
|
160
|
+
color: var(--text-tertiary);
|
|
161
|
+
}
|
|
@@ -11,6 +11,7 @@ import { EntityAvatar } from "../_fx";
|
|
|
11
11
|
import { EMPTY } from "../_ui/list-card";
|
|
12
12
|
import { StatusPill } from "../audit/status-pill";
|
|
13
13
|
import { When } from "../audit/when";
|
|
14
|
+
import { kycLabel, STAGE_LABELS } from "./account-stage";
|
|
14
15
|
import { useAccountActions } from "./use-account-actions";
|
|
15
16
|
import { useModuleOn } from "../_shell/modules-context";
|
|
16
17
|
import list from "../_ui/list-card.module.css";
|
|
@@ -76,11 +77,19 @@ function AccountListRow({
|
|
|
76
77
|
] }) }),
|
|
77
78
|
visible.has("provider") && /* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: PROVIDER_LABELS[a.authProvider] ?? (a.authProvider || EMPTY) }),
|
|
78
79
|
/* @__PURE__ */ jsx("td", { className: list.nowrap, children: /* @__PURE__ */ jsx(StatusPill, { status: a.status }) }),
|
|
80
|
+
visible.has("kyc") && /* @__PURE__ */ jsx("td", { className: list.nowrap, children: kycLabel(a.kycStatus) ?? /* @__PURE__ */ jsx("span", { className: list.muted, children: EMPTY }) }),
|
|
81
|
+
visible.has("stage") && /* @__PURE__ */ jsx("td", { className: list.nowrap, children: a.stage ? /* @__PURE__ */ jsx(
|
|
82
|
+
StatusPill,
|
|
83
|
+
{
|
|
84
|
+
status: a.stage === "active" ? "active" : "pending",
|
|
85
|
+
label: STAGE_LABELS[a.stage]
|
|
86
|
+
}
|
|
87
|
+
) : /* @__PURE__ */ jsx("span", { className: list.muted, children: EMPTY }) }),
|
|
79
88
|
visible.has("emailVerified") && /* @__PURE__ */ jsx("td", { className: list.nowrap, children: /* @__PURE__ */ jsx(StatusPill, { status: a.emailVerified ? "verified" : "unverified" }) }),
|
|
80
89
|
visible.has("workspaces") && /* @__PURE__ */ jsx("td", { className: list.num, children: a.workspaceCount ?? EMPTY }),
|
|
81
|
-
visible.has("lastLogin") && /* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children:
|
|
90
|
+
visible.has("lastLogin") && /* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: day(a.lastLoginAt) ?? "Never" }),
|
|
82
91
|
visible.has("joined") && /* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: day(a.createdAt) }),
|
|
83
|
-
visible.has("
|
|
92
|
+
visible.has("firstSale") && /* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: day(a.firstSaleAt) ?? "None yet" }),
|
|
84
93
|
visible.has("activeSessions") && /* @__PURE__ */ jsx("td", { className: list.num, children: a.activeSessions ?? EMPTY }),
|
|
85
94
|
visible.has("lastSeen") && /* @__PURE__ */ jsx("td", { className: `${list.nowrap} ${list.muted}`, children: a.lastSeenAt ? /* @__PURE__ */ jsx(When, { iso: a.lastSeenAt, className: styles.when }) : "Never" }),
|
|
86
95
|
/* @__PURE__ */ jsx("td", { className: styles.menuCell, onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs(OverflowMenu, { label: `Actions for ${label}`, children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/accounts/account-row.tsx"],"sourcesContent":["'use client';\n\nimport {\n OverflowItem,\n OverflowMenu,\n TableRow,\n useConfirm,\n useToast,\n} from '@withpotter/ui-kit';\nimport type { AccountRow as Account } from '@withpotter/lib/admin-api';\nimport { EntityAvatar } from '../_fx';\nimport { EMPTY } from '../_ui/list-card';\nimport { StatusPill } from '../audit/status-pill';\nimport { When } from '../audit/when';\nimport { useAccountActions } from './use-account-actions';\nimport { useModuleOn } from '../_shell/modules-context';\nimport list from '../_ui/list-card.module.css';\nimport styles from './rows.module.css';\n\nexport const PROVIDER_LABELS: Record<string, string> = {\n local: 'Email',\n google: 'Google',\n github: 'GitHub',\n};\n\nconst day = (iso?: string | null) =>\n iso ? new Date(iso).toLocaleDateString() : null;\n\n/** One account: who, how they sign in, status, activity and a ••• menu. */\nexport function AccountListRow({\n a,\n active,\n onOpen,\n visible,\n}: {\n a: Account;\n active: boolean;\n onOpen: () => void;\n /** Which optional columns are showing, in the order the header renders them. */\n visible: Set<string>;\n}) {\n const confirm = useConfirm();\n const { toast } = useToast();\n const { setStatus, verify, impersonate } = useAccountActions();\n const ownerView = useModuleOn('ownerView');\n const suspended = a.status === 'suspended';\n const label = a.name ?? a.email;\n\n const toggleStatus = async () => {\n const ok = await confirm({\n title: suspended ? `Restore ${label}?` : `Suspend ${label}?`,\n description: suspended\n ? 'They can sign in and use their workspaces again.'\n : 'They are signed out everywhere and cannot sign in until restored.',\n confirmLabel: suspended ? 'Restore' : 'Suspend',\n tone: suspended ? 'default' : 'danger',\n });\n if (!ok) return;\n setStatus.mutate(\n { id: a.id, status: suspended ? 'active' : 'suspended' },\n {\n onSuccess: () =>\n toast({ title: `${label} ${suspended ? 'restored' : 'suspended'}` }),\n onError: (e) =>\n toast({\n title: 'The status was not changed',\n description: e.message,\n variant: 'destructive',\n }),\n },\n );\n };\n\n return (\n <TableRow active={active} onClick={onOpen}>\n <td>\n <span className={styles.identity}>\n <EntityAvatar name={label} size={32} round />\n <span className={styles.identityText}>\n <button\n type=\"button\"\n className={styles.nameButton}\n title={label}\n aria-label={`Open ${label}`}\n onClick={(e) => {\n e.stopPropagation();\n onOpen();\n }}\n >\n {a.name ?? EMPTY}\n </button>\n <span className={styles.secondary} title={a.email}>\n {a.email}\n </span>\n </span>\n </span>\n </td>\n {visible.has('provider') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {PROVIDER_LABELS[a.authProvider] ?? (a.authProvider || EMPTY)}\n </td>\n )}\n <td className={list.nowrap}>\n <StatusPill status={a.status} />\n </td>\n {visible.has('emailVerified') && (\n <td className={list.nowrap}>\n <StatusPill status={a.emailVerified ? 'verified' : 'unverified'} />\n </td>\n )}\n {visible.has('workspaces') && (\n <td className={list.num}>{a.workspaceCount ?? EMPTY}</td>\n )}\n {visible.has('lastLogin') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/accounts/account-row.tsx"],"sourcesContent":["'use client';\n\nimport {\n OverflowItem,\n OverflowMenu,\n TableRow,\n useConfirm,\n useToast,\n} from '@withpotter/ui-kit';\nimport type { AccountRow as Account } from '@withpotter/lib/admin-api';\nimport { EntityAvatar } from '../_fx';\nimport { EMPTY } from '../_ui/list-card';\nimport { StatusPill } from '../audit/status-pill';\nimport { When } from '../audit/when';\nimport { kycLabel, STAGE_LABELS } from './account-stage';\nimport { useAccountActions } from './use-account-actions';\nimport { useModuleOn } from '../_shell/modules-context';\nimport list from '../_ui/list-card.module.css';\nimport styles from './rows.module.css';\n\nexport const PROVIDER_LABELS: Record<string, string> = {\n local: 'Email',\n google: 'Google',\n github: 'GitHub',\n};\n\nconst day = (iso?: string | null) =>\n iso ? new Date(iso).toLocaleDateString() : null;\n\n/** One account: who, how they sign in, status, activity and a ••• menu. */\nexport function AccountListRow({\n a,\n active,\n onOpen,\n visible,\n}: {\n a: Account;\n active: boolean;\n onOpen: () => void;\n /** Which optional columns are showing, in the order the header renders them. */\n visible: Set<string>;\n}) {\n const confirm = useConfirm();\n const { toast } = useToast();\n const { setStatus, verify, impersonate } = useAccountActions();\n const ownerView = useModuleOn('ownerView');\n const suspended = a.status === 'suspended';\n const label = a.name ?? a.email;\n\n const toggleStatus = async () => {\n const ok = await confirm({\n title: suspended ? `Restore ${label}?` : `Suspend ${label}?`,\n description: suspended\n ? 'They can sign in and use their workspaces again.'\n : 'They are signed out everywhere and cannot sign in until restored.',\n confirmLabel: suspended ? 'Restore' : 'Suspend',\n tone: suspended ? 'default' : 'danger',\n });\n if (!ok) return;\n setStatus.mutate(\n { id: a.id, status: suspended ? 'active' : 'suspended' },\n {\n onSuccess: () =>\n toast({ title: `${label} ${suspended ? 'restored' : 'suspended'}` }),\n onError: (e) =>\n toast({\n title: 'The status was not changed',\n description: e.message,\n variant: 'destructive',\n }),\n },\n );\n };\n\n return (\n <TableRow active={active} onClick={onOpen}>\n <td>\n <span className={styles.identity}>\n <EntityAvatar name={label} size={32} round />\n <span className={styles.identityText}>\n <button\n type=\"button\"\n className={styles.nameButton}\n title={label}\n aria-label={`Open ${label}`}\n onClick={(e) => {\n e.stopPropagation();\n onOpen();\n }}\n >\n {a.name ?? EMPTY}\n </button>\n <span className={styles.secondary} title={a.email}>\n {a.email}\n </span>\n </span>\n </span>\n </td>\n {visible.has('provider') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {PROVIDER_LABELS[a.authProvider] ?? (a.authProvider || EMPTY)}\n </td>\n )}\n <td className={list.nowrap}>\n <StatusPill status={a.status} />\n </td>\n {visible.has('kyc') && (\n <td className={list.nowrap}>\n {kycLabel(a.kycStatus) ?? <span className={list.muted}>{EMPTY}</span>}\n </td>\n )}\n {visible.has('stage') && (\n <td className={list.nowrap}>\n {a.stage ? (\n <StatusPill\n status={a.stage === 'active' ? 'active' : 'pending'}\n label={STAGE_LABELS[a.stage]}\n />\n ) : (\n <span className={list.muted}>{EMPTY}</span>\n )}\n </td>\n )}\n {visible.has('emailVerified') && (\n <td className={list.nowrap}>\n <StatusPill status={a.emailVerified ? 'verified' : 'unverified'} />\n </td>\n )}\n {visible.has('workspaces') && (\n <td className={list.num}>{a.workspaceCount ?? EMPTY}</td>\n )}\n {visible.has('lastLogin') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {day(a.lastLoginAt) ?? 'Never'}\n </td>\n )}\n {visible.has('joined') && (\n <td className={`${list.nowrap} ${list.muted}`}>{day(a.createdAt)}</td>\n )}\n {visible.has('firstSale') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {day(a.firstSaleAt) ?? 'None yet'}\n </td>\n )}\n {visible.has('activeSessions') && (\n <td className={list.num}>{a.activeSessions ?? EMPTY}</td>\n )}\n {visible.has('lastSeen') && (\n <td className={`${list.nowrap} ${list.muted}`}>\n {a.lastSeenAt ? (\n <When iso={a.lastSeenAt} className={styles.when} />\n ) : (\n 'Never'\n )}\n </td>\n )}\n <td className={styles.menuCell} onClick={(e) => e.stopPropagation()}>\n <OverflowMenu label={`Actions for ${label}`}>\n <OverflowItem onClick={onOpen}>View account</OverflowItem>\n {ownerView && (\n <OverflowItem onClick={() => impersonate.mutate(a.id)}>\n View as user\n </OverflowItem>\n )}\n {!a.emailVerified && (\n <OverflowItem\n onClick={() =>\n verify.mutate(a.id, {\n onSuccess: () => toast({ title: `${a.email} verified` }),\n })\n }\n >\n Verify email\n </OverflowItem>\n )}\n <OverflowItem\n onClick={() => void toggleStatus()}\n destructive={!suspended}\n >\n {suspended ? 'Restore' : 'Suspend'}\n </OverflowItem>\n </OverflowMenu>\n </td>\n </TableRow>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button, EmptyNote } from "@withpotter/ui-kit";
|
|
4
|
+
import { AdmLoading } from "../_ui";
|
|
5
|
+
import { useAccountSessions } from "../hooks";
|
|
6
|
+
import styles from "./account-sessions.module.css";
|
|
7
|
+
const ENDED = {
|
|
8
|
+
active: "Active now",
|
|
9
|
+
revoked: "Signed out",
|
|
10
|
+
expired: "Expired"
|
|
11
|
+
};
|
|
12
|
+
const when = (iso) => iso ? new Date(iso).toLocaleString(void 0, {
|
|
13
|
+
dateStyle: "medium",
|
|
14
|
+
timeStyle: "short"
|
|
15
|
+
}) : null;
|
|
16
|
+
function deviceOf(s) {
|
|
17
|
+
return s.name || s.kind || s.userAgent || "Unknown device";
|
|
18
|
+
}
|
|
19
|
+
function AccountSessions({ accountId }) {
|
|
20
|
+
const { data, isLoading, isError, refetch } = useAccountSessions(accountId);
|
|
21
|
+
if (isLoading) return /* @__PURE__ */ jsx(AdmLoading, { label: "Loading sessions\u2026", size: "sm" });
|
|
22
|
+
if (isError) {
|
|
23
|
+
return /* @__PURE__ */ jsxs(EmptyNote, { children: [
|
|
24
|
+
"We could not load their sessions just now.",
|
|
25
|
+
" ",
|
|
26
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => void refetch(), children: "Try again" })
|
|
27
|
+
] });
|
|
28
|
+
}
|
|
29
|
+
if (!data?.length) {
|
|
30
|
+
return /* @__PURE__ */ jsx(EmptyNote, { children: "No sign-ins recorded for this person yet." });
|
|
31
|
+
}
|
|
32
|
+
const ordered = [...data].sort(
|
|
33
|
+
(a, b) => b.startedAt.localeCompare(a.startedAt)
|
|
34
|
+
);
|
|
35
|
+
return /* @__PURE__ */ jsx("ul", { className: styles.list, children: ordered.map((s) => /* @__PURE__ */ jsxs("li", { className: styles.item, children: [
|
|
36
|
+
/* @__PURE__ */ jsxs("span", { className: styles.top, children: [
|
|
37
|
+
/* @__PURE__ */ jsx("span", { className: styles.device, title: s.userAgent ?? void 0, children: deviceOf(s) }),
|
|
38
|
+
/* @__PURE__ */ jsx("span", { className: styles.how, "data-live": s.endedHow === "active", children: ENDED[s.endedHow] })
|
|
39
|
+
] }),
|
|
40
|
+
/* @__PURE__ */ jsx("span", { className: styles.meta, children: [s.ipAddress, s.location].filter(Boolean).join(" \xB7 ") || "No address recorded" }),
|
|
41
|
+
/* @__PURE__ */ jsxs("span", { className: styles.meta, children: [
|
|
42
|
+
"Started ",
|
|
43
|
+
when(s.startedAt),
|
|
44
|
+
s.endedAt && ` \xB7 Ended ${when(s.endedAt)}`
|
|
45
|
+
] })
|
|
46
|
+
] }, s.id)) });
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
AccountSessions
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=account-sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/accounts/account-sessions.tsx"],"sourcesContent":["'use client';\n\nimport { Button, EmptyNote } from '@withpotter/ui-kit';\nimport type { AccountSession } from '@withpotter/lib/admin-api';\nimport { AdmLoading } from '../_ui';\nimport { useAccountSessions } from '../hooks';\nimport styles from './account-sessions.module.css';\n\nconst ENDED: Record<AccountSession['endedHow'], string> = {\n active: 'Active now',\n revoked: 'Signed out',\n expired: 'Expired',\n};\n\nconst when = (iso: string | null) =>\n iso\n ? new Date(iso).toLocaleString(undefined, {\n dateStyle: 'medium',\n timeStyle: 'short',\n })\n : null;\n\n/** A short device name from the session, falling back to the raw agent. */\nfunction deviceOf(s: AccountSession): string {\n return s.name || s.kind || s.userAgent || 'Unknown device';\n}\n\n/** Where and when this person signed in, newest first. */\nexport function AccountSessions({ accountId }: { accountId: string }) {\n const { data, isLoading, isError, refetch } = useAccountSessions(accountId);\n\n if (isLoading) return <AdmLoading label=\"Loading sessions…\" size=\"sm\" />;\n\n if (isError) {\n return (\n <EmptyNote>\n We could not load their sessions just now.{' '}\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void refetch()}>\n Try again\n </Button>\n </EmptyNote>\n );\n }\n\n if (!data?.length) {\n return <EmptyNote>No sign-ins recorded for this person yet.</EmptyNote>;\n }\n\n const ordered = [...data].sort((a: AccountSession, b: AccountSession) =>\n b.startedAt.localeCompare(a.startedAt),\n );\n\n return (\n <ul className={styles.list}>\n {ordered.map((s: AccountSession) => (\n <li key={s.id} className={styles.item}>\n <span className={styles.top}>\n <span className={styles.device} title={s.userAgent ?? undefined}>\n {deviceOf(s)}\n </span>\n <span className={styles.how} data-live={s.endedHow === 'active'}>\n {ENDED[s.endedHow]}\n </span>\n </span>\n <span className={styles.meta}>\n {[s.ipAddress, s.location].filter(Boolean).join(' · ') ||\n 'No address recorded'}\n </span>\n <span className={styles.meta}>\n Started {when(s.startedAt)}\n {s.endedAt && ` · Ended ${when(s.endedAt)}`}\n </span>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":";AA+BwB,cAIlB,YAJkB;AA7BxB,SAAS,QAAQ,iBAAiB;AAElC,SAAS,kBAAkB;AAC3B,SAAS,0BAA0B;AACnC,OAAO,YAAY;AAEnB,MAAM,QAAoD;AAAA,EACxD,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AACX;AAEA,MAAM,OAAO,CAAC,QACZ,MACI,IAAI,KAAK,GAAG,EAAE,eAAe,QAAW;AAAA,EACtC,WAAW;AAAA,EACX,WAAW;AACb,CAAC,IACD;AAGN,SAAS,SAAS,GAA2B;AAC3C,SAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa;AAC5C;AAGO,SAAS,gBAAgB,EAAE,UAAU,GAA0B;AACpE,QAAM,EAAE,MAAM,WAAW,SAAS,QAAQ,IAAI,mBAAmB,SAAS;AAE1E,MAAI,UAAW,QAAO,oBAAC,cAAW,OAAM,0BAAoB,MAAK,MAAK;AAEtE,MAAI,SAAS;AACX,WACE,qBAAC,aAAU;AAAA;AAAA,MACkC;AAAA,MAC3C,oBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAEjE;AAAA,OACF;AAAA,EAEJ;AAEA,MAAI,CAAC,MAAM,QAAQ;AACjB,WAAO,oBAAC,aAAU,uDAAyC;AAAA,EAC7D;AAEA,QAAM,UAAU,CAAC,GAAG,IAAI,EAAE;AAAA,IAAK,CAAC,GAAmB,MACjD,EAAE,UAAU,cAAc,EAAE,SAAS;AAAA,EACvC;AAEA,SACE,oBAAC,QAAG,WAAW,OAAO,MACnB,kBAAQ,IAAI,CAAC,MACZ,qBAAC,QAAc,WAAW,OAAO,MAC/B;AAAA,yBAAC,UAAK,WAAW,OAAO,KACtB;AAAA,0BAAC,UAAK,WAAW,OAAO,QAAQ,OAAO,EAAE,aAAa,QACnD,mBAAS,CAAC,GACb;AAAA,MACA,oBAAC,UAAK,WAAW,OAAO,KAAK,aAAW,EAAE,aAAa,UACpD,gBAAM,EAAE,QAAQ,GACnB;AAAA,OACF;AAAA,IACA,oBAAC,UAAK,WAAW,OAAO,MACrB,WAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,OAAO,EAAE,KAAK,QAAK,KACnD,uBACJ;AAAA,IACA,qBAAC,UAAK,WAAW,OAAO,MAAM;AAAA;AAAA,MACnB,KAAK,EAAE,SAAS;AAAA,MACxB,EAAE,WAAW,eAAY,KAAK,EAAE,OAAO,CAAC;AAAA,OAC3C;AAAA,OAhBO,EAAE,EAiBX,CACD,GACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
.list {
|
|
2
|
+
display: grid;
|
|
3
|
+
gap: 8px;
|
|
4
|
+
max-height: 320px;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
overflow-y: auto;
|
|
8
|
+
list-style: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.item {
|
|
12
|
+
display: grid;
|
|
13
|
+
gap: 2px;
|
|
14
|
+
padding: 12px 16px;
|
|
15
|
+
border-radius: var(--adm-card-radius);
|
|
16
|
+
border: 1px solid var(--adm-line);
|
|
17
|
+
background: var(--bg-base);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.top {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: baseline;
|
|
23
|
+
justify-content: space-between;
|
|
24
|
+
gap: 12px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.device {
|
|
28
|
+
min-width: 0;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
text-overflow: ellipsis;
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
line-height: 20px;
|
|
35
|
+
color: var(--text-primary);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.how {
|
|
39
|
+
flex-shrink: 0;
|
|
40
|
+
font-size: 12px;
|
|
41
|
+
line-height: 16px;
|
|
42
|
+
color: var(--text-tertiary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.how[data-live='true'] {
|
|
46
|
+
color: var(--success);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.meta {
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
line-height: 16px;
|
|
52
|
+
color: var(--text-tertiary);
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { FilterSelect } from "../_ui";
|
|
4
|
+
import { STAGE_LABELS, STAGE_ORDER } from "./account-stage";
|
|
5
|
+
const OPTIONS = [
|
|
6
|
+
{ value: "", label: "All stages" },
|
|
7
|
+
...STAGE_ORDER.map((s) => ({
|
|
8
|
+
value: s,
|
|
9
|
+
label: STAGE_LABELS[s]
|
|
10
|
+
}))
|
|
11
|
+
];
|
|
12
|
+
function AccountStageFilter({
|
|
13
|
+
value,
|
|
14
|
+
onChange
|
|
15
|
+
}) {
|
|
16
|
+
return /* @__PURE__ */ jsx(
|
|
17
|
+
FilterSelect,
|
|
18
|
+
{
|
|
19
|
+
label: "Stage",
|
|
20
|
+
value,
|
|
21
|
+
onChange: (v) => onChange(v),
|
|
22
|
+
options: OPTIONS
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
AccountStageFilter
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=account-stage-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/accounts/account-stage-filter.tsx"],"sourcesContent":["'use client';\n\nimport type { AccountStage } from '@withpotter/lib/admin-api';\nimport { FilterSelect } from '../_ui';\nimport { STAGE_LABELS, STAGE_ORDER } from './account-stage';\n\nconst OPTIONS = [\n { value: '', label: 'All stages' },\n ...STAGE_ORDER.map((s: AccountStage) => ({\n value: s,\n label: STAGE_LABELS[s],\n })),\n];\n\n/** Narrow the accounts list to everyone who stopped at one stage. */\nexport function AccountStageFilter({\n value,\n onChange,\n}: {\n value: AccountStage | '';\n onChange: (value: AccountStage | '') => void;\n}) {\n return (\n <FilterSelect\n label=\"Stage\"\n value={value}\n onChange={(v: string) => onChange(v as AccountStage | '')}\n options={OPTIONS}\n />\n );\n}\n"],"mappings":";AAuBI;AApBJ,SAAS,oBAAoB;AAC7B,SAAS,cAAc,mBAAmB;AAE1C,MAAM,UAAU;AAAA,EACd,EAAE,OAAO,IAAI,OAAO,aAAa;AAAA,EACjC,GAAG,YAAY,IAAI,CAAC,OAAqB;AAAA,IACvC,OAAO;AAAA,IACP,OAAO,aAAa,CAAC;AAAA,EACvB,EAAE;AACJ;AAGO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AACF,GAGG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN;AAAA,MACA,UAAU,CAAC,MAAc,SAAS,CAAsB;AAAA,MACxD,SAAS;AAAA;AAAA,EACX;AAEJ;","names":[]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const STAGE_LABELS = {
|
|
2
|
+
signed_up: "Signed up",
|
|
3
|
+
verified: "Email verified",
|
|
4
|
+
onboarding: "Onboarding",
|
|
5
|
+
kyc_approved: "KYC approved",
|
|
6
|
+
active: "Active"
|
|
7
|
+
};
|
|
8
|
+
const STAGE_ORDER = [
|
|
9
|
+
"signed_up",
|
|
10
|
+
"verified",
|
|
11
|
+
"onboarding",
|
|
12
|
+
"kyc_approved",
|
|
13
|
+
"active"
|
|
14
|
+
];
|
|
15
|
+
const KYC_LABELS = {
|
|
16
|
+
NOT_STARTED: "Not started",
|
|
17
|
+
CHECKING: "Checking",
|
|
18
|
+
WAITING_REVIEW: "In review",
|
|
19
|
+
NEEDS_MORE: "Needs more",
|
|
20
|
+
APPROVED: "Approved",
|
|
21
|
+
DENIED: "Denied"
|
|
22
|
+
};
|
|
23
|
+
const ACTIVE_HINT = "Active means the email is verified, KYC or KYB is approved, and at least one sale was paid.";
|
|
24
|
+
function kycLabel(status) {
|
|
25
|
+
if (!status) return null;
|
|
26
|
+
return KYC_LABELS[status] ?? status;
|
|
27
|
+
}
|
|
28
|
+
function stoppedAt(a) {
|
|
29
|
+
if (a.stage === void 0 && a.emailVerified === void 0) return null;
|
|
30
|
+
if (a.stage === "active") return null;
|
|
31
|
+
if (a.emailVerified === false) return "Verify their email";
|
|
32
|
+
if (a.workspaceCount === 0) return "Add a business";
|
|
33
|
+
if (a.kycStatus !== "APPROVED") {
|
|
34
|
+
return a.kycStatus === "DENIED" ? "Finish KYC or KYB (denied)" : "Finish KYC or KYB";
|
|
35
|
+
}
|
|
36
|
+
if (!a.firstSaleAt) return "Make a first sale";
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
ACTIVE_HINT,
|
|
41
|
+
KYC_LABELS,
|
|
42
|
+
STAGE_LABELS,
|
|
43
|
+
STAGE_ORDER,
|
|
44
|
+
kycLabel,
|
|
45
|
+
stoppedAt
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=account-stage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/accounts/account-stage.ts"],"sourcesContent":["import type {\n AccountKycStatus,\n AccountRow,\n AccountStage,\n} from '@withpotter/lib/admin-api';\n\nexport const STAGE_LABELS: Record<AccountStage, string> = {\n signed_up: 'Signed up',\n verified: 'Email verified',\n onboarding: 'Onboarding',\n kyc_approved: 'KYC approved',\n active: 'Active',\n};\n\nexport const STAGE_ORDER: AccountStage[] = [\n 'signed_up',\n 'verified',\n 'onboarding',\n 'kyc_approved',\n 'active',\n];\n\nexport const KYC_LABELS: Record<AccountKycStatus, string> = {\n NOT_STARTED: 'Not started',\n CHECKING: 'Checking',\n WAITING_REVIEW: 'In review',\n NEEDS_MORE: 'Needs more',\n APPROVED: 'Approved',\n DENIED: 'Denied',\n};\n\n/** What \"active\" means, said once for every place that needs it. */\nexport const ACTIVE_HINT =\n 'Active means the email is verified, KYC or KYB is approved, and at least one sale was paid.';\n\n/** Plain words for a KYC or KYB status; the raw value for unknown ones. */\nexport function kycLabel(status: string | null | undefined): string | null {\n if (!status) return null;\n\n return KYC_LABELS[status as AccountKycStatus] ?? status;\n}\n\ntype Progress = Pick<\n AccountRow,\n 'emailVerified' | 'workspaceCount' | 'kycStatus' | 'firstSaleAt' | 'stage'\n>;\n\n/**\n * The first step this account has not done, from the fields the list sends.\n * Returns null when every step is done (or when nothing is known yet).\n */\nexport function stoppedAt(a: Partial<Progress>): string | null {\n if (a.stage === undefined && a.emailVerified === undefined) return null;\n if (a.stage === 'active') return null;\n if (a.emailVerified === false) return 'Verify their email';\n if (a.workspaceCount === 0) return 'Add a business';\n if (a.kycStatus !== 'APPROVED') {\n return a.kycStatus === 'DENIED'\n ? 'Finish KYC or KYB (denied)'\n : 'Finish KYC or KYB';\n }\n if (!a.firstSaleAt) return 'Make a first sale';\n\n return null;\n}\n"],"mappings":"AAMO,MAAM,eAA6C;AAAA,EACxD,WAAW;AAAA,EACX,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,QAAQ;AACV;AAEO,MAAM,cAA8B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,MAAM,aAA+C;AAAA,EAC1D,aAAa;AAAA,EACb,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AACV;AAGO,MAAM,cACX;AAGK,SAAS,SAAS,QAAkD;AACzE,MAAI,CAAC,OAAQ,QAAO;AAEpB,SAAO,WAAW,MAA0B,KAAK;AACnD;AAWO,SAAS,UAAU,GAAqC;AAC7D,MAAI,EAAE,UAAU,UAAa,EAAE,kBAAkB,OAAW,QAAO;AACnE,MAAI,EAAE,UAAU,SAAU,QAAO;AACjC,MAAI,EAAE,kBAAkB,MAAO,QAAO;AACtC,MAAI,EAAE,mBAAmB,EAAG,QAAO;AACnC,MAAI,EAAE,cAAc,YAAY;AAC9B,WAAO,EAAE,cAAc,WACnB,+BACA;AAAA,EACN;AACA,MAAI,CAAC,EAAE,YAAa,QAAO;AAE3B,SAAO;AACT;","names":[]}
|