@fonderie/vue-admin-screens 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +535 -367
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -3
- package/dist/index.d.ts +68 -3
- package/dist/index.js +523 -356
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/screens/AdminShell.ts
|
|
2
2
|
import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
|
|
3
3
|
import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
|
|
4
|
-
import { computed as
|
|
4
|
+
import { computed as computed3, defineComponent as defineComponent15, h as h17, onBeforeUnmount, onMounted, ref as ref9 } from "vue";
|
|
5
5
|
|
|
6
6
|
// src/styles.ts
|
|
7
7
|
var t = {
|
|
@@ -557,6 +557,12 @@ function empty(title, body, iconName = "check") {
|
|
|
557
557
|
body ? h("div", body) : null
|
|
558
558
|
]);
|
|
559
559
|
}
|
|
560
|
+
function refreshButton(onClick, busy, label = "Refresh") {
|
|
561
|
+
return h("button", { type: "button", style: styles.button, onClick, disabled: busy }, [
|
|
562
|
+
icon("refresh", 14),
|
|
563
|
+
busy ? "Working\u2026" : label
|
|
564
|
+
]);
|
|
565
|
+
}
|
|
560
566
|
function methodChip(method) {
|
|
561
567
|
const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
|
|
562
568
|
const color = toneColor[tone];
|
|
@@ -608,7 +614,7 @@ function table(headers, rows) {
|
|
|
608
614
|
]);
|
|
609
615
|
}
|
|
610
616
|
var td = (children, extra = {}) => h2("td", { style: { ...styles.td, ...extra } }, children);
|
|
611
|
-
var
|
|
617
|
+
var refreshButton2 = (label, disabled, onClick) => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
|
|
612
618
|
icon("refresh", 14),
|
|
613
619
|
disabled ? "Working\u2026" : label
|
|
614
620
|
]);
|
|
@@ -634,7 +640,7 @@ var AdminLogScreen = defineComponent({
|
|
|
634
640
|
pageHeader(
|
|
635
641
|
"Admin log",
|
|
636
642
|
"Every request made through this surface, newest first \u2014 refused ones included.",
|
|
637
|
-
[
|
|
643
|
+
[refreshButton2("Refresh", isLoading.value, () => void refresh())]
|
|
638
644
|
),
|
|
639
645
|
error.value ? h3(
|
|
640
646
|
"p",
|
|
@@ -775,7 +781,7 @@ var AttentionScreen = defineComponent2({
|
|
|
775
781
|
)
|
|
776
782
|
];
|
|
777
783
|
},
|
|
778
|
-
[
|
|
784
|
+
[refreshButton2("Refresh", isLoading.value, () => void refresh())],
|
|
779
785
|
{
|
|
780
786
|
lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
|
|
781
787
|
}
|
|
@@ -955,7 +961,7 @@ var DoctorScreen = defineComponent4({
|
|
|
955
961
|
report.value.ok ? "ok" : "bad",
|
|
956
962
|
report.value.ok ? "all checks pass" : "a check failed"
|
|
957
963
|
) : null,
|
|
958
|
-
|
|
964
|
+
refreshButton2("Run again", isLoading.value, () => void refresh())
|
|
959
965
|
],
|
|
960
966
|
{
|
|
961
967
|
loadingText: "Running the checks\u2026",
|
|
@@ -1158,7 +1164,7 @@ var MigrationsScreen = defineComponent7({
|
|
|
1158
1164
|
...behind.map((m) => section(m, r.everApplied))
|
|
1159
1165
|
].filter(Boolean);
|
|
1160
1166
|
},
|
|
1161
|
-
[
|
|
1167
|
+
[refreshButton2("Refresh", isLoading.value, () => void refresh())],
|
|
1162
1168
|
{
|
|
1163
1169
|
lead: "Schema changes each module ships, and whether this database has them.",
|
|
1164
1170
|
errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
|
|
@@ -1673,28 +1679,233 @@ import {
|
|
|
1673
1679
|
useAdminUserSessions,
|
|
1674
1680
|
useAdminUsers
|
|
1675
1681
|
} from "@fonderie/vue-admin";
|
|
1676
|
-
import { computed, defineComponent as
|
|
1677
|
-
|
|
1682
|
+
import { computed as computed2, defineComponent as defineComponent11, h as h13, ref as ref6 } from "vue";
|
|
1683
|
+
|
|
1684
|
+
// src/billing.ts
|
|
1685
|
+
import { ref as ref4 } from "vue";
|
|
1686
|
+
function periodEnd(s) {
|
|
1687
|
+
if (!s.currentPeriodEnd) return null;
|
|
1688
|
+
const date = new Date(s.currentPeriodEnd).toLocaleDateString();
|
|
1689
|
+
if (s.status === "canceled")
|
|
1690
|
+
return { label: new Date(s.currentPeriodEnd) < /* @__PURE__ */ new Date() ? "Ended" : "Ends", date };
|
|
1691
|
+
if (s.cancelAtPeriodEnd) return { label: "Ends", date };
|
|
1692
|
+
return { label: "Renews", date };
|
|
1693
|
+
}
|
|
1694
|
+
var statusTone = (status) => status === "active" ? "ok" : status === "trialing" ? "info" : status === "past_due" || status === "unpaid" ? "warn" : "neutral";
|
|
1695
|
+
function useSubscriptionIndex(client) {
|
|
1696
|
+
const index = ref4(null);
|
|
1697
|
+
if (!client) return index;
|
|
1698
|
+
void (async () => {
|
|
1699
|
+
const map = /* @__PURE__ */ new Map();
|
|
1700
|
+
let cursor;
|
|
1701
|
+
for (let page2 = 0; page2 < 20; page2++) {
|
|
1702
|
+
const { result } = await client.listSubscriptions({
|
|
1703
|
+
limit: 100,
|
|
1704
|
+
...cursor ? { cursor } : {}
|
|
1705
|
+
});
|
|
1706
|
+
for (const s of result.subscriptions) {
|
|
1707
|
+
const key = `${s.subscriberType}/${s.subscriberId}`;
|
|
1708
|
+
if (!map.has(key)) map.set(key, s);
|
|
1709
|
+
}
|
|
1710
|
+
if (!result.nextCursor) break;
|
|
1711
|
+
cursor = result.nextCursor;
|
|
1712
|
+
}
|
|
1713
|
+
index.value = map;
|
|
1714
|
+
})().catch(() => {
|
|
1715
|
+
index.value = /* @__PURE__ */ new Map();
|
|
1716
|
+
});
|
|
1717
|
+
return index;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/screens/SubscriberBilling.ts
|
|
1721
|
+
import { useAdminSubscriber } from "@fonderie/vue-admin";
|
|
1722
|
+
import { computed, defineComponent as defineComponent10, h as h12, ref as ref5 } from "vue";
|
|
1723
|
+
var SubscriberBilling = defineComponent10({
|
|
1724
|
+
name: "FonderieSubscriberBilling",
|
|
1725
|
+
props: {
|
|
1726
|
+
client: { type: Object, required: true },
|
|
1727
|
+
subscriberType: { type: String, required: true },
|
|
1728
|
+
subscriberId: { type: String, required: true }
|
|
1729
|
+
},
|
|
1730
|
+
setup(props) {
|
|
1731
|
+
const subscriber = computed(() => ({ type: props.subscriberType, id: props.subscriberId }));
|
|
1732
|
+
const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
|
|
1733
|
+
const amount = ref5("");
|
|
1734
|
+
const note = ref5("");
|
|
1735
|
+
const granted = ref5(null);
|
|
1736
|
+
return () => {
|
|
1737
|
+
const s = subscription.value;
|
|
1738
|
+
const w = wallet.value;
|
|
1739
|
+
if (isLoading.value && !w && !s) return h12("p", { style: styles.status }, "Loading billing\u2026");
|
|
1740
|
+
const end = s ? periodEnd(s) : null;
|
|
1741
|
+
return h12("div", [
|
|
1742
|
+
error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
|
|
1743
|
+
h12("div", { style: { ...styles.grid, marginBottom: "16px" } }, [
|
|
1744
|
+
h12("div", { style: styles.card }, [
|
|
1745
|
+
h12("div", { style: styles.statLabel }, "Plan"),
|
|
1746
|
+
h12(
|
|
1747
|
+
"div",
|
|
1748
|
+
{
|
|
1749
|
+
style: {
|
|
1750
|
+
display: "flex",
|
|
1751
|
+
alignItems: "center",
|
|
1752
|
+
gap: "8px",
|
|
1753
|
+
marginTop: "6px",
|
|
1754
|
+
flexWrap: "wrap"
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
[
|
|
1758
|
+
h12("strong", { style: { fontSize: "18px" } }, s ? s.plan : "Free"),
|
|
1759
|
+
...s ? [
|
|
1760
|
+
h12("span", { style: styles.muted }, `\xB7 ${s.interval}`),
|
|
1761
|
+
pill(statusTone(s.status), s.status)
|
|
1762
|
+
] : [h12("span", { style: styles.muted }, "no subscription")]
|
|
1763
|
+
]
|
|
1764
|
+
),
|
|
1765
|
+
end ? h12("div", { style: styles.statHint }, `${end.label} ${end.date}`) : null,
|
|
1766
|
+
s?.providerSubscriptionId ? h12(
|
|
1767
|
+
"div",
|
|
1768
|
+
{ style: { ...styles.statHint, fontFamily: "var(--fonderie-mono,monospace)" } },
|
|
1769
|
+
s.providerSubscriptionId
|
|
1770
|
+
) : null
|
|
1771
|
+
]),
|
|
1772
|
+
h12("div", { style: styles.card }, [
|
|
1773
|
+
h12("div", { style: styles.statLabel }, "Credits"),
|
|
1774
|
+
...w ? [
|
|
1775
|
+
h12("div", { style: styles.statValue }, [
|
|
1776
|
+
`${w.balance} `,
|
|
1777
|
+
h12("span", { style: { fontSize: "14px", fontWeight: 500 } }, w.currency)
|
|
1778
|
+
]),
|
|
1779
|
+
h12(
|
|
1780
|
+
"div",
|
|
1781
|
+
{ style: styles.statHint },
|
|
1782
|
+
`${w.purchased ?? "0"} permanent \xB7 ${w.granted ?? "0"} plan allowance${w.grantedExpiresAt ? ` (expires ${new Date(w.grantedExpiresAt).toLocaleDateString()})` : ""}`
|
|
1783
|
+
)
|
|
1784
|
+
] : [
|
|
1785
|
+
h12(
|
|
1786
|
+
"div",
|
|
1787
|
+
{ style: styles.statHint },
|
|
1788
|
+
"No wallet \u2014 billing has no wallet configuration."
|
|
1789
|
+
)
|
|
1790
|
+
]
|
|
1791
|
+
])
|
|
1792
|
+
]),
|
|
1793
|
+
w ? h12(
|
|
1794
|
+
"form",
|
|
1795
|
+
{
|
|
1796
|
+
style: {
|
|
1797
|
+
...styles.card,
|
|
1798
|
+
display: "flex",
|
|
1799
|
+
gap: "8px",
|
|
1800
|
+
alignItems: "center",
|
|
1801
|
+
flexWrap: "wrap",
|
|
1802
|
+
marginBottom: "16px"
|
|
1803
|
+
},
|
|
1804
|
+
onSubmit: (e) => {
|
|
1805
|
+
e.preventDefault();
|
|
1806
|
+
const key = `admin-grant-${props.subscriberType}-${props.subscriberId}-${Date.now()}`;
|
|
1807
|
+
const a = amount.value.trim();
|
|
1808
|
+
void grant({
|
|
1809
|
+
amount: a,
|
|
1810
|
+
...note.value ? { description: note.value } : {},
|
|
1811
|
+
idempotencyKey: key
|
|
1812
|
+
}).then(() => {
|
|
1813
|
+
granted.value = `Granted ${a} ${w.currency}.`;
|
|
1814
|
+
amount.value = "";
|
|
1815
|
+
note.value = "";
|
|
1816
|
+
}).catch(() => {
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
},
|
|
1820
|
+
[
|
|
1821
|
+
h12("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Grant credits"),
|
|
1822
|
+
h12("input", {
|
|
1823
|
+
value: amount.value,
|
|
1824
|
+
onInput: (e) => amount.value = e.target.value,
|
|
1825
|
+
placeholder: `amount (${w.currency})`,
|
|
1826
|
+
inputmode: "numeric",
|
|
1827
|
+
style: { ...styles.input, width: "150px" },
|
|
1828
|
+
"aria-label": "Amount"
|
|
1829
|
+
}),
|
|
1830
|
+
h12("input", {
|
|
1831
|
+
value: note.value,
|
|
1832
|
+
onInput: (e) => note.value = e.target.value,
|
|
1833
|
+
placeholder: "reason (optional)",
|
|
1834
|
+
style: { ...styles.input, flex: 1, minWidth: "180px" },
|
|
1835
|
+
"aria-label": "Reason"
|
|
1836
|
+
}),
|
|
1837
|
+
h12(
|
|
1838
|
+
"button",
|
|
1839
|
+
{
|
|
1840
|
+
type: "submit",
|
|
1841
|
+
style: styles.buttonPrimary,
|
|
1842
|
+
disabled: !/^\d+$/.test(amount.value.trim())
|
|
1843
|
+
},
|
|
1844
|
+
[icon("plus", 14), "Grant"]
|
|
1845
|
+
),
|
|
1846
|
+
granted.value ? h12("span", { style: styles.ok }, granted.value) : null
|
|
1847
|
+
]
|
|
1848
|
+
) : null,
|
|
1849
|
+
w ? ledger.value.length === 0 ? empty("No credit movements yet", void 0, "subscriber") : h12("div", [
|
|
1850
|
+
table(
|
|
1851
|
+
["When", "Kind", "Amount", "Note"],
|
|
1852
|
+
ledger.value.map(
|
|
1853
|
+
(t2) => h12("tr", { key: t2.id }, [
|
|
1854
|
+
td(new Date(t2.createdAt).toLocaleString(), {
|
|
1855
|
+
...styles.muted,
|
|
1856
|
+
whiteSpace: "nowrap"
|
|
1857
|
+
}),
|
|
1858
|
+
td(t2.type),
|
|
1859
|
+
td(t2.amount, styles.mono),
|
|
1860
|
+
td(t2.description ?? "", styles.muted)
|
|
1861
|
+
])
|
|
1862
|
+
)
|
|
1863
|
+
),
|
|
1864
|
+
hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger()) : null
|
|
1865
|
+
]) : null
|
|
1866
|
+
]);
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
// src/screens/UsersScreen.ts
|
|
1872
|
+
var UsersScreen = defineComponent11({
|
|
1678
1873
|
name: "FonderieUsersScreen",
|
|
1679
1874
|
props: {
|
|
1680
1875
|
client: { type: Object, required: true },
|
|
1681
|
-
pageSize: { type: Number, default: 50 }
|
|
1876
|
+
pageSize: { type: Number, default: 50 },
|
|
1877
|
+
// Given ⇒ a Plan column, and each user's plan, credits and grant form on
|
|
1878
|
+
// their page. Everyone has a wallet; no subscription means the free tier.
|
|
1879
|
+
billingClient: { type: Object, default: void 0 },
|
|
1880
|
+
// Open this user on arrival (e.g. from the Subscriptions list).
|
|
1881
|
+
openUserId: { type: String, default: void 0 }
|
|
1682
1882
|
},
|
|
1683
1883
|
setup(props) {
|
|
1684
|
-
const input =
|
|
1685
|
-
const email =
|
|
1686
|
-
const selectedId =
|
|
1884
|
+
const input = ref6("");
|
|
1885
|
+
const email = ref6("");
|
|
1886
|
+
const selectedId = ref6(props.openUserId ?? "");
|
|
1887
|
+
const plans = useSubscriptionIndex(props.billingClient);
|
|
1687
1888
|
const list = useAdminUsers(props.client, { limit: props.pageSize });
|
|
1688
1889
|
const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
|
|
1689
1890
|
props.client,
|
|
1690
1891
|
{ email, id: selectedId }
|
|
1691
1892
|
);
|
|
1692
|
-
const userId =
|
|
1893
|
+
const userId = computed2(() => user.value?.id ?? null);
|
|
1693
1894
|
const sessions = useAdminUserSessions(props.client, userId);
|
|
1694
1895
|
const history = useAdminLoginHistory(props.client, userId, { limit: 20 });
|
|
1695
|
-
const showList =
|
|
1896
|
+
const showList = computed2(() => !email.value && !selectedId.value);
|
|
1696
1897
|
const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
|
|
1697
|
-
const row = (k, v) =>
|
|
1898
|
+
const row = (k, v) => h13("tr", [td(k), td(v)]);
|
|
1899
|
+
const planCell = (id) => {
|
|
1900
|
+
if (!plans.value) return h13("span", { style: styles.muted }, "\u2026");
|
|
1901
|
+
const sub = plans.value.get(`user/${id}`);
|
|
1902
|
+
if (!sub || sub.status === "canceled") return h13("span", { style: styles.muted }, "free");
|
|
1903
|
+
return [
|
|
1904
|
+
h13("strong", sub.plan),
|
|
1905
|
+
" ",
|
|
1906
|
+
sub.status !== "active" ? pill(statusTone(sub.status), sub.status) : null
|
|
1907
|
+
];
|
|
1908
|
+
};
|
|
1698
1909
|
const clear = () => {
|
|
1699
1910
|
email.value = "";
|
|
1700
1911
|
selectedId.value = "";
|
|
@@ -1702,12 +1913,12 @@ var UsersScreen = defineComponent10({
|
|
|
1702
1913
|
};
|
|
1703
1914
|
return () => {
|
|
1704
1915
|
const u = user.value;
|
|
1705
|
-
return
|
|
1916
|
+
return h13("div", { style: styles.container }, [
|
|
1706
1917
|
pageHeader(
|
|
1707
1918
|
"Users",
|
|
1708
1919
|
"Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
|
|
1709
1920
|
),
|
|
1710
|
-
|
|
1921
|
+
h13(
|
|
1711
1922
|
"form",
|
|
1712
1923
|
{
|
|
1713
1924
|
style: styles.toolbar,
|
|
@@ -1718,7 +1929,7 @@ var UsersScreen = defineComponent10({
|
|
|
1718
1929
|
}
|
|
1719
1930
|
},
|
|
1720
1931
|
[
|
|
1721
|
-
|
|
1932
|
+
h13("input", {
|
|
1722
1933
|
type: "email",
|
|
1723
1934
|
value: input.value,
|
|
1724
1935
|
onInput: (e) => input.value = e.target.value,
|
|
@@ -1726,22 +1937,22 @@ var UsersScreen = defineComponent10({
|
|
|
1726
1937
|
style: { ...styles.input, minWidth: "280px" },
|
|
1727
1938
|
"aria-label": "Email"
|
|
1728
1939
|
}),
|
|
1729
|
-
|
|
1940
|
+
h13(
|
|
1730
1941
|
"button",
|
|
1731
1942
|
{ type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
|
|
1732
1943
|
[icon("search", 14), "Look up"]
|
|
1733
1944
|
),
|
|
1734
|
-
showList.value ?
|
|
1945
|
+
showList.value ? refreshButton2("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
|
|
1735
1946
|
]
|
|
1736
1947
|
),
|
|
1737
|
-
showList.value ?
|
|
1738
|
-
list.error.value ?
|
|
1948
|
+
showList.value ? h13("div", [
|
|
1949
|
+
list.error.value ? h13("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
|
|
1739
1950
|
list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
|
|
1740
|
-
["Email", "Name", "Created", "Status"],
|
|
1951
|
+
props.billingClient ? ["Email", "Name", "Plan", "Created", "Status"] : ["Email", "Name", "Created", "Status"],
|
|
1741
1952
|
list.users.value.map(
|
|
1742
|
-
(u2) =>
|
|
1953
|
+
(u2) => h13("tr", { key: u2.id }, [
|
|
1743
1954
|
td(
|
|
1744
|
-
|
|
1955
|
+
h13(
|
|
1745
1956
|
"button",
|
|
1746
1957
|
{
|
|
1747
1958
|
type: "button",
|
|
@@ -1752,8 +1963,9 @@ var UsersScreen = defineComponent10({
|
|
|
1752
1963
|
)
|
|
1753
1964
|
),
|
|
1754
1965
|
td(
|
|
1755
|
-
`${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() ||
|
|
1966
|
+
`${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h13("span", { style: styles.muted }, "\u2014")
|
|
1756
1967
|
),
|
|
1968
|
+
props.billingClient ? td(planCell(u2.id)) : null,
|
|
1757
1969
|
td(new Date(u2.createdAt).toLocaleDateString()),
|
|
1758
1970
|
td(
|
|
1759
1971
|
u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
|
|
@@ -1761,24 +1973,42 @@ var UsersScreen = defineComponent10({
|
|
|
1761
1973
|
])
|
|
1762
1974
|
)
|
|
1763
1975
|
),
|
|
1764
|
-
list.isLoading.value ?
|
|
1976
|
+
list.isLoading.value ? h13("p", { style: styles.status }, "Loading\u2026") : null,
|
|
1765
1977
|
list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
|
|
1766
1978
|
]) : null,
|
|
1767
|
-
error.value ?
|
|
1979
|
+
error.value && !(error.value.status === 404 && selectedId.value) ? h13(
|
|
1768
1980
|
"p",
|
|
1769
1981
|
{ style: styles.error, role: "alert" },
|
|
1770
1982
|
error.value.status === 404 ? "No user with that email." : error.value.explanation
|
|
1771
1983
|
) : null,
|
|
1984
|
+
// Opened by id (e.g. from Subscriptions) and the account is gone —
|
|
1985
|
+
// deleted or purged — while its billing rows remain. Say so, and
|
|
1986
|
+
// still show the money.
|
|
1987
|
+
error.value?.status === 404 && selectedId.value ? [
|
|
1988
|
+
h13("div", { style: styles.notice, role: "status" }, [
|
|
1989
|
+
h13("strong", "No account with id "),
|
|
1990
|
+
h13("code", { style: styles.code }, selectedId.value),
|
|
1991
|
+
h13("strong", "."),
|
|
1992
|
+
" It was deleted, or never existed here. Its billing records remain."
|
|
1993
|
+
]),
|
|
1994
|
+
props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
|
|
1995
|
+
props.billingClient ? h13(SubscriberBilling, {
|
|
1996
|
+
key: `missing-${selectedId.value}`,
|
|
1997
|
+
client: props.billingClient,
|
|
1998
|
+
subscriberType: "user",
|
|
1999
|
+
subscriberId: selectedId.value
|
|
2000
|
+
}) : null
|
|
2001
|
+
] : null,
|
|
1772
2002
|
u ? [
|
|
1773
|
-
|
|
2003
|
+
h13("h2", { style: styles.subtitle }, [
|
|
1774
2004
|
u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
|
|
1775
2005
|
" ",
|
|
1776
2006
|
u.suspended ? pill("warn", "suspended") : null,
|
|
1777
2007
|
u.deletedAt ? pill("neutral", "deleted") : null
|
|
1778
2008
|
]),
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
row("id",
|
|
2009
|
+
h13("table", { style: styles.table }, [
|
|
2010
|
+
h13("tbody", [
|
|
2011
|
+
row("id", h13("span", { style: styles.mono }, u.id)),
|
|
1782
2012
|
row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
|
|
1783
2013
|
row("MFA", yesNo(u.mfaEnabled)),
|
|
1784
2014
|
row(
|
|
@@ -1787,12 +2017,12 @@ var UsersScreen = defineComponent10({
|
|
|
1787
2017
|
),
|
|
1788
2018
|
row(
|
|
1789
2019
|
"last login",
|
|
1790
|
-
u.lastLogin ? new Date(u.lastLogin).toLocaleString() :
|
|
2020
|
+
u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h13("span", { style: styles.muted }, "never")
|
|
1791
2021
|
),
|
|
1792
2022
|
row("created", new Date(u.createdAt).toLocaleString())
|
|
1793
2023
|
])
|
|
1794
2024
|
]),
|
|
1795
|
-
|
|
2025
|
+
h13("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
|
|
1796
2026
|
u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
|
|
1797
2027
|
"Suspend",
|
|
1798
2028
|
isLoading.value,
|
|
@@ -1805,19 +2035,26 @@ var UsersScreen = defineComponent10({
|
|
|
1805
2035
|
() => void revokeSessions().then(() => sessions.refresh())
|
|
1806
2036
|
)
|
|
1807
2037
|
]),
|
|
1808
|
-
|
|
1809
|
-
|
|
2038
|
+
props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
|
|
2039
|
+
props.billingClient ? h13(SubscriberBilling, {
|
|
2040
|
+
key: u.id,
|
|
2041
|
+
client: props.billingClient,
|
|
2042
|
+
subscriberType: "user",
|
|
2043
|
+
subscriberId: u.id
|
|
2044
|
+
}) : null,
|
|
2045
|
+
h13("h2", { style: styles.subtitle }, "Live sessions"),
|
|
2046
|
+
sessions.sessions.value.length === 0 ? h13("p", { style: styles.muted }, "None.") : h13(
|
|
1810
2047
|
"ul",
|
|
1811
2048
|
{ style: styles.list },
|
|
1812
2049
|
sessions.sessions.value.map(
|
|
1813
|
-
(s) =>
|
|
1814
|
-
|
|
2050
|
+
(s) => h13("li", { key: s.id, style: styles.row }, [
|
|
2051
|
+
h13("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
|
|
1815
2052
|
" ",
|
|
1816
|
-
|
|
2053
|
+
h13("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
|
|
1817
2054
|
" ",
|
|
1818
|
-
|
|
2055
|
+
h13("span", { style: styles.muted }, s.userAgent ?? ""),
|
|
1819
2056
|
" ",
|
|
1820
|
-
|
|
2057
|
+
h13(
|
|
1821
2058
|
"span",
|
|
1822
2059
|
{ style: styles.muted },
|
|
1823
2060
|
`since ${new Date(s.createdAt).toLocaleString()}`
|
|
@@ -1825,22 +2062,22 @@ var UsersScreen = defineComponent10({
|
|
|
1825
2062
|
])
|
|
1826
2063
|
)
|
|
1827
2064
|
),
|
|
1828
|
-
|
|
1829
|
-
history.events.value.length === 0 && !history.isLoading.value ?
|
|
2065
|
+
h13("h2", { style: styles.subtitle }, "Recent sign-ins"),
|
|
2066
|
+
history.events.value.length === 0 && !history.isLoading.value ? h13("p", { style: styles.muted }, "None recorded.") : h13(
|
|
1830
2067
|
"ul",
|
|
1831
2068
|
{ style: styles.list },
|
|
1832
2069
|
history.events.value.map(
|
|
1833
|
-
(e) =>
|
|
2070
|
+
(e) => h13("li", { key: e.id, style: styles.row }, [
|
|
1834
2071
|
pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
|
|
1835
2072
|
" ",
|
|
1836
|
-
|
|
2073
|
+
h13("span", { style: styles.muted }, e.method),
|
|
1837
2074
|
" ",
|
|
1838
|
-
|
|
2075
|
+
h13("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
|
|
1839
2076
|
" ",
|
|
1840
|
-
|
|
2077
|
+
h13("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
|
|
1841
2078
|
e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
|
|
1842
2079
|
" ",
|
|
1843
|
-
|
|
2080
|
+
h13("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
|
|
1844
2081
|
])
|
|
1845
2082
|
)
|
|
1846
2083
|
),
|
|
@@ -1853,8 +2090,8 @@ var UsersScreen = defineComponent10({
|
|
|
1853
2090
|
|
|
1854
2091
|
// src/screens/CatalogScreen.ts
|
|
1855
2092
|
import { useAdminCatalog } from "@fonderie/vue-admin";
|
|
1856
|
-
import { defineComponent as
|
|
1857
|
-
var CatalogScreen =
|
|
2093
|
+
import { defineComponent as defineComponent12, h as h14 } from "vue";
|
|
2094
|
+
var CatalogScreen = defineComponent12({
|
|
1858
2095
|
name: "FonderieCatalogScreen",
|
|
1859
2096
|
props: { client: { type: Object, required: true } },
|
|
1860
2097
|
setup(props) {
|
|
@@ -1862,17 +2099,17 @@ var CatalogScreen = defineComponent11({
|
|
|
1862
2099
|
const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
|
|
1863
2100
|
return () => {
|
|
1864
2101
|
const c = catalog.value;
|
|
1865
|
-
return
|
|
2102
|
+
return h14("div", { style: styles.container }, [
|
|
1866
2103
|
pageHeader(
|
|
1867
2104
|
"Catalog",
|
|
1868
2105
|
"What you sell: plans as configured in code, and as stored in the database, side by side.",
|
|
1869
|
-
[
|
|
2106
|
+
[refreshButton2("Refresh", isLoading.value, () => void refresh())]
|
|
1870
2107
|
),
|
|
1871
|
-
error.value ?
|
|
1872
|
-
isLoading.value && !c ?
|
|
2108
|
+
error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
|
|
2109
|
+
isLoading.value && !c ? h14("p", { style: styles.status }, "Loading\u2026") : null,
|
|
1873
2110
|
c ? [
|
|
1874
|
-
|
|
1875
|
-
|
|
2111
|
+
h14("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
|
|
2112
|
+
h14(
|
|
1876
2113
|
"pre",
|
|
1877
2114
|
{
|
|
1878
2115
|
style: {
|
|
@@ -1886,7 +2123,7 @@ var CatalogScreen = defineComponent11({
|
|
|
1886
2123
|
},
|
|
1887
2124
|
JSON.stringify(c.configured, null, 2)
|
|
1888
2125
|
),
|
|
1889
|
-
|
|
2126
|
+
h14("h2", { style: styles.subtitle }, "Stored (database)"),
|
|
1890
2127
|
c.stored.length === 0 ? empty(
|
|
1891
2128
|
"No stored plans",
|
|
1892
2129
|
"Plans are configured in code; nothing has been written to the database.",
|
|
@@ -1894,10 +2131,10 @@ var CatalogScreen = defineComponent11({
|
|
|
1894
2131
|
) : table(
|
|
1895
2132
|
["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
|
|
1896
2133
|
c.stored.map(
|
|
1897
|
-
(p) =>
|
|
2134
|
+
(p) => h14("tr", { key: p.id }, [
|
|
1898
2135
|
td([
|
|
1899
|
-
|
|
1900
|
-
p.description ?
|
|
2136
|
+
h14("strong", p.name),
|
|
2137
|
+
p.description ? h14("div", { style: styles.muted }, p.description) : ""
|
|
1901
2138
|
]),
|
|
1902
2139
|
td(String(p.tier)),
|
|
1903
2140
|
td(p.seats == null ? "\u221E" : String(p.seats)),
|
|
@@ -1905,7 +2142,7 @@ var CatalogScreen = defineComponent11({
|
|
|
1905
2142
|
td(money(p.pricing?.yearly)),
|
|
1906
2143
|
td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
|
|
1907
2144
|
td(
|
|
1908
|
-
|
|
2145
|
+
h14(
|
|
1909
2146
|
"button",
|
|
1910
2147
|
{
|
|
1911
2148
|
type: "button",
|
|
@@ -1929,259 +2166,171 @@ var CatalogScreen = defineComponent11({
|
|
|
1929
2166
|
});
|
|
1930
2167
|
|
|
1931
2168
|
// src/screens/SubscriberScreen.ts
|
|
1932
|
-
import {
|
|
1933
|
-
import { defineComponent as
|
|
1934
|
-
var
|
|
2169
|
+
import { useAdminSubscribers } from "@fonderie/vue-admin";
|
|
2170
|
+
import { defineComponent as defineComponent13, h as h15, ref as ref7 } from "vue";
|
|
2171
|
+
var FILTERS = [
|
|
2172
|
+
{ label: "All", match: () => true },
|
|
2173
|
+
{ label: "Active", match: (s) => s === "active" },
|
|
2174
|
+
{ label: "Trialing", match: (s) => s === "trialing" },
|
|
2175
|
+
{ label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
|
|
2176
|
+
{ label: "Canceled", match: (s) => s === "canceled" }
|
|
2177
|
+
];
|
|
2178
|
+
var SubscriberScreen = defineComponent13({
|
|
1935
2179
|
name: "FonderieSubscriberScreen",
|
|
1936
2180
|
props: {
|
|
1937
2181
|
client: { type: Object, required: true },
|
|
1938
|
-
pageSize: { type: Number, default: 50 }
|
|
2182
|
+
pageSize: { type: Number, default: 50 },
|
|
2183
|
+
/**
|
|
2184
|
+
* A user subscriber opens on the Users page, where their plan and credits
|
|
2185
|
+
* live beside their account. Omit to open everything here.
|
|
2186
|
+
*/
|
|
2187
|
+
onOpenUser: { type: Function, default: void 0 }
|
|
1939
2188
|
},
|
|
1940
2189
|
setup(props) {
|
|
1941
|
-
const type = ref5("user");
|
|
1942
|
-
const idInput = ref5("");
|
|
1943
|
-
const subscriber = ref5(null);
|
|
1944
2190
|
const list = useAdminSubscribers(props.client, { limit: props.pageSize });
|
|
1945
|
-
const
|
|
1946
|
-
const open = (
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2191
|
+
const filter = ref7("All");
|
|
2192
|
+
const open = ref7(null);
|
|
2193
|
+
const type = ref7("workspace");
|
|
2194
|
+
const idInput = ref7("");
|
|
2195
|
+
const openRow = (t2, id) => {
|
|
2196
|
+
if (t2 === "user" && props.onOpenUser) props.onOpenUser(id);
|
|
2197
|
+
else open.value = { type: t2, id };
|
|
1951
2198
|
};
|
|
1952
|
-
const amount = ref5("");
|
|
1953
|
-
const note = ref5("");
|
|
1954
|
-
const granted = ref5(null);
|
|
1955
|
-
const input = (model, placeholder, extra = {}) => h14("input", {
|
|
1956
|
-
value: model.value,
|
|
1957
|
-
onInput: (e) => model.value = e.target.value,
|
|
1958
|
-
placeholder,
|
|
1959
|
-
style: { ...styles.input, ...extra },
|
|
1960
|
-
"aria-label": placeholder
|
|
1961
|
-
});
|
|
1962
2199
|
return () => {
|
|
1963
|
-
const
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
"Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
|
|
1969
|
-
),
|
|
1970
|
-
h14(
|
|
1971
|
-
"form",
|
|
1972
|
-
{
|
|
1973
|
-
style: styles.toolbar,
|
|
1974
|
-
onSubmit: (e) => {
|
|
1975
|
-
e.preventDefault();
|
|
1976
|
-
granted.value = null;
|
|
1977
|
-
subscriber.value = idInput.value.trim() ? { type: type.value, id: idInput.value.trim() } : null;
|
|
1978
|
-
}
|
|
1979
|
-
},
|
|
1980
|
-
[
|
|
1981
|
-
h14(
|
|
1982
|
-
"select",
|
|
1983
|
-
{
|
|
1984
|
-
value: type.value,
|
|
1985
|
-
onChange: (e) => type.value = e.target.value,
|
|
1986
|
-
style: styles.input,
|
|
1987
|
-
"aria-label": "Type"
|
|
1988
|
-
},
|
|
1989
|
-
[
|
|
1990
|
-
h14("option", { value: "user" }, "user"),
|
|
1991
|
-
h14("option", { value: "workspace" }, "workspace")
|
|
1992
|
-
]
|
|
1993
|
-
),
|
|
1994
|
-
input(idInput, "subscriber id", { minWidth: "280px" }),
|
|
1995
|
-
h14(
|
|
1996
|
-
"button",
|
|
1997
|
-
{ type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
|
|
1998
|
-
[icon("search", 14), "Look up"]
|
|
1999
|
-
),
|
|
2000
|
-
subscriber.value ? h14(
|
|
2001
|
-
"button",
|
|
2002
|
-
{
|
|
2003
|
-
type: "button",
|
|
2004
|
-
style: styles.button,
|
|
2005
|
-
onClick: () => {
|
|
2006
|
-
subscriber.value = null;
|
|
2007
|
-
idInput.value = "";
|
|
2008
|
-
granted.value = null;
|
|
2009
|
-
}
|
|
2010
|
-
},
|
|
2011
|
-
[icon("back", 14), "All subscribers"]
|
|
2012
|
-
) : h14(
|
|
2200
|
+
const o = open.value;
|
|
2201
|
+
if (o) {
|
|
2202
|
+
return h15("div", { style: styles.container }, [
|
|
2203
|
+
pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
|
|
2204
|
+
h15(
|
|
2013
2205
|
"button",
|
|
2014
|
-
{
|
|
2015
|
-
|
|
2016
|
-
style: styles.button,
|
|
2017
|
-
disabled: list.isLoading.value,
|
|
2018
|
-
onClick: () => void list.refresh()
|
|
2019
|
-
},
|
|
2020
|
-
"Refresh"
|
|
2206
|
+
{ type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
|
|
2207
|
+
[icon("back", 14), "All subscriptions"]
|
|
2021
2208
|
)
|
|
2022
|
-
]
|
|
2209
|
+
]),
|
|
2210
|
+
h15(SubscriberBilling, {
|
|
2211
|
+
key: `${o.type}/${o.id}`,
|
|
2212
|
+
client: props.client,
|
|
2213
|
+
subscriberType: o.type,
|
|
2214
|
+
subscriberId: o.id
|
|
2215
|
+
})
|
|
2216
|
+
]);
|
|
2217
|
+
}
|
|
2218
|
+
const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
|
|
2219
|
+
const rows = list.subscriptions.value.filter((s) => match(s.status));
|
|
2220
|
+
return h15("div", { style: styles.container }, [
|
|
2221
|
+
pageHeader(
|
|
2222
|
+
"Subscriptions",
|
|
2223
|
+
"Who pays, and who is behind. A user's plan and credits are also on their page under Users \u2014 free users appear only there.",
|
|
2224
|
+
[refreshButton(() => void list.refresh(), list.isLoading.value)]
|
|
2023
2225
|
),
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : h14("span", { style: styles.muted }, "\u2014")
|
|
2055
|
-
)
|
|
2056
|
-
])
|
|
2057
|
-
)
|
|
2226
|
+
h15("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
|
|
2227
|
+
h15(
|
|
2228
|
+
"fieldset",
|
|
2229
|
+
{
|
|
2230
|
+
style: {
|
|
2231
|
+
display: "flex",
|
|
2232
|
+
gap: "6px",
|
|
2233
|
+
flexWrap: "wrap",
|
|
2234
|
+
border: "none",
|
|
2235
|
+
margin: 0,
|
|
2236
|
+
padding: 0,
|
|
2237
|
+
minWidth: 0
|
|
2238
|
+
},
|
|
2239
|
+
"aria-label": "Filter by status"
|
|
2240
|
+
},
|
|
2241
|
+
FILTERS.map((f) => {
|
|
2242
|
+
const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
|
|
2243
|
+
const on = filter.value === f.label;
|
|
2244
|
+
return h15(
|
|
2245
|
+
"button",
|
|
2246
|
+
{
|
|
2247
|
+
key: f.label,
|
|
2248
|
+
type: "button",
|
|
2249
|
+
"aria-pressed": on,
|
|
2250
|
+
onClick: () => filter.value = f.label,
|
|
2251
|
+
style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
|
|
2252
|
+
},
|
|
2253
|
+
[f.label, h15("span", { style: { opacity: 0.7 } }, String(n))]
|
|
2254
|
+
);
|
|
2255
|
+
})
|
|
2058
2256
|
),
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
"button",
|
|
2257
|
+
h15(
|
|
2258
|
+
"form",
|
|
2062
2259
|
{
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2260
|
+
style: { display: "flex", gap: "6px" },
|
|
2261
|
+
onSubmit: (e) => {
|
|
2262
|
+
e.preventDefault();
|
|
2263
|
+
if (idInput.value.trim()) openRow(type.value, idInput.value.trim());
|
|
2264
|
+
}
|
|
2066
2265
|
},
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
|
|
2109
|
-
}
|
|
2110
|
-
},
|
|
2111
|
-
w.balance
|
|
2112
|
-
),
|
|
2113
|
-
` ${w.currency} `,
|
|
2114
|
-
h14(
|
|
2115
|
-
"span",
|
|
2116
|
-
{ style: styles.muted },
|
|
2117
|
-
`(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
|
|
2118
|
-
)
|
|
2119
|
-
]
|
|
2120
|
-
),
|
|
2121
|
-
h14(
|
|
2122
|
-
"form",
|
|
2123
|
-
{
|
|
2124
|
-
style: styles.toolbar,
|
|
2125
|
-
onSubmit: (e) => {
|
|
2126
|
-
e.preventDefault();
|
|
2127
|
-
const sub = subscriber.value;
|
|
2128
|
-
if (!sub) return;
|
|
2129
|
-
const key = `admin-grant-${sub.type}-${sub.id}-${Date.now()}`;
|
|
2130
|
-
const amt = amount.value.trim();
|
|
2131
|
-
void grant({
|
|
2132
|
-
amount: amt,
|
|
2133
|
-
...note.value ? { description: note.value } : {},
|
|
2134
|
-
idempotencyKey: key
|
|
2135
|
-
}).then(() => {
|
|
2136
|
-
granted.value = `Granted ${amt} ${w.currency}.`;
|
|
2137
|
-
amount.value = "";
|
|
2138
|
-
note.value = "";
|
|
2139
|
-
}).catch(() => {
|
|
2140
|
-
});
|
|
2141
|
-
}
|
|
2142
|
-
},
|
|
2143
|
-
[
|
|
2144
|
-
input(amount, "amount (minor units)"),
|
|
2145
|
-
input(note, "reason (optional)", { minWidth: "220px" }),
|
|
2146
|
-
h14(
|
|
2266
|
+
[
|
|
2267
|
+
h15(
|
|
2268
|
+
"select",
|
|
2269
|
+
{
|
|
2270
|
+
value: type.value,
|
|
2271
|
+
onChange: (e) => type.value = e.target.value,
|
|
2272
|
+
style: styles.input,
|
|
2273
|
+
"aria-label": "Type"
|
|
2274
|
+
},
|
|
2275
|
+
[
|
|
2276
|
+
h15("option", { value: "workspace" }, "workspace"),
|
|
2277
|
+
h15("option", { value: "user" }, "user")
|
|
2278
|
+
]
|
|
2279
|
+
),
|
|
2280
|
+
h15("input", {
|
|
2281
|
+
value: idInput.value,
|
|
2282
|
+
onInput: (e) => idInput.value = e.target.value,
|
|
2283
|
+
placeholder: "subscriber id",
|
|
2284
|
+
style: { ...styles.input, width: "220px" },
|
|
2285
|
+
"aria-label": "Subscriber id"
|
|
2286
|
+
}),
|
|
2287
|
+
h15(
|
|
2288
|
+
"button",
|
|
2289
|
+
{ type: "submit", style: styles.button, disabled: !idInput.value.trim() },
|
|
2290
|
+
[icon("search", 14), "Open"]
|
|
2291
|
+
)
|
|
2292
|
+
]
|
|
2293
|
+
)
|
|
2294
|
+
]),
|
|
2295
|
+
list.error.value ? h15("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
|
|
2296
|
+
rows.length === 0 && !list.isLoading.value ? empty(
|
|
2297
|
+
filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
|
|
2298
|
+
"Subscriptions appear here once someone checks out.",
|
|
2299
|
+
"subscriber"
|
|
2300
|
+
) : table(
|
|
2301
|
+
["Subscriber", "Plan", "Status", "Renews / ends"],
|
|
2302
|
+
rows.map((s) => {
|
|
2303
|
+
const end = periodEnd(s);
|
|
2304
|
+
return h15("tr", { key: s.id }, [
|
|
2305
|
+
td(
|
|
2306
|
+
h15(
|
|
2147
2307
|
"button",
|
|
2148
2308
|
{
|
|
2149
|
-
type: "
|
|
2150
|
-
style: styles.
|
|
2151
|
-
|
|
2309
|
+
type: "button",
|
|
2310
|
+
style: { ...styles.link, ...styles.mono },
|
|
2311
|
+
onClick: () => openRow(s.subscriberType, s.subscriberId)
|
|
2152
2312
|
},
|
|
2153
|
-
|
|
2154
|
-
)
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
(
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2313
|
+
`${s.subscriberType}/${s.subscriberId}`
|
|
2314
|
+
)
|
|
2315
|
+
),
|
|
2316
|
+
td([
|
|
2317
|
+
h15("strong", s.plan),
|
|
2318
|
+
" ",
|
|
2319
|
+
h15("span", { style: styles.muted }, `\xB7 ${s.interval}`)
|
|
2320
|
+
]),
|
|
2321
|
+
td([
|
|
2322
|
+
pill(statusTone(s.status), s.status),
|
|
2323
|
+
s.cancelAtPeriodEnd && s.status !== "canceled" ? h15("span", [" ", pill("warn", "cancels", false)]) : null
|
|
2324
|
+
]),
|
|
2325
|
+
td(
|
|
2326
|
+
end ? [h15("span", { style: styles.muted }, end.label), ` ${end.date}`] : h15("span", { style: styles.muted }, "\u2014"),
|
|
2327
|
+
{ whiteSpace: "nowrap" }
|
|
2168
2328
|
)
|
|
2169
|
-
)
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
style: { ...styles.button, marginTop: "8px" },
|
|
2175
|
-
onClick: () => void loadMoreLedger()
|
|
2176
|
-
},
|
|
2177
|
-
"Load more"
|
|
2178
|
-
) : null
|
|
2179
|
-
] : h14(
|
|
2180
|
-
"p",
|
|
2181
|
-
{ style: styles.muted },
|
|
2182
|
-
"No wallet \u2014 billing has no wallet configuration."
|
|
2183
|
-
)
|
|
2184
|
-
] : null
|
|
2329
|
+
]);
|
|
2330
|
+
})
|
|
2331
|
+
),
|
|
2332
|
+
list.isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
|
|
2333
|
+
list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
|
|
2185
2334
|
]);
|
|
2186
2335
|
};
|
|
2187
2336
|
}
|
|
@@ -2189,33 +2338,33 @@ var SubscriberScreen = defineComponent12({
|
|
|
2189
2338
|
|
|
2190
2339
|
// src/screens/AuditScreen.ts
|
|
2191
2340
|
import { useAdminAudit } from "@fonderie/vue-admin";
|
|
2192
|
-
import { defineComponent as
|
|
2193
|
-
var AuditScreen =
|
|
2341
|
+
import { defineComponent as defineComponent14, h as h16, ref as ref8 } from "vue";
|
|
2342
|
+
var AuditScreen = defineComponent14({
|
|
2194
2343
|
name: "FonderieAuditScreen",
|
|
2195
2344
|
props: {
|
|
2196
2345
|
client: { type: Object, required: true },
|
|
2197
2346
|
pageSize: { type: Number, default: 50 }
|
|
2198
2347
|
},
|
|
2199
2348
|
setup(props) {
|
|
2200
|
-
const draft =
|
|
2201
|
-
const query =
|
|
2349
|
+
const draft = ref8({ workspaceId: "", type: "", actorId: "" });
|
|
2350
|
+
const query = ref8({ limit: props.pageSize });
|
|
2202
2351
|
const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(
|
|
2203
2352
|
props.client,
|
|
2204
2353
|
query
|
|
2205
2354
|
);
|
|
2206
|
-
const field = (key, placeholder) =>
|
|
2355
|
+
const field = (key, placeholder) => h16("input", {
|
|
2207
2356
|
value: draft.value[key],
|
|
2208
2357
|
onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
|
|
2209
2358
|
placeholder,
|
|
2210
2359
|
style: styles.input,
|
|
2211
2360
|
"aria-label": placeholder
|
|
2212
2361
|
});
|
|
2213
|
-
return () =>
|
|
2362
|
+
return () => h16("div", { style: styles.container }, [
|
|
2214
2363
|
pageHeader(
|
|
2215
2364
|
"Audit",
|
|
2216
2365
|
"What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
|
|
2217
2366
|
),
|
|
2218
|
-
|
|
2367
|
+
h16(
|
|
2219
2368
|
"form",
|
|
2220
2369
|
{
|
|
2221
2370
|
style: styles.toolbar,
|
|
@@ -2234,19 +2383,19 @@ var AuditScreen = defineComponent13({
|
|
|
2234
2383
|
field("workspaceId", "workspace id (all if empty)"),
|
|
2235
2384
|
field("type", "event type"),
|
|
2236
2385
|
field("actorId", "actor id"),
|
|
2237
|
-
|
|
2386
|
+
h16(
|
|
2238
2387
|
"button",
|
|
2239
2388
|
{ type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
|
|
2240
2389
|
[icon("search", 14), "Filter"]
|
|
2241
2390
|
),
|
|
2242
|
-
|
|
2391
|
+
refreshButton2("Refresh", isLoading.value, () => void refresh())
|
|
2243
2392
|
]
|
|
2244
2393
|
),
|
|
2245
|
-
error.value ?
|
|
2394
|
+
error.value ? h16("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
|
|
2246
2395
|
events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
|
|
2247
2396
|
["When", "Type", "Workspace", "Actor", "Request"],
|
|
2248
2397
|
events.value.map(
|
|
2249
|
-
(e) =>
|
|
2398
|
+
(e) => h16("tr", { key: e.id }, [
|
|
2250
2399
|
td(new Date(e.createdAt).toLocaleString(), styles.muted),
|
|
2251
2400
|
td(e.type, styles.mono),
|
|
2252
2401
|
td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
|
|
@@ -2255,7 +2404,7 @@ var AuditScreen = defineComponent13({
|
|
|
2255
2404
|
])
|
|
2256
2405
|
)
|
|
2257
2406
|
),
|
|
2258
|
-
isLoading.value ?
|
|
2407
|
+
isLoading.value ? h16("p", { style: styles.status }, "Loading\u2026") : null,
|
|
2259
2408
|
hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
|
|
2260
2409
|
]);
|
|
2261
2410
|
}
|
|
@@ -2278,7 +2427,7 @@ var NAV = [
|
|
|
2278
2427
|
group: "Money",
|
|
2279
2428
|
items: [
|
|
2280
2429
|
{ page: "catalog", label: "Catalog", needs: "billing" },
|
|
2281
|
-
{ page: "subscriber", label: "
|
|
2430
|
+
{ page: "subscriber", label: "Subscriptions", needs: "billing" }
|
|
2282
2431
|
]
|
|
2283
2432
|
},
|
|
2284
2433
|
{ group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
|
|
@@ -2312,7 +2461,7 @@ var ICON = {
|
|
|
2312
2461
|
migrations: "migrations"
|
|
2313
2462
|
};
|
|
2314
2463
|
var NARROW = "(max-width: 820px)";
|
|
2315
|
-
var AdminShell =
|
|
2464
|
+
var AdminShell = defineComponent15({
|
|
2316
2465
|
name: "FonderieAdminShell",
|
|
2317
2466
|
props: {
|
|
2318
2467
|
client: { type: Object, required: true },
|
|
@@ -2346,60 +2495,74 @@ var AdminShell = defineComponent14({
|
|
|
2346
2495
|
emits: { navigate: (_page) => true },
|
|
2347
2496
|
setup(props, { emit, slots }) {
|
|
2348
2497
|
const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
|
|
2349
|
-
const narrow =
|
|
2350
|
-
const drawer =
|
|
2498
|
+
const narrow = ref9(query?.matches ?? false);
|
|
2499
|
+
const drawer = ref9(false);
|
|
2351
2500
|
const onMedia = (e) => narrow.value = e.matches;
|
|
2352
2501
|
onMounted(() => query?.addEventListener("change", onMedia));
|
|
2353
2502
|
onBeforeUnmount(() => query?.removeEventListener("change", onMedia));
|
|
2354
|
-
const own =
|
|
2355
|
-
const current =
|
|
2356
|
-
const editing =
|
|
2503
|
+
const own = ref9("attention");
|
|
2504
|
+
const current = computed3(() => props.page ?? own.value);
|
|
2505
|
+
const editing = ref9(null);
|
|
2506
|
+
const openUser = ref9(void 0);
|
|
2357
2507
|
const go = (p) => {
|
|
2358
2508
|
editing.value = null;
|
|
2359
2509
|
emit("navigate", p);
|
|
2360
2510
|
if (props.page === void 0) own.value = p;
|
|
2361
2511
|
drawer.value = false;
|
|
2362
2512
|
};
|
|
2363
|
-
const back = (label) =>
|
|
2364
|
-
|
|
2513
|
+
const back = (label) => h17("div", { style: { padding: "24px 40px 0" } }, [
|
|
2514
|
+
h17(
|
|
2365
2515
|
"button",
|
|
2366
2516
|
{ type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
|
|
2367
2517
|
[icon("back", 14), label]
|
|
2368
2518
|
)
|
|
2369
2519
|
]);
|
|
2370
|
-
const missing = (text) =>
|
|
2520
|
+
const missing = (text) => h17("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
|
|
2371
2521
|
const body = () => {
|
|
2372
2522
|
const c = props.client;
|
|
2373
2523
|
switch (current.value) {
|
|
2374
2524
|
case "attention":
|
|
2375
|
-
return
|
|
2525
|
+
return h17(AttentionScreen, { client: c });
|
|
2376
2526
|
case "modules":
|
|
2377
|
-
return
|
|
2527
|
+
return h17(ModulesScreen, { client: c });
|
|
2378
2528
|
case "doctor":
|
|
2379
|
-
return
|
|
2529
|
+
return h17(DoctorScreen, { client: c });
|
|
2380
2530
|
case "environment":
|
|
2381
|
-
return
|
|
2531
|
+
return h17(EnvironmentScreen, { client: c });
|
|
2382
2532
|
case "routes":
|
|
2383
|
-
return
|
|
2533
|
+
return h17(RoutesScreen, { client: c });
|
|
2384
2534
|
case "tokens":
|
|
2385
|
-
return
|
|
2535
|
+
return h17(TokensScreen, { client: c });
|
|
2386
2536
|
case "operators":
|
|
2387
|
-
return
|
|
2537
|
+
return h17(OperatorsScreen, {
|
|
2388
2538
|
client: c,
|
|
2389
2539
|
...props.currentOperator ? { me: props.currentOperator } : {}
|
|
2390
2540
|
});
|
|
2391
2541
|
case "migrations":
|
|
2392
|
-
return
|
|
2542
|
+
return h17(MigrationsScreen, { client: c });
|
|
2393
2543
|
case "log":
|
|
2394
|
-
return
|
|
2544
|
+
return h17(AdminLogScreen, { client: c });
|
|
2395
2545
|
case "catalog":
|
|
2396
|
-
return props.billingClient ?
|
|
2546
|
+
return props.billingClient ? h17(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
|
|
2397
2547
|
case "subscriber":
|
|
2398
|
-
return props.billingClient ?
|
|
2548
|
+
return props.billingClient ? h17(SubscriberScreen, {
|
|
2549
|
+
client: props.billingClient,
|
|
2550
|
+
...props.authClient ? {
|
|
2551
|
+
onOpenUser: (id) => {
|
|
2552
|
+
openUser.value = id;
|
|
2553
|
+
go("users");
|
|
2554
|
+
}
|
|
2555
|
+
} : {}
|
|
2556
|
+
}) : missing("Pass a BillingAdminClient to look up subscribers here.");
|
|
2399
2557
|
case "audit":
|
|
2400
|
-
return props.auditClient ?
|
|
2558
|
+
return props.auditClient ? h17(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
|
|
2401
2559
|
case "users":
|
|
2402
|
-
return props.authClient ?
|
|
2560
|
+
return props.authClient ? h17(UsersScreen, {
|
|
2561
|
+
key: openUser.value ?? "list",
|
|
2562
|
+
client: props.authClient,
|
|
2563
|
+
...props.billingClient ? { billingClient: props.billingClient } : {},
|
|
2564
|
+
...openUser.value ? { openUserId: openUser.value } : {}
|
|
2565
|
+
}) : missing("Pass an AuthAdminClient to look up users here.");
|
|
2403
2566
|
case "settings": {
|
|
2404
2567
|
const cc = props.configClient;
|
|
2405
2568
|
if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
|
|
@@ -2407,7 +2570,7 @@ var AdminShell = defineComponent14({
|
|
|
2407
2570
|
if (e && e.kind !== "template")
|
|
2408
2571
|
return [
|
|
2409
2572
|
back("Config & secrets"),
|
|
2410
|
-
|
|
2573
|
+
h17(ConfigEditorScreen, {
|
|
2411
2574
|
client: cc,
|
|
2412
2575
|
kind: e.kind,
|
|
2413
2576
|
configKey: e.key,
|
|
@@ -2415,7 +2578,7 @@ var AdminShell = defineComponent14({
|
|
|
2415
2578
|
onSaved: () => editing.value = null
|
|
2416
2579
|
})
|
|
2417
2580
|
];
|
|
2418
|
-
return
|
|
2581
|
+
return h17(ConfigListScreen, {
|
|
2419
2582
|
client: cc,
|
|
2420
2583
|
...props.environment !== void 0 ? { environment: props.environment } : {},
|
|
2421
2584
|
"onSelect-config": (key) => editing.value = { kind: "config", key },
|
|
@@ -2432,13 +2595,13 @@ var AdminShell = defineComponent14({
|
|
|
2432
2595
|
if (e?.kind === "template")
|
|
2433
2596
|
return [
|
|
2434
2597
|
back("Templates"),
|
|
2435
|
-
|
|
2598
|
+
h17(TemplateEditorScreen, {
|
|
2436
2599
|
client: kc,
|
|
2437
2600
|
type: e.type,
|
|
2438
2601
|
onSaved: () => editing.value = null
|
|
2439
2602
|
})
|
|
2440
2603
|
];
|
|
2441
|
-
return
|
|
2604
|
+
return h17(TemplateListScreen, {
|
|
2442
2605
|
client: kc,
|
|
2443
2606
|
"onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
|
|
2444
2607
|
});
|
|
@@ -2448,11 +2611,11 @@ var AdminShell = defineComponent14({
|
|
|
2448
2611
|
const visible = (i) => !i.needs || (i.needs === "operators" ? props.operators : i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
|
|
2449
2612
|
const brand = () => {
|
|
2450
2613
|
const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
|
|
2451
|
-
return
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
props.envLabel ?
|
|
2614
|
+
return h17("div", { style: styles.navBrand }, [
|
|
2615
|
+
h17("div", { style: styles.navMark }, [icon("mark", 15)]),
|
|
2616
|
+
h17("div", { style: { minWidth: 0, flex: 1 } }, [
|
|
2617
|
+
h17("div", { style: styles.navBrandTitle }, props.appName),
|
|
2618
|
+
props.envLabel ? h17(
|
|
2456
2619
|
"span",
|
|
2457
2620
|
{
|
|
2458
2621
|
style: {
|
|
@@ -2469,7 +2632,7 @@ var AdminShell = defineComponent14({
|
|
|
2469
2632
|
props.envLabel
|
|
2470
2633
|
) : null
|
|
2471
2634
|
]),
|
|
2472
|
-
narrow.value ?
|
|
2635
|
+
narrow.value ? h17(
|
|
2473
2636
|
"button",
|
|
2474
2637
|
{
|
|
2475
2638
|
type: "button",
|
|
@@ -2481,7 +2644,7 @@ var AdminShell = defineComponent14({
|
|
|
2481
2644
|
) : null
|
|
2482
2645
|
]);
|
|
2483
2646
|
};
|
|
2484
|
-
const nav = () =>
|
|
2647
|
+
const nav = () => h17(
|
|
2485
2648
|
"nav",
|
|
2486
2649
|
{
|
|
2487
2650
|
style: {
|
|
@@ -2492,17 +2655,17 @@ var AdminShell = defineComponent14({
|
|
|
2492
2655
|
},
|
|
2493
2656
|
[
|
|
2494
2657
|
brand(),
|
|
2495
|
-
|
|
2658
|
+
h17(
|
|
2496
2659
|
"div",
|
|
2497
2660
|
{ style: styles.navScroll },
|
|
2498
2661
|
NAV.map((g) => {
|
|
2499
2662
|
const items = g.items.filter(visible);
|
|
2500
2663
|
if (items.length === 0) return null;
|
|
2501
|
-
return
|
|
2502
|
-
|
|
2664
|
+
return h17("div", { key: g.group }, [
|
|
2665
|
+
h17("div", { style: styles.navGroup }, g.group),
|
|
2503
2666
|
...items.map((i) => {
|
|
2504
2667
|
const active = current.value === i.page;
|
|
2505
|
-
return
|
|
2668
|
+
return h17(
|
|
2506
2669
|
"button",
|
|
2507
2670
|
{
|
|
2508
2671
|
key: i.page,
|
|
@@ -2510,7 +2673,10 @@ var AdminShell = defineComponent14({
|
|
|
2510
2673
|
class: "fonderie-admin-nav-item",
|
|
2511
2674
|
style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
|
|
2512
2675
|
"aria-current": active ? "page" : void 0,
|
|
2513
|
-
onClick: () =>
|
|
2676
|
+
onClick: () => {
|
|
2677
|
+
openUser.value = void 0;
|
|
2678
|
+
go(i.page);
|
|
2679
|
+
}
|
|
2514
2680
|
},
|
|
2515
2681
|
[icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
|
|
2516
2682
|
);
|
|
@@ -2518,18 +2684,18 @@ var AdminShell = defineComponent14({
|
|
|
2518
2684
|
]);
|
|
2519
2685
|
})
|
|
2520
2686
|
),
|
|
2521
|
-
slots["footer"] ?
|
|
2687
|
+
slots["footer"] ? h17("div", { style: styles.navFooter }, slots["footer"]()) : null
|
|
2522
2688
|
]
|
|
2523
2689
|
);
|
|
2524
2690
|
return () => {
|
|
2525
2691
|
const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
|
|
2526
|
-
return
|
|
2692
|
+
return h17(
|
|
2527
2693
|
"div",
|
|
2528
2694
|
{ style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
|
|
2529
2695
|
[
|
|
2530
2696
|
...narrow.value ? [
|
|
2531
|
-
|
|
2532
|
-
|
|
2697
|
+
h17("div", { style: styles.topbar }, [
|
|
2698
|
+
h17(
|
|
2533
2699
|
"button",
|
|
2534
2700
|
{
|
|
2535
2701
|
type: "button",
|
|
@@ -2540,19 +2706,19 @@ var AdminShell = defineComponent14({
|
|
|
2540
2706
|
},
|
|
2541
2707
|
[icon("menu")]
|
|
2542
2708
|
),
|
|
2543
|
-
|
|
2544
|
-
props.envLabel ?
|
|
2709
|
+
h17("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
|
|
2710
|
+
props.envLabel ? h17("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
|
|
2545
2711
|
]),
|
|
2546
|
-
drawer.value ?
|
|
2712
|
+
drawer.value ? h17("div", { style: styles.drawer }, [
|
|
2547
2713
|
nav(),
|
|
2548
|
-
|
|
2714
|
+
h17("div", {
|
|
2549
2715
|
style: styles.scrim,
|
|
2550
2716
|
"aria-hidden": "true",
|
|
2551
2717
|
onClick: () => drawer.value = false
|
|
2552
2718
|
})
|
|
2553
2719
|
]) : null
|
|
2554
2720
|
] : [nav()],
|
|
2555
|
-
|
|
2721
|
+
h17("main", { class: "fonderie-admin-main", style: styles.main }, body())
|
|
2556
2722
|
]
|
|
2557
2723
|
);
|
|
2558
2724
|
};
|
|
@@ -2570,6 +2736,7 @@ export {
|
|
|
2570
2736
|
ModulesScreen,
|
|
2571
2737
|
OperatorsScreen,
|
|
2572
2738
|
RoutesScreen,
|
|
2739
|
+
SubscriberBilling,
|
|
2573
2740
|
SubscriberScreen,
|
|
2574
2741
|
TokensScreen,
|
|
2575
2742
|
UsersScreen
|