@growth-operations/ui-shared 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/package.json +1 -1
- package/src/index.js +4 -0
- package/src/lib/format.js +14 -0
- package/src/usage/UsageTab.jsx +145 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -21,9 +21,13 @@ export { PlanGrid } from "./billing/PlanGrid";
|
|
|
21
21
|
// Alerts — the shared customer alerts table (level filter, sort, dismiss).
|
|
22
22
|
export { AlertsTab } from "./alerts/AlertsTab";
|
|
23
23
|
|
|
24
|
+
// Usage — the shared credit usage view (per-day LineChart + window stats).
|
|
25
|
+
export { UsageTab } from "./usage/UsageTab";
|
|
26
|
+
|
|
24
27
|
// Lib helpers.
|
|
25
28
|
export {
|
|
26
29
|
fmtDate,
|
|
30
|
+
fmtDateTime,
|
|
27
31
|
fmtMoney,
|
|
28
32
|
daysUntil,
|
|
29
33
|
humanizeStatus,
|
package/src/lib/format.js
CHANGED
|
@@ -14,6 +14,20 @@ export function fmtDate(value) {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Date + time, for logs where multiple events share a day (usage history). "—"
|
|
18
|
+
// when null/unparseable.
|
|
19
|
+
export function fmtDateTime(value) {
|
|
20
|
+
if (value == null) return "—";
|
|
21
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
22
|
+
if (Number.isNaN(d.getTime())) return "—";
|
|
23
|
+
return d.toLocaleString(undefined, {
|
|
24
|
+
month: "short",
|
|
25
|
+
day: "numeric",
|
|
26
|
+
hour: "numeric",
|
|
27
|
+
minute: "2-digit",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
// Whole days from now until `value` (negative = past). null if unparseable.
|
|
18
32
|
export function daysUntil(value) {
|
|
19
33
|
if (value == null) return null;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// UsageTab — the shared in-app credit usage view for credit apps.
|
|
2
|
+
//
|
|
3
|
+
// The off-Anvil answer to "show usage directly in-app", chart-first (matches
|
|
4
|
+
// the sparkfly/toast usage tabs): a LineChart of credits used per day over a
|
|
5
|
+
// window + headline Statistics (used in window / remaining / granted). Fed by
|
|
6
|
+
// the backend usage series at {basePath}/usage?portalId=&days= — credit_meter
|
|
7
|
+
// .recent on the home payload is only the last ~20 events, so the chart reads
|
|
8
|
+
// the dedicated, zero-filled, server-bucketed series instead.
|
|
9
|
+
//
|
|
10
|
+
// Self-hosted apps (sparkfly/toast) serve /api/v1/usage from their own service;
|
|
11
|
+
// base-hosted credit apps (line-item) pass `basePath` + `portalId` to hit base's
|
|
12
|
+
// /v1/hubspot/app_pages/{app}/usage. Trial/non-credit apps don't render this.
|
|
13
|
+
import React, { useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
hubspot,
|
|
16
|
+
Flex,
|
|
17
|
+
Heading,
|
|
18
|
+
Text,
|
|
19
|
+
Tile,
|
|
20
|
+
Statistics,
|
|
21
|
+
StatisticsItem,
|
|
22
|
+
LineChart,
|
|
23
|
+
Select,
|
|
24
|
+
LoadingSpinner,
|
|
25
|
+
Alert,
|
|
26
|
+
EmptyState,
|
|
27
|
+
} from "@hubspot/ui-extensions";
|
|
28
|
+
|
|
29
|
+
import { useStrictModeEffect } from "../lib/useStrictModeEffect";
|
|
30
|
+
|
|
31
|
+
const DEFAULT_BASE_PATH = "/api/v1/usage";
|
|
32
|
+
|
|
33
|
+
const WINDOW_OPTIONS = [
|
|
34
|
+
{ label: "Last 7 days", value: "7" },
|
|
35
|
+
{ label: "Last 30 days", value: "30" },
|
|
36
|
+
{ label: "Last 90 days", value: "90" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// context: extension context. basePath/portalId: target the right backend
|
|
40
|
+
// (base-hosted apps pass both). appName: chart copy.
|
|
41
|
+
export function UsageTab({
|
|
42
|
+
context,
|
|
43
|
+
basePath = DEFAULT_BASE_PATH,
|
|
44
|
+
portalId,
|
|
45
|
+
appName = "this app",
|
|
46
|
+
}) {
|
|
47
|
+
const [data, setData] = useState(null);
|
|
48
|
+
const [days, setDays] = useState("30");
|
|
49
|
+
const [loading, setLoading] = useState(true);
|
|
50
|
+
const [error, setError] = useState(null);
|
|
51
|
+
|
|
52
|
+
useStrictModeEffect(
|
|
53
|
+
async ({ mounted }) => {
|
|
54
|
+
setLoading(true);
|
|
55
|
+
try {
|
|
56
|
+
const base = context.variables?.BASE_URL ?? "";
|
|
57
|
+
const pid = portalId ?? context.portal.id;
|
|
58
|
+
const url = `${base}${basePath}?portalId=${pid}&days=${days}`;
|
|
59
|
+
const res = await hubspot
|
|
60
|
+
.fetch(url, { method: "GET", timeout: 15000 })
|
|
61
|
+
.then((r) => r.json());
|
|
62
|
+
if (mounted.current) {
|
|
63
|
+
setData(res);
|
|
64
|
+
setError(null);
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (mounted.current) setError(String(err));
|
|
68
|
+
} finally {
|
|
69
|
+
if (mounted.current) setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[context, days]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (loading) return <LoadingSpinner showLabel label="Loading usage…" />;
|
|
76
|
+
|
|
77
|
+
if (error) {
|
|
78
|
+
return (
|
|
79
|
+
<Flex direction="column" gap="medium">
|
|
80
|
+
<Heading>Usage</Heading>
|
|
81
|
+
<Alert title="Couldn't load usage" variant="warning">
|
|
82
|
+
<Text>{error}</Text>
|
|
83
|
+
</Alert>
|
|
84
|
+
</Flex>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const series = data?.series ?? [];
|
|
89
|
+
const usedInWindow = data?.used_in_window ?? 0;
|
|
90
|
+
// Legacy (Anvil) installs have no credit balance — the backend returns null
|
|
91
|
+
// granted/remaining. Show only the window usage stat for them; credit installs
|
|
92
|
+
// get the full granted/remaining breakdown.
|
|
93
|
+
const remaining = data?.remaining;
|
|
94
|
+
const granted = data?.granted;
|
|
95
|
+
const hasBalance = remaining != null && granted != null;
|
|
96
|
+
const hasActivity = usedInWindow > 0;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Flex direction="column" gap="medium">
|
|
100
|
+
<Flex direction="row" justify="between" align="end">
|
|
101
|
+
<Heading>Usage</Heading>
|
|
102
|
+
<Select
|
|
103
|
+
label="Window"
|
|
104
|
+
name="window"
|
|
105
|
+
options={WINDOW_OPTIONS}
|
|
106
|
+
value={days}
|
|
107
|
+
onChange={setDays}
|
|
108
|
+
/>
|
|
109
|
+
</Flex>
|
|
110
|
+
|
|
111
|
+
<Tile>
|
|
112
|
+
<Statistics>
|
|
113
|
+
<StatisticsItem label="Used (window)" number={usedInWindow} />
|
|
114
|
+
{hasBalance && <StatisticsItem label="Remaining" number={remaining} />}
|
|
115
|
+
{hasBalance && <StatisticsItem label="Granted" number={granted} />}
|
|
116
|
+
</Statistics>
|
|
117
|
+
</Tile>
|
|
118
|
+
|
|
119
|
+
{!hasActivity ? (
|
|
120
|
+
<EmptyState title="No usage yet" layout="vertical">
|
|
121
|
+
<Text>
|
|
122
|
+
Once you run a metered {appName} action, your credit usage shows here
|
|
123
|
+
over time.
|
|
124
|
+
</Text>
|
|
125
|
+
</EmptyState>
|
|
126
|
+
) : (
|
|
127
|
+
<Tile>
|
|
128
|
+
<Flex direction="column" gap="small">
|
|
129
|
+
<Text format={{ fontWeight: "bold" }}>Credits used per day</Text>
|
|
130
|
+
<LineChart
|
|
131
|
+
data={series}
|
|
132
|
+
axes={{
|
|
133
|
+
x: { field: "date", fieldType: "datetime", label: "Day" },
|
|
134
|
+
y: { field: "credits", fieldType: "linear", label: "Credits" },
|
|
135
|
+
}}
|
|
136
|
+
options={{ showTooltips: true }}
|
|
137
|
+
/>
|
|
138
|
+
</Flex>
|
|
139
|
+
</Tile>
|
|
140
|
+
)}
|
|
141
|
+
</Flex>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default UsageTab;
|