From dd0841da0ee17a544f713e2222b2b6b72ed9b4e4 Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Tue, 7 Jan 2025 13:23:21 -0800 Subject: [PATCH] feat(weave): dataset editing UI --- .../Home/Browse3/datasets/CellRenderers.tsx | 232 ++++++++ .../Browse3/datasets/DatasetEditorContext.tsx | 132 +++++ .../Browse3/datasets/DatasetVersionPage.tsx | 349 ++++++++++-- .../Browse3/datasets/EditableDatasetView.tsx | 498 ++++++++++++++++++ .../Browse3/pages/CallPage/DataTableView.tsx | 4 +- .../Home/Browse3/pages/ObjectVersionPage.tsx | 7 +- 6 files changed, 1164 insertions(+), 58 deletions(-) create mode 100644 weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/CellRenderers.tsx create mode 100644 weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/DatasetEditorContext.tsx create mode 100644 weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/EditableDatasetView.tsx diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/CellRenderers.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/CellRenderers.tsx new file mode 100644 index 000000000000..04f0731ae9b0 --- /dev/null +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/CellRenderers.tsx @@ -0,0 +1,232 @@ +import {Box, TextField, Typography} from '@mui/material'; +import {Popover} from '@mui/material'; +import { + GridRenderCellParams, + GridRenderEditCellParams, +} from '@mui/x-data-grid-pro'; +import {Button} from '@wandb/weave/components/Button'; +import {Icon} from '@wandb/weave/components/Icon'; +import React, {useState} from 'react'; +import {ResizableBox} from 'react-resizable'; + +import {DraggableGrow, DraggableHandle} from '../../../../DraggablePopups'; + +const cellViewingStyles = { + height: '100%', + width: '100%', + fontFamily: '"Source Sans Pro", sans-serif', + fontSize: '14px', + lineHeight: '1.5', + padding: '8px 12px', + display: 'flex', + alignItems: 'center', + transition: 'background-color 0.2s ease', +}; + +interface CellViewingRendererProps extends GridRenderCellParams { + isEdited?: boolean; + isDeleted?: boolean; + isNew?: boolean; +} + +export const CellViewingRenderer: React.FC = ({ + value, + isEdited = false, + isDeleted = false, + isNew = false, + api, + id, + field, +}) => { + const [isHovered, setIsHovered] = useState(false); + + const handleEditClick = (event: React.MouseEvent) => { + event.stopPropagation(); + api.startCellEditMode({id, field}); + }; + + const getBackgroundColor = () => { + if (isDeleted) { + return 'rgba(255, 0, 0, 0.1)'; + } + if (isEdited) { + return 'rgba(0, 128, 128, 0.1)'; + } + if (isNew) { + return 'rgba(0, 255, 0, 0.1)'; + } + return 'transparent'; + }; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + sx={{ + ...cellViewingStyles, + position: 'relative', + cursor: 'pointer', + backgroundColor: getBackgroundColor(), + opacity: isDeleted ? 0.5 : 1, + textDecoration: isDeleted ? 'line-through' : 'none', + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.04)', + }, + }}> + {value} + {isHovered && ( + + + + )} + + ); +}; + +export const CellEditingRenderer: React.FC< + GridRenderEditCellParams +> = params => { + const {id, value, field, hasFocus, api} = params; + const inputRef = React.useRef(null); + const [anchorEl, setAnchorEl] = useState(null); + + // Calculate initial position when edit mode starts + React.useLayoutEffect(() => { + const element = api.getCellElement(id, field); + if (element) { + setAnchorEl(element); + } + }, [api, id, field]); + + React.useLayoutEffect(() => { + if (hasFocus) { + inputRef.current?.focus(); + } + }, [hasFocus]); + + const handleValueChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + api.setEditCellValue({id, field, value: newValue}); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && !event.metaKey) { + event.stopPropagation(); + } + }; + + return ( + <> + + setAnchorEl(null)} + TransitionComponent={DraggableGrow} + sx={{ + '& .MuiPaper-root': { + backgroundColor: 'white', + boxShadow: + '0px 4px 20px rgba(0, 0, 0, 0.15), 0px 0px 40px rgba(0, 0, 0, 0.05)', + border: 'none', + borderRadius: '4px', + overflow: 'hidden', + padding: '0px', + }, + }}> + + + + + + ⌘+Enter to close + + + + + ); + + const renderPublishPopover = () => ( + + + + Publish changes to a new version of{' '} + {objectName}? + + + + + + + + ); + return ( -
-
-

Name

- -
- {objectName} - {objectVersions.loading ? ( - - ) : ( - - ({objectVersionCount} version - {objectVersionCount !== 1 ? 's' : ''}) - - )} - -
-
+
+
+
+

Name

+ +
+ {objectName} + {objectVersions.loading ? ( + + ) : ( + + ({objectVersionCount} version + {objectVersionCount !== 1 ? 's' : ''}) + + )} + +
+
+
+
+

Version

+

{objectVersionIndex}

+
-
-

Version

-

{objectVersionIndex}

+
+ {isEditing ? ( + renderEditingControls() + ) : ( + + )}
+ {renderPublishPopover()}
} @@ -114,22 +359,16 @@ export const DatasetVersionPage: React.FC<{ label: 'Rows', content: ( - + {data.loading ? ( ) : ( - diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/EditableDatasetView.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/EditableDatasetView.tsx new file mode 100644 index 000000000000..cc16f001b13e --- /dev/null +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/EditableDatasetView.tsx @@ -0,0 +1,498 @@ +import {Box} from '@mui/material'; +import { + GridColDef, + GridPaginationModel, + GridRenderCellParams, + GridRenderEditCellParams, + GridRowModel, + GridSortModel, +} from '@mui/x-data-grid-pro'; +import {A} from '@wandb/weave/common/util/links'; +import {Button} from '@wandb/weave/components/Button'; +import {RowId} from '@wandb/weave/components/PagePanelComponents/Home/Browse3/pages/CallPage/DataTableView'; +import {Tooltip} from '@wandb/weave/components/Tooltip'; +import React, { + FC, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import {useHistory} from 'react-router-dom'; +import {v4 as uuidv4} from 'uuid'; + +import {isWeaveObjectRef, parseRef, parseRefMaybe} from '../../../../../react'; +import {flattenObjectPreservingWeaveTypes} from '../../Browse2/browse2Util'; +import {CellValue} from '../../Browse2/CellValue'; +import {useWeaveflowCurrentRouteContext} from '../context'; +import {WeaveCHTableSourceRefContext} from '../pages/CallPage/DataTableView'; +import {TABLE_ID_EDGE_NAME} from '../pages/wfReactInterface/constants'; +import {useWFHooks} from '../pages/wfReactInterface/context'; +import {SortBy} from '../pages/wfReactInterface/traceServerClientTypes'; +import {StyledDataGrid} from '../StyledDataGrid'; +import {CellEditingRenderer, CellViewingRenderer} from './CellRenderers'; +import {useDatasetEditContext} from './DatasetEditorContext'; + +const ADDED_ROW_ID_PREFIX = 'new-'; + +// Dataset object schema as it is stored in the database. +interface DatasetObjectVal { + _type: 'Dataset'; + name: string | null; + description: string | null; + rows: string; + _class_name: 'Dataset'; + _bases: ['Object', 'BaseModel']; +} + +interface EditableDataTableViewProps { + datasetObject: DatasetObjectVal; + isEditing: boolean; +} + +interface ControlCellProps { + params: GridRenderCellParams; + deleteRow: (absoluteIndex: number) => void; + restoreRow: (absoluteIndex: number) => void; + deleteAddedRow: (rowId: string) => void; + isDeleted: boolean; + isNew: boolean; +} + +interface ControlsColumnHeaderProps { + onAddRow: () => void; +} + +const ControlCell: FC = ({ + params, + deleteRow, + restoreRow, + deleteAddedRow, + isDeleted, + isNew, +}) => ( + +