Add SDLC review and eval controls
This commit is contained in:
1 parent
bc14a2fbcb
commit
bbf90a1a62
19 files changed
+927
-2
No files matched your search
@@ -7,6 +7,7 @@ import { BillingPanel } from "./settings/BillingPanel";
|
||||
import { ModelsPanel } from "./settings/ModelsPanel";
|
||||
import { PoliciesPanel } from "./settings/PoliciesPanel";
|
||||
import { OperationsPanel } from "./settings/OperationsPanel";
|
||||
import { SdlcPanel } from "./settings/SdlcPanel";
|
||||
import { MigrationPanel } from "./settings/MigrationPanel";
|
||||
import { CompliancePanel } from "./settings/CompliancePanel";
|
||||
import { NotificationsPanel, ProfilePanel, SecurityPanel } from "./settings/AccountPanels";
|
||||
@@ -22,6 +23,7 @@ type SettingsTab =
|
||||
| "skills"
|
||||
| "policies"
|
||||
| "operations"
|
||||
| "sdlc"
|
||||
| "compliance"
|
||||
| "migration"
|
||||
| "profile"
|
||||
@@ -44,6 +46,7 @@ const AGENTS_GROUP: NavSpec[] = [
|
||||
{ id: "skills", label: "Skills" },
|
||||
{ id: "policies", label: "Policies" },
|
||||
{ id: "operations", label: "Operations" },
|
||||
{ id: "sdlc", label: "SDLC" },
|
||||
{ id: "compliance", label: "Compliance" },
|
||||
{ id: "migration", label: "Migration" },
|
||||
];
|
||||
@@ -91,6 +94,12 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
const operationalEvents = useMemo(() => snapshot.operationalEvents.filter((item) => item.orgId === orgId), [snapshot.operationalEvents, orgId]);
|
||||
const chaosDrills = useMemo(() => snapshot.chaosDrills.filter((item) => item.orgId === orgId), [snapshot.chaosDrills, orgId]);
|
||||
const chaosDrillRuns = useMemo(() => snapshot.chaosDrillRuns.filter((item) => item.orgId === orgId), [snapshot.chaosDrillRuns, orgId]);
|
||||
const sdlcReviewAgents = useMemo(() => snapshot.sdlcReviewAgents.filter((item) => item.orgId === orgId), [snapshot.sdlcReviewAgents, orgId]);
|
||||
const sdlcReviewRuns = useMemo(() => snapshot.sdlcReviewRuns.filter((item) => item.orgId === orgId), [snapshot.sdlcReviewRuns, orgId]);
|
||||
const evalSuites = useMemo(() => snapshot.evalSuites.filter((item) => item.orgId === orgId), [snapshot.evalSuites, orgId]);
|
||||
const evalCases = useMemo(() => snapshot.evalCases.filter((item) => item.orgId === orgId), [snapshot.evalCases, orgId]);
|
||||
const evalRuns = useMemo(() => snapshot.evalRuns.filter((item) => item.orgId === orgId), [snapshot.evalRuns, orgId]);
|
||||
const prBotGates = useMemo(() => snapshot.prBotGates.filter((item) => item.orgId === orgId), [snapshot.prBotGates, orgId]);
|
||||
|
||||
return (
|
||||
<div className="split-main" style={{ height: "calc(100vh - 44px)", margin: "-28px -32px -40px" }}>
|
||||
@@ -158,6 +167,20 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
{tab === "sdlc" && (
|
||||
<SdlcPanel
|
||||
orgId={orgId}
|
||||
reviewAgents={sdlcReviewAgents}
|
||||
reviewRuns={sdlcReviewRuns}
|
||||
evalSuites={evalSuites}
|
||||
evalCases={evalCases}
|
||||
evalRuns={evalRuns}
|
||||
prBotGates={prBotGates}
|
||||
migrationReports={migrationReports}
|
||||
policies={policies}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
{tab === "compliance" && (
|
||||
<CompliancePanel
|
||||
orgId={orgId}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { Button, Pill } from "../../shared/ui";
|
||||
import type {
|
||||
EvalCaseRecord,
|
||||
EvalRunRecord,
|
||||
EvalSuiteRecord,
|
||||
MigrationReportRecord,
|
||||
PolicyRuleRecord,
|
||||
PrBotGateRecord,
|
||||
SdlcReviewAgentRecord,
|
||||
SdlcReviewRunRecord,
|
||||
SdlcReviewTargetType,
|
||||
} from "../../../../shared/domain";
|
||||
import { SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE } from "../../../../shared/domain";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import { formatRelative } from "../types";
|
||||
|
||||
export function SdlcPanel({
|
||||
orgId,
|
||||
reviewAgents,
|
||||
reviewRuns,
|
||||
evalSuites,
|
||||
evalCases,
|
||||
evalRuns,
|
||||
prBotGates,
|
||||
migrationReports,
|
||||
policies,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
reviewAgents: SdlcReviewAgentRecord[];
|
||||
reviewRuns: SdlcReviewRunRecord[];
|
||||
evalSuites: EvalSuiteRecord[];
|
||||
evalCases: EvalCaseRecord[];
|
||||
evalRuns: EvalRunRecord[];
|
||||
prBotGates: PrBotGateRecord[];
|
||||
migrationReports: MigrationReportRecord[];
|
||||
policies: PolicyRuleRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<div className="page">
|
||||
<h1 className="page-h1">SDLC</h1>
|
||||
<p className="page-sub">{reviewAgents.length} agents / {evalSuites.length} suites / {prBotGates.length} gates.</p>
|
||||
<ReviewAgentsTable
|
||||
orgId={orgId}
|
||||
agents={reviewAgents}
|
||||
runs={reviewRuns}
|
||||
migrationReports={migrationReports}
|
||||
policies={policies}
|
||||
actions={actions}
|
||||
/>
|
||||
<EvalSuitesTable orgId={orgId} suites={evalSuites} cases={evalCases} runs={evalRuns} actions={actions} />
|
||||
<PrBotGatesTable orgId={orgId} gates={prBotGates} actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewAgentsTable({
|
||||
orgId,
|
||||
agents,
|
||||
runs,
|
||||
migrationReports,
|
||||
policies,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
agents: SdlcReviewAgentRecord[];
|
||||
runs: SdlcReviewRunRecord[];
|
||||
migrationReports: MigrationReportRecord[];
|
||||
policies: PolicyRuleRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Review agents</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 24 }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) 96px 82px" }}>
|
||||
<div>Agent</div><div>Target</div><div>Last run</div><div>Action</div>
|
||||
</div>
|
||||
{agents.map((agent, index) => {
|
||||
const target = resolveReviewTarget(agent, orgId, migrationReports, policies);
|
||||
const run = runs.find((item) => item.reviewAgentId === agent.id) ?? null;
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) 96px 82px",
|
||||
borderBottom: index === agents.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{agent.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{agent.specialty}</div>
|
||||
</div>
|
||||
<div><Pill>{target.label}</Pill></div>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{run ? `${run.state} · ${formatRelative(run.createdAt)}` : "0 runs"}</div>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={!agent.enabled || !target.ready}
|
||||
onClick={() => void actions.run(() => window.agentCompany.runSdlcReview({
|
||||
orgId,
|
||||
reviewAgentId: agent.id,
|
||||
targetType: target.type,
|
||||
targetRef: target.ref,
|
||||
}))}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EvalSuitesTable({
|
||||
orgId,
|
||||
suites,
|
||||
cases,
|
||||
runs,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
suites: EvalSuiteRecord[];
|
||||
cases: EvalCaseRecord[];
|
||||
runs: EvalRunRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Eval suites</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 24 }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) 90px 96px 82px" }}>
|
||||
<div>Suite</div><div>Cases</div><div>Last run</div><div>Action</div>
|
||||
</div>
|
||||
{suites.map((suite, index) => {
|
||||
const run = runs.find((item) => item.suiteId === suite.id) ?? null;
|
||||
const caseCount = cases.filter((item) => item.suiteId === suite.id).length;
|
||||
return (
|
||||
<div
|
||||
key={suite.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) 90px 96px 82px",
|
||||
borderBottom: index === suites.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{suite.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{suite.scope}</div>
|
||||
</div>
|
||||
<div className="num mono">{caseCount}</div>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{run ? `${run.state} · ${formatRelative(run.createdAt)}` : "0 runs"}</div>
|
||||
<Button size="small" onClick={() => void actions.run(() => window.agentCompany.runEvalSuite({ orgId, suiteId: suite.id }))}>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PrBotGatesTable({
|
||||
orgId,
|
||||
gates,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
gates: PrBotGateRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>PR bot gates</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) 112px minmax(0, 1fr)" }}>
|
||||
<div>Gate</div><div>State</div><div>Updated</div>
|
||||
</div>
|
||||
{gates.map((gate, index) => (
|
||||
<div
|
||||
key={gate.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) 112px minmax(0, 1fr)",
|
||||
borderBottom: index === gates.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{gate.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{gate.gateType}</div>
|
||||
</div>
|
||||
<label className="row" style={{ gap: 8, fontSize: 12, color: "var(--text-2)" }}>
|
||||
<input
|
||||
aria-label={`${gate.label} enabled`}
|
||||
type="checkbox"
|
||||
checked={gate.enabled}
|
||||
onChange={(event) => {
|
||||
void actions.run(() => window.agentCompany.setPrBotGate({ orgId, gateId: gate.id, enabled: event.target.checked }));
|
||||
}}
|
||||
/>
|
||||
{gate.enabled ? "On" : "Off"}
|
||||
</label>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(gate.updatedAt)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveReviewTarget(
|
||||
agent: SdlcReviewAgentRecord,
|
||||
orgId: string,
|
||||
migrationReports: MigrationReportRecord[],
|
||||
policies: PolicyRuleRecord[],
|
||||
): { type: SdlcReviewTargetType; ref: string; label: string; ready: boolean } {
|
||||
if (agent.key === SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER) {
|
||||
const report = migrationReports[0] ?? null;
|
||||
return {
|
||||
type: SDLC_REVIEW_TARGET_TYPE.MIGRATION,
|
||||
ref: report?.id ?? orgId,
|
||||
label: report ? "migration" : "migration report",
|
||||
ready: Boolean(report),
|
||||
};
|
||||
}
|
||||
if (agent.key === SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER) {
|
||||
const policy = policies[0] ?? null;
|
||||
return {
|
||||
type: SDLC_REVIEW_TARGET_TYPE.POLICY,
|
||||
ref: policy?.id ?? orgId,
|
||||
label: policy ? "policy" : "policy rule",
|
||||
ready: Boolean(policy),
|
||||
};
|
||||
}
|
||||
return { type: SDLC_REVIEW_TARGET_TYPE.WORKSPACE, ref: orgId, label: "workspace", ready: true };
|
||||
}
|
||||
Reference in new issue
Block a user