From 13a49d3c3a6439d446a30ca14356c6bb0f8afc8f Mon Sep 17 00:00:00 2001 From: CodingInAVan Date: Sun, 29 Mar 2026 10:23:36 -0700 Subject: [PATCH] Adding remove button --- src/components/SourceCorrelationView.tsx | 60 +++++++++++++++++++++--- src/pages/SessionList.tsx | 55 +++++++++++++++++----- src/store/useStore.ts | 6 +++ 3 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/components/SourceCorrelationView.tsx b/src/components/SourceCorrelationView.tsx index c50322e..fe80259 100644 --- a/src/components/SourceCorrelationView.tsx +++ b/src/components/SourceCorrelationView.tsx @@ -1,6 +1,8 @@ import { useEffect, useMemo, useState } from 'react' import { Table, Empty } from 'antd' import type { ColumnsType } from 'antd/es/table' +import { Resizable } from 'react-resizable' +import 'react-resizable/css/styles.css' import { useStore } from '@/store/useStore' import { apiFetch } from '@/api' @@ -49,6 +51,27 @@ function divColor(pct: number): string { return '#16a34a' } +function ResizableTitle( + props: React.HTMLAttributes & { + onResize?: (e: React.SyntheticEvent, data: { size: { width: number } }) => void + width?: number + } +) { + const { onResize, width, ...restProps } = props + if (!width || !onResize) return + return ( + e.stopPropagation()} />} + onResize={onResize} + draggableOpts={{ enableUserSelectHack: false }} + > + + + ) +} + export default function SourceCorrelationView() { const profileSamples = useStore((s) => s.profileSamples) const currentSessionId = useStore((s) => s.currentSessionId) @@ -56,6 +79,21 @@ export default function SourceCorrelationView() { const [sourceLines, setSourceLines] = useState([]) const [disassembly, setDisassembly] = useState>(new Map()) + const [colWidths, setColWidths] = useState>({ + sourceLine: 70, + stallHits: 100, + stallShare: 90, + instExec: 110, + threadExec: 120, + divergencePct: 110, + coalescingFactor: 90, + }) + + function handleResize(key: string) { + return (_: React.SyntheticEvent, { size }: { size: { width: number } }) => + setColWidths((prev) => ({ ...prev, [key]: Math.max(40, size.width) })) + } + const functionEntries: FunctionEntry[] = useMemo(() => { const samples = profileSamples.filter((s) => s.sessionId === currentSessionId) if (samples.length === 0) return [] @@ -190,7 +228,8 @@ export default function SourceCorrelationView() { { title: 'Line', dataIndex: 'sourceLine', - width: 70, + width: colWidths.sourceLine, + onHeaderCell: () => ({ width: colWidths.sourceLine, onResize: handleResize('sourceLine') }), render: (v: number | null) => ( {v != null ? v : dash} ), @@ -237,39 +276,44 @@ export default function SourceCorrelationView() { { title: 'Stall Hits', dataIndex: 'stallHits', - width: 100, + width: colWidths.stallHits, align: 'right', defaultSortOrder: 'descend', sorter: (a, b) => a.stallHits - b.stallHits, + onHeaderCell: () => ({ width: colWidths.stallHits, onResize: handleResize('stallHits') }), render: (v: number) => (v > 0 ? v.toLocaleString() : dash), }, { title: 'Stall %', dataIndex: 'stallShare', - width: 90, + width: colWidths.stallShare, align: 'right', + onHeaderCell: () => ({ width: colWidths.stallShare, onResize: handleResize('stallShare') }), render: (v: number) => (v > 0 ? `${(v * 100).toFixed(1)}%` : dash), }, { title: 'Warp Instr', dataIndex: 'instExec', - width: 110, + width: colWidths.instExec, align: 'right', + onHeaderCell: () => ({ width: colWidths.instExec, onResize: handleResize('instExec') }), render: (v: number) => (v > 0 ? v.toLocaleString() : dash), }, { title: 'Thread Instr', dataIndex: 'threadExec', - width: 120, + width: colWidths.threadExec, align: 'right', + onHeaderCell: () => ({ width: colWidths.threadExec, onResize: handleResize('threadExec') }), render: (v: number) => (v > 0 ? v.toLocaleString() : dash), }, { title: 'Divergence', dataIndex: 'divergencePct', - width: 110, + width: colWidths.divergencePct, align: 'right', sorter: (a, b) => (a.divergencePct ?? -1) - (b.divergencePct ?? -1), + onHeaderCell: () => ({ width: colWidths.divergencePct, onResize: handleResize('divergencePct') }), render: (v: number | null) => v != null ? ( {v.toFixed(1)}% @@ -280,9 +324,10 @@ export default function SourceCorrelationView() { { title: 'Coal. ×', dataIndex: 'coalescingFactor', - width: 90, + width: colWidths.coalescingFactor, align: 'right' as const, sorter: (a: SourceLineRow, b: SourceLineRow) => (a.coalescingFactor ?? -1) - (b.coalescingFactor ?? -1), + onHeaderCell: () => ({ width: colWidths.coalescingFactor, onResize: handleResize('coalescingFactor') }), render: (v: number | null) => v != null ? ( {v.toFixed(1)}× @@ -399,6 +444,7 @@ export default function SourceCorrelationView() { pagination={false} scroll={{ y: 'calc(100vh - 300px)' }} onRow={(record) => ({ style: rowStyle(record.stallShare) })} + components={{ header: { cell: ResizableTitle } }} /> )} diff --git a/src/pages/SessionList.tsx b/src/pages/SessionList.tsx index 71bff4d..f2d85bc 100644 --- a/src/pages/SessionList.tsx +++ b/src/pages/SessionList.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react' -import { Button, Tag, Typography, Table, Empty, Spin } from 'antd' +import { Button, Popconfirm, Tag, Typography, Table, Empty, Spin } from 'antd' import type { ColumnsType } from 'antd/es/table' import { useNavigate } from 'react-router-dom' -import { DesktopOutlined, RightOutlined } from '@ant-design/icons' +import { DeleteOutlined, DesktopOutlined, RightOutlined } from '@ant-design/icons' import { useStore } from '@/store/useStore' +import { useAuthStore } from '@/store/useAuthStore' import { SessionSummary, CudaGpuInfo } from '@/types' import dayjs from 'dayjs' @@ -76,9 +77,12 @@ const sessionColumns: ColumnsType = [ export default function SessionList() { const hosts = useStore((s) => s.hosts) const fetchHosts = useStore((s) => s.fetchHosts) + const deleteSession = useStore((s) => s.deleteSession) + const role = useAuthStore((s) => s.role) const navigate = useNavigate() const [loading, setLoading] = useState(false) + const [deletingId, setDeletingId] = useState(null) // selectedGpuKey: `${hostname}::${deviceId}` — null means show all sessions for the host const [selectedGpuKey, setSelectedGpuKey] = useState(null) const [expandedHost, setExpandedHost] = useState(null) @@ -88,22 +92,47 @@ export default function SessionList() { fetchHosts().finally(() => setLoading(false)) }, [fetchHosts]) + const canDelete = role === 'ADMIN' || role === 'USER' + const viewColumn: ColumnsType[number] = { title: '', key: 'actions', align: 'right' as const, render: (_: unknown, record: SessionSummary) => ( - +
+ + {canDelete && ( + { + setDeletingId(record.sessionId) + try { + await deleteSession(record.sessionId) + } finally { + setDeletingId(null) + } + }} + okText="Delete" + okButtonProps={{ danger: true }} + cancelText="Cancel" + > +
), } diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 3e851dc..fd1941b 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -32,6 +32,7 @@ interface AppState { // actions fetchHosts: () => Promise; + deleteSession: (sessionId: string) => Promise; fetchInit: () => Promise; fetchSystemMetrics: (sessionId: string) => Promise; fetchProfileSamples: (sessionId: string) => Promise; @@ -84,6 +85,11 @@ export const useStore = create((set, get) => ({ console.error('Failed to fetch hosts', err); } }, + deleteSession: async (sessionId: string) => { + const res = await apiFetch(`/api/v1/events/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }); + if (!res.ok) throw new Error(`Failed to delete session: HTTP ${res.status}`); + await get().fetchHosts(); + }, fetchInit: async () => { try { const dateTo = new Date();