Skip to content

Commit 287d989

Browse files
committed
ccp:wq
1 parent 12fac43 commit 287d989

File tree

12 files changed

+672
-53
lines changed

12 files changed

+672
-53
lines changed

packages/skydeck/frontend/src/App.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ExperimentGroupView } from './components/ExperimentGroupView';
88
import { ExpandedDetails } from './components/ExpandedDetails';
99
import { JobsTable } from './components/JobsTable';
1010
import { Notifications } from './components/Notifications';
11+
import { OperationsLogPanel } from './components/OperationsLogPanel';
1112
import './App.css';
1213

1314
const MY_USER_ID = 'daveey';
@@ -26,6 +27,7 @@ function AppContent() {
2627
const [expandedExperiments, setExpandedExperiments] = useState<Set<string>>(new Set());
2728
const [selectedExperiments, setSelectedExperiments] = useState<Set<string>>(new Set());
2829
const [editingConfigs, setEditingConfigs] = useState<Set<string>>(new Set());
30+
const [isOperationsLogOpen, setIsOperationsLogOpen] = useState(false);
2931

3032
// Jobs filters
3133
const [showStoppedJobs, setShowStoppedJobs] = useState(false);
@@ -616,6 +618,7 @@ function AppContent() {
616618
});
617619
}}
618620
onRefreshData={loadData}
621+
health={health}
619622
/>
620623
),
621624
};
@@ -624,6 +627,26 @@ function AppContent() {
624627
<div className="container">
625628
<header>
626629
<div className="header-right">
630+
<button
631+
onClick={() => setIsOperationsLogOpen(true)}
632+
style={{
633+
padding: '6px 12px',
634+
marginRight: '12px',
635+
backgroundColor: '#2196F3',
636+
color: 'white',
637+
border: 'none',
638+
borderRadius: '12px',
639+
fontSize: '12px',
640+
fontWeight: 500,
641+
cursor: 'pointer',
642+
transition: 'background-color 0.15s',
643+
}}
644+
onMouseEnter={e => (e.currentTarget.style.backgroundColor = '#1976D2')}
645+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = '#2196F3')}
646+
title="View operations log"
647+
>
648+
Operations Log
649+
</button>
627650
<HealthStatus health={health} backendStaleness={backendStaleness} />
628651
</div>
629652
</header>
@@ -677,6 +700,11 @@ function AppContent() {
677700
</section>
678701

679702
<Notifications />
703+
704+
<OperationsLogPanel
705+
isOpen={isOperationsLogOpen}
706+
onClose={() => setIsOperationsLogOpen(false)}
707+
/>
680708
</div>
681709
);
682710
}

packages/skydeck/frontend/src/components/ExpandedDetails.tsx

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useCallback } from 'react';
2-
import type { Experiment, Checkpoint, Job } from '../types';
2+
import type { Experiment, Checkpoint, Job, HealthData } from '../types';
33
import { useApi } from '../hooks/useApi';
44
import { useNotifications } from '../hooks/useNotifications';
55
import { ScrollPanel } from './ScrollPanel';
@@ -8,6 +8,7 @@ interface ExpandedDetailsProps {
88
experiment: Experiment;
99
onSetEditingConfig: (id: string, editing: boolean) => void;
1010
onRefreshData: () => void;
11+
health?: HealthData | null;
1112
}
1213

