@startsimpli/integrations 0.1.3 → 0.1.5
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 +11 -4
- package/src/__tests__/connections.test.tsx +116 -1
- package/src/__tests__/corpus-ask.test.ts +279 -0
- package/src/__tests__/corpus-pages.test.tsx +479 -0
- package/src/__tests__/corpus-scope.test.ts +216 -0
- package/src/components/index.ts +5 -0
- package/src/components/pages/AskPage.tsx +384 -0
- package/src/components/pages/ConnectionsPage.tsx +186 -2
- package/src/components/pages/LibraryPage.tsx +612 -0
- package/src/components/pages/types.ts +21 -0
- package/src/corpus/ask.ts +321 -0
- package/src/corpus/client.ts +196 -0
- package/src/corpus/entities.ts +346 -0
- package/src/corpus/fixtures.ts +486 -0
- package/src/corpus/index.ts +79 -0
- package/src/corpus/llm.ts +197 -0
- package/src/corpus/retrieval.ts +237 -0
- package/src/corpus/types.ts +354 -0
- package/src/index.ts +9 -0
|
@@ -60,6 +60,21 @@ const DEFAULTS = {
|
|
|
60
60
|
error: 'text-sm text-red-600',
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* WHAT AN ADMIN IS AGREEING TO, in their words rather than the wire's.
|
|
65
|
+
*
|
|
66
|
+
* Data, not a branch: an unknown key falls through to the key itself, because
|
|
67
|
+
* the capability vocabulary lives in the connector package and will outrun this
|
|
68
|
+
* bundle exactly the way the credential schema does. A capability this build has
|
|
69
|
+
* never heard of must render as a choice, not vanish from the list.
|
|
70
|
+
*/
|
|
71
|
+
const CAPABILITY_COPY: Record<string, string> = {
|
|
72
|
+
poll: 'Fetch new records on a schedule',
|
|
73
|
+
webhook_in: 'Receive events pushed to us',
|
|
74
|
+
action: 'Let us run actions in this account',
|
|
75
|
+
writeback_credential: 'Issue a token this vendor uses to call us',
|
|
76
|
+
};
|
|
77
|
+
|
|
63
78
|
/** A refusal must read as a refusal, not as an empty page. */
|
|
64
79
|
function readFailureMessage(err: unknown): string {
|
|
65
80
|
const status = (err as { status?: number })?.status;
|
|
@@ -120,6 +135,12 @@ export function ConnectionsPage({ api, foundrySlug, classNames = {} }: Connectio
|
|
|
120
135
|
api={api}
|
|
121
136
|
foundrySlug={foundrySlug}
|
|
122
137
|
connection={connection}
|
|
138
|
+
// What the CONNECTOR declares, so the card can offer a capability
|
|
139
|
+
// this connection does not have yet — which is the only way an
|
|
140
|
+
// account connected before the picker existed ever gets one.
|
|
141
|
+
declared={
|
|
142
|
+
catalog.find((entry) => entry.key === connection.connector_key)?.capabilities
|
|
143
|
+
}
|
|
123
144
|
canManage={permissions.manage}
|
|
124
145
|
classNames={cn}
|
|
125
146
|
onChanged={reload}
|
|
@@ -145,6 +166,75 @@ export function ConnectionsPage({ api, foundrySlug, classNames = {} }: Connectio
|
|
|
145
166
|
);
|
|
146
167
|
}
|
|
147
168
|
|
|
169
|
+
// --------------------------------------------------------------------------
|
|
170
|
+
// the capability choice (bd startsim-768w.22.22)
|
|
171
|
+
// --------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* WHICH VERBS THIS ACCOUNT IS ALLOWED — the control this page did not have.
|
|
175
|
+
*
|
|
176
|
+
* `POST /connections/` and `PATCH /connections/<id>/` have both accepted
|
|
177
|
+
* `capabilities_enabled[]` since the endpoints were designed. Nothing here ever
|
|
178
|
+
* sent one, so every connection made through the product was stored with an
|
|
179
|
+
* EMPTY list, and an empty list enables nothing: the card goes green on a real
|
|
180
|
+
* `check()` and can then do none of the things it advertises. That is how a
|
|
181
|
+
* connected Granola account on a live foundry ingested nothing forever.
|
|
182
|
+
*
|
|
183
|
+
* Rendered from `entry.capabilities` — the connector's own declaration, off the
|
|
184
|
+
* catalog row — so a new vendor's verbs appear here with no code change, and a
|
|
185
|
+
* verb this build has never heard of still renders.
|
|
186
|
+
*/
|
|
187
|
+
function CapabilityPicker({
|
|
188
|
+
scope, declared, selected, disabled, idPrefix, onToggle, classNames: cn, children,
|
|
189
|
+
}: {
|
|
190
|
+
scope: 'catalog' | 'connection';
|
|
191
|
+
declared: string[];
|
|
192
|
+
selected: string[];
|
|
193
|
+
disabled: boolean;
|
|
194
|
+
idPrefix: string;
|
|
195
|
+
onToggle: (capability: string, on: boolean) => void;
|
|
196
|
+
classNames: typeof DEFAULTS;
|
|
197
|
+
/** Whatever COMMITS the choice — inline, so the control and the thing that
|
|
198
|
+
saves it are one region for a screen reader and for a test. */
|
|
199
|
+
children?: React.ReactNode;
|
|
200
|
+
}) {
|
|
201
|
+
if (declared.length === 0) return null;
|
|
202
|
+
return (
|
|
203
|
+
<fieldset data-capabilities={scope} className="mt-3 space-y-1 border-0 p-0">
|
|
204
|
+
<legend className="text-sm font-medium text-gray-900">What this account may do</legend>
|
|
205
|
+
{declared.map((capability) => {
|
|
206
|
+
const id = `${idPrefix}-cap-${capability}`;
|
|
207
|
+
return (
|
|
208
|
+
<label key={capability} htmlFor={id} className="flex items-start gap-2 text-sm">
|
|
209
|
+
<input
|
|
210
|
+
id={id}
|
|
211
|
+
type="checkbox"
|
|
212
|
+
value={capability}
|
|
213
|
+
className="mt-0.5"
|
|
214
|
+
checked={selected.includes(capability)}
|
|
215
|
+
disabled={disabled}
|
|
216
|
+
onChange={(event) => onToggle(capability, event.target.checked)}
|
|
217
|
+
/>
|
|
218
|
+
<span>
|
|
219
|
+
<span className="text-gray-900">{capability}</span>
|
|
220
|
+
<span className={`ml-2 ${cn.muted}`}>
|
|
221
|
+
{CAPABILITY_COPY[capability] ?? ''}
|
|
222
|
+
</span>
|
|
223
|
+
</span>
|
|
224
|
+
</label>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
{children}
|
|
228
|
+
</fieldset>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Declaration order is the render order, and the wire order. */
|
|
233
|
+
function orderCapabilities(declared: string[], selected: Iterable<string>): string[] {
|
|
234
|
+
const set = new Set(selected);
|
|
235
|
+
return declared.filter((capability) => set.has(capability));
|
|
236
|
+
}
|
|
237
|
+
|
|
148
238
|
// --------------------------------------------------------------------------
|
|
149
239
|
// catalog
|
|
150
240
|
// --------------------------------------------------------------------------
|
|
@@ -163,6 +253,13 @@ function CatalogCard({
|
|
|
163
253
|
const [values, setValues] = React.useState<Record<string, string>>({});
|
|
164
254
|
const [busy, setBusy] = React.useState(false);
|
|
165
255
|
const [error, setError] = React.useState('');
|
|
256
|
+
// Every declared capability starts ON. Not because "on" is a safe default in
|
|
257
|
+
// general, but because the alternative here is a connection that can do
|
|
258
|
+
// nothing — and the list is right there, unchecked with one click, before
|
|
259
|
+
// anything is saved. The privacy control that is genuinely opt-in lives one
|
|
260
|
+
// level down (a connector's own config, e.g. whether a poll fetches meeting
|
|
261
|
+
// transcript bytes at all) and is unaffected by this box.
|
|
262
|
+
const [capabilities, setCapabilities] = React.useState<string[]>(entry.capabilities);
|
|
166
263
|
|
|
167
264
|
const credentialFields = schemaFields(entry.credential_schema);
|
|
168
265
|
const configFields = schemaFields(entry.config_schema);
|
|
@@ -186,9 +283,12 @@ function CatalogCard({
|
|
|
186
283
|
}
|
|
187
284
|
await api.connect(foundrySlug, {
|
|
188
285
|
connector_key: entry.key, name: entry.name, credentials, config,
|
|
286
|
+
// Always sent. Leaving it off is what wrote the inert cards.
|
|
287
|
+
capabilities_enabled: orderCapabilities(entry.capabilities, capabilities),
|
|
189
288
|
});
|
|
190
289
|
setOpen(false);
|
|
191
290
|
setValues({});
|
|
291
|
+
setCapabilities(entry.capabilities);
|
|
192
292
|
onConnected();
|
|
193
293
|
} catch (err) {
|
|
194
294
|
// The server's typed answer, verbatim. It already scrubbed itself of the
|
|
@@ -237,11 +337,34 @@ function CatalogCard({
|
|
|
237
337
|
}));
|
|
238
338
|
}}
|
|
239
339
|
/>
|
|
340
|
+
<CapabilityPicker
|
|
341
|
+
scope="catalog"
|
|
342
|
+
declared={entry.capabilities}
|
|
343
|
+
selected={capabilities}
|
|
344
|
+
disabled={busy}
|
|
345
|
+
idPrefix={`connect-${entry.key}`}
|
|
346
|
+
onToggle={(capability, on) => setCapabilities((prev) => (
|
|
347
|
+
on ? [...prev, capability] : prev.filter((c) => c !== capability)
|
|
348
|
+
))}
|
|
349
|
+
classNames={cn}
|
|
350
|
+
/>
|
|
351
|
+
{/* The server refuses an empty list with a 400 rather than storing a
|
|
352
|
+
card that can do nothing. Saying so here costs the admin a round
|
|
353
|
+
trip instead of a confusing error. */}
|
|
354
|
+
{entry.capabilities.length > 0 && capabilities.length === 0 ? (
|
|
355
|
+
<p className={cn.error}>
|
|
356
|
+
Enable at least one — a connection with nothing enabled can do nothing.
|
|
357
|
+
</p>
|
|
358
|
+
) : null}
|
|
240
359
|
{error ? <p className={cn.error} role="alert">{error}</p> : null}
|
|
241
360
|
<button
|
|
242
361
|
type="button"
|
|
243
362
|
className={cn.button}
|
|
244
|
-
disabled={
|
|
363
|
+
disabled={
|
|
364
|
+
busy
|
|
365
|
+
|| missing.length > 0
|
|
366
|
+
|| (entry.capabilities.length > 0 && capabilities.length === 0)
|
|
367
|
+
}
|
|
245
368
|
onClick={() => void submit()}
|
|
246
369
|
>
|
|
247
370
|
{busy ? 'Testing…' : 'Test and save'}
|
|
@@ -257,11 +380,12 @@ function CatalogCard({
|
|
|
257
380
|
// --------------------------------------------------------------------------
|
|
258
381
|
|
|
259
382
|
function ConnectionCard({
|
|
260
|
-
api, foundrySlug, connection, canManage, classNames: cn, onChanged,
|
|
383
|
+
api, foundrySlug, connection, declared, canManage, classNames: cn, onChanged,
|
|
261
384
|
}: {
|
|
262
385
|
api: ConnectionsApi;
|
|
263
386
|
foundrySlug: string;
|
|
264
387
|
connection: Connection;
|
|
388
|
+
declared?: string[];
|
|
265
389
|
canManage: boolean;
|
|
266
390
|
classNames: typeof DEFAULTS;
|
|
267
391
|
onChanged: () => void;
|
|
@@ -273,6 +397,26 @@ function ConnectionCard({
|
|
|
273
397
|
const [error, setError] = React.useState('');
|
|
274
398
|
const [notice, setNotice] = React.useState('');
|
|
275
399
|
|
|
400
|
+
const enabled = React.useMemo(
|
|
401
|
+
() => connection.capabilities_enabled ?? [], [connection.capabilities_enabled]);
|
|
402
|
+
// The catalog's declaration first, then anything this connection holds that
|
|
403
|
+
// the catalog no longer offers. A capability an admin cannot SEE is one they
|
|
404
|
+
// cannot turn off, and a catalog row that will not load must not blank the
|
|
405
|
+
// card's own state.
|
|
406
|
+
const offered = React.useMemo(() => {
|
|
407
|
+
const seen = new Set<string>();
|
|
408
|
+
return [...(declared ?? []), ...enabled].filter((capability) => {
|
|
409
|
+
if (seen.has(capability)) return false;
|
|
410
|
+
seen.add(capability);
|
|
411
|
+
return true;
|
|
412
|
+
});
|
|
413
|
+
}, [declared, enabled]);
|
|
414
|
+
const [selected, setSelected] = React.useState<string[]>(enabled);
|
|
415
|
+
React.useEffect(() => { setSelected(enabled); }, [enabled]);
|
|
416
|
+
|
|
417
|
+
const pending = orderCapabilities(offered, selected);
|
|
418
|
+
const dirty = pending.join(',') !== orderCapabilities(offered, enabled).join(',');
|
|
419
|
+
|
|
276
420
|
async function guard(label: string, run: () => Promise<void>) {
|
|
277
421
|
setBusy(label);
|
|
278
422
|
setError('');
|
|
@@ -330,6 +474,46 @@ function ConnectionCard({
|
|
|
330
474
|
) : null}
|
|
331
475
|
</div>
|
|
332
476
|
|
|
477
|
+
{/* THE FIX FOR THE CARDS THAT ALREADY EXIST. A connection written before
|
|
478
|
+
the connect form asked holds an empty list and can do nothing; the
|
|
479
|
+
connect form can never reach it. Turning `poll` on here is what makes
|
|
480
|
+
the server create the scheduled ingest leg. */}
|
|
481
|
+
<CapabilityPicker
|
|
482
|
+
scope="connection"
|
|
483
|
+
declared={offered}
|
|
484
|
+
selected={selected}
|
|
485
|
+
disabled={!canManage || Boolean(busy)}
|
|
486
|
+
idPrefix={`connection-${connection.id}`}
|
|
487
|
+
onToggle={(capability, on) => setSelected((prev) => (
|
|
488
|
+
on ? [...prev, capability] : prev.filter((c) => c !== capability)
|
|
489
|
+
))}
|
|
490
|
+
classNames={cn}
|
|
491
|
+
>
|
|
492
|
+
{canManage ? (
|
|
493
|
+
<div className="mt-2 flex items-center gap-3">
|
|
494
|
+
<button
|
|
495
|
+
type="button"
|
|
496
|
+
className={cn.button}
|
|
497
|
+
disabled={!dirty || pending.length === 0 || Boolean(busy)}
|
|
498
|
+
onClick={() => void guard('capabilities', async () => {
|
|
499
|
+
await api.updateConnection(foundrySlug, connection.id, {
|
|
500
|
+
capabilities_enabled: pending,
|
|
501
|
+
});
|
|
502
|
+
setNotice('Saved.');
|
|
503
|
+
onChanged();
|
|
504
|
+
})}
|
|
505
|
+
>
|
|
506
|
+
{busy === 'capabilities' ? 'Saving…' : 'Save'}
|
|
507
|
+
</button>
|
|
508
|
+
{pending.length === 0 ? (
|
|
509
|
+
<span className={cn.error}>
|
|
510
|
+
Enable at least one, or disconnect the account.
|
|
511
|
+
</span>
|
|
512
|
+
) : null}
|
|
513
|
+
</div>
|
|
514
|
+
) : null}
|
|
515
|
+
</CapabilityPicker>
|
|
516
|
+
|
|
333
517
|
{notice ? <p className={cn.muted}>{notice}</p> : null}
|
|
334
518
|
{error ? <p className={cn.error} role="alert">{error}</p> : null}
|
|
335
519
|
|