@auxiora/dashboard 1.0.0
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/LICENSE +191 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +69 -0
- package/dist/auth.js.map +1 -0
- package/dist/cloud-types.d.ts +71 -0
- package/dist/cloud-types.d.ts.map +1 -0
- package/dist/cloud-types.js +2 -0
- package/dist/cloud-types.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +2250 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist-ui/assets/index-BfY0i5jw.css +1 -0
- package/dist-ui/assets/index-CXpk9mvw.js +60 -0
- package/dist-ui/icon.svg +59 -0
- package/dist-ui/index.html +20 -0
- package/package.json +32 -0
- package/src/auth.ts +83 -0
- package/src/cloud-types.ts +63 -0
- package/src/index.ts +5 -0
- package/src/router.ts +2494 -0
- package/src/types.ts +269 -0
- package/tests/auth.test.ts +51 -0
- package/tests/cloud-router.test.ts +249 -0
- package/tests/desktop-router.test.ts +151 -0
- package/tests/router.test.ts +388 -0
- package/tests/trust-router.test.ts +170 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/ui/index.html +19 -0
- package/ui/node_modules/.bin/browserslist +17 -0
- package/ui/node_modules/.bin/tsc +17 -0
- package/ui/node_modules/.bin/tsserver +17 -0
- package/ui/node_modules/.bin/vite +17 -0
- package/ui/package.json +23 -0
- package/ui/public/icon.svg +59 -0
- package/ui/src/App.tsx +63 -0
- package/ui/src/api.ts +238 -0
- package/ui/src/components/ActivityFeed.tsx +123 -0
- package/ui/src/components/BehaviorHealth.tsx +105 -0
- package/ui/src/components/DataTable.tsx +39 -0
- package/ui/src/components/Layout.tsx +160 -0
- package/ui/src/components/PasswordStrength.tsx +31 -0
- package/ui/src/components/SetupProgress.tsx +26 -0
- package/ui/src/components/StatusBadge.tsx +12 -0
- package/ui/src/components/ThemeSelector.tsx +39 -0
- package/ui/src/contexts/ThemeContext.tsx +58 -0
- package/ui/src/hooks/useApi.ts +19 -0
- package/ui/src/hooks/usePolling.ts +8 -0
- package/ui/src/main.tsx +16 -0
- package/ui/src/pages/AuditLog.tsx +36 -0
- package/ui/src/pages/Behaviors.tsx +426 -0
- package/ui/src/pages/Chat.tsx +688 -0
- package/ui/src/pages/Login.tsx +64 -0
- package/ui/src/pages/Overview.tsx +56 -0
- package/ui/src/pages/Sessions.tsx +26 -0
- package/ui/src/pages/SettingsAmbient.tsx +185 -0
- package/ui/src/pages/SettingsConnections.tsx +201 -0
- package/ui/src/pages/SettingsNotifications.tsx +241 -0
- package/ui/src/pages/SetupAppearance.tsx +45 -0
- package/ui/src/pages/SetupChannels.tsx +143 -0
- package/ui/src/pages/SetupComplete.tsx +31 -0
- package/ui/src/pages/SetupConnections.tsx +80 -0
- package/ui/src/pages/SetupDashboardPassword.tsx +50 -0
- package/ui/src/pages/SetupIdentity.tsx +68 -0
- package/ui/src/pages/SetupPersonality.tsx +78 -0
- package/ui/src/pages/SetupProvider.tsx +65 -0
- package/ui/src/pages/SetupVault.tsx +50 -0
- package/ui/src/pages/SetupWelcome.tsx +19 -0
- package/ui/src/pages/UnlockVault.tsx +56 -0
- package/ui/src/pages/Webhooks.tsx +158 -0
- package/ui/src/pages/settings/Appearance.tsx +63 -0
- package/ui/src/pages/settings/Channels.tsx +138 -0
- package/ui/src/pages/settings/Identity.tsx +61 -0
- package/ui/src/pages/settings/Personality.tsx +54 -0
- package/ui/src/pages/settings/PersonalityEditor.tsx +577 -0
- package/ui/src/pages/settings/Provider.tsx +537 -0
- package/ui/src/pages/settings/Security.tsx +111 -0
- package/ui/src/styles/global.css +2308 -0
- package/ui/src/styles/themes/index.css +7 -0
- package/ui/src/styles/themes/monolith.css +125 -0
- package/ui/src/styles/themes/nebula.css +90 -0
- package/ui/src/styles/themes/neon.css +149 -0
- package/ui/src/styles/themes/polar.css +151 -0
- package/ui/src/styles/themes/signal.css +163 -0
- package/ui/src/styles/themes/terra.css +146 -0
- package/ui/tsconfig.json +14 -0
- package/ui/vite.config.ts +20 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { api } from '../api';
|
|
4
|
+
|
|
5
|
+
export function Login() {
|
|
6
|
+
const [password, setPassword] = useState('');
|
|
7
|
+
const [error, setError] = useState('');
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
const [checking, setChecking] = useState(true);
|
|
10
|
+
const [agentName, setAgentName] = useState('Auxiora');
|
|
11
|
+
const navigate = useNavigate();
|
|
12
|
+
|
|
13
|
+
// Check if vault needs unlocking before allowing login
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
api.getSetupStatus()
|
|
16
|
+
.then(status => {
|
|
17
|
+
if (status.agentName) setAgentName(status.agentName);
|
|
18
|
+
if (status.needsSetup) {
|
|
19
|
+
navigate('/setup', { replace: true });
|
|
20
|
+
} else if (!status.vaultUnlocked) {
|
|
21
|
+
navigate('/unlock', { replace: true });
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.catch(() => {})
|
|
25
|
+
.finally(() => setChecking(false));
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
setLoading(true);
|
|
31
|
+
setError('');
|
|
32
|
+
try {
|
|
33
|
+
await api.login(password);
|
|
34
|
+
navigate('/');
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
setError(err.message || 'Login failed');
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (checking) return null;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="login-page">
|
|
46
|
+
<div className="login-card">
|
|
47
|
+
<h1>{agentName} Mission Control</h1>
|
|
48
|
+
<form onSubmit={handleSubmit}>
|
|
49
|
+
<input
|
|
50
|
+
type="password"
|
|
51
|
+
value={password}
|
|
52
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
53
|
+
placeholder="Password"
|
|
54
|
+
autoFocus
|
|
55
|
+
/>
|
|
56
|
+
<button type="submit" disabled={loading}>
|
|
57
|
+
{loading ? 'Logging in...' : 'Login'}
|
|
58
|
+
</button>
|
|
59
|
+
{error && <p className="error">{error}</p>}
|
|
60
|
+
</form>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useApi } from '../hooks/useApi';
|
|
2
|
+
import { usePolling } from '../hooks/usePolling';
|
|
3
|
+
import { api } from '../api';
|
|
4
|
+
import { ActivityFeed } from '../components/ActivityFeed';
|
|
5
|
+
import { BehaviorHealth } from '../components/BehaviorHealth';
|
|
6
|
+
|
|
7
|
+
export function Overview() {
|
|
8
|
+
const { data: status, refresh } = useApi(() => api.getStatus(), []);
|
|
9
|
+
const { data: models, refresh: refreshModels } = useApi(() => api.getModels(), []);
|
|
10
|
+
usePolling(() => { refresh(); refreshModels(); });
|
|
11
|
+
|
|
12
|
+
const s = status?.data;
|
|
13
|
+
const primaryProvider = models?.providers?.find((p: any) => p.available)?.displayName ?? 'None';
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="page">
|
|
17
|
+
<h2>Mission Control</h2>
|
|
18
|
+
|
|
19
|
+
{/* Quick status strip */}
|
|
20
|
+
<div className="status-grid">
|
|
21
|
+
<div className="status-card">
|
|
22
|
+
<h3>Connections</h3>
|
|
23
|
+
<div className="value">{s?.connections ?? 0}</div>
|
|
24
|
+
<div className="sub">Active sessions</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div className="status-card">
|
|
27
|
+
<h3>Provider</h3>
|
|
28
|
+
<div className="value">{primaryProvider}</div>
|
|
29
|
+
<div className="sub">{s?.activeModel?.model ?? 'unknown'}</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="status-card">
|
|
32
|
+
<h3>Uptime</h3>
|
|
33
|
+
<div className="value">{s ? formatUptime(s.uptime) : '-'}</div>
|
|
34
|
+
<div className="sub">Since last restart</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Main two-column layout */}
|
|
39
|
+
<div className="mc-columns">
|
|
40
|
+
<div className="mc-left">
|
|
41
|
+
<BehaviorHealth />
|
|
42
|
+
</div>
|
|
43
|
+
<div className="mc-right">
|
|
44
|
+
<ActivityFeed />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatUptime(seconds: number): string {
|
|
52
|
+
const h = Math.floor(seconds / 3600);
|
|
53
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
54
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
55
|
+
return `${m}m`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useApi } from '../hooks/useApi';
|
|
2
|
+
import { usePolling } from '../hooks/usePolling';
|
|
3
|
+
import { api } from '../api';
|
|
4
|
+
import { DataTable } from '../components/DataTable';
|
|
5
|
+
|
|
6
|
+
export function Sessions() {
|
|
7
|
+
const { data, refresh } = useApi(() => api.getSessions(), []);
|
|
8
|
+
usePolling(refresh);
|
|
9
|
+
|
|
10
|
+
const sessions = data?.data ?? [];
|
|
11
|
+
|
|
12
|
+
const columns = [
|
|
13
|
+
{ key: 'id', label: 'Session ID', render: (s: any) => s.id.slice(0, 8) + '...' },
|
|
14
|
+
{ key: 'channelType', label: 'Channel' },
|
|
15
|
+
{ key: 'authenticated', label: 'Auth', render: (s: any) => s.authenticated ? 'Yes' : 'No' },
|
|
16
|
+
{ key: 'voiceActive', label: 'Voice', render: (s: any) => s.voiceActive ? 'Active' : '-' },
|
|
17
|
+
{ key: 'lastActive', label: 'Last Active', render: (s: any) => new Date(s.lastActive).toLocaleString() },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="page">
|
|
22
|
+
<h2>Active Sessions</h2>
|
|
23
|
+
<DataTable columns={columns} rows={sessions} keyField="id" />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useApi } from '../hooks/useApi';
|
|
3
|
+
import { api } from '../api';
|
|
4
|
+
|
|
5
|
+
const CATEGORIES = ['Calendar', 'Email', 'Tasks', 'Patterns'] as const;
|
|
6
|
+
|
|
7
|
+
interface AmbientConfig {
|
|
8
|
+
morningBriefing: {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
time: string;
|
|
11
|
+
categories: string[];
|
|
12
|
+
};
|
|
13
|
+
eveningSummary: {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
time: string;
|
|
16
|
+
};
|
|
17
|
+
deliveryChannel: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CONFIG: AmbientConfig = {
|
|
21
|
+
morningBriefing: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
time: '08:00',
|
|
24
|
+
categories: ['Calendar', 'Email'],
|
|
25
|
+
},
|
|
26
|
+
eveningSummary: {
|
|
27
|
+
enabled: false,
|
|
28
|
+
time: '18:00',
|
|
29
|
+
},
|
|
30
|
+
deliveryChannel: 'all',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function SettingsAmbient() {
|
|
34
|
+
const { data, loading: fetching } = useApi(() => api.getAmbientConfig(), []);
|
|
35
|
+
const [config, setConfig] = useState<AmbientConfig>(DEFAULT_CONFIG);
|
|
36
|
+
const [saving, setSaving] = useState(false);
|
|
37
|
+
const [success, setSuccess] = useState('');
|
|
38
|
+
const [error, setError] = useState('');
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (data?.data) {
|
|
42
|
+
setConfig({
|
|
43
|
+
morningBriefing: {
|
|
44
|
+
enabled: data.data.morningBriefing?.enabled ?? DEFAULT_CONFIG.morningBriefing.enabled,
|
|
45
|
+
time: data.data.morningBriefing?.time ?? DEFAULT_CONFIG.morningBriefing.time,
|
|
46
|
+
categories: data.data.morningBriefing?.categories ?? DEFAULT_CONFIG.morningBriefing.categories,
|
|
47
|
+
},
|
|
48
|
+
eveningSummary: {
|
|
49
|
+
enabled: data.data.eveningSummary?.enabled ?? DEFAULT_CONFIG.eveningSummary.enabled,
|
|
50
|
+
time: data.data.eveningSummary?.time ?? DEFAULT_CONFIG.eveningSummary.time,
|
|
51
|
+
},
|
|
52
|
+
deliveryChannel: data.data.deliveryChannel ?? DEFAULT_CONFIG.deliveryChannel,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}, [data]);
|
|
56
|
+
|
|
57
|
+
const toggleCategory = (category: string) => {
|
|
58
|
+
setConfig(prev => {
|
|
59
|
+
const cats = prev.morningBriefing.categories;
|
|
60
|
+
const next = cats.includes(category)
|
|
61
|
+
? cats.filter(c => c !== category)
|
|
62
|
+
: [...cats, category];
|
|
63
|
+
return {
|
|
64
|
+
...prev,
|
|
65
|
+
morningBriefing: { ...prev.morningBriefing, categories: next },
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleSave = async () => {
|
|
71
|
+
setSaving(true);
|
|
72
|
+
setError('');
|
|
73
|
+
setSuccess('');
|
|
74
|
+
try {
|
|
75
|
+
await api.updateAmbientConfig(config);
|
|
76
|
+
setSuccess('Ambient settings saved successfully');
|
|
77
|
+
} catch (err: unknown) {
|
|
78
|
+
setError(err instanceof Error ? err.message : 'Failed to save settings');
|
|
79
|
+
} finally {
|
|
80
|
+
setSaving(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (fetching) return null;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="page">
|
|
88
|
+
<h2>Ambient Intelligence</h2>
|
|
89
|
+
<div className="settings-form">
|
|
90
|
+
<div className="settings-section">
|
|
91
|
+
<h3>Morning Briefing</h3>
|
|
92
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '1rem' }}>
|
|
93
|
+
<div
|
|
94
|
+
className={`toggle${config.morningBriefing.enabled ? ' active' : ''}`}
|
|
95
|
+
onClick={() => setConfig(prev => ({
|
|
96
|
+
...prev,
|
|
97
|
+
morningBriefing: { ...prev.morningBriefing, enabled: !prev.morningBriefing.enabled },
|
|
98
|
+
}))}
|
|
99
|
+
/>
|
|
100
|
+
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
|
|
101
|
+
{config.morningBriefing.enabled ? 'Enabled' : 'Disabled'}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
{config.morningBriefing.enabled && (
|
|
105
|
+
<>
|
|
106
|
+
<label>Delivery Time</label>
|
|
107
|
+
<input
|
|
108
|
+
type="time"
|
|
109
|
+
value={config.morningBriefing.time}
|
|
110
|
+
onChange={(e) => setConfig(prev => ({
|
|
111
|
+
...prev,
|
|
112
|
+
morningBriefing: { ...prev.morningBriefing, time: e.target.value },
|
|
113
|
+
}))}
|
|
114
|
+
/>
|
|
115
|
+
<label>Categories</label>
|
|
116
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginBottom: '1rem' }}>
|
|
117
|
+
{CATEGORIES.map(cat => (
|
|
118
|
+
<label key={cat} style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', cursor: 'pointer', fontSize: '0.85rem' }}>
|
|
119
|
+
<input
|
|
120
|
+
type="checkbox"
|
|
121
|
+
checked={config.morningBriefing.categories.includes(cat)}
|
|
122
|
+
onChange={() => toggleCategory(cat)}
|
|
123
|
+
style={{ width: 'auto', marginBottom: 0 }}
|
|
124
|
+
/>
|
|
125
|
+
{cat}
|
|
126
|
+
</label>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="settings-section">
|
|
134
|
+
<h3>Evening Summary</h3>
|
|
135
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '1rem' }}>
|
|
136
|
+
<div
|
|
137
|
+
className={`toggle${config.eveningSummary.enabled ? ' active' : ''}`}
|
|
138
|
+
onClick={() => setConfig(prev => ({
|
|
139
|
+
...prev,
|
|
140
|
+
eveningSummary: { ...prev.eveningSummary, enabled: !prev.eveningSummary.enabled },
|
|
141
|
+
}))}
|
|
142
|
+
/>
|
|
143
|
+
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
|
|
144
|
+
{config.eveningSummary.enabled ? 'Enabled' : 'Disabled'}
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
{config.eveningSummary.enabled && (
|
|
148
|
+
<>
|
|
149
|
+
<label>Delivery Time</label>
|
|
150
|
+
<input
|
|
151
|
+
type="time"
|
|
152
|
+
value={config.eveningSummary.time}
|
|
153
|
+
onChange={(e) => setConfig(prev => ({
|
|
154
|
+
...prev,
|
|
155
|
+
eveningSummary: { ...prev.eveningSummary, time: e.target.value },
|
|
156
|
+
}))}
|
|
157
|
+
/>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="settings-section">
|
|
163
|
+
<label>Delivery Channel</label>
|
|
164
|
+
<select
|
|
165
|
+
value={config.deliveryChannel}
|
|
166
|
+
onChange={(e) => setConfig(prev => ({ ...prev, deliveryChannel: e.target.value }))}
|
|
167
|
+
>
|
|
168
|
+
<option value="all">All connected channels</option>
|
|
169
|
+
<option value="webchat">Webchat only</option>
|
|
170
|
+
<option value="discord">Discord</option>
|
|
171
|
+
<option value="telegram">Telegram</option>
|
|
172
|
+
<option value="slack">Slack</option>
|
|
173
|
+
<option value="email">Email</option>
|
|
174
|
+
</select>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<button className="settings-btn" onClick={handleSave} disabled={saving}>
|
|
178
|
+
{saving ? 'Saving...' : 'Save Settings'}
|
|
179
|
+
</button>
|
|
180
|
+
{success && <div className="settings-success">{success}</div>}
|
|
181
|
+
{error && <div className="error">{error}</div>}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { api } from '../api';
|
|
3
|
+
|
|
4
|
+
interface ConnectorState {
|
|
5
|
+
hasCredentials: boolean;
|
|
6
|
+
connected: boolean;
|
|
7
|
+
expiresAt?: number;
|
|
8
|
+
loading: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CONNECTORS = [
|
|
12
|
+
{
|
|
13
|
+
id: 'google-workspace',
|
|
14
|
+
name: 'Google Workspace',
|
|
15
|
+
description: 'Gmail, Google Calendar, and Google Drive',
|
|
16
|
+
scopes: 'Email summaries, calendar awareness, drive file access',
|
|
17
|
+
setupUrl: 'https://console.cloud.google.com/apis/credentials',
|
|
18
|
+
instructions: [
|
|
19
|
+
'Go to Google Cloud Console > APIs & Services > Credentials',
|
|
20
|
+
'Create an OAuth 2.0 Client ID (Web application type)',
|
|
21
|
+
'Add authorized redirect URI: {callbackUrl}',
|
|
22
|
+
'Enable Gmail API, Google Calendar API, and Google Drive API',
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function SettingsConnections() {
|
|
28
|
+
const [states, setStates] = useState<Record<string, ConnectorState>>({});
|
|
29
|
+
const [clientId, setClientId] = useState('');
|
|
30
|
+
const [clientSecret, setClientSecret] = useState('');
|
|
31
|
+
const [error, setError] = useState('');
|
|
32
|
+
const [success, setSuccess] = useState('');
|
|
33
|
+
const [showSetup, setShowSetup] = useState<string | null>(null);
|
|
34
|
+
|
|
35
|
+
const callbackUrl = `${window.location.origin}/api/v1/dashboard/connectors/google-workspace/callback`;
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
void loadStatuses();
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
async function loadStatuses() {
|
|
42
|
+
for (const connector of CONNECTORS) {
|
|
43
|
+
setStates(prev => ({ ...prev, [connector.id]: { ...prev[connector.id]!, loading: true, hasCredentials: false, connected: false } }));
|
|
44
|
+
try {
|
|
45
|
+
const result = await api.getConnectorStatus(connector.id);
|
|
46
|
+
setStates(prev => ({
|
|
47
|
+
...prev,
|
|
48
|
+
[connector.id]: {
|
|
49
|
+
hasCredentials: result.data.hasCredentials,
|
|
50
|
+
connected: result.data.connected,
|
|
51
|
+
expiresAt: result.data.expiresAt,
|
|
52
|
+
loading: false,
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
} catch {
|
|
56
|
+
setStates(prev => ({ ...prev, [connector.id]: { hasCredentials: false, connected: false, loading: false } }));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function handleSaveCredentials(connectorId: string) {
|
|
62
|
+
if (!clientId || !clientSecret) {
|
|
63
|
+
setError('Both Client ID and Client Secret are required');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setError('');
|
|
67
|
+
try {
|
|
68
|
+
await api.saveConnectorCredentials(connectorId, clientId, clientSecret);
|
|
69
|
+
setSuccess('Credentials saved. Click "Connect" to authorize.');
|
|
70
|
+
setClientId('');
|
|
71
|
+
setClientSecret('');
|
|
72
|
+
setShowSetup(null);
|
|
73
|
+
void loadStatuses();
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
setError(err instanceof Error ? err.message : 'Failed to save credentials');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleStartOAuth(connectorId: string) {
|
|
80
|
+
window.location.href = `/api/v1/dashboard/connectors/${connectorId}/auth`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleDisconnect(connectorId: string) {
|
|
84
|
+
try {
|
|
85
|
+
await api.disconnectConnector(connectorId);
|
|
86
|
+
setSuccess('Disconnected successfully');
|
|
87
|
+
void loadStatuses();
|
|
88
|
+
} catch (err: unknown) {
|
|
89
|
+
setError(err instanceof Error ? err.message : 'Failed to disconnect');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="page">
|
|
95
|
+
<h2>Connections</h2>
|
|
96
|
+
<div className="settings-form">
|
|
97
|
+
{success && <div className="settings-success" onClick={() => setSuccess('')}>{success}</div>}
|
|
98
|
+
{error && <div className="error">{error}</div>}
|
|
99
|
+
|
|
100
|
+
{CONNECTORS.map(connector => {
|
|
101
|
+
const state = states[connector.id];
|
|
102
|
+
const isConnected = state?.connected;
|
|
103
|
+
const hasCredentials = state?.hasCredentials;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div key={connector.id} className="settings-section">
|
|
107
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
108
|
+
<div>
|
|
109
|
+
<h3>{connector.name}</h3>
|
|
110
|
+
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
|
|
111
|
+
{connector.description}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
<span className={isConnected ? 'badge badge-green' : 'badge badge-gray'}>
|
|
115
|
+
{state?.loading ? 'Checking...' : isConnected ? 'Connected' : 'Not connected'}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', margin: '0.5rem 0' }}>
|
|
120
|
+
Enables: {connector.scopes}
|
|
121
|
+
</p>
|
|
122
|
+
|
|
123
|
+
{isConnected ? (
|
|
124
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem', alignItems: 'center' }}>
|
|
125
|
+
{state?.expiresAt && (
|
|
126
|
+
<span style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
|
|
127
|
+
Token expires: {new Date(state.expiresAt).toLocaleDateString()}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
<button type="button" className="btn-sm" onClick={() => handleStartOAuth(connector.id)}>
|
|
131
|
+
Reconnect
|
|
132
|
+
</button>
|
|
133
|
+
<button type="button" className="btn-sm btn-danger" onClick={() => handleDisconnect(connector.id)}>
|
|
134
|
+
Disconnect
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
) : hasCredentials ? (
|
|
138
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
|
139
|
+
<button type="button" className="settings-btn" onClick={() => handleStartOAuth(connector.id)}>
|
|
140
|
+
Connect
|
|
141
|
+
</button>
|
|
142
|
+
<button type="button" className="btn-sm" onClick={() => setShowSetup(connector.id)}>
|
|
143
|
+
Update Credentials
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<div style={{ marginTop: '1rem' }}>
|
|
148
|
+
{showSetup !== connector.id ? (
|
|
149
|
+
<button type="button" className="settings-btn" onClick={() => setShowSetup(connector.id)}>
|
|
150
|
+
Set Up
|
|
151
|
+
</button>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{showSetup === connector.id && (
|
|
157
|
+
<div style={{ marginTop: '1rem', padding: '1rem', background: 'var(--bg-hover)', borderRadius: 'var(--radius)', border: '1px solid var(--border)' }}>
|
|
158
|
+
<h4 style={{ margin: '0 0 0.75rem', fontFamily: 'var(--font-display)', fontWeight: 600 }}>Setup Instructions</h4>
|
|
159
|
+
<ol style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', paddingLeft: '1.25rem', margin: '0 0 1rem' }}>
|
|
160
|
+
{connector.instructions.map((step, i) => (
|
|
161
|
+
<li key={i} style={{ marginBottom: '0.35rem' }}>
|
|
162
|
+
{step.replace('{callbackUrl}', callbackUrl)}
|
|
163
|
+
</li>
|
|
164
|
+
))}
|
|
165
|
+
</ol>
|
|
166
|
+
|
|
167
|
+
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginBottom: '1rem', wordBreak: 'break-all' }}>
|
|
168
|
+
Redirect URI: <code style={{ fontFamily: 'var(--font-mono)', background: 'var(--accent-subtle)', color: 'var(--accent)', padding: '0.15rem 0.35rem', borderRadius: '4px', fontSize: '0.82em' }}>{callbackUrl}</code>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<label>Client ID</label>
|
|
172
|
+
<input
|
|
173
|
+
type="text"
|
|
174
|
+
value={clientId}
|
|
175
|
+
onChange={(e) => setClientId(e.target.value)}
|
|
176
|
+
placeholder="your-client-id.apps.googleusercontent.com"
|
|
177
|
+
/>
|
|
178
|
+
<label>Client Secret</label>
|
|
179
|
+
<input
|
|
180
|
+
type="password"
|
|
181
|
+
value={clientSecret}
|
|
182
|
+
onChange={(e) => setClientSecret(e.target.value)}
|
|
183
|
+
placeholder="GOCSPX-..."
|
|
184
|
+
/>
|
|
185
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem' }}>
|
|
186
|
+
<button type="button" className="settings-btn" onClick={() => handleSaveCredentials(connector.id)}>
|
|
187
|
+
Save & Connect
|
|
188
|
+
</button>
|
|
189
|
+
<button type="button" className="btn-sm" onClick={() => { setShowSetup(null); setError(''); }}>
|
|
190
|
+
Cancel
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|