selftune 0.2.9 → 0.2.12
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/README.md +35 -35
- package/apps/local-dashboard/dist/assets/index-4_dAY17K.js +16 -0
- package/apps/local-dashboard/dist/assets/index-BxV5WZHc.css +2 -0
- package/apps/local-dashboard/dist/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +11 -0
- package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +8 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-7xD7fNEU.js +12 -0
- package/apps/local-dashboard/dist/index.html +16 -15
- package/bin/selftune.cjs +1 -1
- package/cli/selftune/activation-rules.ts +1 -0
- package/cli/selftune/alpha-upload/build-payloads.ts +18 -2
- package/cli/selftune/alpha-upload/stage-canonical.ts +94 -0
- package/cli/selftune/auth/device-code.ts +32 -0
- package/cli/selftune/auto-update.ts +12 -0
- package/cli/selftune/badge/badge.ts +1 -0
- package/cli/selftune/canonical-export.ts +5 -0
- package/cli/selftune/claude-agents.ts +154 -0
- package/cli/selftune/contribute/bundle.ts +1 -0
- package/cli/selftune/contribute/contribute.ts +1 -0
- package/cli/selftune/cron/setup.ts +2 -2
- package/cli/selftune/dashboard-server.ts +1 -0
- package/cli/selftune/eval/hooks-to-evals.ts +1 -0
- package/cli/selftune/eval/import-skillsbench.ts +1 -0
- package/cli/selftune/eval/synthetic-evals.ts +2 -3
- package/cli/selftune/eval/unit-test.ts +1 -0
- package/cli/selftune/evolution/deploy-proposal.ts +9 -238
- package/cli/selftune/evolution/evolve-body.ts +93 -6
- package/cli/selftune/evolution/evolve.ts +3 -7
- package/cli/selftune/evolution/propose-body.ts +3 -2
- package/cli/selftune/evolution/propose-routing.ts +3 -2
- package/cli/selftune/evolution/refine-body.ts +3 -2
- package/cli/selftune/evolution/rollback.ts +1 -1
- package/cli/selftune/export.ts +1 -0
- package/cli/selftune/grading/grade-session.ts +8 -0
- package/cli/selftune/hooks/auto-activate.ts +1 -0
- package/cli/selftune/hooks/evolution-guard.ts +1 -1
- package/cli/selftune/hooks/prompt-log.ts +1 -0
- package/cli/selftune/hooks/session-stop.ts +34 -40
- package/cli/selftune/hooks/skill-change-guard.ts +1 -0
- package/cli/selftune/hooks/skill-eval.ts +1 -1
- package/cli/selftune/index.ts +23 -14
- package/cli/selftune/ingestors/claude-replay.ts +1 -0
- package/cli/selftune/ingestors/codex-rollout.ts +1 -0
- package/cli/selftune/ingestors/codex-wrapper.ts +1 -0
- package/cli/selftune/ingestors/openclaw-ingest.ts +1 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +1 -0
- package/cli/selftune/init.ts +121 -29
- package/cli/selftune/localdb/db.ts +1 -0
- package/cli/selftune/localdb/direct-write.ts +39 -0
- package/cli/selftune/localdb/materialize.ts +2 -0
- package/cli/selftune/localdb/queries.ts +53 -0
- package/cli/selftune/localdb/schema.ts +28 -0
- package/cli/selftune/normalization.ts +1 -0
- package/cli/selftune/observability.ts +1 -0
- package/cli/selftune/repair/skill-usage.ts +1 -0
- package/cli/selftune/routes/orchestrate-runs.ts +1 -0
- package/cli/selftune/routes/overview.ts +1 -0
- package/cli/selftune/routes/report.ts +1 -1
- package/cli/selftune/routes/skill-report.ts +2 -1
- package/cli/selftune/status.ts +1 -1
- package/cli/selftune/sync.ts +30 -1
- package/cli/selftune/uninstall.ts +412 -0
- package/cli/selftune/utils/canonical-log.ts +2 -0
- package/cli/selftune/utils/frontmatter.ts +50 -7
- package/cli/selftune/utils/jsonl.ts +1 -0
- package/cli/selftune/utils/llm-call.ts +131 -3
- package/cli/selftune/utils/skill-log.ts +1 -0
- package/cli/selftune/utils/transcript.ts +1 -0
- package/cli/selftune/utils/trigger-check.ts +1 -1
- package/cli/selftune/workflows/skill-md-writer.ts +5 -5
- package/cli/selftune/workflows/workflows.ts +1 -0
- package/package.json +37 -33
- package/packages/telemetry-contract/fixtures/golden.test.ts +1 -0
- package/packages/telemetry-contract/package.json +1 -1
- package/packages/telemetry-contract/src/schemas.ts +1 -0
- package/packages/telemetry-contract/tests/compatibility.test.ts +1 -0
- package/packages/ui/README.md +35 -34
- package/packages/ui/package.json +3 -3
- package/packages/ui/src/components/ActivityTimeline.tsx +50 -43
- package/packages/ui/src/components/EvidenceViewer.tsx +306 -182
- package/packages/ui/src/components/EvolutionTimeline.tsx +83 -72
- package/packages/ui/src/components/InfoTip.tsx +4 -3
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +60 -53
- package/packages/ui/src/components/section-cards.tsx +20 -25
- package/packages/ui/src/components/skill-health-grid.tsx +213 -193
- package/packages/ui/src/lib/constants.tsx +1 -0
- package/packages/ui/src/primitives/badge.tsx +12 -15
- package/packages/ui/src/primitives/button.tsx +7 -7
- package/packages/ui/src/primitives/card.tsx +15 -26
- package/packages/ui/src/primitives/checkbox.tsx +7 -8
- package/packages/ui/src/primitives/collapsible.tsx +5 -5
- package/packages/ui/src/primitives/dropdown-menu.tsx +45 -55
- package/packages/ui/src/primitives/label.tsx +6 -6
- package/packages/ui/src/primitives/select.tsx +28 -37
- package/packages/ui/src/primitives/table.tsx +17 -44
- package/packages/ui/src/primitives/tabs.tsx +14 -21
- package/packages/ui/src/primitives/tooltip.tsx +10 -22
- package/skill/SKILL.md +70 -57
- package/skill/Workflows/AlphaUpload.md +4 -4
- package/skill/Workflows/AutoActivation.md +11 -6
- package/skill/Workflows/Badge.md +22 -16
- package/skill/Workflows/Baseline.md +34 -36
- package/skill/Workflows/Composability.md +16 -11
- package/skill/Workflows/Contribute.md +26 -21
- package/skill/Workflows/Cron.md +23 -22
- package/skill/Workflows/Dashboard.md +32 -27
- package/skill/Workflows/Doctor.md +33 -27
- package/skill/Workflows/Evals.md +48 -47
- package/skill/Workflows/EvolutionMemory.md +31 -21
- package/skill/Workflows/Evolve.md +84 -82
- package/skill/Workflows/EvolveBody.md +58 -47
- package/skill/Workflows/Grade.md +16 -13
- package/skill/Workflows/ImportSkillsBench.md +9 -6
- package/skill/Workflows/Ingest.md +36 -21
- package/skill/Workflows/Initialize.md +108 -40
- package/skill/Workflows/Orchestrate.md +22 -16
- package/skill/Workflows/Replay.md +12 -7
- package/skill/Workflows/Rollback.md +13 -6
- package/skill/Workflows/Schedule.md +6 -6
- package/skill/Workflows/Sync.md +18 -11
- package/skill/Workflows/UnitTest.md +28 -17
- package/skill/Workflows/Watch.md +28 -21
- package/skill/agents/diagnosis-analyst.md +11 -0
- package/skill/agents/evolution-reviewer.md +15 -1
- package/skill/agents/integration-guide.md +10 -0
- package/skill/agents/pattern-analyst.md +12 -1
- package/skill/references/grading-methodology.md +23 -24
- package/skill/references/interactive-config.md +7 -7
- package/skill/references/invocation-taxonomy.md +22 -20
- package/skill/references/logs.md +14 -6
- package/skill/references/setup-patterns.md +4 -2
- package/.claude/agents/diagnosis-analyst.md +0 -156
- package/.claude/agents/evolution-reviewer.md +0 -180
- package/.claude/agents/integration-guide.md +0 -212
- package/.claude/agents/pattern-analyst.md +0 -160
- package/apps/local-dashboard/dist/assets/index-Bs3Y4ixf.css +0 -1
- package/apps/local-dashboard/dist/assets/index-C4UYGWKr.js +0 -15
- package/apps/local-dashboard/dist/assets/vendor-react-BQH_6WrG.js +0 -60
- package/apps/local-dashboard/dist/assets/vendor-table-dK1QMLq9.js +0 -26
- package/apps/local-dashboard/dist/assets/vendor-ui-CO2mrx6e.js +0 -341
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
1
|
import {
|
|
3
2
|
closestCenter,
|
|
4
3
|
DndContext,
|
|
@@ -9,16 +8,16 @@ import {
|
|
|
9
8
|
useSensors,
|
|
10
9
|
type DragEndEvent,
|
|
11
10
|
type UniqueIdentifier,
|
|
12
|
-
} from "@dnd-kit/core"
|
|
13
|
-
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
|
|
11
|
+
} from "@dnd-kit/core";
|
|
12
|
+
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
14
13
|
import {
|
|
15
14
|
arrayMove,
|
|
16
15
|
SortableContext,
|
|
17
16
|
sortableKeyboardCoordinates,
|
|
18
17
|
useSortable,
|
|
19
18
|
verticalListSortingStrategy,
|
|
20
|
-
} from "@dnd-kit/sortable"
|
|
21
|
-
import { CSS } from "@dnd-kit/utilities"
|
|
19
|
+
} from "@dnd-kit/sortable";
|
|
20
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
22
21
|
import {
|
|
23
22
|
flexRender,
|
|
24
23
|
getCoreRowModel,
|
|
@@ -33,45 +32,7 @@ import {
|
|
|
33
32
|
type Row,
|
|
34
33
|
type SortingState,
|
|
35
34
|
type VisibilityState,
|
|
36
|
-
} from "@tanstack/react-table"
|
|
37
|
-
|
|
38
|
-
import { Badge } from "../primitives/badge"
|
|
39
|
-
import { Button } from "../primitives/button"
|
|
40
|
-
import { Checkbox } from "../primitives/checkbox"
|
|
41
|
-
import {
|
|
42
|
-
DropdownMenu,
|
|
43
|
-
DropdownMenuCheckboxItem,
|
|
44
|
-
DropdownMenuContent,
|
|
45
|
-
DropdownMenuRadioGroup,
|
|
46
|
-
DropdownMenuRadioItem,
|
|
47
|
-
DropdownMenuTrigger,
|
|
48
|
-
} from "../primitives/dropdown-menu"
|
|
49
|
-
import { Label } from "../primitives/label"
|
|
50
|
-
import {
|
|
51
|
-
Select,
|
|
52
|
-
SelectContent,
|
|
53
|
-
SelectGroup,
|
|
54
|
-
SelectItem,
|
|
55
|
-
SelectTrigger,
|
|
56
|
-
SelectValue,
|
|
57
|
-
} from "../primitives/select"
|
|
58
|
-
import {
|
|
59
|
-
Table,
|
|
60
|
-
TableBody,
|
|
61
|
-
TableCell,
|
|
62
|
-
TableHead,
|
|
63
|
-
TableHeader,
|
|
64
|
-
TableRow,
|
|
65
|
-
} from "../primitives/table"
|
|
66
|
-
import {
|
|
67
|
-
Tabs,
|
|
68
|
-
TabsContent,
|
|
69
|
-
TabsList,
|
|
70
|
-
TabsTrigger,
|
|
71
|
-
} from "../primitives/tabs"
|
|
72
|
-
import { STATUS_CONFIG } from "../lib/constants"
|
|
73
|
-
import type { SkillCard, SkillHealthStatus } from "../types"
|
|
74
|
-
import { formatRate, timeAgo } from "../lib/format"
|
|
35
|
+
} from "@tanstack/react-table";
|
|
75
36
|
import {
|
|
76
37
|
GripVerticalIcon,
|
|
77
38
|
Columns3Icon,
|
|
@@ -88,17 +49,47 @@ import {
|
|
|
88
49
|
XCircleIcon,
|
|
89
50
|
CircleDotIcon,
|
|
90
51
|
HelpCircleIcon,
|
|
91
|
-
} from "lucide-react"
|
|
52
|
+
} from "lucide-react";
|
|
53
|
+
import * as React from "react";
|
|
54
|
+
|
|
55
|
+
import { STATUS_CONFIG } from "../lib/constants";
|
|
56
|
+
import { formatRate, timeAgo } from "../lib/format";
|
|
57
|
+
import { Badge } from "../primitives/badge";
|
|
58
|
+
import { Button } from "../primitives/button";
|
|
59
|
+
import { Checkbox } from "../primitives/checkbox";
|
|
60
|
+
import {
|
|
61
|
+
DropdownMenu,
|
|
62
|
+
DropdownMenuCheckboxItem,
|
|
63
|
+
DropdownMenuContent,
|
|
64
|
+
DropdownMenuRadioGroup,
|
|
65
|
+
DropdownMenuRadioItem,
|
|
66
|
+
DropdownMenuTrigger,
|
|
67
|
+
} from "../primitives/dropdown-menu";
|
|
68
|
+
import { Label } from "../primitives/label";
|
|
69
|
+
import {
|
|
70
|
+
Select,
|
|
71
|
+
SelectContent,
|
|
72
|
+
SelectGroup,
|
|
73
|
+
SelectItem,
|
|
74
|
+
SelectTrigger,
|
|
75
|
+
SelectValue,
|
|
76
|
+
} from "../primitives/select";
|
|
77
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../primitives/table";
|
|
78
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs";
|
|
79
|
+
import type { SkillCard, SkillHealthStatus } from "../types";
|
|
92
80
|
|
|
93
81
|
// ---------- Drag handle ----------
|
|
94
82
|
|
|
95
|
-
type SortableContextValue = Pick<
|
|
83
|
+
type SortableContextValue = Pick<
|
|
84
|
+
ReturnType<typeof useSortable>,
|
|
85
|
+
"attributes" | "listeners" | "setActivatorNodeRef"
|
|
86
|
+
>;
|
|
96
87
|
|
|
97
|
-
const SortableRowContext = React.createContext<SortableContextValue | null>(null)
|
|
88
|
+
const SortableRowContext = React.createContext<SortableContextValue | null>(null);
|
|
98
89
|
|
|
99
90
|
function DragHandle() {
|
|
100
|
-
const ctx = React.useContext(SortableRowContext)
|
|
101
|
-
if (!ctx) return null
|
|
91
|
+
const ctx = React.useContext(SortableRowContext);
|
|
92
|
+
if (!ctx) return null;
|
|
102
93
|
return (
|
|
103
94
|
<Button
|
|
104
95
|
ref={ctx.setActivatorNodeRef}
|
|
@@ -111,12 +102,14 @@ function DragHandle() {
|
|
|
111
102
|
<GripVerticalIcon className="size-3 text-muted-foreground" />
|
|
112
103
|
<span className="sr-only">Drag to reorder</span>
|
|
113
104
|
</Button>
|
|
114
|
-
)
|
|
105
|
+
);
|
|
115
106
|
}
|
|
116
107
|
|
|
117
108
|
// ---------- Column definitions ----------
|
|
118
109
|
|
|
119
|
-
function createColumns(
|
|
110
|
+
function createColumns(
|
|
111
|
+
renderSkillName?: (skill: SkillCard) => React.ReactNode,
|
|
112
|
+
): ColumnDef<SkillCard>[] {
|
|
120
113
|
return [
|
|
121
114
|
{
|
|
122
115
|
id: "drag",
|
|
@@ -129,10 +122,7 @@ function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode):
|
|
|
129
122
|
<div className="flex items-center justify-center">
|
|
130
123
|
<Checkbox
|
|
131
124
|
checked={table.getIsAllPageRowsSelected()}
|
|
132
|
-
indeterminate={
|
|
133
|
-
table.getIsSomePageRowsSelected() &&
|
|
134
|
-
!table.getIsAllPageRowsSelected()
|
|
135
|
-
}
|
|
125
|
+
indeterminate={table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()}
|
|
136
126
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
137
127
|
aria-label="Select all"
|
|
138
128
|
/>
|
|
@@ -153,77 +143,82 @@ function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode):
|
|
|
153
143
|
{
|
|
154
144
|
accessorKey: "name",
|
|
155
145
|
header: "Skill",
|
|
156
|
-
cell: ({ row }) =>
|
|
157
|
-
?
|
|
158
|
-
|
|
146
|
+
cell: ({ row }) =>
|
|
147
|
+
renderSkillName ? (
|
|
148
|
+
renderSkillName(row.original)
|
|
149
|
+
) : (
|
|
150
|
+
<span className="text-sm font-medium">{row.original.name}</span>
|
|
151
|
+
),
|
|
159
152
|
enableHiding: false,
|
|
160
153
|
},
|
|
161
154
|
{
|
|
162
155
|
accessorKey: "scope",
|
|
163
156
|
header: "Scope",
|
|
164
157
|
cell: ({ row }) => {
|
|
165
|
-
const scope = row.original.scope
|
|
166
|
-
if (!scope) return <span className="text-xs text-muted-foreground">--</span
|
|
158
|
+
const scope = row.original.scope;
|
|
159
|
+
if (!scope) return <span className="text-xs text-muted-foreground">--</span>;
|
|
167
160
|
return (
|
|
168
161
|
<Badge variant="secondary" className="text-[10px]">
|
|
169
162
|
{scope}
|
|
170
163
|
</Badge>
|
|
171
|
-
)
|
|
164
|
+
);
|
|
172
165
|
},
|
|
173
166
|
},
|
|
174
167
|
{
|
|
175
168
|
accessorKey: "status",
|
|
176
169
|
header: "Status",
|
|
177
170
|
cell: ({ row }) => {
|
|
178
|
-
const config = STATUS_CONFIG[row.original.status]
|
|
171
|
+
const config = STATUS_CONFIG[row.original.status];
|
|
179
172
|
return (
|
|
180
173
|
<Badge variant={config.variant} className="gap-1 px-1.5 text-muted-foreground">
|
|
181
174
|
{config.icon}
|
|
182
175
|
{config.label}
|
|
183
176
|
</Badge>
|
|
184
|
-
)
|
|
177
|
+
);
|
|
185
178
|
},
|
|
186
179
|
sortingFn: (rowA, rowB) => {
|
|
187
180
|
const order: Record<SkillHealthStatus, number> = {
|
|
188
|
-
CRITICAL: 0,
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
CRITICAL: 0,
|
|
182
|
+
WARNING: 1,
|
|
183
|
+
UNGRADED: 2,
|
|
184
|
+
UNKNOWN: 3,
|
|
185
|
+
HEALTHY: 4,
|
|
186
|
+
};
|
|
187
|
+
return order[rowA.original.status] - order[rowB.original.status];
|
|
191
188
|
},
|
|
192
189
|
},
|
|
193
190
|
{
|
|
194
191
|
accessorKey: "passRate",
|
|
195
192
|
header: () => <div className="w-full text-right">Pass Rate</div>,
|
|
196
193
|
cell: ({ row }) => {
|
|
197
|
-
const rate = row.original.passRate
|
|
198
|
-
const isLow = rate !== null && rate < 0.5
|
|
194
|
+
const rate = row.original.passRate;
|
|
195
|
+
const isLow = rate !== null && rate < 0.5;
|
|
199
196
|
return (
|
|
200
|
-
<div
|
|
197
|
+
<div
|
|
198
|
+
className={`text-right font-mono tabular-nums ${isLow ? "text-red-600 font-semibold" : ""}`}
|
|
199
|
+
>
|
|
201
200
|
{formatRate(rate)}
|
|
202
201
|
</div>
|
|
203
|
-
)
|
|
202
|
+
);
|
|
204
203
|
},
|
|
205
204
|
sortingFn: (rowA, rowB) => {
|
|
206
|
-
const a = rowA.original.passRate ?? -1
|
|
207
|
-
const b = rowB.original.passRate ?? -1
|
|
208
|
-
return a - b
|
|
205
|
+
const a = rowA.original.passRate ?? -1;
|
|
206
|
+
const b = rowB.original.passRate ?? -1;
|
|
207
|
+
return a - b;
|
|
209
208
|
},
|
|
210
209
|
},
|
|
211
210
|
{
|
|
212
211
|
accessorKey: "checks",
|
|
213
212
|
header: () => <div className="w-full text-right">Checks</div>,
|
|
214
213
|
cell: ({ row }) => (
|
|
215
|
-
<div className="text-right font-mono tabular-nums">
|
|
216
|
-
{row.original.checks}
|
|
217
|
-
</div>
|
|
214
|
+
<div className="text-right font-mono tabular-nums">{row.original.checks}</div>
|
|
218
215
|
),
|
|
219
216
|
},
|
|
220
217
|
{
|
|
221
218
|
accessorKey: "uniqueSessions",
|
|
222
219
|
header: () => <div className="w-full text-right">Sessions</div>,
|
|
223
220
|
cell: ({ row }) => (
|
|
224
|
-
<div className="text-right font-mono tabular-nums">
|
|
225
|
-
{row.original.uniqueSessions}
|
|
226
|
-
</div>
|
|
221
|
+
<div className="text-right font-mono tabular-nums">{row.original.uniqueSessions}</div>
|
|
227
222
|
),
|
|
228
223
|
},
|
|
229
224
|
{
|
|
@@ -243,13 +238,13 @@ function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode):
|
|
|
243
238
|
),
|
|
244
239
|
sortingFn: (rowA, rowB) => {
|
|
245
240
|
const toEpoch = (v: string | null) => {
|
|
246
|
-
if (!v) return 0
|
|
247
|
-
const t = new Date(v).getTime()
|
|
248
|
-
return Number.isNaN(t) ? 0 : t
|
|
249
|
-
}
|
|
250
|
-
const a = toEpoch(rowA.original.lastSeen)
|
|
251
|
-
const b = toEpoch(rowB.original.lastSeen)
|
|
252
|
-
return a - b
|
|
241
|
+
if (!v) return 0;
|
|
242
|
+
const t = new Date(v).getTime();
|
|
243
|
+
return Number.isNaN(t) ? 0 : t;
|
|
244
|
+
};
|
|
245
|
+
const a = toEpoch(rowA.original.lastSeen);
|
|
246
|
+
const b = toEpoch(rowB.original.lastSeen);
|
|
247
|
+
return a - b;
|
|
253
248
|
},
|
|
254
249
|
},
|
|
255
250
|
{
|
|
@@ -264,19 +259,27 @@ function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode):
|
|
|
264
259
|
</Badge>
|
|
265
260
|
),
|
|
266
261
|
},
|
|
267
|
-
]
|
|
262
|
+
];
|
|
268
263
|
}
|
|
269
264
|
|
|
270
265
|
// ---------- Draggable row ----------
|
|
271
266
|
|
|
272
267
|
function DraggableRow({ row }: { row: Row<SkillCard> }) {
|
|
273
|
-
const {
|
|
268
|
+
const {
|
|
269
|
+
transform,
|
|
270
|
+
transition,
|
|
271
|
+
setNodeRef,
|
|
272
|
+
setActivatorNodeRef,
|
|
273
|
+
isDragging,
|
|
274
|
+
attributes,
|
|
275
|
+
listeners,
|
|
276
|
+
} = useSortable({
|
|
274
277
|
id: row.original.name,
|
|
275
|
-
})
|
|
278
|
+
});
|
|
276
279
|
const sortableCtx = React.useMemo(
|
|
277
280
|
() => ({ attributes, listeners, setActivatorNodeRef }),
|
|
278
281
|
[attributes, listeners, setActivatorNodeRef],
|
|
279
|
-
)
|
|
282
|
+
);
|
|
280
283
|
return (
|
|
281
284
|
<SortableRowContext.Provider value={sortableCtx}>
|
|
282
285
|
<TableRow
|
|
@@ -296,7 +299,7 @@ function DraggableRow({ row }: { row: Row<SkillCard> }) {
|
|
|
296
299
|
))}
|
|
297
300
|
</TableRow>
|
|
298
301
|
</SortableRowContext.Provider>
|
|
299
|
-
)
|
|
302
|
+
);
|
|
300
303
|
}
|
|
301
304
|
|
|
302
305
|
// ---------- Main component ----------
|
|
@@ -308,59 +311,64 @@ export function SkillHealthGrid({
|
|
|
308
311
|
onStatusFilterChange,
|
|
309
312
|
renderSkillName,
|
|
310
313
|
}: {
|
|
311
|
-
cards: SkillCard[]
|
|
312
|
-
totalCount: number
|
|
313
|
-
statusFilter?: SkillHealthStatus | "ALL"
|
|
314
|
-
onStatusFilterChange?: (v: SkillHealthStatus | "ALL") => void
|
|
315
|
-
renderSkillName?: (skill: SkillCard) => React.ReactNode
|
|
314
|
+
cards: SkillCard[];
|
|
315
|
+
totalCount: number;
|
|
316
|
+
statusFilter?: SkillHealthStatus | "ALL";
|
|
317
|
+
onStatusFilterChange?: (v: SkillHealthStatus | "ALL") => void;
|
|
318
|
+
renderSkillName?: (skill: SkillCard) => React.ReactNode;
|
|
316
319
|
}) {
|
|
317
|
-
const [activeView, setActiveView] = React.useState("all")
|
|
318
|
-
const [data, setData] = React.useState<SkillCard[]>([])
|
|
319
|
-
const [rowSelection, setRowSelection] = React.useState({})
|
|
320
|
-
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
|
|
321
|
-
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
|
322
|
-
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
320
|
+
const [activeView, setActiveView] = React.useState("all");
|
|
321
|
+
const [data, setData] = React.useState<SkillCard[]>([]);
|
|
322
|
+
const [rowSelection, setRowSelection] = React.useState({});
|
|
323
|
+
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
|
324
|
+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
|
325
|
+
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
323
326
|
const [pagination, setPagination] = React.useState({
|
|
324
327
|
pageIndex: 0,
|
|
325
328
|
pageSize: 20,
|
|
326
|
-
})
|
|
329
|
+
});
|
|
327
330
|
|
|
328
|
-
const columns = React.useMemo(() => createColumns(renderSkillName), [renderSkillName])
|
|
331
|
+
const columns = React.useMemo(() => createColumns(renderSkillName), [renderSkillName]);
|
|
329
332
|
|
|
330
333
|
// View counts for tab badges
|
|
331
|
-
const viewCounts = React.useMemo(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
334
|
+
const viewCounts = React.useMemo(
|
|
335
|
+
() => ({
|
|
336
|
+
all: cards.length,
|
|
337
|
+
attention: cards.filter((c) => c.status === "CRITICAL" || c.status === "WARNING").length,
|
|
338
|
+
recent: cards.filter((c) => c.lastSeen !== null).length,
|
|
339
|
+
ungraded: cards.filter((c) => c.status === "UNGRADED" || c.status === "UNKNOWN").length,
|
|
340
|
+
}),
|
|
341
|
+
[cards],
|
|
342
|
+
);
|
|
337
343
|
|
|
338
344
|
// Filter cards based on active view tab, then sync into local state for DnD
|
|
339
345
|
React.useEffect(() => {
|
|
340
|
-
let filtered = cards
|
|
346
|
+
let filtered = cards;
|
|
341
347
|
if (activeView === "attention") {
|
|
342
|
-
filtered = cards.filter((c) => c.status === "CRITICAL" || c.status === "WARNING")
|
|
348
|
+
filtered = cards.filter((c) => c.status === "CRITICAL" || c.status === "WARNING");
|
|
343
349
|
} else if (activeView === "recent") {
|
|
344
|
-
filtered =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
350
|
+
filtered = cards
|
|
351
|
+
.filter((c) => c.lastSeen !== null)
|
|
352
|
+
.sort((a: SkillCard, b: SkillCard) => {
|
|
353
|
+
const aTime = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
|
|
354
|
+
const bTime = b.lastSeen ? new Date(b.lastSeen).getTime() : 0;
|
|
355
|
+
return bTime - aTime;
|
|
356
|
+
});
|
|
349
357
|
} else if (activeView === "ungraded") {
|
|
350
|
-
filtered = cards.filter((c) => c.status === "UNGRADED" || c.status === "UNKNOWN")
|
|
358
|
+
filtered = cards.filter((c) => c.status === "UNGRADED" || c.status === "UNKNOWN");
|
|
351
359
|
}
|
|
352
|
-
setData(filtered)
|
|
353
|
-
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
|
|
354
|
-
}, [cards, activeView])
|
|
360
|
+
setData(filtered);
|
|
361
|
+
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
|
362
|
+
}, [cards, activeView]);
|
|
355
363
|
|
|
356
|
-
const sortableId = React.useId()
|
|
364
|
+
const sortableId = React.useId();
|
|
357
365
|
const sensors = useSensors(
|
|
358
366
|
useSensor(MouseSensor, {}),
|
|
359
367
|
useSensor(TouchSensor, {}),
|
|
360
368
|
useSensor(KeyboardSensor, {
|
|
361
369
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
362
|
-
})
|
|
363
|
-
)
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
364
372
|
|
|
365
373
|
const table = useReactTable({
|
|
366
374
|
data,
|
|
@@ -385,26 +393,26 @@ export function SkillHealthGrid({
|
|
|
385
393
|
getSortedRowModel: getSortedRowModel(),
|
|
386
394
|
getFacetedRowModel: getFacetedRowModel(),
|
|
387
395
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
388
|
-
})
|
|
396
|
+
});
|
|
389
397
|
|
|
390
398
|
const dataIds = React.useMemo<UniqueIdentifier[]>(
|
|
391
399
|
() => table.getRowModel().rows.map((r) => r.id),
|
|
392
|
-
[table.getRowModel().rows]
|
|
393
|
-
)
|
|
400
|
+
[table.getRowModel().rows],
|
|
401
|
+
);
|
|
394
402
|
|
|
395
|
-
const isSorted = sorting.length > 0
|
|
403
|
+
const isSorted = sorting.length > 0;
|
|
396
404
|
|
|
397
405
|
function handleDragEnd(event: DragEndEvent) {
|
|
398
|
-
if (isSorted) return
|
|
399
|
-
const { active, over } = event
|
|
406
|
+
if (isSorted) return;
|
|
407
|
+
const { active, over } = event;
|
|
400
408
|
if (active && over && active.id !== over.id) {
|
|
401
409
|
setData((prev) => {
|
|
402
|
-
const ids = prev.map((d) => d.name)
|
|
403
|
-
const oldIndex = ids.indexOf(active.id as string)
|
|
404
|
-
const newIndex = ids.indexOf(over.id as string)
|
|
405
|
-
if (oldIndex === -1 || newIndex === -1) return prev
|
|
406
|
-
return arrayMove(prev, oldIndex, newIndex)
|
|
407
|
-
})
|
|
410
|
+
const ids = prev.map((d) => d.name);
|
|
411
|
+
const oldIndex = ids.indexOf(active.id as string);
|
|
412
|
+
const newIndex = ids.indexOf(over.id as string);
|
|
413
|
+
if (oldIndex === -1 || newIndex === -1) return prev;
|
|
414
|
+
return arrayMove(prev, oldIndex, newIndex);
|
|
415
|
+
});
|
|
408
416
|
}
|
|
409
417
|
}
|
|
410
418
|
|
|
@@ -419,15 +427,8 @@ export function SkillHealthGrid({
|
|
|
419
427
|
<Label htmlFor="view-selector" className="sr-only">
|
|
420
428
|
View
|
|
421
429
|
</Label>
|
|
422
|
-
<Select
|
|
423
|
-
|
|
424
|
-
onValueChange={(v) => v && setActiveView(v)}
|
|
425
|
-
>
|
|
426
|
-
<SelectTrigger
|
|
427
|
-
className="flex w-fit @4xl/main:hidden"
|
|
428
|
-
size="sm"
|
|
429
|
-
id="view-selector"
|
|
430
|
-
>
|
|
430
|
+
<Select value={activeView} onValueChange={(v) => v && setActiveView(v)}>
|
|
431
|
+
<SelectTrigger className="flex w-fit @4xl/main:hidden" size="sm" id="view-selector">
|
|
431
432
|
<SelectValue placeholder="Select a view" />
|
|
432
433
|
</SelectTrigger>
|
|
433
434
|
<SelectContent>
|
|
@@ -447,21 +448,15 @@ export function SkillHealthGrid({
|
|
|
447
448
|
</TabsTrigger>
|
|
448
449
|
<TabsTrigger value="attention">
|
|
449
450
|
Needs Attention{" "}
|
|
450
|
-
{viewCounts.attention > 0 &&
|
|
451
|
-
<Badge variant="secondary">{viewCounts.attention}</Badge>
|
|
452
|
-
)}
|
|
451
|
+
{viewCounts.attention > 0 && <Badge variant="secondary">{viewCounts.attention}</Badge>}
|
|
453
452
|
</TabsTrigger>
|
|
454
453
|
<TabsTrigger value="recent">
|
|
455
454
|
Recently Active{" "}
|
|
456
|
-
{viewCounts.recent > 0 &&
|
|
457
|
-
<Badge variant="secondary">{viewCounts.recent}</Badge>
|
|
458
|
-
)}
|
|
455
|
+
{viewCounts.recent > 0 && <Badge variant="secondary">{viewCounts.recent}</Badge>}
|
|
459
456
|
</TabsTrigger>
|
|
460
457
|
<TabsTrigger value="ungraded">
|
|
461
458
|
Ungraded{" "}
|
|
462
|
-
{viewCounts.ungraded > 0 &&
|
|
463
|
-
<Badge variant="secondary">{viewCounts.ungraded}</Badge>
|
|
464
|
-
)}
|
|
459
|
+
{viewCounts.ungraded > 0 && <Badge variant="secondary">{viewCounts.ungraded}</Badge>}
|
|
465
460
|
</TabsTrigger>
|
|
466
461
|
</TabsList>
|
|
467
462
|
|
|
@@ -470,7 +465,9 @@ export function SkillHealthGrid({
|
|
|
470
465
|
<DropdownMenu>
|
|
471
466
|
<DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
|
|
472
467
|
<FilterIcon data-icon="inline-start" className="size-3.5" />
|
|
473
|
-
{statusFilter && statusFilter !== "ALL"
|
|
468
|
+
{statusFilter && statusFilter !== "ALL"
|
|
469
|
+
? statusFilter.charAt(0) + statusFilter.slice(1).toLowerCase()
|
|
470
|
+
: "Status"}
|
|
474
471
|
<ChevronDownIcon data-icon="inline-end" />
|
|
475
472
|
</DropdownMenuTrigger>
|
|
476
473
|
<DropdownMenuContent align="end" className="w-40">
|
|
@@ -478,14 +475,40 @@ export function SkillHealthGrid({
|
|
|
478
475
|
value={statusFilter ?? "ALL"}
|
|
479
476
|
onValueChange={(v) => onStatusFilterChange(v as SkillHealthStatus | "ALL")}
|
|
480
477
|
>
|
|
481
|
-
{(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
478
|
+
{(
|
|
479
|
+
[
|
|
480
|
+
{
|
|
481
|
+
label: "All",
|
|
482
|
+
value: "ALL" as const,
|
|
483
|
+
icon: <LayersIcon className="size-3.5" />,
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
label: "Healthy",
|
|
487
|
+
value: "HEALTHY" as const,
|
|
488
|
+
icon: <CheckCircleIcon className="size-3.5 text-emerald-600" />,
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
label: "Warning",
|
|
492
|
+
value: "WARNING" as const,
|
|
493
|
+
icon: <AlertTriangleIcon className="size-3.5 text-amber-500" />,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
label: "Critical",
|
|
497
|
+
value: "CRITICAL" as const,
|
|
498
|
+
icon: <XCircleIcon className="size-3.5 text-red-500" />,
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
label: "Ungraded",
|
|
502
|
+
value: "UNGRADED" as const,
|
|
503
|
+
icon: <CircleDotIcon className="size-3.5 text-muted-foreground" />,
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
label: "Unknown",
|
|
507
|
+
value: "UNKNOWN" as const,
|
|
508
|
+
icon: <HelpCircleIcon className="size-3.5 text-muted-foreground/60" />,
|
|
509
|
+
},
|
|
510
|
+
] as const
|
|
511
|
+
).map((f) => (
|
|
489
512
|
<DropdownMenuRadioItem key={f.value} value={f.value}>
|
|
490
513
|
<span className="flex items-center gap-2">
|
|
491
514
|
{f.icon}
|
|
@@ -506,26 +529,25 @@ export function SkillHealthGrid({
|
|
|
506
529
|
<DropdownMenuContent align="end" className="w-40">
|
|
507
530
|
{table
|
|
508
531
|
.getAllColumns()
|
|
509
|
-
.filter(
|
|
510
|
-
(column) =>
|
|
511
|
-
typeof column.accessorFn !== "undefined" &&
|
|
512
|
-
column.getCanHide()
|
|
513
|
-
)
|
|
532
|
+
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
|
|
514
533
|
.map((column) => (
|
|
515
534
|
<DropdownMenuCheckboxItem
|
|
516
535
|
key={column.id}
|
|
517
536
|
className="capitalize"
|
|
518
537
|
checked={column.getIsVisible()}
|
|
519
|
-
onCheckedChange={(value) =>
|
|
520
|
-
column.toggleVisibility(!!value)
|
|
521
|
-
}
|
|
538
|
+
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
|
522
539
|
>
|
|
523
|
-
{column.id === "scope"
|
|
524
|
-
|
|
525
|
-
: column.id === "
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
540
|
+
{column.id === "scope"
|
|
541
|
+
? "Scope"
|
|
542
|
+
: column.id === "passRate"
|
|
543
|
+
? "Pass Rate"
|
|
544
|
+
: column.id === "uniqueSessions"
|
|
545
|
+
? "Sessions"
|
|
546
|
+
: column.id === "lastSeen"
|
|
547
|
+
? "Last Seen"
|
|
548
|
+
: column.id === "hasEvidence"
|
|
549
|
+
? "Evidence"
|
|
550
|
+
: column.id}
|
|
529
551
|
</DropdownMenuCheckboxItem>
|
|
530
552
|
))}
|
|
531
553
|
</DropdownMenuContent>
|
|
@@ -533,7 +555,10 @@ export function SkillHealthGrid({
|
|
|
533
555
|
</div>
|
|
534
556
|
</div>
|
|
535
557
|
|
|
536
|
-
<TabsContent
|
|
558
|
+
<TabsContent
|
|
559
|
+
value={activeView}
|
|
560
|
+
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
|
|
561
|
+
>
|
|
537
562
|
<div className="overflow-hidden rounded-lg border">
|
|
538
563
|
<DndContext
|
|
539
564
|
collisionDetection={closestCenter}
|
|
@@ -556,13 +581,12 @@ export function SkillHealthGrid({
|
|
|
556
581
|
<div className="flex items-center gap-1">
|
|
557
582
|
{header.isPlaceholder
|
|
558
583
|
? null
|
|
559
|
-
: flexRender(
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
: null}
|
|
584
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
585
|
+
{header.column.getIsSorted() === "asc"
|
|
586
|
+
? " ↑"
|
|
587
|
+
: header.column.getIsSorted() === "desc"
|
|
588
|
+
? " ↓"
|
|
589
|
+
: null}
|
|
566
590
|
</div>
|
|
567
591
|
</TableHead>
|
|
568
592
|
))}
|
|
@@ -571,10 +595,7 @@ export function SkillHealthGrid({
|
|
|
571
595
|
</TableHeader>
|
|
572
596
|
<TableBody className="**:data-[slot=table-cell]:first:w-8">
|
|
573
597
|
{table.getRowModel().rows?.length ? (
|
|
574
|
-
<SortableContext
|
|
575
|
-
items={dataIds}
|
|
576
|
-
strategy={verticalListSortingStrategy}
|
|
577
|
-
>
|
|
598
|
+
<SortableContext items={dataIds} strategy={verticalListSortingStrategy}>
|
|
578
599
|
{table.getRowModel().rows.map((row) => (
|
|
579
600
|
<DraggableRow key={row.id} row={row} />
|
|
580
601
|
))}
|
|
@@ -610,7 +631,7 @@ export function SkillHealthGrid({
|
|
|
610
631
|
<Select
|
|
611
632
|
value={`${table.getState().pagination.pageSize}`}
|
|
612
633
|
onValueChange={(value) => {
|
|
613
|
-
table.setPageSize(Number(value))
|
|
634
|
+
table.setPageSize(Number(value));
|
|
614
635
|
}}
|
|
615
636
|
items={[10, 20, 50, 100].map((s) => ({
|
|
616
637
|
label: `${s}`,
|
|
@@ -633,8 +654,7 @@ export function SkillHealthGrid({
|
|
|
633
654
|
</div>
|
|
634
655
|
{table.getRowModel().rows.length > 0 && (
|
|
635
656
|
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
|
636
|
-
Page {table.getState().pagination.pageIndex + 1} of{
|
|
637
|
-
{table.getPageCount()}
|
|
657
|
+
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
638
658
|
</div>
|
|
639
659
|
)}
|
|
640
660
|
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
|
@@ -682,5 +702,5 @@ export function SkillHealthGrid({
|
|
|
682
702
|
</div>
|
|
683
703
|
</TabsContent>
|
|
684
704
|
</Tabs>
|
|
685
|
-
)
|
|
705
|
+
);
|
|
686
706
|
}
|