From caae9d63cf79bd47dc0d375306ad588596db0301 Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Fri, 13 Dec 2024 10:35:53 -0800 Subject: [PATCH] chore(dev): dataset editor mvp --- .../Home/Browse3/grid/pagination.ts | 2 +- .../Browse3/pages/CallPage/DataTableView.tsx | 2 +- .../pages/CallPage/EditableDataTableView.tsx | 302 ++++++++++++++++++ .../pages/CallPage/ObjectViewerSection.tsx | 30 +- .../Home/Browse3/pages/ObjectVersionPage.tsx | 16 +- .../wfReactInterface/tsDataModelHooks.ts | 35 +- .../wfDataModelHooksInterface.ts | 6 + 7 files changed, 380 insertions(+), 13 deletions(-) create mode 100644 weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/EditableDataTableView.tsx diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/grid/pagination.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/grid/pagination.ts index ed9adf9d22a..e8dda623341 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/grid/pagination.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/grid/pagination.ts @@ -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, diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/DataTableView.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/DataTableView.tsx index 9a954858926..5ca1e3ec733 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/DataTableView.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/DataTableView.tsx @@ -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'; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/EditableDataTableView.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/EditableDataTableView.tsx new file mode 100644 index 00000000000..6c8383fe497 --- /dev/null +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/EditableDataTableView.tsx @@ -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; + }; +}; + +type TablePopSpec = { + pop: { + index: number; + }; +}; + +type TableInsertSpec = { + insert: { + index: number; + row: Record; + }; +}; + +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 = props => { + const {useTableRowsQuery, useTableQueryStats, useObjCreate} = useWFHooks(); + const [sortBy] = useState([]); + const [editedCellsMap, setEditedCellsMap] = useState>( + new Map() + ); + + const [paginationModel, setPaginationModel] = useState({ + 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 = ( + {rowLabel}} content={id} /> + ); + return {}}>{rowSpan}; + }, + }; + } + 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 ( + + + setIsPublishModalOpen(true)}> + Publish + + setIsPublishModalOpen(false)} + aria-labelledby="publish-modal-title" + aria-describedby="publish-modal-description"> + + + Confirm Publish + + + Are you sure you want to publish the changes? + + + setIsPublishModalOpen(false)} + sx={{mr: 1}}> + Cancel + + + Publish + + + + + + ); +}; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewerSection.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewerSection.tsx index d02a2e881ca..f9e3492ec4d 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewerSection.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ObjectViewerSection.tsx @@ -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'; @@ -235,6 +236,7 @@ export const ObjectViewerSection = ({ isExpanded, }: ObjectViewerSectionProps) => { const currentRef = useContext(WeaveCHTableSourceRefContext); + const [isEditing, setIsEditing] = useState(false); if (isCustomWeaveTypePayload(data)) { return ( @@ -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); @@ -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 = ( @@ -307,9 +305,29 @@ export const ObjectViewerSection = ({ height: '100%', overflow: 'hidden', }}> - + + {title} +