1314
function abbreviateStatus(status: string): string {
@@ -27,15 +28,21 @@ function abbreviateStatus(status: string): string {
2728
return statusMap[lower] || status.charAt(0).toUpperCase();
2829
}
2930

30-
function formatDuration(startedAt: string | null): string {
31+
function formatDuration(startedAt: string | null, endedAt: string | null): string {
3132
if (!startedAt) return '-';
3233
const start = new Date(startedAt);
33-
const now = new Date();
34-
const diffMs = now.getTime() - start.getTime();
34+
// Use ended_at if available (completed jobs), otherwise use now (running jobs)
35+
const end = endedAt ? new Date(endedAt) : new Date();
36+
const diffMs = end.getTime() - start.getTime();
37+
38+
if (diffMs < 0) return '-';
39+
3540
const hours = Math.floor(diffMs / 3600000);
3641
const mins = Math.floor((diffMs % 3600000) / 60000);
42+
3743
if (hours > 0) return `${hours}h ${mins}m`;
38-
return `${mins}m`;
44+
if (mins > 0) return `${mins}m`;
45+
return '<1m';
3946
}
4047

4148
function formatAge(timestamp: string | null): string {
@@ -56,6 +63,19 @@ function formatAge(timestamp: string | null): string {
5663
return `${days}d ago`;
5764
}
5865

66+
function formatStaleness(seconds: number | null): string | null {
67+
if (seconds === null || seconds <= 30) return null;
68+
69+
const mins = Math.floor(seconds / 60);
70+
if (mins < 60) return `${mins}m`;
71+
72+
const hours = Math.floor(mins / 60);
73+
if (hours < 24) return `${hours}h`;
74+
75+
const days = Math.floor(hours / 24);
76+
return `${days}d`;
77+
}
78+
5979
function buildCommand(exp: Experiment): string {
6080
const parts = [exp.base_command];
6181
parts.push(`--gpus=${exp.gpus}`);
@@ -79,7 +99,7 @@ function buildCommand(exp: Experiment): string {
7999
return parts.join(' ');
80100
}
81101

82-
export function ExpandedDetails({ experiment, onSetEditingConfig, onRefreshData }: ExpandedDetailsProps) {
102+
export function ExpandedDetails({ experiment, onSetEditingConfig, onRefreshData, health }: ExpandedDetailsProps) {
83103
const { apiCall } = useApi();
84104
const { showNotification } = useNotifications();
85105

@@ -484,7 +504,14 @@ export function ExpandedDetails({ experiment, onSetEditingConfig, onRefreshData
484504

485505
{/* Jobs Panel */}
486506
<div className="detail-section" style={{ width: 'auto', flexShrink: 0 }}>
487-
<h3>Jobs</h3>
507+
<h3>
508+
Jobs
509+
{health?.skypilot && formatStaleness(health.skypilot.staleness_seconds) && (
510+
<span style={{ marginLeft: '8px', fontSize: '11px', color: '#999', fontWeight: 'normal' }}>
511+
(stale: {formatStaleness(health.skypilot.staleness_seconds)})
512+
</span>
513+
)}
514+
</h3>
488515
<ScrollPanel deps={[jobs]} maxHeight={300}>
489516
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '11px' }}>
490517
<thead>
@@ -515,7 +542,7 @@ export function ExpandedDetails({ experiment, onSetEditingConfig, onRefreshData
515542
{formatAge(job.started_at || job.created_at)}
516543
</td>
517544
<td style={{ padding: '4px 6px', fontSize: '11px', borderBottom: '1px solid #eee', color: '#666' }}>
518-
{formatDuration(job.started_at)}
545+
{formatDuration(job.started_at, job.ended_at)}
519546
</td>
520547
<td style={{ padding: '4px 6px', fontSize: '11px', borderBottom: '1px solid #eee' }}>
521548
<a href={`https://skypilot-api.softmax-research.net/dashboard/jobs/${job.id}`} target="_blank" className="wandb-link" rel="noreferrer">sky</a>
@@ -535,7 +562,14 @@ export function ExpandedDetails({ experiment, onSetEditingConfig, onRefreshData
535562

536563
{/* Checkpoints Panel */}
537564
<div className="detail-section" style={{ flexShrink: 0 }}>
538-
<h3>Checkpoints</h3>
565+
<h3>
566+
Checkpoints
567+
{health?.s3 && formatStaleness(health.s3.staleness_seconds) && (
568+
<span style={{ marginLeft: '8px', fontSize: '11px', color: '#999', fontWeight: 'normal' }}>
569+
(stale: {formatStaleness(health.s3.staleness_seconds)})
570+
</span>
571+
)}
572+
</h3>
539573
<ScrollPanel deps={[checkpoints]} maxHeight={300}>
540574
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '11px' }}>
541575
<thead>
@@ -551,7 +585,7 @@ export function ExpandedDetails({ experiment, onSetEditingConfig, onRefreshData
551585
{checkpoints.map(cp => (
552586
<tr key={`${cp.epoch}-${cp.created_at}`}>
553587
<td style={{ padding: '4px 6px', borderBottom: '1px solid #eee' }}>{cp.epoch}</td>
554-
<td style={{ padding: '4px 6px', borderBottom: '1px solid #eee', color: '#666' }}>{formatDuration(cp.created_at)}</td>
588+
<td style={{ padding: '4px 6px', borderBottom: '1px solid #eee', color: '#666' }}>{formatAge(cp.created_at)}</td>
555589
<td style={{ padding: '4px 6px', borderBottom: '1px solid #eee', color: '#666' }}>{cp.policy_version || '-'}</td>
556590
<td style={{ padding: '4px 6px', borderBottom: '1px solid #eee' }}>
557591
{cp.model_path && (

packages/skydeck/frontend/src/components/ExperimentGroupView.tsx

Lines changed: 128 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,6 @@ interface ExperimentGroupViewProps {
3434
}
3535

3636
// Utility functions
37-
function abbreviateStatus(status: string): string {
38-
const statusMap: Record<string, string> = {
39-
running: 'R',
40-
stopped: 'S',
41-
pending: 'P',
42-
starting: 'P', // Pending (starting up)
43-
failed: 'F',
44-
succeeded: 'D', // Done
45-
init: 'S', // Stopped
46-
terminated: 'T',
47-
cancelled: 'S', // Stopped
48-
unknown: '?',
49-
};
50-
const lower = status.toLowerCase();
51-
return statusMap[lower] || status.charAt(0).toUpperCase();
52-
}
53-
5437
function formatLargeNumber(value: number): string {
5538
const absValue = Math.abs(value);
5639
if (absValue >= 1_000_000_000) {
@@ -235,30 +218,31 @@ function StateTransition({ current, desired, experimentId, onSetDesiredState }:
235218
);
236219
}
237220

238-
const currentAbbrev = abbreviateStatus(currentLower);
239-
const desiredAbbrev = abbreviateStatus(desiredLower);
221+
// Use full status text with smaller font
222+
const currentText = currentLower;
223+
const desiredText = desiredLower;
240224

241225
if (currentLower === desiredLower) {
242226
return (
243227
<span onClick={handleClick} style={{ cursor: 'pointer' }}>
244-
<span className={`status-badge ${currentLower}`} title={current}>{currentAbbrev}</span>
228+
<span className={`status-badge ${currentLower}`} style={{ fontSize: '10px' }} title={current}>{currentText}</span>
245229
</span>
246230
);
247231
}
248232

249233
if (currentLower === 'failed' && desiredLower === 'stopped') {
250234
return (
251235
<span onClick={handleClick} style={{ cursor: 'pointer' }}>
252-
<span className={`status-badge ${currentLower}`} title={current}>{currentAbbrev}</span>
236+
<span className={`status-badge ${currentLower}`} style={{ fontSize: '10px' }} title={current}>{currentText}</span>
253237
</span>
254238
);
255239
}
256240

257241
return (
258242
<span onClick={handleClick} style={{ cursor: 'pointer' }}>
259-
<span className={`status-badge ${currentLower}`} title={current}>{currentAbbrev}</span>
243+
<span className={`status-badge ${currentLower}`} style={{ fontSize: '10px' }} title={current}>{currentText}</span>
260244
{' → '}
261-
<span className={`status-badge ${desiredLower}`} title={desired}>{desiredAbbrev}</span>
245+
<span className={`status-badge ${desiredLower}`} style={{ fontSize: '10px' }} title={desired}>{desiredText}</span>
262246
</span>
263247
);
264248
}
@@ -304,6 +288,8 @@ export function ExperimentGroupView({
304288
const [flagDefinitions, setFlagDefinitions] = useState<Array<{ flag: string; type: string; default: unknown; required: boolean }>>([]);
305289
const [editingResource, setEditingResource] = useState<'nodes' | 'gpus' | null>(null);
306290
const [editedResourceValue, setEditedResourceValue] = useState<string>('');
291+
const [isEditingNamePrefix, setIsEditingNamePrefix] = useState(false);
292+
const [editedNamePrefix, setEditedNamePrefix] = useState<string>('');
307293
const [isEditingToolPath, setIsEditingToolPath] = useState(false);
308294
const [editedToolPath, setEditedToolPath] = useState<string>('');
309295
const [confirmingDelete, setConfirmingDelete] = useState(false);
@@ -425,6 +411,22 @@ export function ExperimentGroupView({
425411
}
426412
}, [experiments, apiCall, showNotification, onRefreshData]);
427413

414+
// Update group name_prefix
415+
const updateGroupNamePrefix = useCallback(async (value: string) => {
416+
if (!group) return;
417+
try {
418+
await apiCall(`/groups/${group.id}`, {
419+
method: 'PATCH',
420+
body: JSON.stringify({ name_prefix: value || null }),
421+
});
422+
showNotification(`Updated name prefix for group`, 'success');
423+
onRefreshData();
424+
} catch (error) {
425+
console.error('Error updating name prefix:', error);
426+
showNotification('Error updating name prefix', 'error');
427+
}
428+
}, [group, apiCall, showNotification, onRefreshData]);
429+
428430
// Delete a flag from all experiments in the group
429431
const deleteFlagForAll = useCallback(async (flagKey: string) => {
430432
try {
@@ -790,7 +792,109 @@ export function ExperimentGroupView({
790792
{/* Common flags display - editable */}
791793
{(Object.keys(commonFlags).length > 0 || commonNodes !== null || commonGpus !== null || commonToolPath !== null || !isUngrouped) && (
792794
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginLeft: '8px', alignItems: 'center' }}>
793-
{/* Tool path first - editable */}
795+
{/* Name prefix - editable */}
796+
{!isUngrouped && group && (
797+
isEditingNamePrefix ? (
798+
<span
799+
style={{
800+
display: 'inline-flex',
801+
alignItems: 'center',
802+
gap: '4px',
803+
padding: '4px 8px',
804+
backgroundColor: '#fff',
805+
border: '1px solid #ff9800',
806+
borderRadius: '4px',
807+
fontSize: '11px',
808+
fontFamily: 'monospace',
809+
}}
810+
>
811+
<input
812+
type="text"
813+
value={editedNamePrefix}
814+
onChange={e => setEditedNamePrefix(e.target.value)}
815+
onKeyDown={e => {
816+
if (e.key === 'Enter') {
817+
updateGroupNamePrefix(editedNamePrefix);
818+
setIsEditingNamePrefix(false);
819+
setEditedNamePrefix('');
820+
}
821+
if (e.key === 'Escape') { setIsEditingNamePrefix(false); setEditedNamePrefix(''); }
822+
}}
823+
autoFocus
824+
placeholder="name prefix"
825+
style={{
826+
border: 'none',
827+
outline: 'none',
828+
width: '150px',
829+
fontSize: '11px',
830+
fontFamily: 'monospace',
831+
padding: '2px 4px',
832+
backgroundColor: '#f5f5f5',
833+
borderRadius: '2px',
834+
}}
835+
/>
836+
<span
837+
onClick={() => {
838+
updateGroupNamePrefix(editedNamePrefix);
839+
setIsEditingNamePrefix(false);
840+
setEditedNamePrefix('');
841+
}}
842+
style={{ cursor: 'pointer', color: '#4CAF50', fontWeight: 'bold', padding: '0 2px' }}
843+
title="Save"
844+
>
845+
846+
</span>
847+
<span
848+
onClick={() => { setIsEditingNamePrefix(false); setEditedNamePrefix(''); }}
849+
style={{ cursor: 'pointer', color: '#999', padding: '0 2px' }}
850+
title="Cancel"
851+
>
852+
853+
</span>
854+
</span>
855+
) : group.name_prefix ? (
856+
<span
857+
onClick={() => { setIsEditingNamePrefix(true); setEditedNamePrefix(group.name_prefix || ''); }}
858+
style={{
859+
display: 'inline-block',
860+
padding: '6px 10px',
861+
backgroundColor: '#fff3e0',
862+
borderRadius: '4px',
863+
fontSize: '11px',
864+
fontFamily: 'monospace',
865+
color: '#e65100',
866+
whiteSpace: 'nowrap',
867+
cursor: 'pointer',
868+
transition: 'background-color 0.15s',
869+
}}
870+
onMouseEnter={e => (e.currentTarget.style.backgroundColor = '#ffe0b2')}
871+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = '#fff3e0')}
872+
title="Click to edit name prefix"
873+
>
874+
prefix: {group.name_prefix}
875+
</span>
876+
) : (
877+
<span
878+
onClick={() => { setIsEditingNamePrefix(true); setEditedNamePrefix(''); }}
879+
style={{
880+
display: 'inline-flex',
881+
alignItems: 'center',
882+
justifyContent: 'center',
883+
padding: '6px 10px',
884+
backgroundColor: '#fff3e0',
885+
borderRadius: '4px',
886+
fontSize: '11px',
887+
color: '#ff9800',
888+
cursor: 'pointer',
889+
fontStyle: 'italic',
890+
}}
891+
title="Add name prefix"
892+
>
893+
+ prefix
894+
</span>
895+
)
896+
)}
897+
{/* Tool path - editable */}
794898
{commonToolPath !== null && !isUngrouped && (
795899
isEditingToolPath ? (
796900
<span

0 commit comments

Comments
 (0)