feat(frontend): Spectrum UX port — Tabs + AlertBanner + BackLink + compact tables (sprint 11) #13

Merged
knacky merged 11 commits from sprint/11-spectrum-ux into main 2026-06-22 08:25:38 +00:00
4 changed files with 50 additions and 50 deletions
Showing only changes of commit 1819089925 - Show all commits

View File

@@ -172,25 +172,25 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
</div> </div>
<div className="card-product overflow-hidden p-0"> <div className="card-product overflow-hidden p-0">
<table className="w-full text-left"> <table className="table-compact w-full text-left">
<thead className="bg-cloud border-b border-hairline"> <thead className="bg-cloud border-b border-hairline">
<tr className="text-[12px] uppercase tracking-[0.5px] text-graphite"> <tr>
<th className="px-xl py-md">Name</th> <th>Name</th>
<th className="px-xl py-md">MITRE</th> <th>MITRE</th>
<th className="px-xl py-md">Status</th> <th>Status</th>
<th className="px-xl py-md">Executed at</th> <th>Executed at</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.map((sim) => ( {data.map((sim) => (
<tr <tr
key={sim.id} key={sim.id}
className="border-b border-hairline last:border-0 hover:bg-cloud cursor-pointer" className="hover:bg-cloud cursor-pointer"
onClick={() => onClick={() =>
navigate(`/engagements/${engagementId}/simulations/${sim.id}/edit`) navigate(`/engagements/${engagementId}/simulations/${sim.id}/edit`)
} }
> >
<td className="px-xl py-md"> <td>
<Link <Link
to={`/engagements/${engagementId}/simulations/${sim.id}/edit`} to={`/engagements/${engagementId}/simulations/${sim.id}/edit`}
className="text-ink font-medium hover:underline" className="text-ink font-medium hover:underline"
@@ -199,7 +199,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
{sim.name} {sim.name}
</Link> </Link>
</td> </td>
<td className="px-xl py-md text-charcoal text-[14px] font-mono"> <td className="text-charcoal font-mono">
{(() => { {(() => {
const items = [ const items = [
...(sim.tactics ?? []).map((t) => t.id), ...(sim.tactics ?? []).map((t) => t.id),
@@ -210,10 +210,10 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
return `${items[0]} +${items.length - 1}`; return `${items[0]} +${items.length - 1}`;
})()} })()}
</td> </td>
<td className="px-xl py-md"> <td>
<SimulationStatusBadge status={sim.status} /> <SimulationStatusBadge status={sim.status} />
</td> </td>
<td className="px-xl py-md text-charcoal text-[14px] font-mono"> <td className="text-charcoal font-mono">
{formatDate(sim.executed_at)} {formatDate(sim.executed_at)}
</td> </td>
</tr> </tr>

View File

@@ -69,32 +69,32 @@ export function EngagementsListPage(): JSX.Element {
{!isLoading && !isError && data && data.length > 0 ? ( {!isLoading && !isError && data && data.length > 0 ? (
<div className="card-product overflow-hidden p-0"> <div className="card-product overflow-hidden p-0">
<table className="w-full text-left"> <table className="table-compact w-full text-left">
<thead className="bg-cloud border-b border-hairline"> <thead className="bg-cloud border-b border-hairline">
<tr className="text-[12px] uppercase tracking-[0.5px] text-graphite"> <tr>
<th className="px-xl py-md">Name</th> <th>Name</th>
<th className="px-xl py-md">Status</th> <th>Status</th>
<th className="px-xl py-md">Start</th> <th>Start</th>
<th className="px-xl py-md">End</th> <th>End</th>
<th className="px-xl py-md">Created by</th> <th>Created by</th>
<th className="px-xl py-md text-right">Actions</th> <th className="text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.map((eng) => ( {data.map((eng) => (
<tr key={eng.id} className="border-b border-hairline last:border-0"> <tr key={eng.id}>
<td className="px-xl py-md"> <td>
<Link to={`/engagements/${eng.id}`} className="text-ink font-medium hover:underline"> <Link to={`/engagements/${eng.id}`} className="text-ink font-medium hover:underline">
{eng.name} {eng.name}
</Link> </Link>
</td> </td>
<td className="px-xl py-md"> <td>
<StatusBadge status={eng.status} /> <StatusBadge status={eng.status} />
</td> </td>
<td className="px-xl py-md text-charcoal font-mono">{formatDate(eng.start_date)}</td> <td className="text-charcoal font-mono">{formatDate(eng.start_date)}</td>
<td className="px-xl py-md text-charcoal font-mono">{formatDate(eng.end_date)}</td> <td className="text-charcoal font-mono">{formatDate(eng.end_date)}</td>
<td className="px-xl py-md text-charcoal">{eng.created_by.username}</td> <td className="text-charcoal">{eng.created_by.username}</td>
<td className="px-xl py-md text-right"> <td className="text-right">
<div className="inline-flex gap-sm"> <div className="inline-flex gap-sm">
<Link to={`/engagements/${eng.id}`} className="btn-text-link"> <Link to={`/engagements/${eng.id}`} className="btn-text-link">
View View

View File

@@ -69,20 +69,20 @@ export function TemplatesListPage(): JSX.Element {
{!isLoading && !isError && data && data.length > 0 ? ( {!isLoading && !isError && data && data.length > 0 ? (
<div className="card-product overflow-hidden p-0"> <div className="card-product overflow-hidden p-0">
<table className="w-full text-left"> <table className="table-compact w-full text-left">
<thead className="bg-cloud border-b border-hairline"> <thead className="bg-cloud border-b border-hairline">
<tr className="text-[12px] uppercase tracking-[0.5px] text-graphite"> <tr>
<th className="px-xl py-md">Name</th> <th>Name</th>
<th className="px-xl py-md">MITRE</th> <th>MITRE</th>
<th className="px-xl py-md">Created by</th> <th>Created by</th>
<th className="px-xl py-md">Updated</th> <th>Updated</th>
<th className="px-xl py-md text-right">Actions</th> <th className="text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.map((t) => ( {data.map((t) => (
<tr key={t.id} className="border-b border-hairline last:border-0"> <tr key={t.id}>
<td className="px-xl py-md"> <td>
<Link <Link
to={`/admin/templates/${t.id}/edit`} to={`/admin/templates/${t.id}/edit`}
className="text-ink font-medium hover:underline" className="text-ink font-medium hover:underline"
@@ -90,12 +90,12 @@ export function TemplatesListPage(): JSX.Element {
{t.name} {t.name}
</Link> </Link>
</td> </td>
<td className="px-xl py-md text-charcoal text-[14px]"> <td className="text-charcoal">
{mitreCount(t) === 0 ? '—' : mitreCount(t)} {mitreCount(t) === 0 ? '—' : mitreCount(t)}
</td> </td>
<td className="px-xl py-md text-charcoal">{t.created_by.username}</td> <td className="text-charcoal">{t.created_by.username}</td>
<td className="px-xl py-md text-charcoal font-mono">{formatDate(t.updated_at)}</td> <td className="text-charcoal font-mono">{formatDate(t.updated_at)}</td>
<td className="px-xl py-md text-right"> <td className="text-right">
<div className="inline-flex gap-sm"> <div className="inline-flex gap-sm">
<Link to={`/admin/templates/${t.id}/edit`} className="btn-text-link"> <Link to={`/admin/templates/${t.id}/edit`} className="btn-text-link">
Edit Edit

View File

@@ -188,13 +188,13 @@ export function UsersAdminPage(): JSX.Element {
{!list.isLoading && !list.isError && list.data && list.data.length > 0 ? ( {!list.isLoading && !list.isError && list.data && list.data.length > 0 ? (
<div className="card-product overflow-hidden p-0"> <div className="card-product overflow-hidden p-0">
<table className="w-full text-left"> <table className="table-compact w-full text-left">
<thead className="bg-cloud border-b border-hairline"> <thead className="bg-cloud border-b border-hairline">
<tr className="text-[12px] uppercase tracking-[0.5px] text-graphite"> <tr>
<th className="px-xl py-md">Username</th> <th>Username</th>
<th className="px-xl py-md">Role</th> <th>Role</th>
<th className="px-xl py-md">Created</th> <th>Created</th>
<th className="px-xl py-md text-right">Actions</th> <th className="text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -204,8 +204,8 @@ export function UsersAdminPage(): JSX.Element {
// Fragment must carry the key — `<>` cannot, which broke // Fragment must carry the key — `<>` cannot, which broke
// per-row reconciliation (reset-password state leaked across rows). // per-row reconciliation (reset-password state leaked across rows).
<Fragment key={u.id}> <Fragment key={u.id}>
<tr className="border-b border-hairline last:border-0"> <tr>
<td className="px-xl py-md text-ink"> <td className="text-ink">
<span className="font-mono font-medium">{u.username}</span> <span className="font-mono font-medium">{u.username}</span>
{isSelf ? ( {isSelf ? (
<span className="ml-sm font-sans text-[12px] text-graphite"> <span className="ml-sm font-sans text-[12px] text-graphite">
@@ -213,7 +213,7 @@ export function UsersAdminPage(): JSX.Element {
</span> </span>
) : null} ) : null}
</td> </td>
<td className="px-xl py-md"> <td>
<Select <Select
value={u.role} value={u.role}
onChange={(e) => onRoleChange(u, e.target.value as Role)} onChange={(e) => onRoleChange(u, e.target.value as Role)}
@@ -222,8 +222,8 @@ export function UsersAdminPage(): JSX.Element {
disabled={patchMutation.isPending} disabled={patchMutation.isPending}
/> />
</td> </td>
<td className="px-xl py-md text-charcoal font-mono">{u.created_at}</td> <td className="text-charcoal font-mono">{u.created_at}</td>
<td className="px-xl py-md text-right"> <td className="text-right">
<div className="inline-flex gap-sm"> <div className="inline-flex gap-sm">
<button <button
type="button" type="button"