feat(ui): checkpoints table collapsible, default collapsed
This commit is contained in:
@@ -400,3 +400,39 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Collapsible Checkpoints header */
|
||||||
|
.checkpointsSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkpointsHeader {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkpointsHeader:hover {
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkpointsChevron {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
width: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkpointsCount {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,10 +25,23 @@ const stoppedDep: Deployment = {
|
|||||||
deployedConfigSnapshot: { jarVersionId: 'v6id', agentConfig: null, containerConfig: {}, sensitiveKeys: null },
|
deployedConfigSnapshot: { jarVersionId: 'v6id', agentConfig: null, containerConfig: {}, sensitiveKeys: null },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function expand() {
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /checkpoints/i }));
|
||||||
|
}
|
||||||
|
|
||||||
describe('CheckpointsTable', () => {
|
describe('CheckpointsTable', () => {
|
||||||
it('renders a row per checkpoint with version, jar, deployer', () => {
|
it('defaults to collapsed — header visible, rows hidden', () => {
|
||||||
wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
||||||
currentDeploymentId={null} jarRetentionCount={5} onSelect={() => {}} />);
|
currentDeploymentId={null} jarRetentionCount={5} onSelect={() => {}} />);
|
||||||
|
expect(screen.getByRole('button', { name: /checkpoints \(1\)/i })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('v6')).toBeNull();
|
||||||
|
expect(screen.queryByText('my-app-1.2.3.jar')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking header expands to show rows', () => {
|
||||||
|
wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
||||||
|
currentDeploymentId={null} jarRetentionCount={5} onSelect={() => {}} />);
|
||||||
|
expand();
|
||||||
expect(screen.getByText('v6')).toBeInTheDocument();
|
expect(screen.getByText('v6')).toBeInTheDocument();
|
||||||
expect(screen.getByText('my-app-1.2.3.jar')).toBeInTheDocument();
|
expect(screen.getByText('my-app-1.2.3.jar')).toBeInTheDocument();
|
||||||
expect(screen.getByText('alice')).toBeInTheDocument();
|
expect(screen.getByText('alice')).toBeInTheDocument();
|
||||||
@@ -38,7 +51,7 @@ describe('CheckpointsTable', () => {
|
|||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
||||||
currentDeploymentId={null} jarRetentionCount={5} onSelect={onSelect} />);
|
currentDeploymentId={null} jarRetentionCount={5} onSelect={onSelect} />);
|
||||||
// Use the version text as the row anchor (most stable selector)
|
expand();
|
||||||
fireEvent.click(screen.getByText('v6').closest('tr')!);
|
fireEvent.click(screen.getByText('v6').closest('tr')!);
|
||||||
expect(onSelect).toHaveBeenCalledWith('d1');
|
expect(onSelect).toHaveBeenCalledWith('d1');
|
||||||
});
|
});
|
||||||
@@ -47,6 +60,7 @@ describe('CheckpointsTable', () => {
|
|||||||
const noActor = { ...stoppedDep, createdBy: null };
|
const noActor = { ...stoppedDep, createdBy: null };
|
||||||
wrap(<CheckpointsTable deployments={[noActor]} versions={[v6]}
|
wrap(<CheckpointsTable deployments={[noActor]} versions={[v6]}
|
||||||
currentDeploymentId={null} jarRetentionCount={5} onSelect={() => {}} />);
|
currentDeploymentId={null} jarRetentionCount={5} onSelect={() => {}} />);
|
||||||
|
expand();
|
||||||
expect(screen.getByText('—')).toBeInTheDocument();
|
expect(screen.getByText('—')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,14 +68,14 @@ describe('CheckpointsTable', () => {
|
|||||||
const pruned = { ...stoppedDep, appVersionId: 'unknown' };
|
const pruned = { ...stoppedDep, appVersionId: 'unknown' };
|
||||||
wrap(<CheckpointsTable deployments={[pruned]} versions={[]}
|
wrap(<CheckpointsTable deployments={[pruned]} versions={[]}
|
||||||
currentDeploymentId={null} jarRetentionCount={5} onSelect={() => {}} />);
|
currentDeploymentId={null} jarRetentionCount={5} onSelect={() => {}} />);
|
||||||
|
expand();
|
||||||
expect(screen.getByText(/archived/i)).toBeInTheDocument();
|
expect(screen.getByText(/archived/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('excludes the currently-running deployment', () => {
|
it('renders nothing when there are no checkpoints', () => {
|
||||||
wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
const { container } = wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
||||||
currentDeploymentId="d1" jarRetentionCount={5} onSelect={() => {}} />);
|
currentDeploymentId="d1" jarRetentionCount={5} onSelect={() => {}} />);
|
||||||
expect(screen.queryByText('v6')).toBeNull();
|
expect(container).toBeEmptyDOMElement();
|
||||||
expect(screen.getByText(/no past deployments/i)).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('caps visible rows at jarRetentionCount and shows expander', () => {
|
it('caps visible rows at jarRetentionCount and shows expander', () => {
|
||||||
@@ -70,7 +84,7 @@ describe('CheckpointsTable', () => {
|
|||||||
}));
|
}));
|
||||||
wrap(<CheckpointsTable deployments={many} versions={[v6]}
|
wrap(<CheckpointsTable deployments={many} versions={[v6]}
|
||||||
currentDeploymentId={null} jarRetentionCount={3} onSelect={() => {}} />);
|
currentDeploymentId={null} jarRetentionCount={3} onSelect={() => {}} />);
|
||||||
// 3 visible rows + 1 header row = 4 rows max in the table
|
expand();
|
||||||
expect(screen.getAllByRole('row').length).toBeLessThanOrEqual(4);
|
expect(screen.getAllByRole('row').length).toBeLessThanOrEqual(4);
|
||||||
expect(screen.getByText(/show older \(7\)/i)).toBeInTheDocument();
|
expect(screen.getByText(/show older \(7\)/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -78,6 +92,7 @@ describe('CheckpointsTable', () => {
|
|||||||
it('shows all rows when jarRetentionCount >= total', () => {
|
it('shows all rows when jarRetentionCount >= total', () => {
|
||||||
wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
wrap(<CheckpointsTable deployments={[stoppedDep]} versions={[v6]}
|
||||||
currentDeploymentId={null} jarRetentionCount={10} onSelect={() => {}} />);
|
currentDeploymentId={null} jarRetentionCount={10} onSelect={() => {}} />);
|
||||||
|
expand();
|
||||||
expect(screen.queryByText(/show older/i)).toBeNull();
|
expect(screen.queryByText(/show older/i)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,6 +102,7 @@ describe('CheckpointsTable', () => {
|
|||||||
}));
|
}));
|
||||||
wrap(<CheckpointsTable deployments={fifteen} versions={[v6]}
|
wrap(<CheckpointsTable deployments={fifteen} versions={[v6]}
|
||||||
currentDeploymentId={null} jarRetentionCount={null} onSelect={() => {}} />);
|
currentDeploymentId={null} jarRetentionCount={null} onSelect={() => {}} />);
|
||||||
|
expand();
|
||||||
expect(screen.getByText(/show older \(5\)/i)).toBeInTheDocument();
|
expect(screen.getByText(/show older \(5\)/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function CheckpointsTable({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}: CheckpointsTableProps) {
|
}: CheckpointsTableProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const versionMap = new Map(versions.map((v) => [v.id, v]));
|
const versionMap = new Map(versions.map((v) => [v.id, v]));
|
||||||
|
|
||||||
const checkpoints = deployments
|
const checkpoints = deployments
|
||||||
@@ -37,6 +38,19 @@ export function CheckpointsTable({
|
|||||||
const hidden = checkpoints.length - visible.length;
|
const hidden = checkpoints.length - visible.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={styles.checkpointsSection}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.checkpointsHeader}
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
<span className={styles.checkpointsChevron}>{open ? '\u25BE' : '\u25B8'}</span>
|
||||||
|
<span>Checkpoints</span>
|
||||||
|
{' '}
|
||||||
|
<span className={styles.checkpointsCount}>({checkpoints.length})</span>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
<div className={styles.checkpointsTable}>
|
<div className={styles.checkpointsTable}>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -108,5 +122,7 @@ export function CheckpointsTable({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user