Skip to content
Pilotoo
v0.1

Settings UI

React sections from @pilotoo/ui/settings cover the user and admin sides. Each takes a client from @pilotoo/chat-core; neither touches window or fetch by itself.

Component Audience Shows
ConnectionsSection Anyone with a connection role One block per app: the reader’s own accounts (a Google account that serves Gmail and Drive appears once), the organisation’s shared accounts, connect (OAuth, API key or fields), reconnect, disconnect.
ConnectionsAdminSection Connection admins Tabs Apps, Audit log and Health.
ConnectionsAppsSection Connection admins The Apps view on its own page.
ConnectionsAuditSection Connection admins The audit log on its own page.
ConnectionsHealthSection Connection admins Usage metrics and the health of the token vault on its own page.

An app is a shared OAuth credential type (Google, with Gmail, Drive and Calendar) or a provider on its own (GitHub, an API-key service). The admin sees one card per app with whether it needs setup, which services are on, and how many accounts it has and how many need reconnecting. An app page has three steps: Setup (the OAuth client, or nothing for an app whose accounts bring their own credentials), Services (a switch per provider, and an “N tools” control for its tool list and name patterns) and Accounts (every stored connection of the app, and “Connect an organisation account”). A host that gives each view its own route uses ConnectionsAppsSection with app (the shared credential type id or the provider id, null for the list) and onAppChange to keep the open app in its URL.

import { connectProvider, createConnectionAdminClient, createConnectionClient } from '@pilotoo/chat-core';
import { ConnectionsAdminSection, ConnectionsSection } from '@pilotoo/ui/settings';
const connections = createConnectionClient(apiUrl, fetch, { credentials });
const connectionAdmin = createConnectionAdminClient(apiUrl, fetch, { credentials });
<ConnectionsSection
client={connections}
onConnect={(provider) =>
connectProvider({
client: connections,
provider,
opener: browserOpener, // you supply it, see below
allowedOrigin: new URL(apiUrl, location.href).origin,
})
}
/>
<ConnectionsAdminSection
client={connectionAdmin}
onConnectShared={(provider) =>
connectProvider({ client: connectionAdmin, provider, opener: browserOpener, allowedOrigin: new URL(apiUrl, location.href).origin })
}
/>

The user section never displays a credential, including the API key the user just entered.

Prop On Meaning
client both ConnectionClient or ConnectionAdminClient.
onConnect(provider) user Runs the consent flow and resolves with the callback result.
onConnectShared(provider) admin The same for an organisation account: connectProvider with the admin client. Required.
callbackResult user The query result when the app navigated to returnUrl instead of using a popup.
labels both Partial ConnectionsLabels to translate or rename strings. Defaults are DEFAULT_CONNECTIONS_LABELS.
locale admin Formats dates and numbers. Defaults to the reader’s.
app, onAppChange(app) ConnectionsAppsSection The open app, as the shared credential type id or the provider id; null is the list. Give both to keep it in the host’s route; without them the section keeps it.

Showing the sections only to people who can use them

Section titled “Showing the sections only to people who can use them”

Both endpoints answer 403 to a caller without the role. The demo web app calls client.list() and shows the user section only when canConnect is true. It shows the admin section only when providers() succeeds. Do the same, and treat ConnectionError.kind === 'unavailable' (501) as “this server keeps no connections”.

connectProvider reserves a popup in the click, asks the server for the consent URL, navigates the popup and waits for the callback page’s message. You supply the window access as a ConnectionOpener:

Method Does
reserve() Opens the popup synchronously in the click (an await first would get it blocked). Returns { navigate, close, closed? }.
listen(listener) Subscribes to message events and returns an unsubscribe.
const browserOpener: ConnectionOpener = {
reserve() {
const popup = window.open('about:blank', 'pilotoo-connection', 'popup,width=520,height=720');
if (!popup) throw new Error('Allow pop-ups for this site and try again.');
return { navigate: (url) => (popup.location.href = url), close: () => popup.close(), closed: () => popup.closed };
},
listen(listener) {
const onMessage = (event: MessageEvent) => listener({ origin: event.origin, data: event.data });
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
},
};

allowedOrigin must be the API’s origin, because the callback page is served by the API. Messages from other origins, without type: 'pilotoo:connection', or for another provider are ignored. The app’s own origin must also be listed in the server’s CONNECTIONS_RETURN_ORIGINS for the page to post to it.

connectProvider rejects with a ConnectionCallbackError whose reason is invalid, aborted, closed or timeout (default ten minutes), or with a ConnectionError from the server. A refusal at the provider resolves with connection: 'error' and an error code instead.

Call client.authorize(provider, returnUrl) and navigate the window to authorizationUrl. The server redirects back to returnUrl with ?connection=connected|error&provider=...&error=...; pass those to callbackResult. The returnUrl origin must be in CONNECTIONS_RETURN_ORIGINS.

ConnectionClient ConnectionAdminClient
list() providers(), saveProvider(id, request), removeProvider(id)
authorize(provider, returnUrl?) connections(filter?), revoke(id), refresh(id)
setApiKey(provider, apiKey) authorizeShared(provider, returnUrl?), setSharedApiKey(provider, apiKey)
disconnect(id) setSharing(id, request), setDefault(id)
setDefault(id) audit(filter?), metrics(windowHours?), health(), rekey(limit?)

ConnectionAdminClient also has the optional gateway methods gateways(), createGateway(request), updateGateway(provider, request), deleteGateway(provider), refreshGateway(provider) and rotateGatewayWorkerToken(provider), which call the routes of Custom gateway.

Failures throw ConnectionError with status, a kind (invalid, forbidden, not-found, conflict, unavailable, http) and the server’s code. Branch on code, not on the message. See Chat UI for the rest of the settings sections and Server integration for the routes behind them.