Changeset 0.37.6 (#218)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
159
src/js/sw/surfaces/admin/routing.js
Normal file
159
src/js/sw/surfaces/admin/routing.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Admin > Routing > Routing Policies
|
||||
*/
|
||||
const { html } = window;
|
||||
const { useState, useEffect, useCallback } = hooks;
|
||||
|
||||
const POLICY_TYPES = ['provider_prefer', 'team_route', 'cost_limit', 'model_alias', 'capability_match'];
|
||||
|
||||
export default function RoutingSection() {
|
||||
const [policies, setPolicies] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editing, setEditing] = useState(null); // null | 'new' | policy
|
||||
const [testModel, setTestModel] = useState('');
|
||||
const [testUser, setTestUser] = useState('');
|
||||
const [testResult, setTestResult] = useState(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const data = await sw.api.admin.routing.policies();
|
||||
setPolicies(Array.isArray(data) ? data : data.data || []);
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
async function savePolicy(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const data = {
|
||||
name: form.pname.value.trim(),
|
||||
scope: form.scope.value,
|
||||
priority: parseInt(form.priority.value) || 0,
|
||||
policy_type: form.policy_type.value,
|
||||
config: JSON.parse(form.config.value || '{}'),
|
||||
is_active: form.is_active.checked,
|
||||
};
|
||||
try {
|
||||
if (editing === 'new') {
|
||||
await sw.api.admin.routing.createPolicy(data);
|
||||
sw.toast('Policy created', 'success');
|
||||
} else {
|
||||
await sw.api.admin.routing.updatePolicy(editing.id, data);
|
||||
sw.toast('Policy updated', 'success');
|
||||
}
|
||||
setEditing(null);
|
||||
load();
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function deletePolicy(id) {
|
||||
const ok = await sw.confirm('Delete this routing policy?', true);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await sw.api.admin.routing.deletePolicy(id);
|
||||
sw.toast('Policy deleted', 'success');
|
||||
load();
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
if (!testModel) return sw.toast('Model is required', 'error');
|
||||
try {
|
||||
const result = await sw.api.admin.routing.test({
|
||||
model: testModel,
|
||||
user_id: testUser || undefined,
|
||||
});
|
||||
setTestResult(result);
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
|
||||
|
||||
if (editing) {
|
||||
const isNew = editing === 'new';
|
||||
const p = isNew ? {} : editing;
|
||||
return html`
|
||||
<form onSubmit=${savePolicy}>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<button type="button" class="btn-small" onClick=${() => setEditing(null)}>\u2190 Back</button>
|
||||
<h4 style="margin:0;">${isNew ? 'New Policy' : `Edit: ${p.name}`}</h4>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Name</label><input name="pname" value=${p.name || ''} placeholder="Prefer Anthropic" /></div>
|
||||
<div class="form-group" style="width:100px;"><label>Priority</label>
|
||||
<input name="priority" type="number" value=${p.priority ?? 10} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Scope</label>
|
||||
<select name="scope">
|
||||
<option value="global" selected=${p.scope !== 'team'}>global</option>
|
||||
<option value="team" selected=${p.scope === 'team'}>team</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>Type</label>
|
||||
<select name="policy_type">
|
||||
${POLICY_TYPES.map(t => html`<option key=${t} value=${t} selected=${p.policy_type === t}>${t}</option>`)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label>Config (JSON)</label>
|
||||
<textarea name="config" rows="4" style="font-family:monospace;font-size:12px;">${JSON.stringify(p.config || {}, null, 2)}</textarea>
|
||||
</div>
|
||||
<label class="toggle-label" style="margin:8px 0;">
|
||||
<input type="checkbox" name="is_active" checked=${p.is_active !== false} />
|
||||
<span class="toggle-track"></span><span>Active</span>
|
||||
</label>
|
||||
<div class="form-row" style="margin-top:12px;gap:8px;">
|
||||
<button type="submit" class="btn-small btn-primary">${isNew ? 'Create' : 'Save'}</button>
|
||||
<button type="button" class="btn-small" onClick=${() => setEditing(null)}>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<div style="margin-bottom:12px;">
|
||||
<button class="btn-md btn-primary" onClick=${() => setEditing('new')}>+ New Policy</button>
|
||||
</div>
|
||||
|
||||
<div class="admin-list">
|
||||
${policies.length === 0 && html`<div class="empty-hint">No routing policies</div>`}
|
||||
${policies.map(p => html`
|
||||
<div class="admin-surface-row" key=${p.id}>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<strong>${p.name}</strong>
|
||||
<span class="badge" style="margin-left:6px;">${p.policy_type}</span>
|
||||
<span class="text-muted" style="margin-left:6px;font-size:11px;">priority: ${p.priority}</span>
|
||||
</div>
|
||||
<span class="badge">${p.scope}</span>
|
||||
<span class="badge ${p.is_active ? 'badge-active' : 'badge-inactive'}">${p.is_active ? 'active' : 'off'}</span>
|
||||
<div class="admin-actions-cell">
|
||||
<button class="btn-small" onClick=${() => setEditing(p)}>Edit</button>
|
||||
<button class="btn-small btn-danger" onClick=${() => deletePolicy(p.id)}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<div class="settings-section" style="margin-top:24px;">
|
||||
<h4>Test Routing</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Model</label>
|
||||
<input value=${testModel} onInput=${e => setTestModel(e.target.value)} placeholder="gpt-4o" />
|
||||
</div>
|
||||
<div class="form-group"><label>User ID</label>
|
||||
<input value=${testUser} onInput=${e => setTestUser(e.target.value)} placeholder="optional" />
|
||||
</div>
|
||||
<button class="btn-small btn-primary" style="align-self:flex-end;" onClick=${runTest}>Test</button>
|
||||
</div>
|
||||
${testResult && html`
|
||||
<pre class="code-block" style="margin-top:8px;font-size:12px;max-height:200px;overflow:auto;">${JSON.stringify(testResult, null, 2)}</pre>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user