@growth-operations/ui-shared 1.6.16 → 1.6.17
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/sandbox/SandboxInstall.jsx +97 -10
package/package.json
CHANGED
|
@@ -15,20 +15,34 @@
|
|
|
15
15
|
// unconditionally with no signal a sandbox already existed, and a customer
|
|
16
16
|
// could keep minting more with zero visibility). At the limit, shows the
|
|
17
17
|
// linked list + a "X of Y used" indicator with no mint action underneath —
|
|
18
|
-
// not a disabled/dead button, just nothing to click.
|
|
18
|
+
// not a disabled/dead button, just nothing to click. Each linked sandbox is a
|
|
19
|
+
// card showing who connected it and when, a link to its own app-pages console,
|
|
20
|
+
// and an uninstall action (armed/confirm — same two-step pattern as the
|
|
21
|
+
// migration console's rollback).
|
|
19
22
|
//
|
|
20
23
|
// Extensible per app: the caller passes the mint `path` on its OWN backend
|
|
21
24
|
// (base-hosted apps -> /v1/hubspot/app_pages/... no; the install mint lives at
|
|
22
25
|
// /v2/hubspot/install/{app}/sandbox-link on base; self-hosted apps serve their
|
|
23
26
|
// own), plus the portalId. The SAME path is used for both the GET (status) and
|
|
24
|
-
// POST (mint) — the backend distinguishes by HTTP method.
|
|
27
|
+
// POST (mint) — the backend distinguishes by HTTP method. The uninstall action
|
|
28
|
+
// posts to `${path}/{sandboxPortalId}/uninstall`. callAppApi targets
|
|
25
29
|
// context.variables.BASE_URL, so each app's BASE_URL routes to the right
|
|
26
30
|
// service. Apps not allowlisted get a 403 from the endpoint, surfaced as a
|
|
27
31
|
// calm message.
|
|
28
32
|
import React, { useEffect, useState } from "react";
|
|
29
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
Flex,
|
|
35
|
+
Tile,
|
|
36
|
+
Heading,
|
|
37
|
+
Text,
|
|
38
|
+
Link,
|
|
39
|
+
Alert,
|
|
40
|
+
Button,
|
|
41
|
+
StatusTag,
|
|
42
|
+
} from "@hubspot/ui-extensions";
|
|
30
43
|
|
|
31
44
|
import { callAppApi, AppApiError } from "../sdk/app/base";
|
|
45
|
+
import { fmtDateTime } from "../lib/format";
|
|
32
46
|
|
|
33
47
|
// context: extension context. path: the status+mint endpoint on the app's
|
|
34
48
|
// backend (e.g. `/v2/hubspot/install/${appKey}/sandbox-link`). portalId: the
|
|
@@ -47,14 +61,18 @@ export function SandboxInstall({
|
|
|
47
61
|
const [installUrl, setInstallUrl] = useState(null);
|
|
48
62
|
const [loading, setLoading] = useState(true);
|
|
49
63
|
const [error, setError] = useState(null);
|
|
64
|
+
const [armedUninstall, setArmedUninstall] = useState(null); // portal_id or null
|
|
65
|
+
const [uninstalling, setUninstalling] = useState(null); // portal_id or null
|
|
66
|
+
const [uninstallError, setUninstallError] = useState(null);
|
|
67
|
+
|
|
68
|
+
const pid = portalId ?? context.portal.id;
|
|
69
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
70
|
+
const qs = `${path}${sep}portalId=${pid}`;
|
|
50
71
|
|
|
51
72
|
useEffect(() => {
|
|
52
73
|
let cancelled = false;
|
|
53
74
|
(async () => {
|
|
54
75
|
try {
|
|
55
|
-
const pid = portalId ?? context.portal.id;
|
|
56
|
-
const sep = path.includes("?") ? "&" : "?";
|
|
57
|
-
const qs = `${path}${sep}portalId=${pid}`;
|
|
58
76
|
const status = await callAppApi(context, qs, "GET");
|
|
59
77
|
if (cancelled) return;
|
|
60
78
|
const linked = status?.linked_sandboxes ?? [];
|
|
@@ -84,6 +102,22 @@ export function SandboxInstall({
|
|
|
84
102
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
103
|
}, [path, portalId]);
|
|
86
104
|
|
|
105
|
+
async function doUninstall(sandboxPortalId) {
|
|
106
|
+
setUninstalling(sandboxPortalId);
|
|
107
|
+
setUninstallError(null);
|
|
108
|
+
try {
|
|
109
|
+
await callAppApi(context, `${qs}/${sandboxPortalId}/uninstall`, "POST");
|
|
110
|
+
setLinkedSandboxes((prev) =>
|
|
111
|
+
prev.filter((s) => s.portal_id !== sandboxPortalId)
|
|
112
|
+
);
|
|
113
|
+
setArmedUninstall(null);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
setUninstallError(err instanceof AppApiError ? err.message : String(err));
|
|
116
|
+
} finally {
|
|
117
|
+
setUninstalling(null);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
const atLimit = linkedSandboxes.length >= limit;
|
|
88
122
|
|
|
89
123
|
return (
|
|
@@ -111,13 +145,66 @@ export function SandboxInstall({
|
|
|
111
145
|
</Alert>
|
|
112
146
|
)}
|
|
113
147
|
|
|
148
|
+
{uninstallError && (
|
|
149
|
+
<Alert title="Couldn't uninstall that sandbox" variant="warning">
|
|
150
|
+
<Text>{uninstallError}</Text>
|
|
151
|
+
</Alert>
|
|
152
|
+
)}
|
|
153
|
+
|
|
114
154
|
{linkedSandboxes.length > 0 && (
|
|
115
|
-
<Flex direction="column" gap="
|
|
155
|
+
<Flex direction="column" gap="small">
|
|
116
156
|
<Text format={{ fontWeight: "bold" }}>Already linked</Text>
|
|
117
157
|
{linkedSandboxes.map((s) => (
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
<Tile key={s.portal_id}>
|
|
159
|
+
<Flex direction="column" gap="extra-small">
|
|
160
|
+
<Flex direction="row" gap="small" align="center">
|
|
161
|
+
<Text format={{ fontWeight: "demibold" }}>
|
|
162
|
+
{s.name || s.portal_id}
|
|
163
|
+
</Text>
|
|
164
|
+
<StatusTag variant="default">{s.status}</StatusTag>
|
|
165
|
+
</Flex>
|
|
166
|
+
<Text variant="microcopy">
|
|
167
|
+
Connected {s.connected_at ? fmtDateTime(s.connected_at) : "—"}
|
|
168
|
+
{s.connected_by ? ` by ${s.connected_by}` : ""}
|
|
169
|
+
</Text>
|
|
170
|
+
<Flex direction="row" gap="small" align="center">
|
|
171
|
+
{s.console_url && (
|
|
172
|
+
<Link href={{ url: s.console_url, external: true }}>
|
|
173
|
+
Open in HubSpot →
|
|
174
|
+
</Link>
|
|
175
|
+
)}
|
|
176
|
+
{armedUninstall === s.portal_id ? (
|
|
177
|
+
<Flex direction="row" gap="small" align="center">
|
|
178
|
+
<Text variant="microcopy">Uninstall this sandbox?</Text>
|
|
179
|
+
<Button
|
|
180
|
+
size="xs"
|
|
181
|
+
variant="destructive"
|
|
182
|
+
onClick={() => doUninstall(s.portal_id)}
|
|
183
|
+
disabled={uninstalling === s.portal_id}
|
|
184
|
+
>
|
|
185
|
+
Confirm uninstall
|
|
186
|
+
</Button>
|
|
187
|
+
<Button
|
|
188
|
+
size="xs"
|
|
189
|
+
variant="secondary"
|
|
190
|
+
onClick={() => setArmedUninstall(null)}
|
|
191
|
+
disabled={uninstalling === s.portal_id}
|
|
192
|
+
>
|
|
193
|
+
Cancel
|
|
194
|
+
</Button>
|
|
195
|
+
</Flex>
|
|
196
|
+
) : (
|
|
197
|
+
<Button
|
|
198
|
+
size="xs"
|
|
199
|
+
variant="destructive"
|
|
200
|
+
onClick={() => setArmedUninstall(s.portal_id)}
|
|
201
|
+
>
|
|
202
|
+
Uninstall…
|
|
203
|
+
</Button>
|
|
204
|
+
)}
|
|
205
|
+
</Flex>
|
|
206
|
+
</Flex>
|
|
207
|
+
</Tile>
|
|
121
208
|
))}
|
|
122
209
|
</Flex>
|
|
123
210
|
)}
|