@startsimpli/integrations 0.2.33 → 0.2.35
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/__tests__/connection-activity.test.tsx +160 -0
- package/src/__tests__/corpus-folders.test.tsx +727 -0
- package/src/__tests__/corpus-pages.test.tsx +4 -4
- package/src/__tests__/library-failure-reason.test.tsx +178 -0
- package/src/components/FolderTree.tsx +425 -0
- package/src/components/index.ts +6 -0
- package/src/components/pages/ConnectionsPage.tsx +174 -68
- package/src/components/pages/LibraryPage.tsx +333 -35
- package/src/corpus/client.ts +123 -9
- package/src/corpus/entities.ts +75 -0
- package/src/corpus/fixtures.ts +263 -1
- package/src/corpus/index.ts +8 -1
- package/src/corpus/types.ts +121 -0
|
@@ -47,6 +47,7 @@ import type {
|
|
|
47
47
|
ConnectionChoicePage,
|
|
48
48
|
ConnectionInvokeResult,
|
|
49
49
|
ConnectionPermissions,
|
|
50
|
+
ConnectionRun,
|
|
50
51
|
ConnectionTarget,
|
|
51
52
|
ConnectorAction,
|
|
52
53
|
ConnectorCatalogEntry,
|
|
@@ -135,6 +136,16 @@ interface BoundApi {
|
|
|
135
136
|
invokeConnectionAction?(id: string, action: string, input: {
|
|
136
137
|
destination_id: string; record_type?: string; records?: unknown[];
|
|
137
138
|
}): Promise<ConnectionInvokeResult>;
|
|
139
|
+
/**
|
|
140
|
+
* RECENT ACTIVITY on both legs (bd startsim-ei8gm.2). Required on both
|
|
141
|
+
* injected interfaces and, until this bead, called by nothing — so the reason
|
|
142
|
+
* a run failed reached the payload and never a pixel.
|
|
143
|
+
*
|
|
144
|
+
* Optional HERE for the reason every other verb on this type is: an app
|
|
145
|
+
* pinned to an older `@startsimpli/api` keeps type-checking, and the card
|
|
146
|
+
* renders no control rather than a button whose handler is `undefined`.
|
|
147
|
+
*/
|
|
148
|
+
connectionRuns?(id: string): Promise<{ results: ConnectionRun[] }>;
|
|
138
149
|
/** Per-member governance (bd startsim-768w.22.25.8). Foundry mode only, and
|
|
139
150
|
absent on an api that predates the endpoints — see `ConnectionsApi`. */
|
|
140
151
|
/** The OAuth hand-off (bd startsim-768w.22.25.14/.23). Optional: an app on an
|
|
@@ -179,7 +190,16 @@ function bindApi(props: ConnectionsPageProps): BoundApi {
|
|
|
179
190
|
? { disconnectMemberConnection: (id: string) =>
|
|
180
191
|
api.disconnectMemberConnection!(slug, id) }
|
|
181
192
|
: {};
|
|
193
|
+
// Bound only when the injected client HAS it, even though `ConnectionsApi`
|
|
194
|
+
// declares it required: apps consume `@startsimpli/*` as PUBLISHED packages,
|
|
195
|
+
// so a runtime object that predates the verb is a real state the TYPE cannot
|
|
196
|
+
// see (bd startsim-ei8gm.2). Same rule as the four above — a wrapper that
|
|
197
|
+
// always exists and throws inside would render the control and fail on click.
|
|
198
|
+
const runs = api.connectionRuns
|
|
199
|
+
? { connectionRuns: (id: string) => api.connectionRuns(slug, id) }
|
|
200
|
+
: {};
|
|
182
201
|
return {
|
|
202
|
+
...runs,
|
|
183
203
|
...memberPolicy,
|
|
184
204
|
...oauth,
|
|
185
205
|
...roster,
|
|
@@ -216,6 +236,21 @@ const DEFAULTS = {
|
|
|
216
236
|
error: 'text-sm text-red-600',
|
|
217
237
|
};
|
|
218
238
|
|
|
239
|
+
/**
|
|
240
|
+
* WHICH LEG A RUN BELONGS TO, in a reader's words (bd startsim-ei8gm.2).
|
|
241
|
+
*
|
|
242
|
+
* The card merges three tables — `SourceRun`, `DestinationRun` and
|
|
243
|
+
* `DestinationWebhookEvent` — into one list, and "failed" means a different
|
|
244
|
+
* thing and needs a different action on each. Data with a fall-through to the
|
|
245
|
+
* raw key, the same rule `CAPABILITY_COPY` below states: a `kind` this bundle
|
|
246
|
+
* has never heard of renders as itself rather than vanishing from the list.
|
|
247
|
+
*/
|
|
248
|
+
const RUN_KIND_COPY: Record<string, string> = {
|
|
249
|
+
source_run: 'Fetched in',
|
|
250
|
+
destination_run: 'Sent out',
|
|
251
|
+
webhook_event: 'Received',
|
|
252
|
+
};
|
|
253
|
+
|
|
219
254
|
/**
|
|
220
255
|
* WHAT AN ADMIN IS AGREEING TO, in their words rather than the wire's.
|
|
221
256
|
*
|
|
@@ -1088,6 +1123,10 @@ function ConnectionCard({
|
|
|
1088
1123
|
const [replacing, setReplacing] = React.useState(false);
|
|
1089
1124
|
const [settingsOpen, setSettingsOpen] = React.useState(false);
|
|
1090
1125
|
const [confirmingDisconnect, setConfirmingDisconnect] = React.useState(false);
|
|
1126
|
+
const [activityOpen, setActivityOpen] = React.useState(false);
|
|
1127
|
+
/** `null` is NOT LOADED YET, which reads differently from a connection that
|
|
1128
|
+
has never run — the distinction the empty state below turns on. */
|
|
1129
|
+
const [runs, setRuns] = React.useState<ConnectionRun[] | null>(null);
|
|
1091
1130
|
|
|
1092
1131
|
const declared = entry?.capabilities;
|
|
1093
1132
|
const enabled = React.useMemo(
|
|
@@ -1138,102 +1177,169 @@ function ConnectionCard({
|
|
|
1138
1177
|
<p className={cn.error}>{connection.check_error}</p>
|
|
1139
1178
|
) : null}
|
|
1140
1179
|
</div>
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
onChanged();
|
|
1151
|
-
})}
|
|
1152
|
-
>
|
|
1153
|
-
Test
|
|
1154
|
-
</button>
|
|
1155
|
-
{/* THE CONTROL THIS PAGE WAS MISSING. A vendor key gets rotated —
|
|
1156
|
-
that is normal operations, not an exception — and the only place
|
|
1157
|
-
to put the new one was a form that creates a second account. */}
|
|
1180
|
+
<div className="flex flex-wrap justify-end gap-2">
|
|
1181
|
+
{/* ACTIVITY IS OUTSIDE THE MANAGE GATE, and deliberately. Reading the
|
|
1182
|
+
run ledger needs `view_connections` server-side, which is what put
|
|
1183
|
+
this card on the screen at all — and the person who most needs to
|
|
1184
|
+
know WHY a connection is failing is often the one who cannot fix
|
|
1185
|
+
it themselves. Gating the answer behind the permission to act
|
|
1186
|
+
would leave them with the word "Failing" and nothing else, which
|
|
1187
|
+
is the sixteen days this bead is about. */}
|
|
1188
|
+
{api.connectionRuns ? (
|
|
1158
1189
|
<button
|
|
1159
1190
|
type="button"
|
|
1160
1191
|
className={cn.button}
|
|
1161
1192
|
disabled={Boolean(busy)}
|
|
1162
1193
|
onClick={() => {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1194
|
+
const opening = !activityOpen;
|
|
1195
|
+
setActivityOpen(opening);
|
|
1196
|
+
if (!opening) return;
|
|
1197
|
+
void guard('activity', async () => {
|
|
1198
|
+
const page = await api.connectionRuns!(connection.id);
|
|
1199
|
+
setRuns(page.results ?? []);
|
|
1200
|
+
});
|
|
1166
1201
|
}}
|
|
1167
1202
|
>
|
|
1168
|
-
{
|
|
1203
|
+
{activityOpen ? 'Hide activity' : 'Activity'}
|
|
1169
1204
|
</button>
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
flag — there is no way to render a button here whose handler is
|
|
1174
|
-
undefined. */}
|
|
1175
|
-
{api.connectionChoices && api.connectionActions ? (
|
|
1205
|
+
) : null}
|
|
1206
|
+
{canManage ? (
|
|
1207
|
+
<>
|
|
1176
1208
|
<button
|
|
1177
1209
|
type="button"
|
|
1178
1210
|
className={cn.button}
|
|
1179
1211
|
disabled={Boolean(busy)}
|
|
1180
|
-
onClick={() => void guard('
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
setActions(available.results ?? []);
|
|
1212
|
+
onClick={() => void guard('check', async () => {
|
|
1213
|
+
const res = await api.checkConnection(connection.id);
|
|
1214
|
+
setNotice(res.detail || (res.ok ? 'Connection is healthy.' : ''));
|
|
1215
|
+
onChanged();
|
|
1185
1216
|
})}
|
|
1186
1217
|
>
|
|
1187
|
-
|
|
1218
|
+
Test
|
|
1188
1219
|
</button>
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1220
|
+
{/* THE CONTROL THIS PAGE WAS MISSING. A vendor key gets rotated —
|
|
1221
|
+
that is normal operations, not an exception — and the only place
|
|
1222
|
+
to put the new one was a form that creates a second account. */}
|
|
1223
|
+
<button
|
|
1224
|
+
type="button"
|
|
1225
|
+
className={cn.button}
|
|
1226
|
+
disabled={Boolean(busy)}
|
|
1227
|
+
onClick={() => {
|
|
1228
|
+
setConfirmingDisconnect(false);
|
|
1229
|
+
setError('');
|
|
1230
|
+
setReplacing((v) => !v);
|
|
1231
|
+
}}
|
|
1232
|
+
>
|
|
1233
|
+
{replacing ? 'Cancel' : 'Replace key'}
|
|
1234
|
+
</button>
|
|
1235
|
+
{/* DISCOVER -> WIRE -> FIRE is foundry-level and stays there. The
|
|
1236
|
+
member router serves no choices, targets or actions at all, so
|
|
1237
|
+
the control is gated on the verb EXISTING rather than on a mode
|
|
1238
|
+
flag — there is no way to render a button here whose handler is
|
|
1239
|
+
undefined. */}
|
|
1240
|
+
{api.connectionChoices && api.connectionActions ? (
|
|
1241
|
+
<button
|
|
1242
|
+
type="button"
|
|
1243
|
+
className={cn.button}
|
|
1244
|
+
disabled={Boolean(busy)}
|
|
1245
|
+
onClick={() => void guard('discover', async () => {
|
|
1246
|
+
const page = await api.connectionChoices!(connection.id);
|
|
1247
|
+
setChoices(page.results ?? []);
|
|
1248
|
+
const available = await api.connectionActions!(connection.id);
|
|
1249
|
+
setActions(available.results ?? []);
|
|
1250
|
+
})}
|
|
1251
|
+
>
|
|
1252
|
+
{busy === 'discover' ? 'Loading…' : 'Workflows'}
|
|
1253
|
+
</button>
|
|
1254
|
+
) : null}
|
|
1255
|
+
{/* SETTINGS, which this card has never had. A connector's
|
|
1256
|
+
`config_schema` ships on every catalog row and was rendered
|
|
1257
|
+
exactly once — on the connect form, before the account existed.
|
|
1258
|
+
After that a setting could not be changed from the product at
|
|
1259
|
+
all, which for Granola means the switch deciding whether verbatim
|
|
1260
|
+
recordings of people's conversations are stored was reachable
|
|
1261
|
+
only by editing the database. `ReplaceKeyForm` already pointed at
|
|
1262
|
+
"the connection's own settings" as if this existed.
|
|
1198
1263
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1264
|
+
GATED ON THE SETTINGS VERB, not on the general one (bd
|
|
1265
|
+
startsim-sfms2). The member surface has `updateConnectionConfig`
|
|
1266
|
+
and deliberately not `updateConnection`, because a personal
|
|
1267
|
+
connection's config is the MEMBER's privacy boundary while its
|
|
1268
|
+
name and what it may do stay the foundry's. One gate for both
|
|
1269
|
+
would have made opening this control open the capability Save
|
|
1270
|
+
beside it, which is a decision this change did not make. */}
|
|
1271
|
+
{configFields.length && api.updateConnectionConfig ? (
|
|
1272
|
+
<button
|
|
1273
|
+
type="button"
|
|
1274
|
+
className={cn.button}
|
|
1275
|
+
disabled={Boolean(busy) || confirmingDisconnect}
|
|
1276
|
+
onClick={() => {
|
|
1277
|
+
setReplacing(false);
|
|
1278
|
+
setError('');
|
|
1279
|
+
setSettingsOpen((v) => !v);
|
|
1280
|
+
}}
|
|
1281
|
+
>
|
|
1282
|
+
{settingsOpen ? 'Close settings' : 'Settings'}
|
|
1283
|
+
</button>
|
|
1284
|
+
) : null}
|
|
1207
1285
|
<button
|
|
1208
1286
|
type="button"
|
|
1209
1287
|
className={cn.button}
|
|
1210
1288
|
disabled={Boolean(busy) || confirmingDisconnect}
|
|
1211
1289
|
onClick={() => {
|
|
1212
1290
|
setReplacing(false);
|
|
1291
|
+
setSettingsOpen(false);
|
|
1213
1292
|
setError('');
|
|
1214
|
-
|
|
1293
|
+
setConfirmingDisconnect(true);
|
|
1215
1294
|
}}
|
|
1216
1295
|
>
|
|
1217
|
-
|
|
1296
|
+
Disconnect
|
|
1218
1297
|
</button>
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
className={cn.button}
|
|
1223
|
-
disabled={Boolean(busy) || confirmingDisconnect}
|
|
1224
|
-
onClick={() => {
|
|
1225
|
-
setReplacing(false);
|
|
1226
|
-
setSettingsOpen(false);
|
|
1227
|
-
setError('');
|
|
1228
|
-
setConfirmingDisconnect(true);
|
|
1229
|
-
}}
|
|
1230
|
-
>
|
|
1231
|
-
Disconnect
|
|
1232
|
-
</button>
|
|
1233
|
-
</div>
|
|
1234
|
-
) : null}
|
|
1298
|
+
</>
|
|
1299
|
+
) : null}
|
|
1300
|
+
</div>
|
|
1235
1301
|
</div>
|
|
1236
1302
|
|
|
1303
|
+
{activityOpen ? (
|
|
1304
|
+
<div className="mt-3 space-y-2 border-t pt-3" role="group"
|
|
1305
|
+
aria-label="Recent activity">
|
|
1306
|
+
{/* THE FIELD NOTHING HAS EVER READ. `ConnectionRun.error_message` has
|
|
1307
|
+
been declared in this package's types since the endpoint existed;
|
|
1308
|
+
until #375 the server answered `""` for every source run ever
|
|
1309
|
+
recorded, and even after it did not, no component asked. A failed
|
|
1310
|
+
run rendered as a status word, which is a fact the reader already
|
|
1311
|
+
had from the card above it.
|
|
1312
|
+
|
|
1313
|
+
RENDERED AS THE SERVER WROTE IT. The text is scrubbed against this
|
|
1314
|
+
connection's stored credential and the platform app_secret before
|
|
1315
|
+
it leaves Django (`surface.py::_reason_reader`), and a reason that
|
|
1316
|
+
could not be scrubbed arrives as a sentence saying so. There is
|
|
1317
|
+
one redaction path in this estate and it is not in the browser —
|
|
1318
|
+
which does not hold the secrets to run it. */}
|
|
1319
|
+
{busy === 'activity' && runs === null ? (
|
|
1320
|
+
<p className={cn.muted}>Loading recent activity…</p>
|
|
1321
|
+
) : null}
|
|
1322
|
+
{runs !== null && runs.length === 0 ? (
|
|
1323
|
+
<p className={cn.muted}>No runs yet for this connection.</p>
|
|
1324
|
+
) : null}
|
|
1325
|
+
{(runs ?? []).map((run) => (
|
|
1326
|
+
<div key={run.id} data-run={run.id}>
|
|
1327
|
+
<p className={cn.muted}>
|
|
1328
|
+
<span className="font-medium text-gray-900">
|
|
1329
|
+
{RUN_KIND_COPY[run.kind] ?? run.kind}
|
|
1330
|
+
</span>
|
|
1331
|
+
{run.name ? ` · ${run.name}` : ''}
|
|
1332
|
+
{` · ${run.status}`}
|
|
1333
|
+
{run.occurred_at ? ` · ${new Date(run.occurred_at).toLocaleString()}` : ''}
|
|
1334
|
+
</p>
|
|
1335
|
+
{run.error_message ? (
|
|
1336
|
+
<p className={cn.error}>{run.error_message}</p>
|
|
1337
|
+
) : null}
|
|
1338
|
+
</div>
|
|
1339
|
+
))}
|
|
1340
|
+
</div>
|
|
1341
|
+
) : null}
|
|
1342
|
+
|
|
1237
1343
|
{settingsOpen ? (
|
|
1238
1344
|
<ConnectionSettingsForm
|
|
1239
1345
|
api={api}
|