Skip to content

Commit

Permalink
feat(weave): dataset editing UI
Browse files Browse the repository at this point in the history
  • Loading branch information
bcsherma committed Jan 11, 2025
1 parent 6841aa7 commit cc07849
Show file tree
Hide file tree
Showing 6 changed files with 1,252 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<Box
onClick={handleEditClick}
onMouseEnter={() => 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)',
},
}}>
<span style={{flex: 1, position: 'relative', overflow: 'hidden'}}>
{value}
{isEditing && (
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background:
'linear-gradient(90deg, transparent 0%, transparent 10%, rgba(255, 255, 255, 0.8) 35%, transparent 60%, transparent 100%)',
transform: 'translateX(-100%)',
animation: 'shimmer 3s infinite linear',
pointerEvents: 'none',
}}
/>
)}
</span>
{isHovered && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
opacity: 0,
transition: 'opacity 0.2s ease',
cursor: 'pointer',
animation: 'fadeIn 0.2s ease forwards',
'@keyframes fadeIn': {
from: {opacity: 0},
to: {opacity: 0.5},
},
'&:hover': {
opacity: 0.8,
},
}}>
<Icon name="pencil-edit" height={14} width={14} />
</Box>
)}
</Box>
);
};

export const CellEditingRenderer: React.FC<
GridRenderEditCellParams
> = params => {
const {id, value, field, hasFocus, api} = params;
const inputRef = React.useRef<HTMLTextAreaElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
const [hasInitialFocus, setHasInitialFocus] = useState(false);
const initialWidth = React.useRef<number>();
const initialHeight = React.useRef<number>();

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<HTMLInputElement>) => {
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 (
<>
<CellViewingRenderer {...params} isEditing={true} />
<Popover
open={!!anchorEl}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={() => {
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',
},
}}>
<ResizableBox
width={initialWidth.current ?? 400}
height={initialHeight.current ?? 300}>
<Box
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}>
<DraggableHandle>
<Box
sx={{
cursor: 'grab',
backgroundColor: 'white',
display: 'flex',
justifyContent: 'space-between',
padding: '8px',
borderBottom: '1px solid rgba(0, 0, 0, 0.05)',
}}>
<Box />
<Typography
variant="caption"
sx={{
fontFamily: '"Source Sans Pro", sans-serif',
color: 'gray.500',
}}>
⌘+Enter to close
</Typography>
<Button
icon="close"
size="small"
variant="ghost"
onClick={() => api.stopCellEditMode({id, field})}
/>
</Box>
</DraggableHandle>
<Box
sx={{
flex: 1,
overflow: 'auto',
padding: '12px',
}}>
<TextField
inputRef={inputRef}
value={value}
onChange={handleValueChange}
onKeyDown={handleKeyDown}
onFocus={e => {
const target = e.target as HTMLTextAreaElement;
target.setSelectionRange(0, target.value.length);
}}
fullWidth
multiline
autoFocus
sx={{
'& .MuiInputBase-root': {
fontFamily: '"Source Sans Pro", sans-serif',
fontSize: '14px',
border: 'none',
backgroundColor: 'white',
},
'& .MuiInputBase-input': {
padding: '0px',
},
// Hide the blue border of the text field field.
'& .MuiOutlinedInput-notchedOutline': {
border: 'none',
},
'& textarea': {
overflow: 'visible !important',
},
}}
/>
</Box>
</Box>
</ResizableBox>
</Popover>
</>
);
};
Loading

0 comments on commit cc07849

Please sign in to comment.