Skip to content

Commit

Permalink
chore(dev): dataset editor mvp
Browse files Browse the repository at this point in the history
  • Loading branch information
bcsherma committed Dec 13, 2024
1 parent 32fc0da commit caae9d6
Show file tree
Hide file tree
Showing 7 changed files with 380 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {GridPaginationModel} from '@mui/x-data-grid-pro';

const MAX_PAGE_SIZE = 100;
export const DEFAULT_PAGE_SIZE = 100;
export const DEFAULT_PAGE_SIZE = 25;

export const getValidPaginationModel = (
queryPage: string | undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {TABLE_ID_EDGE_NAME} from '../wfReactInterface/constants';
import {useWFHooks} from '../wfReactInterface/context';
import {SortBy} from '../wfReactInterface/traceServerClientTypes';

const RowId = styled.span`
export const RowId = styled.span`
font-family: 'Inconsolata', monospace;
`;
RowId.displayName = 'S.RowId';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
import {Box, Button as MuiButton, Modal, Typography} from '@mui/material';
import {GridPaginationModel, GridRenderCellParams} from '@mui/x-data-grid-pro';
import React, {FC, useCallback, useMemo, useState} from 'react';
import styled from 'styled-components';

import {isWeaveObjectRef, parseRefMaybe} from '../../../../../../react';
import {Tooltip} from '../../../../../Tooltip';
import {flattenObjectPreservingWeaveTypes} from '../../../Browse2/browse2Util';
import {DEFAULT_PAGE_SIZE} from '../../grid/pagination';
import {StyledDataGrid} from '../../StyledDataGrid';
import {A} from '../common/Links';
import {useWFHooks} from '../wfReactInterface/context';
import {SortBy} from '../wfReactInterface/traceServerClientTypes';
import {RowId} from './DataTableView'; // Import shared components

export type TableUpdateSpec = TableAppendSpec | TablePopSpec | TableInsertSpec;

type TableAppendSpec = {
append: {
row: Record<string, any>;
};
};

type TablePopSpec = {
pop: {
index: number;
};
};

type TableInsertSpec = {
insert: {
index: number;
row: Record<string, any>;
};
};

interface EditableDataTableViewProps {
datasetObjectId: string;
datasetVersionIndex: number;
tableRefUri: string;
fullHeight?: boolean;
}

// Add styled component for edited cells
const StyledBox = styled(Box)`
.edited-cell {
background-color: rgba(25, 118, 210, 0.1);
border-radius: 4px;
transition: background-color 0.3s ease;
}
`;

export const EditableDataTableView: FC<EditableDataTableViewProps> = props => {
const {useTableRowsQuery, useTableQueryStats, useObjCreate} = useWFHooks();
const [sortBy] = useState<SortBy[]>([]);
const [editedCellsMap, setEditedCellsMap] = useState<Map<string, any>>(
new Map()
);

const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
page: 0,
pageSize: DEFAULT_PAGE_SIZE,
});

const [isPublishModalOpen, setIsPublishModalOpen] = useState(false);

// Parse table ref
const parsedRef = useMemo(
() => parseRefMaybe(props.tableRefUri),
[props.tableRefUri]
);
const lookupKey = useMemo(() => {
if (
parsedRef == null ||
!isWeaveObjectRef(parsedRef) ||
parsedRef.weaveKind !== 'table'
) {
return null;
}
return {
entity: parsedRef.entityName,
project: parsedRef.projectName,
digest: parsedRef.artifactVersion,
};
}, [parsedRef]);

// Fetch row count
const numRowsQuery = useTableQueryStats(
lookupKey?.entity ?? '',
lookupKey?.project ?? '',
lookupKey?.digest ?? '',
{skip: lookupKey == null}
);

// Fetch rows
const fetchQuery = useTableRowsQuery(
lookupKey?.entity ?? '',
lookupKey?.project ?? '',
lookupKey?.digest ?? '',
undefined,
paginationModel.pageSize,
paginationModel.page * paginationModel.pageSize,
sortBy,
{skip: lookupKey == null}
);

// Convert data to list of dictionaries and flatten nested objects
const dataAsListOfDict = useMemo(() => {
return (fetchQuery.result?.rows ?? []).map(row => {
let val = row;
if (val == null) {
return {};
} else if (typeof val === 'object' && !Array.isArray(val)) {
if ('val' in val) {
val = val.val; // Extract val field
}
return flattenObjectPreservingWeaveTypes(val);
}
return {'': val};
});
}, [fetchQuery.result?.rows]);

// Reapply edits when rows are fetched
const rows = useMemo(() => {
if (!fetchQuery.loading && fetchQuery.result?.rows) {
return dataAsListOfDict.map((row, i) => {
const digest = fetchQuery.result!.rows[i].digest;
const rowKey = `${digest}`;
const editedRow = editedCellsMap.get(rowKey);
const baseRow = editedRow ? {...row, ...editedRow} : row;
return {
id: digest,
...baseRow,
};
});
}
return [];
}, [fetchQuery.loading, fetchQuery.result, dataAsListOfDict, editedCellsMap]);

// Generate columns with cell class names for edited cells
const columns = useMemo(() => {
const firstRow = rows[0] ?? {};
return Object.keys(firstRow).map(field => {
if (field === 'id') {
return {
field,
headerName: 'id',
width: 50,
editable: false,
filterable: false,
sortable: false,
renderCell: (params: GridRenderCellParams) => {
const id = params.value;
const rowLabel = id ? id.slice(-4) : params.id;
const rowSpan = (
<Tooltip trigger={<RowId>{rowLabel}</RowId>} content={id} />
);
return <A onClick={() => {}}>{rowSpan}</A>;
},
};
}
return {
field,
headerName: field,
flex: 1,
editable: true,
sortable: false,
filterable: false,
cellClassName: (params: any) => {
const rowKey = `${params.row.id}`;
const editedRow = editedCellsMap.get(rowKey);
return editedRow && editedRow[field] !== undefined
? 'edited-cell'
: '';
},
};
});
}, [rows, editedCellsMap]);

// Function to convert edited cells to TableUpdateSpec
const convertEditsToTableUpdateSpec = useCallback(() => {
const updates: TableUpdateSpec[] = [];
editedCellsMap.forEach((editedRow, rowKey) => {
const rowIndex = rows.findIndex(row => row.id === rowKey);
if (rowIndex !== -1) {
const insertSpec: TableInsertSpec = {
insert: {
index: rowIndex,
row: editedRow,
},
};
updates.push(insertSpec);
}
});
return updates;
}, [editedCellsMap, rows]);

const objCreate = useObjCreate();
const entity = lookupKey?.entity ?? '';
const project = lookupKey?.project ?? '';
const projectId = `${entity}/${project}`;
const handlePublish = useCallback(async () => {
setIsPublishModalOpen(false);
const tableUpdateSpecs = convertEditsToTableUpdateSpec();
const resp = objCreate(projectId, 'updateSpec', tableUpdateSpecs);
console.log('resp', resp);
// Here you can handle the update specs, e.g., send them to a server
}, [convertEditsToTableUpdateSpec, projectId, objCreate]);

// Handle cell edits
const processRowUpdate = useCallback((newRow: any, oldRow: any) => {
const changedField = Object.keys(newRow).find(
key => newRow[key] !== oldRow[key] && key !== 'id'
);

if (changedField) {
const rowKey = `${newRow.id}`;
setEditedCellsMap(prev => {
const updatedMap = new Map(prev);
const existingEdits = updatedMap.get(rowKey) || {};
updatedMap.set(rowKey, {
...existingEdits,
[changedField]: newRow[changedField],
});
return updatedMap;
});
}
return newRow;
}, []);

// Handle pagination model change
const handlePaginationModelChange = useCallback(
(newModel: GridPaginationModel) => {
setPaginationModel(newModel);
},
[]
);

return (
<StyledBox
sx={{
height: props.fullHeight ? '50%' : 400,
width: '100%',
}}>
<StyledDataGrid
rows={rows}
columns={columns}
editMode="cell"
pagination={true}
paginationMode="server"
paginationModel={paginationModel}
onPaginationModelChange={handlePaginationModelChange}
rowCount={numRowsQuery.result?.count ?? 0}
pageSizeOptions={[5, 10, 20, 50, 100]}
disableMultipleColumnsSorting
loading={fetchQuery.loading}
disableRowSelectionOnClick
sx={{border: 'none'}}
processRowUpdate={processRowUpdate}
/>
<MuiButton
variant="contained"
onClick={() => setIsPublishModalOpen(true)}>
Publish
</MuiButton>
<Modal
open={isPublishModalOpen}
onClose={() => setIsPublishModalOpen(false)}
aria-labelledby="publish-modal-title"
aria-describedby="publish-modal-description">
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
}}>
<Typography id="publish-modal-title" variant="h6" component="h2">
Confirm Publish
</Typography>
<Typography id="publish-modal-description" sx={{mt: 2}}>
Are you sure you want to publish the changes?
</Typography>
<Box sx={{mt: 2, display: 'flex', justifyContent: 'flex-end'}}>
<MuiButton
onClick={() => setIsPublishModalOpen(false)}
sx={{mr: 1}}>
Cancel
</MuiButton>
<MuiButton variant="contained" onClick={handlePublish}>
Publish
</MuiButton>
</Box>
</Box>
</Modal>
</StyledBox>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {isCustomWeaveTypePayload} from '../../typeViews/customWeaveType.types';
import {CustomWeaveTypeDispatcher} from '../../typeViews/CustomWeaveTypeDispatcher';
import {OBJECT_ATTR_EDGE_NAME} from '../wfReactInterface/constants';
import {WeaveCHTable, WeaveCHTableSourceRefContext} from './DataTableView';
import {EditableDataTableView} from './EditableDataTableView';
import {ObjectViewer} from './ObjectViewer';
import {getValueType, traverse} from './traverse';
import {ValueView} from './ValueView';
Expand Down Expand Up @@ -235,6 +236,7 @@ export const ObjectViewerSection = ({
isExpanded,
}: ObjectViewerSectionProps) => {
const currentRef = useContext(WeaveCHTableSourceRefContext);
const [isEditing, setIsEditing] = useState(false);

if (isCustomWeaveTypePayload(data)) {
return (
Expand All @@ -261,8 +263,6 @@ export const ObjectViewerSection = ({
if (numKeys === 1 && '_result' in data) {
let value = data._result;
if (isWeaveRef(value)) {
// Little hack to make sure that we render refs
// inside the expansion table view
value = {' ': value};
}
const valueType = getValueType(value);
Expand Down Expand Up @@ -291,14 +291,12 @@ export const ObjectViewerSection = ({
);
}

// Here we have a very special case for when the section is viewing a dataset.
// Instead of rending the generic renderer, we directly render a full-screen
// data table.
if (
data._type === 'Dataset' &&
data._class_name === 'Dataset' &&
_.isEqual(data._bases, ['Object', 'BaseModel'])
) {
const editorToggleIcon = isEditing ? 'stop' : 'pencil-edit';
const parsed = parseRef(data.rows);
if (isWeaveObjectRef(parsed) && parsed.weaveKind === 'table') {
const inner = (
Expand All @@ -307,9 +305,29 @@ export const ObjectViewerSection = ({
height: '100%',
overflow: 'hidden',
}}>
<WeaveCHTable tableRefUri={data.rows} fullHeight />
<TitleRow>
<Title>{title}</Title>
<Button
variant="quiet"
icon={editorToggleIcon}
active={isEditing}
onClick={() => setIsEditing(!isEditing)}
tooltip={isEditing ? 'View table' : 'Edit table'}
/>
</TitleRow>
{isEditing ? (
<EditableDataTableView
tableRefUri={data.rows}
datasetObjectId={data.objectId}
datasetVersionIndex={data.versionIndex}
fullHeight
/>
) : (
<WeaveCHTable tableRefUri={data.rows} fullHeight />
)}
</Box>
);

if (currentRef != null) {
return (
<WeaveCHTableSourceRefContext.Provider
Expand Down
Loading

0 comments on commit caae9d6

Please sign in to comment.