@@ -1846,6 +1954,8 @@ function ConnectionActions({
}) {
const [open, setOpen] = useState(false);
const [working, setWorking] = useState(false);
+ const [globalWorking, setGlobalWorking] = useState(false);
+ const [globalMessage, setGlobalMessage] = useState("");
async function logout() {
setWorking(true);
@@ -1858,6 +1968,20 @@ function ConnectionActions({
}
}
+ async function makeGlobal() {
+ setGlobalWorking(true);
+ setGlobalMessage("");
+ try {
+ await setGlobalConnection(data.provider, data.connection_name);
+ setGlobalMessage("Global connection updated.");
+ onRefresh();
+ } catch (error) {
+ setGlobalMessage(error instanceof Error ? error.message : "Global connection could not be updated.");
+ } finally {
+ setGlobalWorking(false);
+ }
+ }
+
return (
<>
@@ -1871,6 +1995,13 @@ function ConnectionActions({
) : null}
+ {data.can_set_global ? (
+
+ ) : null}
+ {globalMessage ?
{globalMessage}
: null}
View provider
@@ -1899,13 +2030,24 @@ function ConnectionActions({
);
}
-function ActiveView({ connectionFilter, data, view }: {
+function ActiveView({ connectionFilter, data, onRefresh, view }: {
connectionFilter?: string;
data: DashboardData;
+ onRefresh: () => void;
view: View;
}) {
if (view === "providers") return
;
- if (view === "connections") return
;
+ if (view === "connections") {
+ return (
+
+ );
+ }
if (view === "agents") return
;
if (view === "principals") return
;
if (view === "vault") return
;
@@ -1943,6 +2085,7 @@ export function AuthsomeDashboard({ connectionFilter, view = "dashboard" }: { co
void mutate()}
view={activeView}
/>
diff --git a/ui/src/lib/authsome-api.ts b/ui/src/lib/authsome-api.ts
index 386f5273..ab130baa 100644
--- a/ui/src/lib/authsome-api.ts
+++ b/ui/src/lib/authsome-api.ts
@@ -18,6 +18,7 @@ export type ProviderView = {
status: "available" | "connected" | "reauth" | string;
scopeCount: number;
connectionCount: number;
+ globalConnectionCount: number;
requiresNamedLogin: boolean;
};
@@ -29,6 +30,10 @@ export type ConnectionRow = {
authTypeLabel: string;
};
+export type GlobalConnectionRow = ConnectionRow & {
+ accountLabel: string | null;
+};
+
export type IdentityRow = {
handle: string;
isActive: boolean;
@@ -59,6 +64,7 @@ export type DashboardData = {
providers: ProviderView[];
connectedProviders: ProviderView[];
connections: ConnectionRow[];
+ globalConnections: GlobalConnectionRow[];
identities: IdentityRow[];
vault: {
vaultId: string | null;
@@ -97,6 +103,17 @@ type ConnectionSummary = {
expires_at?: string | null;
};
+type GlobalConnectionSummary = {
+ provider: string;
+ provider_display_name: string;
+ connection_name: string;
+ status: string;
+ auth_type: string;
+ account_label: string | null;
+ api_url?: string | null;
+ source: "global";
+};
+
export type ProviderResponse = {
name: string;
display_name?: string;
@@ -185,6 +202,7 @@ export type ConnectionDetail = {
credentials: Record
;
};
can_set_default: boolean;
+ can_set_global: boolean;
};
type ConnectionsResponse = {
@@ -192,6 +210,7 @@ type ConnectionsResponse = {
name: string;
connections: ConnectionSummary[];
}>;
+ global_connections: GlobalConnectionSummary[];
by_source: Record;
};
@@ -318,11 +337,11 @@ function providerApiUrl(provider: ProviderResponse): string {
return provider.api_url || provider.oauth?.base_url || provider.name;
}
-function providerStatus(connections: ConnectionSummary[]): ProviderView["status"] {
- if (!connections.length) {
+function providerStatus(connections: ConnectionSummary[], globalConnections: GlobalConnectionSummary[]): ProviderView["status"] {
+ if (!connections.length && !globalConnections.length) {
return "available";
}
- return connections.some((connection) => ["error", "expired"].includes(connection.status || ""))
+ return [...connections, ...globalConnections].some((connection) => ["error", "expired"].includes(connection.status || ""))
? "reauth"
: "connected";
}
@@ -331,6 +350,7 @@ function providerView(
provider: ProviderResponse,
source: string,
connections: ConnectionSummary[],
+ globalConnections: GlobalConnectionSummary[],
): ProviderView {
const displayName = provider.display_name || provider.name;
return {
@@ -343,17 +363,26 @@ function providerView(
source,
logo: provider.logo || null,
logoInitial: (displayName[0] || "?").toUpperCase(),
- status: providerStatus(connections),
+ status: providerStatus(connections, globalConnections),
scopeCount: connections[0]?.scopes?.length || 0,
- connectionCount: connections.length,
+ connectionCount: connections.length + globalConnections.length,
+ globalConnectionCount: globalConnections.length,
requiresNamedLogin: connections.some((connection) => connection.connection_name === "default"),
};
}
function buildProviders(data: ConnectionsResponse): ProviderView[] {
const connectionMap = new Map(data.connections.map((group) => [group.name, group.connections]));
+ const globalConnectionMap = new Map();
+ for (const connection of data.global_connections || []) {
+ const entries = globalConnectionMap.get(connection.provider) || [];
+ entries.push(connection);
+ globalConnectionMap.set(connection.provider, entries);
+ }
return Object.entries(data.by_source).flatMap(([source, providers]) =>
- providers.map((provider) => providerView(provider, source, connectionMap.get(provider.name) || [])),
+ providers.map((provider) =>
+ providerView(provider, source, connectionMap.get(provider.name) || [], globalConnectionMap.get(provider.name) || []),
+ ),
);
}
@@ -373,6 +402,19 @@ function buildConnectionRows(data: ConnectionsResponse, providers: ProviderView[
.sort((a, b) => `${a.providerDisplayName}:${a.connectionName}`.localeCompare(`${b.providerDisplayName}:${b.connectionName}`));
}
+function buildGlobalConnectionRows(data: ConnectionsResponse): GlobalConnectionRow[] {
+ return (data.global_connections || [])
+ .map((connection) => ({
+ providerName: connection.provider,
+ providerDisplayName: connection.provider_display_name,
+ connectionName: connection.connection_name,
+ status: connection.status || "unknown",
+ authTypeLabel: authTypeLabel(connection.auth_type),
+ accountLabel: connection.account_label,
+ }))
+ .sort((a, b) => `${a.providerDisplayName}:${a.connectionName}`.localeCompare(`${b.providerDisplayName}:${b.connectionName}`));
+}
+
function formatRelative(value: string | null | undefined): string | null {
if (!value) {
return null;
@@ -457,6 +499,7 @@ export async function fetchDashboard(): Promise {
const audit = isAdmin ? await requestJson("/api/audit/events?limit=100") : { entries: [] };
const providers = buildProviders(connectionsData);
const connections = buildConnectionRows(connectionsData, providers);
+ const globalConnections = buildGlobalConnectionRows(connectionsData);
const connectedProviders = providers.filter((provider) => provider.status !== "available");
const activeIdentity = whoami.identity || whoami.active_identity || null;
const identityHandles = new Set(identitiesData.identities.map((identity) => identity.handle));
@@ -483,6 +526,7 @@ export async function fetchDashboard(): Promise {
providers,
connectedProviders: connectedProviders.slice(0, 6),
connections,
+ globalConnections,
identities: Array.from(identityHandles, (handle) => ({ handle, isActive: handle === activeIdentity })),
vault: {
vaultId: whoami.vault_id || null,
@@ -555,6 +599,23 @@ export async function logoutConnection(
});
}
+export async function setGlobalConnection(
+ provider: string,
+ connection: string,
+): Promise<{ status: string; provider: string; connection_name: string }> {
+ return sendJson(`/api/connections/${encodeURIComponent(provider)}/${encodeURIComponent(connection)}/global`, {
+ method: "POST",
+ body: "{}",
+ });
+}
+
+export async function unsetGlobalConnection(provider: string): Promise<{ status: string; provider: string; deleted: boolean }> {
+ return sendJson(`/api/connections/${encodeURIComponent(provider)}/global`, {
+ method: "DELETE",
+ body: "{}",
+ });
+}
+
export async function revokeProvider(provider: string): Promise<{ status: string; provider?: string }> {
return sendJson(`/api/connections/${encodeURIComponent(provider)}/revoke`, {
method: "POST",