Skip to content

Commit

Permalink
feat(ui): object comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
jamie-rasmussen committed Nov 15, 2024
1 parent db93564 commit 108cbba
Show file tree
Hide file tree
Showing 28 changed files with 2,254 additions and 18 deletions.
10 changes: 10 additions & 0 deletions weave-js/src/components/PagePanelComponents/Home/Browse3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {Button} from '../../Button';
import {ErrorBoundary} from '../../ErrorBoundary';
import {Browse2EntityPage} from './Browse2/Browse2EntityPage';
import {Browse2HomePage} from './Browse2/Browse2HomePage';
import {ComparePage} from './Browse3/compare/ComparePage';
import {
baseContext,
browse2Context,
Expand Down Expand Up @@ -537,6 +538,9 @@ const Browse3ProjectRoot: FC<{
]}>
<PlaygroundPageBinding />
</Route>
<Route path={`${projectRoot}/compare`}>
<ComparePageBinding />
</Route>
</Switch>
</Box>
);
Expand Down Expand Up @@ -1040,6 +1044,12 @@ const TablesPageBinding = () => {
return <TablesPage entity={params.entity} project={params.project} />;
};

const ComparePageBinding = () => {
const params = useParamsDecoded<Browse3TabItemParams>();

return <ComparePage entity={params.entity} project={params.project} />;
};

