From ba8008dc04590bc255121953787d21c9c4bb3375 Mon Sep 17 00:00:00 2001 From: Yue Chen Date: Sat, 23 May 2026 22:39:41 -0700 Subject: [PATCH] feat(dashboard): show admin bot profiles --- dashboard/src/pages/AdminBotPage.test.tsx | 100 ++++++++++++++++++++++ dashboard/src/pages/AdminBotPage.tsx | 67 +++++++++++++-- dashboard/src/pages/NewProfile.test.tsx | 57 ++++++++++++ dashboard/src/pages/NewProfile.tsx | 26 +++++- 4 files changed, 241 insertions(+), 9 deletions(-) create mode 100644 dashboard/src/pages/AdminBotPage.test.tsx create mode 100644 dashboard/src/pages/NewProfile.test.tsx diff --git a/dashboard/src/pages/AdminBotPage.test.tsx b/dashboard/src/pages/AdminBotPage.test.tsx new file mode 100644 index 0000000000..fa06aa1607 --- /dev/null +++ b/dashboard/src/pages/AdminBotPage.test.tsx @@ -0,0 +1,100 @@ +import { render, screen } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import AdminBotPage from './AdminBotPage' +import type { ProfileResponse } from '../types' + +const mockListProfiles = vi.fn() +const mockMonitorStatus = vi.fn() +const mockToggleWatchdog = vi.fn() +const mockToggleAlerts = vi.fn() + +vi.mock('../api', () => ({ + api: { + listProfiles: () => mockListProfiles(), + monitorStatus: () => mockMonitorStatus(), + toggleWatchdog: (enabled: boolean) => mockToggleWatchdog(enabled), + toggleAlerts: (enabled: boolean) => mockToggleAlerts(enabled), + }, +})) + +function profile( + id: string, + name: string, + adminMode: boolean, + running: boolean, +): ProfileResponse { + return { + id, + name, + enabled: true, + data_dir: null, + parent_id: null, + public_subdomain: null, + config: { + channels: [], + gateway: {}, + env_vars: {}, + admin_mode: adminMode, + }, + created_at: '2026-05-01T00:00:00Z', + updated_at: '2026-05-01T00:00:00Z', + status: { + running, + pid: running ? 123 : null, + started_at: running ? '2026-05-01T00:00:00Z' : null, + uptime_secs: running ? 30 : null, + }, + email: null, + } +} + +function renderPage() { + return render( + + + , + ) +} + +describe('AdminBotPage', () => { + beforeEach(() => { + vi.clearAllMocks() + mockMonitorStatus.mockResolvedValue({ + watchdog_enabled: true, + alerts_enabled: false, + }) + }) + + it('lists admin-mode profiles and highlights the running admin bot', async () => { + mockListProfiles.mockResolvedValue([ + profile('ops-admin', 'Ops Admin', true, true), + profile('backup-admin', 'Backup Admin', true, false), + profile('regular-user', 'Regular User', false, true), + ]) + + renderPage() + + expect(await screen.findAllByText('Ops Admin')).toHaveLength(2) + expect(screen.getByText('Backup Admin')).toBeInTheDocument() + expect(screen.queryByText('Regular User')).not.toBeInTheDocument() + expect(screen.getByText('Running')).toBeInTheDocument() + expect(screen.getByText('Stopped')).toBeInTheDocument() + expect(screen.getByText('Active admin bot:')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Create admin profile' })).toHaveAttribute( + 'href', + '/profiles/new?adminMode=true', + ) + }) + + it('shows an empty state when no admin-mode profile exists', async () => { + mockListProfiles.mockResolvedValue([ + profile('regular-user', 'Regular User', false, true), + ]) + + renderPage() + + expect(await screen.findByText('No admin-mode profiles found.')).toBeInTheDocument() + expect(screen.getByText('None running')).toBeInTheDocument() + }) +}) diff --git a/dashboard/src/pages/AdminBotPage.tsx b/dashboard/src/pages/AdminBotPage.tsx index 6ef6d7df0d..187edc7f03 100644 --- a/dashboard/src/pages/AdminBotPage.tsx +++ b/dashboard/src/pages/AdminBotPage.tsx @@ -1,17 +1,23 @@ import { useState, useEffect, useCallback } from 'react' +import { Link } from 'react-router-dom' import { api } from '../api' import { useToast } from '../components/Toast' -import type { MonitorStatus } from '../types' +import type { MonitorStatus, ProfileResponse } from '../types' export default function AdminBotPage() { const { toast } = useToast() const [loading, setLoading] = useState(true) const [monitorStatus, setMonitorStatus] = useState({ watchdog_enabled: false, alerts_enabled: false }) + const [profiles, setProfiles] = useState([]) const loadData = useCallback(async () => { try { - const monitor = await api.monitorStatus().catch(() => ({ watchdog_enabled: false, alerts_enabled: false })) + const [monitor, profileList] = await Promise.all([ + api.monitorStatus().catch(() => ({ watchdog_enabled: false, alerts_enabled: false })), + api.listProfiles(), + ]) setMonitorStatus(monitor) + setProfiles(profileList) } catch (e: any) { toast(e.message, 'error') } finally { @@ -51,6 +57,9 @@ export default function AdminBotPage() { ) } + const adminProfiles = profiles.filter((profile) => profile.config.admin_mode) + const activeAdminProfile = adminProfiles.find((profile) => profile.status.running) + return (
@@ -79,12 +88,54 @@ export default function AdminBotPage() {
-

Admin Bot Profile

-

- To set up an admin bot, create a regular profile and enable Admin Mode in - its settings. Admin mode restricts the gateway to admin-only tools (profile management, - monitoring, logs) and uses a built-in admin system prompt. -

+
+
+

Admin Bot Profile

+

+ Active admin bot:{' '} + + {activeAdminProfile ? activeAdminProfile.name : 'None running'} + +

+
+ + Create admin profile + +
+ + {adminProfiles.length > 0 ? ( +
+ {adminProfiles.map((profile) => ( +
+
+ + {profile.name} + +

{profile.id}

+
+ + {profile.status.running ? 'Running' : 'Stopped'} + +
+ ))} +
+ ) : ( +
+ No admin-mode profiles found. +
+ )}
) diff --git a/dashboard/src/pages/NewProfile.test.tsx b/dashboard/src/pages/NewProfile.test.tsx new file mode 100644 index 0000000000..54175aa302 --- /dev/null +++ b/dashboard/src/pages/NewProfile.test.tsx @@ -0,0 +1,57 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter, Route, Routes } from 'react-router-dom' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import NewProfile from './NewProfile' + +const mockCreateProfile = vi.fn() + +vi.mock('../api', () => ({ + api: { + createProfile: (data: unknown) => mockCreateProfile(data), + }, +})) + +function renderNewProfile(path: string) { + return render( + + + } /> + Profile created} /> + + , + ) +} + +describe('NewProfile admin mode shortcut', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCreateProfile.mockResolvedValue({}) + }) + + it('preselects admin mode from the query string and submits admin config', async () => { + const user = userEvent.setup() + renderNewProfile('/profiles/new?adminMode=true') + + expect(screen.getByRole('checkbox', { name: 'Admin Mode' })).toBeChecked() + + await user.type(screen.getAllByPlaceholderText('alice-bot')[0], 'ops-admin') + await user.type(screen.getByPlaceholderText("Alice's Bot"), 'Ops Admin') + await user.click(screen.getByRole('button', { name: 'Create Profile' })) + + await waitFor(() => { + expect(mockCreateProfile).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'ops-admin', + name: 'Ops Admin', + config: expect.objectContaining({ + admin_mode: true, + channels: [], + gateway: {}, + env_vars: {}, + }), + }), + ) + }) + }) +}) diff --git a/dashboard/src/pages/NewProfile.tsx b/dashboard/src/pages/NewProfile.tsx index 48d02196be..b15488aa7a 100644 --- a/dashboard/src/pages/NewProfile.tsx +++ b/dashboard/src/pages/NewProfile.tsx @@ -1,16 +1,19 @@ import { useState } from 'react' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useSearchParams } from 'react-router-dom' import { useToast } from '../components/Toast' import { api } from '../api' export default function NewProfile() { const navigate = useNavigate() + const [searchParams] = useSearchParams() const { toast } = useToast() + const adminModeRequested = searchParams.get('adminMode') === 'true' const [loading, setLoading] = useState(false) const [id, setId] = useState('') const [name, setName] = useState('') const [publicSubdomain, setPublicSubdomain] = useState('') const [enabled, setEnabled] = useState(true) + const [adminMode, setAdminMode] = useState(adminModeRequested) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -21,6 +24,16 @@ export default function NewProfile() { name, public_subdomain: publicSubdomain.trim() || null, enabled, + ...(adminMode + ? { + config: { + channels: [], + gateway: {}, + env_vars: {}, + admin_mode: true, + }, + } + : {}), }) toast('Profile created') navigate(`/profile/${id}`) @@ -93,6 +106,17 @@ export default function NewProfile() { Auto-start gateway when server starts +
+ +