Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): object comparison #2942

Merged
merged 2 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -536,6 +537,9 @@ const Browse3ProjectRoot: FC<{
]}>
<PlaygroundPageBinding />
</Route>
<Route path={`${projectRoot}/compare`}>
<ComparePageBinding />
</Route>
</Switch>
</Box>
);
Expand Down Expand Up @@ -1039,6 +1043,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,268 @@
/**
* 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;

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

export const MAX_OBJECT_COLS = 6;

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,
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
.slice(0, MAX_OBJECT_COLS)
.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
Loading