Skip to content

Commit c6ebd94

Browse files
committed
Table History WIP
1 parent a2aa880 commit c6ebd94

2 files changed

Lines changed: 256 additions & 24 deletions

File tree

packages/dashboard-core-plugins/src/TableHistoryPlugin.tsx

Lines changed: 195 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import React, { useCallback, useEffect, useMemo } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
import {
33
PluginType,
44
type WidgetMiddlewarePlugin,
55
type WidgetMiddlewareComponentProps,
66
type WidgetMiddlewarePanelProps,
77
} from '@deephaven/plugin';
88
import { type dh } from '@deephaven/jsapi-types';
9-
import { Button } from '@deephaven/components';
10-
import { vsHistory, vsTrash } from '@deephaven/icons';
9+
import {
10+
Button,
11+
ConfirmationDialog,
12+
DialogTrigger,
13+
SpectrumButton,
14+
Text,
15+
} from '@deephaven/components';
16+
import { vsEdit, vsHistory, vsTrash } from '@deephaven/icons';
1117
import { usePersistentState } from '@deephaven/dashboard';
1218
import { type MoveOperation } from '@deephaven/grid';
1319
import {
@@ -32,6 +38,8 @@ interface DehydratedStateSnapshot {
3238
id: string;
3339
/** When the snapshot was taken (ISO string for JSON serialization) */
3440
timestamp: string;
41+
/** User-defined name for the snapshot (optional) */
42+
name?: string;
3543
/** Quick filters state (dehydrated) */
3644
quickFilters: readonly DehydratedQuickFilter[];
3745
/** Advanced filters state (dehydrated) */
@@ -226,6 +234,78 @@ function TableHistoryPanel(_props: TableOptionPanelProps): JSX.Element {
226234
setState({ snapshots: [] });
227235
}, [setState]);
228236

237+
/**
238+
* Reset the table to its initial state.
239+
* Clears all filters, sorts, search, select distinct, custom columns,
240+
* and column re-ordering, restoring the table to its default view.
241+
*/
242+
const handleResetTable = useCallback(() => {
243+
// Clear select distinct columns first (triggers handler that resets other state)
244+
dispatch({
245+
type: 'SET_SELECT_DISTINCT_COLUMNS',
246+
columns: [],
247+
});
248+
249+
// Clear custom columns
250+
dispatch({
251+
type: 'SET_CUSTOM_COLUMNS',
252+
columns: [],
253+
});
254+
255+
// Clear all filters
256+
dispatch({
257+
type: 'SET_QUICK_FILTERS',
258+
filters: new Map(),
259+
});
260+
dispatch({
261+
type: 'SET_ADVANCED_FILTERS',
262+
filters: new Map(),
263+
});
264+
265+
// Clear search
266+
dispatch({
267+
type: 'SET_CROSS_COLUMN_SEARCH',
268+
searchValue: '',
269+
selectedSearchColumns: [],
270+
invertSearchColumns: false,
271+
});
272+
273+
// Clear sorts and reverse
274+
dispatch({ type: 'SET_SORTS', sorts: [] });
275+
dispatch({ type: 'SET_REVERSE', reverse: false });
276+
277+
// Reset column order
278+
dispatch({
279+
type: 'SET_MOVED_COLUMNS',
280+
columns: [],
281+
});
282+
}, [dispatch]);
283+
284+
// State for inline editing of snapshot names
285+
const [editingId, setEditingId] = useState<string | null>(null);
286+
const [editValue, setEditValue] = useState('');
287+
288+
const handleStartEdit = useCallback((snapshot: DehydratedStateSnapshot) => {
289+
setEditingId(snapshot.id);
290+
setEditValue(snapshot.name ?? '');
291+
}, []);
292+
293+
const handleSaveEdit = useCallback(() => {
294+
if (editingId == null) return;
295+
setState(prev => ({
296+
snapshots: prev.snapshots.map(s =>
297+
s.id === editingId ? { ...s, name: editValue.trim() || undefined } : s
298+
),
299+
}));
300+
setEditingId(null);
301+
setEditValue('');
302+
}, [editingId, editValue, setState]);
303+
304+
const handleCancelEdit = useCallback(() => {
305+
setEditingId(null);
306+
setEditValue('');
307+
}, []);
308+
229309
// Compute what has changed since the last saved snapshot
230310
const changedProperties = useMemo(() => {
231311
const lastSnapshot = snapshots[snapshots.length - 1];
@@ -333,37 +413,103 @@ function TableHistoryPanel(_props: TableOptionPanelProps): JSX.Element {
333413
{snapshots.length > 0 && (
334414
<>
335415
<div className="mt-3">
336-
<h6 className="text-muted mb-2">Saved Snapshots</h6>
416+
<h6 className="text-muted mb-2">Saved Snapshots (newest first)</h6>
337417
<ul className="list-unstyled">
338-
{snapshots.map(snapshot => (
418+
{[...snapshots].reverse().map(snapshot => (
339419
<li
340420
key={snapshot.id}
341421
className="d-flex align-items-center justify-content-between py-1"
342422
>
343-
<button
344-
type="button"
345-
className="btn btn-link p-0 text-start"
346-
onClick={() => handleRestoreSnapshot(snapshot)}
347-
title="Click to restore this snapshot"
348-
>
349-
{formatTimestamp(snapshot.timestamp)}
350-
</button>
351-
<Button
352-
kind="ghost"
353-
icon={vsTrash}
354-
tooltip="Delete"
355-
aria-label="Delete snapshot"
356-
onClick={() => handleDeleteSnapshot(snapshot.id)}
357-
/>
423+
{editingId === snapshot.id ? (
424+
<div className="d-flex align-items-center gap-1 flex-grow-1">
425+
<input
426+
type="text"
427+
className="form-control form-control-sm"
428+
value={editValue}
429+
onChange={e => setEditValue(e.target.value)}
430+
onKeyDown={e => {
431+
if (e.key === 'Enter') handleSaveEdit();
432+
if (e.key === 'Escape') handleCancelEdit();
433+
}}
434+
placeholder={formatTimestamp(snapshot.timestamp)}
435+
autoFocus
436+
/>
437+
<Button
438+
kind="primary"
439+
onClick={handleSaveEdit}
440+
style={{ padding: '2px 8px', fontSize: '12px' }}
441+
>
442+
Save
443+
</Button>
444+
<Button
445+
kind="secondary"
446+
onClick={handleCancelEdit}
447+
style={{ padding: '2px 8px', fontSize: '12px' }}
448+
>
449+
Cancel
450+
</Button>
451+
</div>
452+
) : (
453+
<>
454+
<button
455+
type="button"
456+
className="btn btn-link p-0 text-start"
457+
onClick={() => handleRestoreSnapshot(snapshot)}
458+
title="Click to restore this snapshot"
459+
>
460+
{snapshot.name || formatTimestamp(snapshot.timestamp)}
461+
{snapshot.name && (
462+
<span className="text-muted small ms-1">
463+
({formatTimestamp(snapshot.timestamp)})
464+
</span>
465+
)}
466+
</button>
467+
<div className="d-flex align-items-center">
468+
<Button
469+
kind="ghost"
470+
icon={vsEdit}
471+
tooltip="Rename"
472+
aria-label="Rename snapshot"
473+
onClick={() => handleStartEdit(snapshot)}
474+
/>
475+
<Button
476+
kind="ghost"
477+
icon={vsTrash}
478+
tooltip="Delete"
479+
aria-label="Delete snapshot"
480+
onClick={() => handleDeleteSnapshot(snapshot.id)}
481+
/>
482+
</div>
483+
</>
484+
)}
358485
</li>
359486
))}
360487
</ul>
361488
</div>
362489

363490
<div className="mt-2">
364-
<Button kind="secondary" onClick={handleClearAll}>
365-
Clear All Snapshots
366-
</Button>
491+
<DialogTrigger>
492+
<SpectrumButton variant="secondary">
493+
Clear All Snapshots
494+
</SpectrumButton>
495+
{(close: () => void) => (
496+
<ConfirmationDialog
497+
heading="Clear All Snapshots"
498+
confirmationButtonLabel="Clear All"
499+
onCancel={close}
500+
onConfirm={() => {
501+
close();
502+
handleClearAll();
503+
}}
504+
>
505+
<Text>
506+
Are you sure you want to clear all {snapshots.length}{' '}
507+
snapshot{snapshots.length === 1 ? '' : 's'}? This action
508+
cannot be undone.
509+
</Text>
510+
</ConfirmationDialog>
511+
)}
512+
</DialogTrigger>
367513
</div>
368514
</>
369515
)}
@@ -372,6 +518,32 @@ function TableHistoryPanel(_props: TableOptionPanelProps): JSX.Element {
372518
Save the current table state (filters, sorts, search) and restore it
373519
later by clicking on a timestamp.
374520
</p>
521+
522+
<div className="mt-4 pt-3 border-top">
523+
<DialogTrigger>
524+
<SpectrumButton variant="secondary">Reset Table</SpectrumButton>
525+
{(close: () => void) => (
526+
<ConfirmationDialog
527+
heading="Reset Table"
528+
confirmationButtonLabel="Reset"
529+
onCancel={close}
530+
onConfirm={() => {
531+
close();
532+
handleResetTable();
533+
}}
534+
>
535+
<Text>
536+
Reset the table to its initial state? This will clear all
537+
filters, sorts, search, and column re-ordering.
538+
</Text>
539+
</ConfirmationDialog>
540+
)}
541+
</DialogTrigger>
542+
<p className="text-muted small mt-2 mb-0">
543+
Clear all filters, sorts, search, custom columns, and column
544+
re-ordering to restore the table to its default state.
545+
</p>
546+
</div>
375547
</div>
376548
);
377549
}

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ import {
125125
TableSaver,
126126
DownloadServiceWorkerUtils,
127127
} from './sidebar';
128-
import IrisGridUtils from './IrisGridUtils';
128+
import IrisGridUtils, { type DehydratedIrisGridState } from './IrisGridUtils';
129129
import CrossColumnSearch from './CrossColumnSearch';
130130
import IrisGridModel from './IrisGridModel';
131131
import {
@@ -2235,6 +2235,66 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
22352235
this.initFormatter();
22362236
}
22372237

2238+
/**
2239+
* Get the current grid state as a JSON-serializable object.
2240+
* This can be used to save the state and restore it later with {@link restoreState}.
2241+
* @returns The dehydrated grid state
2242+
*/
2243+
getState(): DehydratedIrisGridState {
2244+
const { model } = this.props;
2245+
const irisGridUtils = new IrisGridUtils(model.dh);
2246+
return irisGridUtils.dehydrateIrisGridState(model, this.state);
2247+
}
2248+
2249+
/**
2250+
* Restore the grid state from a previously saved state.
2251+
* This applies the state similarly to how loadTableState works for consistency.
2252+
* @param dehydratedState The state to restore, obtained from {@link getState}
2253+
*/
2254+
restoreState(dehydratedState: DehydratedIrisGridState): void {
2255+
const { model } = this.props;
2256+
const irisGridUtils = new IrisGridUtils(model.dh);
2257+
const hydratedState = irisGridUtils.hydrateIrisGridState(
2258+
model,
2259+
dehydratedState
2260+
);
2261+
2262+
// Create search filter from search settings
2263+
const searchFilter = CrossColumnSearch.createSearchFilter(
2264+
model.dh,
2265+
hydratedState.searchValue,
2266+
hydratedState.selectedSearchColumns ?? [],
2267+
model.columns,
2268+
hydratedState.invertSearchColumns
2269+
);
2270+
2271+
// Apply the hydrated state
2272+
this.startLoading('Restoring state...', { resetRanges: true });
2273+
this.setState({
2274+
advancedFilters: hydratedState.advancedFilters,
2275+
aggregationSettings: hydratedState.aggregationSettings,
2276+
customColumnFormatMap: hydratedState.customColumnFormatMap,
2277+
columnAlignmentMap: hydratedState.columnAlignmentMap,
2278+
isFilterBarShown: hydratedState.isFilterBarShown,
2279+
quickFilters: hydratedState.quickFilters,
2280+
sorts: hydratedState.sorts,
2281+
customColumns: hydratedState.customColumns,
2282+
conditionalFormats: hydratedState.conditionalFormats,
2283+
reverse: hydratedState.reverse,
2284+
rollupConfig: hydratedState.rollupConfig,
2285+
showSearchBar: hydratedState.showSearchBar,
2286+
searchValue: hydratedState.searchValue,
2287+
searchFilter,
2288+
selectDistinctColumns: hydratedState.selectDistinctColumns,
2289+
selectedSearchColumns: hydratedState.selectedSearchColumns,
2290+
invertSearchColumns: hydratedState.invertSearchColumns,
2291+
pendingDataMap: hydratedState.pendingDataMap,
2292+
frozenColumns: hydratedState.frozenColumns,
2293+
columnHeaderGroups: hydratedState.columnHeaderGroups,
2294+
partitionConfig: hydratedState.partitionConfig,
2295+
});
2296+
}
2297+
22382298
async loadPartitionsTable(model: PartitionedGridModel): Promise<void> {
22392299
try {
22402300
const { partitionConfig } = this.state;

0 commit comments

Comments
 (0)