Ship Composio as a built-in integration and trim Settings
- Resolve the API key from cred.txt at the project root in development and the packaged resources directory in production; the file is the source of truth, so a rotated key replaces the stored one. Fixes the 401 packaged builds hit when a stale database key won. - Remove the user-facing Composio key editor; the key ships with the product and is not configurable. - Remove the Migration console from Settings. - Remove the Connectors and Home refresh buttons; connector statuses sync when the page opens.
This commit is contained in:
1 parent
947cd8605b
commit
256e0680c6
16 files changed
+53
-396
No files matched your search
@@ -66,6 +66,12 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
|
||||
if (pollTimer.current) clearTimeout(pollTimer.current);
|
||||
}, []);
|
||||
|
||||
// Account statuses sync from Composio when the page opens; a failure here
|
||||
// is already expressed by the catalog error state.
|
||||
useEffect(() => {
|
||||
void window.agentCompany.refreshComposioAccounts({ orgId });
|
||||
}, [orgId]);
|
||||
|
||||
const refreshAccounts = () =>
|
||||
actions.run(() => window.agentCompany.refreshComposioAccounts({ orgId }));
|
||||
|
||||
@@ -104,17 +110,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1060 }}>
|
||||
<div className="row" style={{ marginBottom: 18 }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: 16, fontWeight: 600 }}>Connectors</h2>
|
||||
<div style={{ fontSize: 12.5, color: "var(--text-3)", marginTop: 2 }}>
|
||||
Company connections your agents can be granted to use.
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: "auto" }}>
|
||||
<Button size="small" onClick={() => void refreshAccounts()}>Refresh</Button>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style={{ margin: "0 0 18px", fontSize: 16, fontWeight: 600 }}>Connectors</h2>
|
||||
|
||||
<ConnectedList accounts={accounts} grants={grants} agents={agents} onGrant={grant} onRevoke={revoke} />
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
|
||||
return (
|
||||
<div className="page-wide" style={{ maxWidth: 1240 }}>
|
||||
<TopbarActions>
|
||||
<Button tone="ghost" onClick={() => void actions.reload()} title="Refresh"><Icon name="refresh" size={16} /></Button>
|
||||
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" size={16} /> New task</Button>
|
||||
</TopbarActions>
|
||||
<h1 className="page-h1">{greeting}, {firstName}</h1>
|
||||
|
||||
@@ -43,8 +43,6 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
const workspaces = useMemo(() => snapshot.workspaces.filter((item) => item.orgId === orgId), [snapshot.workspaces, orgId]);
|
||||
const providerRequests = useMemo(() => snapshot.providerRequests.filter((item) => item.orgId === orgId), [snapshot.providerRequests, orgId]);
|
||||
const hiringPolicies = useMemo(() => snapshot.hiringPolicies.filter((item) => item.orgId === orgId), [snapshot.hiringPolicies, orgId]);
|
||||
const migrationReports = useMemo(() => snapshot.migrationReports.filter((report) => report.orgId === orgId), [snapshot.migrationReports, orgId]);
|
||||
const rollbackSnapshots = useMemo(() => snapshot.rollbackSnapshots.filter((item) => item.orgId === orgId), [snapshot.rollbackSnapshots, orgId]);
|
||||
|
||||
return (
|
||||
<div className="split-main">
|
||||
@@ -83,17 +81,12 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
{tab === "advanced" && (
|
||||
<AdvancedPanel
|
||||
orgId={orgId}
|
||||
employeeId={employee.id}
|
||||
agents={agents}
|
||||
providers={providers}
|
||||
catalog={snapshot.providerCatalog}
|
||||
workspaces={workspaces}
|
||||
providerRequests={providerRequests}
|
||||
hiringPolicies={hiringPolicies}
|
||||
composioSettings={snapshot.composioSettings}
|
||||
migrationReports={migrationReports}
|
||||
rollbackSnapshots={rollbackSnapshots}
|
||||
reconciliationItems={snapshot.migrationReconciliationItems.filter((item) => item.orgId === orgId)}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,49 +1,33 @@
|
||||
import type {
|
||||
AgentRecord,
|
||||
HiringPolicyRecord,
|
||||
MigrationReportRecord,
|
||||
ProviderCatalogEntry,
|
||||
ProviderRecord,
|
||||
ProviderRequestRecord,
|
||||
RollbackSnapshotRecord,
|
||||
WorkspaceRecord,
|
||||
} from "../../../../shared/domain";
|
||||
import type { MigrationReconciliationItemRecord } from "../../../../shared/rollback-domain";
|
||||
import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import { BillingPanel } from "./BillingPanel";
|
||||
import { ComposioPanel } from "./ComposioPanel";
|
||||
import { MigrationPanel } from "./MigrationPanel";
|
||||
import { ProviderRequestPanel } from "./ProviderRequestPanel";
|
||||
|
||||
/** The low-frequency operator surfaces, one section each. */
|
||||
export function AdvancedPanel({
|
||||
orgId,
|
||||
employeeId,
|
||||
agents,
|
||||
providers,
|
||||
catalog,
|
||||
workspaces,
|
||||
providerRequests,
|
||||
hiringPolicies,
|
||||
composioSettings,
|
||||
migrationReports,
|
||||
rollbackSnapshots,
|
||||
reconciliationItems,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
employeeId: string;
|
||||
agents: AgentRecord[];
|
||||
providers: ProviderRecord[];
|
||||
catalog: ProviderCatalogEntry[];
|
||||
workspaces: WorkspaceRecord[];
|
||||
providerRequests: ProviderRequestRecord[];
|
||||
hiringPolicies: HiringPolicyRecord[];
|
||||
composioSettings: ComposioSettingRecord[];
|
||||
migrationReports: MigrationReportRecord[];
|
||||
rollbackSnapshots: RollbackSnapshotRecord[];
|
||||
reconciliationItems: MigrationReconciliationItemRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
const connected = providers.filter((provider) => provider.enabled);
|
||||
@@ -51,11 +35,8 @@ export function AdvancedPanel({
|
||||
<div className="page">
|
||||
<h1 className="page-h1">Advanced</h1>
|
||||
|
||||
<div className="section-label" style={{ margin: "16px 0 10px" }}>Composio</div>
|
||||
<ComposioPanel orgId={orgId} settings={composioSettings} actions={actions} />
|
||||
|
||||
{connected.length > 0 && (
|
||||
<div style={{ marginTop: 28 }}>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<ProviderRequestPanel
|
||||
orgId={orgId}
|
||||
providers={connected}
|
||||
@@ -70,17 +51,6 @@ export function AdvancedPanel({
|
||||
<div style={{ marginTop: 28 }}>
|
||||
<BillingPanel agents={agents} providers={providers} providerRequests={providerRequests} hiringPolicies={hiringPolicies} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 28 }}>
|
||||
<MigrationPanel
|
||||
orgId={orgId}
|
||||
employeeId={employeeId}
|
||||
reports={migrationReports}
|
||||
rollbackSnapshots={rollbackSnapshots}
|
||||
reconciliationItems={reconciliationItems}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useMemo, useState, type FormEvent } from "react";
|
||||
import { Button } from "../../shared/ui";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain";
|
||||
|
||||
function hasSavedKey(settings: ComposioSettingRecord[], orgId: string): boolean {
|
||||
return settings.some((s) => s.orgId === orgId && s.key === "api_key" && s.value.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Composio project API key. Which toolkits agents may use is decided by
|
||||
* connections and per-agent grants on the Connectors page, not here.
|
||||
*/
|
||||
export function ComposioPanel({
|
||||
orgId,
|
||||
settings,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
settings: ComposioSettingRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
const keyConfigured = useMemo(() => hasSavedKey(settings, orgId), [settings, orgId]);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
|
||||
const handleSave = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const key = apiKey.trim();
|
||||
if (!key) return;
|
||||
void actions.run(() => window.agentCompany.saveComposioSettings({
|
||||
orgId,
|
||||
apiKey: key,
|
||||
toolkits: [],
|
||||
})).then(() => setApiKey(""));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSave} className="row" style={{ gap: 8, maxWidth: 480 }}>
|
||||
<input
|
||||
type="password"
|
||||
className="input"
|
||||
aria-label="Composio API key"
|
||||
value={apiKey}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
placeholder={keyConfigured ? "••••••••" : "ak_..."}
|
||||
autoComplete="off"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button tone="primary" type="submit" disabled={!apiKey.trim()}>Save</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Button, Field, Icon, Pill } from "../../shared/ui";
|
||||
import { Dialog } from "../../shared/Dialog";
|
||||
import type { MigrationReconciliationItemRecord, MigrationReportRecord, RollbackSnapshotRecord } from "../../../../shared/domain";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import { formatRelative } from "../types";
|
||||
|
||||
interface ParsedReport {
|
||||
tableCounts: Record<string, number>;
|
||||
tableChecksums: Record<string, string>;
|
||||
idMaps: Record<string, string[]>;
|
||||
failures: Array<{ table: string; reason: string }>;
|
||||
rollbackPack?: { sourceDatabasePath: string; capturedAt: string; restoreStrategy: string };
|
||||
}
|
||||
|
||||
export function MigrationPanel({
|
||||
orgId,
|
||||
employeeId,
|
||||
reports,
|
||||
rollbackSnapshots,
|
||||
reconciliationItems,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
employeeId: string;
|
||||
reports: MigrationReportRecord[];
|
||||
rollbackSnapshots: RollbackSnapshotRecord[];
|
||||
reconciliationItems: MigrationReconciliationItemRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
void actions.run(() => window.agentCompany.createMigrationReport({
|
||||
orgId,
|
||||
sourceDatabasePath: String(form.get("sourceDatabasePath") ?? "").trim(),
|
||||
})).then((result) => { if (result) setOpen(false); });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="reveal-row">
|
||||
<div className="row" style={{ marginBottom: 10 }}>
|
||||
<div className="section-label">Migration</div>
|
||||
<div className="reveal" style={{ marginLeft: "auto" }}>
|
||||
<Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" size={16} /> Report</Button>
|
||||
</div>
|
||||
</div>
|
||||
{reports.length > 0 && (
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
{reports.map((report, index) => (
|
||||
<ReportRow
|
||||
key={report.id}
|
||||
orgId={orgId}
|
||||
employeeId={employeeId}
|
||||
report={report}
|
||||
actions={actions}
|
||||
last={index === reports.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<section className="card" style={{ marginTop: 16, padding: 0, overflow: "hidden" }}>
|
||||
<div className="row" style={{ padding: "14px 16px", borderBottom: "1px solid var(--border-soft)" }}>
|
||||
<Icon name="refresh" size={14} color="var(--text-3)" />
|
||||
<div style={{ fontSize: 14, fontWeight: 600 }}>Rollback snapshots</div>
|
||||
</div>
|
||||
{rollbackSnapshots.length > 0 ? rollbackSnapshots.map((snapshot, index) => (
|
||||
<RollbackRow
|
||||
key={snapshot.id}
|
||||
orgId={orgId}
|
||||
employeeId={employeeId}
|
||||
snapshot={snapshot}
|
||||
actions={actions}
|
||||
last={index === rollbackSnapshots.length - 1}
|
||||
/>
|
||||
)) : <div style={{ padding: 16 }}><Pill>0 rollback snapshots</Pill></div>}
|
||||
</section>
|
||||
<section className="card" style={{ marginTop: 16, padding: 0, overflow: "hidden" }}>
|
||||
<div className="row" style={{ padding: "14px 16px", borderBottom: "1px solid var(--border-soft)" }}>
|
||||
<Icon name="list" size={14} color="var(--text-3)" />
|
||||
<div style={{ fontSize: 14, fontWeight: 600 }}>Reconciliation</div>
|
||||
</div>
|
||||
{reconciliationItems.length > 0 ? reconciliationItems.map((item, index) => (
|
||||
<ReconciliationRow
|
||||
key={item.id}
|
||||
orgId={orgId}
|
||||
employeeId={employeeId}
|
||||
item={item}
|
||||
actions={actions}
|
||||
last={index === reconciliationItems.length - 1}
|
||||
/>
|
||||
)) : <div style={{ padding: 16 }}><Pill>0 reconciliation items</Pill></div>}
|
||||
</section>
|
||||
<Dialog title="Migration report" open={open} submitLabel="Create" onClose={() => setOpen(false)} onSubmit={submit}>
|
||||
<Field label="Source database path"><input className="input" name="sourceDatabasePath" required /></Field>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReportRow({
|
||||
orgId,
|
||||
employeeId,
|
||||
report,
|
||||
actions,
|
||||
last,
|
||||
}: {
|
||||
orgId: string;
|
||||
employeeId: string;
|
||||
report: MigrationReportRecord;
|
||||
actions: SnapshotActions;
|
||||
last: boolean;
|
||||
}) {
|
||||
const body = parseReport(report.reportJson);
|
||||
const idCount = Object.values(body.idMaps).reduce((sum, ids) => sum + ids.length, 0);
|
||||
return (
|
||||
<div
|
||||
className="table-row"
|
||||
style={{ gridTemplateColumns: "minmax(0, 1.35fr) minmax(0, 1fr) 120px 88px 96px", borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={report.sourceRef}>{report.sourceRef}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(report.createdAt)}</div>
|
||||
</div>
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: 4 }}>
|
||||
{Object.entries(body.tableCounts).map(([table, count]) => (
|
||||
<Pill key={table}>{table}: {count}</Pill>
|
||||
))}
|
||||
</div>
|
||||
<Pill>{Object.keys(body.tableChecksums).length} checksums</Pill>
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: 4 }}>
|
||||
<Pill>{idCount} ids</Pill>
|
||||
{body.failures.length > 0 ? <Pill tone="amber">{body.failures.length} failures</Pill> : null}
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={!body.rollbackPack}
|
||||
onClick={() => void actions.run(() => window.agentCompany.createRollbackSnapshot({ orgId, migrationReportId: report.id, createdByEmployeeId: employeeId }))}
|
||||
>
|
||||
Snapshot
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RollbackRow({
|
||||
orgId,
|
||||
employeeId,
|
||||
snapshot,
|
||||
actions,
|
||||
last,
|
||||
}: {
|
||||
orgId: string;
|
||||
employeeId: string;
|
||||
snapshot: RollbackSnapshotRecord;
|
||||
actions: SnapshotActions;
|
||||
last: boolean;
|
||||
}) {
|
||||
const manifest = JSON.parse(snapshot.workspaceManifestJson) as { workspaces: unknown[]; files: unknown[] };
|
||||
return (
|
||||
<div
|
||||
className="table-row"
|
||||
style={{ gridTemplateColumns: "minmax(0, 1.2fr) minmax(0, .8fr) 96px 96px 72px", borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={snapshot.sourceDatabasePath}>{snapshot.sourceDatabasePath}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(snapshot.createdAt)}</div>
|
||||
</div>
|
||||
<Pill>{snapshot.sourceDatabaseSha256.slice(0, 12)}</Pill>
|
||||
<Pill>{manifest.workspaces.length} workspaces</Pill>
|
||||
<Pill>{manifest.files.length} files</Pill>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={snapshot.active}
|
||||
onClick={() => void actions.run(() => window.agentCompany.setRollbackSnapshotActive({ orgId, snapshotId: snapshot.id, active: true, employeeId }))}
|
||||
>
|
||||
{snapshot.active ? "active" : "Set"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReconciliationRow({
|
||||
orgId,
|
||||
employeeId,
|
||||
item,
|
||||
actions,
|
||||
last,
|
||||
}: {
|
||||
orgId: string;
|
||||
employeeId: string;
|
||||
item: MigrationReconciliationItemRecord;
|
||||
actions: SnapshotActions;
|
||||
last: boolean;
|
||||
}) {
|
||||
const resolved = item.state === "resolved";
|
||||
return (
|
||||
<div
|
||||
className="table-row"
|
||||
style={{ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr) 86px 112px 84px", borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{item.targetType} · {item.targetId}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(item.createdAt)}</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{item.reason}</div>
|
||||
<Pill tone={resolved ? "green" : "amber"}>{item.state}</Pill>
|
||||
<Pill>{item.sourceEventId.slice(0, 12)}</Pill>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={resolved}
|
||||
onClick={() => void actions.run(() => window.agentCompany.resolveMigrationReconciliation({ orgId, reconciliationItemId: item.id, employeeId }))}
|
||||
>
|
||||
{resolved ? "Resolved" : "Resolve"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseReport(reportJson: string): ParsedReport {
|
||||
const parsed = JSON.parse(reportJson) as ParsedReport;
|
||||
if (!parsed.tableCounts || !parsed.tableChecksums || !parsed.idMaps || !Array.isArray(parsed.failures)) {
|
||||
throw new Error("Migration report is invalid.");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
Reference in new issue
Block a user