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..a2b1965ea580 --- /dev/null +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/datasets/CellRenderers.tsx @@ -0,0 +1,320 @@ +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, {useCallback, 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 { + isEdited?: boolean; + isDeleted?: boolean; + isNew?: boolean; + isEditing?: boolean; +} + +export const CellViewingRenderer: React.FC< + GridRenderCellParams & CellViewingRendererProps +> = ({ + value, + isEdited = false, + isDeleted = false, + isNew = false, + api, + id, + field, + isEditing = false, +}) => { + 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', + '@keyframes shimmer': { + '0%': { + transform: 'translateX(-100%)', + }, + '100%': { + transform: 'translateX(100%)', + }, + }, + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.04)', + }, + }}> + + {value} + {isEditing && ( + + )} + + {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); + const [hasInitialFocus, setHasInitialFocus] = useState(false); + const initialWidth = React.useRef(); + const initialHeight = React.useRef(); + + const getPopoverWidth = useCallback(() => { + if (typeof value !== 'string') { + return 400; + } + const approximateWidth = Math.min(Math.max(value.length * 8, 400), 960); + return approximateWidth; + }, [value]); + + const getPopoverHeight = useCallback(() => { + if (typeof value !== 'string') { + return 300; + } + const width = getPopoverWidth(); + const charsPerLine = Math.floor(width / 8); + const lines = value.split('\n').reduce((acc, line) => { + return acc + Math.ceil(line.length / charsPerLine); + }, 0); + + const contentHeight = Math.min(Math.max(lines * 24 + 80, 120), 600); + return contentHeight; + }, [value, getPopoverWidth]); + + React.useLayoutEffect(() => { + const element = api.getCellElement(id, field); + if (element) { + setAnchorEl(element); + if (!initialWidth.current) { + initialWidth.current = getPopoverWidth(); + } + if (!initialHeight.current) { + initialHeight.current = getPopoverHeight(); + } + } + }, [ + api, + id, + field, + initialWidth, + initialHeight, + getPopoverWidth, + getPopoverHeight, + ]); + + React.useEffect(() => { + if (hasFocus && !hasInitialFocus) { + setTimeout(() => { + const textarea = inputRef.current?.querySelector('textarea'); + if (textarea) { + textarea.focus(); + textarea.setSelectionRange(0, textarea.value.length); + setHasInitialFocus(true); + } + }, 0); + } + }, [hasFocus, hasInitialFocus]); + + 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); + api.stopCellEditMode({id, field}); + initialWidth.current = undefined; + initialHeight.current = undefined; + }} + 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' : ''}) - - )} - -
-
-
-
-

Version

-

{objectVersionIndex}

+
+
+
+

Name

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

Version

+

{objectVersionIndex}

+
- {showDeleteButton && ( -
+
+ {isEditing ? ( + renderEditingControls() + ) : ( +
- )} + )} +
+ {renderPublishPopover()}
} @@ -121,22 +366,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..3a3c63e02059 --- /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, +}) => ( + +