const AppBarLink = (props: ComponentProps<typeof RouterLink>) => (
<MaterialLink
sx={{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Show code in the Monaco editor. If code has changed show a diff.
*/

import {DiffEditor, Editor} from '@monaco-editor/react';
import {Box} from '@mui/material';
import React from 'react';

import {sanitizeString} from '../../../../../util/sanitizeSecrets';
import {Loading} from '../../../../Loading';
import {useWFHooks} from '../pages/wfReactInterface/context';

// Simple language detection based on file extension or content
// TODO: Unify this utility method with Browse2OpDefCode.tsx
const detectLanguage = (uri: string, code: string) => {
if (uri.endsWith('.py')) {
return 'python';
}
if (uri.endsWith('.js') || uri.endsWith('.ts')) {
return 'javascript';
}
if (code.includes('def ') || code.includes('import ')) {
return 'python';
}
if (code.includes('function ') || code.includes('const ')) {
return 'javascript';
}
return 'plaintext';
};

type CodeDiffProps = {
oldValueRef: string;
newValueRef: string;
};

export const CodeDiff = ({oldValueRef, newValueRef}: CodeDiffProps) => {
const {
derived: {useCodeForOpRef},
} = useWFHooks();
const opContentsQueryOld = useCodeForOpRef(oldValueRef);
const opContentsQueryNew = useCodeForOpRef(newValueRef);
const textOld = opContentsQueryOld.result ?? '';
const textNew = opContentsQueryNew.result ?? '';
const loading = opContentsQueryOld.loading || opContentsQueryNew.loading;

if (loading) {
return <Loading centered size={25} />;
}

const sanitizedOld = sanitizeString(textOld);
const sanitizedNew = sanitizeString(textNew);
const languageOld = detectLanguage(oldValueRef, sanitizedOld);
const languageNew = detectLanguage(newValueRef, sanitizedNew);

const inner =
sanitizedOld !== sanitizedNew ? (
<DiffEditor
height="100%"
originalLanguage={languageOld}
modifiedLanguage={languageNew}
loading={loading}
original={sanitizedOld}
modified={sanitizedNew}
options={{
readOnly: true,
minimap: {enabled: false},
scrollBeyondLastLine: false,
padding: {top: 10, bottom: 10},
renderSideBySide: false,
}}
/>
) : (
<Editor
height="100%"
language={languageNew}
loading={loading}
value={sanitizedNew}
options={{
readOnly: true,
minimap: {enabled: false},
scrollBeyondLastLine: false,
padding: {top: 10, bottom: 10},
}}
/>
);

const maxRowsInView = 20;
const totalLines = sanitizedNew.split('\n').length ?? 0;
const showLines = Math.min(totalLines, maxRowsInView);
const lineHeight = 18;
const padding = 20;
const height = showLines * lineHeight + padding + 'px';
return <Box sx={{height}}>{inner}</Box>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/**
* This is similar to the ObjectViewer but allows for multiple objects
* to be displayed side-by-side.
*/

import {
DataGridProProps,
GRID_TREE_DATA_GROUPING_FIELD,
GridColDef,
GridPinnedColumnFields,
GridRowHeightParams,
GridRowId,
GridValidRowModel,
useGridApiRef,
} from '@mui/x-data-grid-pro';
import React, {useCallback, useEffect, useMemo} from 'react';

import {WeaveObjectRef} from '../../../../../react';
import {SmallRef} from '../../Browse2/SmallRef';
import {ObjectVersionSchema} from '../pages/wfReactInterface/wfDataModelHooksInterface';
import {StyledDataGrid} from '../StyledDataGrid';
import {RowDataWithDiff, UNCHANGED} from './compare';
import {CompareGridCell} from './CompareGridCell';
import {CompareGridGroupingCell} from './CompareGridGroupingCell';
import {ComparableObject, Mode} from './types';

type CompareGridProps = {
objectType: 'object' | 'call';
objectIds: string[];
objects: ComparableObject[];
rows: RowDataWithDiff[];
mode: Mode;
baselineEnabled: boolean;
onlyChanged: boolean;
isExpanded?: boolean;

expandedIds: GridRowId[];
setExpandedIds: React.Dispatch<React.SetStateAction<GridRowId[]>>;
addExpandedRefs: (path: string, refs: string[]) => void;
};

const objectVersionSchemaToRef = (
objVersion: ObjectVersionSchema
): WeaveObjectRef => {
return {
scheme: 'weave',
entityName: objVersion.entity,
projectName: objVersion.project,
weaveKind: 'object',
artifactName: objVersion.objectId,
artifactVersion: objVersion.versionHash,
};
};

export const CompareGrid = ({
objectType,
objectIds,
objects,
rows,
mode,
baselineEnabled,
onlyChanged,
isExpanded,
expandedIds,
setExpandedIds,
addExpandedRefs,
}: CompareGridProps) => {
const apiRef = useGridApiRef();

const filteredRows = onlyChanged
? rows.filter(row => row.changeType !== UNCHANGED)
: rows;

const pinnedColumns: GridPinnedColumnFields = {
left: [
GRID_TREE_DATA_GROUPING_FIELD,
...(baselineEnabled ? [objectIds[0]] : []),
],
};
const columns: GridColDef[] = [];
if (mode === 'unified' && objectIds.length === 2) {
columns.push({
field: 'value',
headerName: 'Value',
flex: 1,
display: 'flex',
sortable: false,
renderCell: cellParams => {
const objId = objectIds[1];
const compareIdx = baselineEnabled
? 0
: Math.max(0, objectIds.indexOf(objId) - 1);
const compareId = objectIds[compareIdx];
const compareValue = cellParams.row.values[compareId];
const compareValueType = cellParams.row.types[compareId];
const value = cellParams.row.values[objId];
const valueType = cellParams.row.types[objId];
const rowChangeType = cellParams.row.changeType;
return (
<div className="w-full p-8">
<CompareGridCell
path={cellParams.row.path}
displayType="both"
value={value}
valueType={valueType}
compareValue={compareValue}
compareValueType={compareValueType}
rowChangeType={rowChangeType}
/>
</div>
);
},
});
} else {
const versionCols: GridColDef[] = objectIds.map(objId => ({
field: objId,
headerName: objId,
flex: 1,
display: 'flex',
width: 500,
sortable: false,
valueGetter: (unused: any, row: any) => {
return row.values[objId];
},
renderHeader: (params: any) => {
if (objectType === 'call') {
// TODO: Make this a peek drawer link
return objId;
}
const idx = objectIds.indexOf(objId);
const objVersion = objects[idx];
const objRef = objectVersionSchemaToRef(
objVersion as ObjectVersionSchema
);
return <SmallRef objRef={objRef} />;
},
renderCell: (cellParams: any) => {
const compareIdx = baselineEnabled
? 0
: Math.max(0, objectIds.indexOf(objId) - 1);
const compareId = objectIds[compareIdx];
const compareValue = cellParams.row.values[compareId];
const compareValueType = cellParams.row.types[compareId];
const value = cellParams.row.values[objId];
const valueType = cellParams.row.types[objId];
const rowChangeType = cellParams.row.changeType;
return (
<div className="w-full p-8">
<CompareGridCell
path={cellParams.row.path}
displayType="diff"
value={value}
valueType={valueType}
compareValue={compareValue}
compareValueType={compareValueType}
rowChangeType={rowChangeType}
/>
</div>
);
},
}));
columns.push(...versionCols);
}

const groupingColDef: DataGridProProps['groupingColDef'] = useMemo(
() => ({
field: '__group__',
hideDescendantCount: true,
width: 300,
renderHeader: () => {
// Keep padding in sync with
// INSET_SPACING (32) + left change indication border (3) - header padding (10)
return <div className="pl-[25px]">Path</div>;
},
renderCell: params => {
return (
<CompareGridGroupingCell
{...params}
onClick={() => {
setExpandedIds(eIds => {
if (eIds.includes(params.row.id)) {
return eIds.filter(id => id !== params.row.id);
}
return [...eIds, params.row.id];
});
addExpandedRefs(params.row.id, params.row.expandableRefs);
}}
/>
);
},
}),
[addExpandedRefs, setExpandedIds]
);

const getRowId = (row: GridValidRowModel) => {
return row.path.toString();
};

// Next we define a function that updates the row expansion state. This
// function is responsible for setting the expansion state of rows that have
// been expanded by the user. It is bound to the `rowsSet` event so that it is
// called whenever the rows are updated. The MUI data grid will rerender and
// close all expanded rows when the rows are updated. This function is
// responsible for re-expanding the rows that were previously expanded.
const updateRowExpand = useCallback(() => {
expandedIds.forEach(id => {
if (apiRef.current.getRow(id)) {
const children = apiRef.current.getRowGroupChildren({groupId: id});
if (children.length !== 0) {
apiRef.current.setRowChildrenExpansion(id, true);
}
}
});
}, [apiRef, expandedIds]);
useEffect(() => {
updateRowExpand();
return apiRef.current.subscribeEvent('rowsSet', () => {
updateRowExpand();
});
}, [apiRef, expandedIds, updateRowExpand]);

const getGroupIds = useCallback(() => {
const rowIds = apiRef.current.getAllRowIds();
return rowIds.filter(rowId => {
const rowNode = apiRef.current.getRowNode(rowId);
return rowNode && rowNode.type === 'group';
});
}, [apiRef]);

// On first render expand groups
useEffect(() => {
setExpandedIds(getGroupIds());
}, [setExpandedIds, getGroupIds]);

return (
<StyledDataGrid
apiRef={apiRef}
getRowId={getRowId}
autoHeight
treeData
groupingColDef={groupingColDef}
getTreeDataPath={row => row.path.toStringArray()}
columns={columns}
rows={filteredRows}
isGroupExpandedByDefault={node => {
return expandedIds.includes(node.id);
}}
columnHeaderHeight={38}
disableColumnReorder={true}
disableColumnMenu={true}
getRowHeight={(params: GridRowHeightParams) => {
return 'auto';
}}
rowSelection={false}
hideFooter
pinnedColumns={pinnedColumns}
keepBorders
sx={{
'& .MuiDataGrid-cell': {
alignItems: 'flex-start',
padding: 0,
},
}}
/>
);
};
Loading

0 comments on commit 108cbba

Please sign in to comment.