pgadmin4/web/pgadmin/tools/schema_diff/static/js/components/ResultGridComponent.jsx

757 lines
23 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { SelectColumn } from 'react-data-grid';
import React, { useContext, useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/styles';
import KeyboardArrowRightRoundedIcon from '@material-ui/icons/KeyboardArrowRightRounded';
import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded';
import InfoIcon from '@material-ui/icons/InfoRounded';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import { FILTER_NAME, SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
import { SchemaDiffContext, SchemaDiffEventsContext } from './SchemaDiffComponent';
import { InputCheckbox } from '../../../../../static/js/components/FormComponents';
import PgReactDataGrid from '../../../../../static/js/components/PgReactDataGrid';
import Notifier from '../../../../../static/js/helpers/Notifier';
const useStyles = makeStyles((theme) => ({
root: {
paddingTop: '0.5rem',
display: 'flex',
height: '100%',
flexDirection: 'column',
color: theme.palette.text.primary,
backgroundColor: theme.otherVars.qtDatagridBg,
border: 'none',
fontSize: '13px',
'& .rdg': {
flex: 1,
borderTop: '1px solid' + theme.otherVars.borderColor,
},
'--rdg-background-color': theme.palette.default.main,
'--rdg-selection-color': theme.palette.primary.main,
'& .rdg-cell': {
padding: 0,
boxShadow: 'none',
color: theme.otherVars.schemaDiff.diffColorFg + ' !important',
...theme.mixins.panelBorder.right,
...theme.mixins.panelBorder.bottom,
'&[aria-colindex="1"]': {
padding: 0,
},
'&[aria-selected=true]:not([role="columnheader"]):not([aria-colindex="1"])': {
outlineWidth: '0',
outlineOffset: '-1px',
color: theme.otherVars.qtDatagridSelectFg,
},
'&[aria-selected=true][aria-colindex="1"]': {
outlineWidth: 0,
}
},
'& .rdg-header-row .rdg-cell': {
padding: 0,
paddingLeft: '0.5rem',
boxShadow: 'none',
},
'& .rdg-header-row': {
backgroundColor: theme.palette.background.default,
},
'& .rdg-row': {
backgroundColor: theme.palette.background.default,
'&[aria-selected=true]': {
backgroundColor: theme.palette.primary.light,
color: theme.otherVars.qtDatagridSelectFg,
'& .rdg-cell:nth-child(1)': {
backgroundColor: 'transparent',
outlineColor: 'transparent',
color: theme.palette.primary.contrastText,
}
},
}
},
grid: {
fontSize: '13px',
'--rdg-selection-color': 'none'
},
subRow: {
paddingLeft: '1rem'
},
recordRow: {
marginLeft: '2.7rem',
height: '1.3rem',
width: '1.3rem',
display: 'inline-block',
marginRight: '0.3rem',
paddingLeft: '0.5rem',
},
rowIcon: {
display: 'inline-block !important',
height: '1.3rem',
width: '1.3rem'
},
cellExpand: {
display: 'table',
blockSize: '100%',
'& span': {
display: 'table-cell',
verticalAlign: 'middle',
cursor: 'pointer',
}
},
gridPanel: {
'--rdg-background-color': theme.palette.default.main + ' !important',
},
source: {
backgroundColor: theme.otherVars.schemaDiff.sourceRowColor,
color: theme.otherVars.schemaDiff.diffSelectFG,
paddingLeft: '0.5rem',
},
target: {
backgroundColor: theme.otherVars.schemaDiff.targetRowColor,
color: theme.otherVars.schemaDiff.diffSelectFG,
paddingLeft: '0.5rem',
},
different: {
backgroundColor: theme.otherVars.schemaDiff.diffRowColor,
color: theme.otherVars.schemaDiff.diffSelectFG,
paddingLeft: '0.5rem',
},
identical: {
paddingLeft: '0.5rem',
color: theme.otherVars.schemaDiff.diffColorFg,
},
selectCell: {
padding: '0 0.3rem'
},
headerSelectCell: {
padding: '0rem 0.3rem 0 0.3rem'
},
count: {
display: 'inline-block !important',
},
countStyle: {
fontWeight: 900,
fontSize: '0.8rem',
paddingLeft: '0.3rem',
},
countLabel: {
paddingLeft: '1rem',
},
selectedRow: {
paddingLeft: '0.5rem',
backgroundColor: theme.palette.primary.light
},
selectedRowCheckBox: {
paddingLeft: '0.5rem',
backgroundColor: theme.otherVars.schemaDiff.diffSelCheckbox,
},
selChBox: {
paddingLeft: 0,
},
noRowsIcon:{
width: '1.1rem',
height: '1.1rem',
marginRight: '0.5rem',
}
}));
function useFocusRef(isSelected) {
const ref = useRef(null);
useLayoutEffect(() => {
if (!isSelected) return;
ref.current?.focus({ preventScroll: true });
}, [isSelected]);
return {
ref,
tabIndex: isSelected ? 0 : -1
};
}
function setRecordCount(row, filterParams) {
row['identicalCount'] = 0;
row['differentCount'] = 0;
row['sourceOnlyCount'] = 0;
row['targetOnlyCount'] = 0;
row.children.map((ch) => {
if (filterParams.includes(ch.status)) {
if (ch.status == FILTER_NAME.IDENTICAL) {
row['identicalCount'] = row['identicalCount'] + 1;
} else if (ch.status == FILTER_NAME.DIFFERENT) {
row['differentCount'] = row['differentCount'] + 1;
} else if (ch.status == FILTER_NAME.SOURCE_ONLY) {
row['sourceOnlyCount'] = row['sourceOnlyCount'] + 1;
} else if (ch.status == FILTER_NAME.TARGET_ONLY) {
row['targetOnlyCount'] = row['targetOnlyCount'] + 1;
}
}
});
}
function CellExpanderFormatter({
row,
isCellSelected,
expanded,
filterParams,
onCellExpand,
classes
}) {
const { ref, tabIndex } = useFocusRef(isCellSelected);
'identicalCount' in row && setRecordCount(row, filterParams);
function handleKeyDown(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onCellExpand();
}
}
return (
<div className={classes.cellExpand}>
<span onClick={onCellExpand} onKeyDown={handleKeyDown}>
<span ref={ref} tabIndex={tabIndex} className={'identicalCount' in row ? classes.subRow : null}>
{expanded ? <KeyboardArrowDownRoundedIcon /> : <KeyboardArrowRightRoundedIcon />} <span className={clsx(row.icon, classes.rowIcon)}></span>{row.label}
{
'identicalCount' in row ?
<span className={clsx(classes.count)}>
{
filterParams.includes(FILTER_NAME.IDENTICAL) && <><span className={classes.countLabel}>{FILTER_NAME.IDENTICAL}:</span> <span className={classes.countStyle}>{row.identicalCount} </span></>
}
{
filterParams.includes(FILTER_NAME.DIFFERENT) && <><span className={classes.countLabel}>{FILTER_NAME.DIFFERENT}:</span> <span className={classes.countStyle}>{row.differentCount} </span></>
}
{
filterParams.includes(FILTER_NAME.SOURCE_ONLY) && <><span className={classes.countLabel}>{FILTER_NAME.SOURCE_ONLY}:</span> <span className={classes.countStyle}>{row.sourceOnlyCount} </span></>
}
{
filterParams.includes(FILTER_NAME.TARGET_ONLY) && <><span className={classes.countLabel}>{FILTER_NAME.TARGET_ONLY}: </span><span className={classes.countStyle}>{row.targetOnlyCount}</span></>
}
</span>
: null
}
</span>
</span>
</div>
);
}
CellExpanderFormatter.propTypes = {
row: PropTypes.object,
isCellSelected: PropTypes.bool,
expanded: PropTypes.bool,
onCellExpand: PropTypes.func,
filterParams: PropTypes.array,
classes: PropTypes.object
};
function toggleSubRow(rows, id, filterParams) {
const newRows = [...rows];
const rowIndex = newRows.findIndex((r) => r.id === id);
const row = newRows[rowIndex];
if (!row) return newRows;
const { children } = row;
if (!children) return newRows;
if (children.length > 0) {
newRows[rowIndex] = { ...row, isExpanded: !row.isExpanded };
if (!row.isExpanded) {
let tempChild = [];
expandRows(children, filterParams, tempChild, newRows, rowIndex);
} else {
collapseRows(newRows, filterParams, rowIndex);
}
return newRows;
} else {
newRows.splice(rowIndex, 1);
return newRows;
}
}
function expandRows(children, filterParams, tempChild, newRows, rowIndex) {
children.map((child) => {
if ('children' in child) {
let tempSubChild = [];
child.children.map((subChild) => {
if (filterParams.includes(subChild.status)) {
tempSubChild.push(subChild);
}
});
if (tempSubChild.length > 0) {
tempChild.push(child);
}
}
else {
if (filterParams.includes(child.status)) {
tempChild.push(child);
}
}
});
if (tempChild.length > 0) {
newRows.splice(rowIndex + 1, 0, ...tempChild);
} else {
newRows.splice(rowIndex, 1);
}
}
function collapseRows(newRows, filterParams, rowIndex) {
let totalChild = 0;
let filteredChild = newRows[rowIndex].children.filter((el) => {
if (el?.children) {
let clist = el.children.filter((subch) => {
return filterParams.includes(subch.status);
});
if (clist.length > 0) {
return el;
}
} else {
return el?.status ? filterParams.includes(el.status) : el;
}
});
let totalChCount = filteredChild.length;
for (let i = 0; i < filteredChild.length; i++) {
let _index = i + 1;
let indx = totalChild ? rowIndex + totalChild + _index : rowIndex + _index;
if (newRows[indx]?.isExpanded) {
let filteredSubChild = newRows[indx].children.filter((el) => {
return filterParams.includes(el.status);
});
totalChild += filteredSubChild.length;
}
}
newRows.splice(rowIndex + 1, totalChild ? totalChCount + totalChild : totalChCount);
}
function getChildrenRows(data) {
if ('children' in data) {
return data.children;
}
return data;
}
function checkRowExpanedStatus(rows, record) {
if (rows.length > 0) {
let tempRecord = rows.filter((rec) => {
return rec.parentId == record.parentId && rec.label == record.label;
});
return tempRecord.length > 0 ? tempRecord[0].isExpanded : false;
}
return false;
}
function prepareRows(rows, gridData, filterParams) {
let newRows = [];
let adedIds = [];
gridData.map((record) => {
let childrens = getChildrenRows(record);
if (childrens.length > 0) {
childrens.map((child) => {
let subChildrens = getChildrenRows(child);
let tempChildList = [];
subChildrens.map((subChild) => {
if (filterParams.includes(subChild.status)) {
tempChildList.push(subChild);
adedIds.push(subChild.id);
}
});
if (!adedIds.includes(record.id) && tempChildList.length > 0) {
adedIds.push(record.id);
record.isExpanded = true;
newRows.push(record);
}
if (!adedIds.includes(child.id) && tempChildList.length > 0) {
adedIds.push(child.id);
child.isExpanded = checkRowExpanedStatus(rows, child);
newRows.push(child);
newRows = checkAndAddChild(child, newRows, tempChildList);
}
});
}
});
return newRows;
}
function checkAndAddChild(child, newRows, tempChildList) {
if (child.isExpanded) {
newRows = newRows.concat(tempChildList);
}
return newRows;
}
function reducer(rows, { type, id, filterParams, gridData }) {
switch (type) {
case 'toggleSubRow':
return toggleSubRow(rows, id, filterParams);
case 'applyFilter':
return prepareRows(rows, gridData, filterParams);
default:
return rows;
}
}
export function ResultGridComponent({ gridData, allRowIds, filterParams, selectedRowIds, transId, sourceData, targetData }) {
const classes = useStyles();
const [rows, dispatch] = useReducer(reducer, [...gridData]);
const [selectedRows, setSelectedRows] = useState([]);
const [rootSelection, setRootSelection] = useState(false);
const [activeRow, setActiveRow] = useState(null);
const schemaDiffToolContext = useContext(SchemaDiffContext);
function checkAllChildInclude(row, tempSelectedRows) {
let isChildAllInclude = true;
row.metadata.children.map((id) => {
if (!tempSelectedRows.includes(id) && id !== `${row.id}`) {
isChildAllInclude = false;
}
});
return isChildAllInclude;
}
function selectedResultRows(row, tempSelectedRows) {
if (row.metadata.isRoot) {
tempSelectedRows.push(`${row.id}`, ...row.metadata.children);
tempSelectedRows.push(...row.metadata.subChildren);
} else if (row.metadata.subChildren) {
tempSelectedRows.push(...row.metadata.dependencies);
tempSelectedRows.push(`${row.id}`, ...row.metadata.subChildren);
let isChildAllInclude = checkAllChildInclude(row, tempSelectedRows);
isChildAllInclude && tempSelectedRows.push(`${row.metadata.parentId}`);
} else {
tempSelectedRows.push(...row.dependencieRowIds);
tempSelectedRows.push(`${row.id}`);
let isChildAllInclude = checkAllChildInclude(row, tempSelectedRows);
isChildAllInclude && tempSelectedRows.push(`${row.metadata.parentId}`);
for(let rowData of rows) {
if (rowData.id == row.metadata.parentId) {
let isChildInclude = checkAllChildInclude(rowData, tempSelectedRows);
isChildInclude && tempSelectedRows.push(`${rowData.metadata.parentId}`);
}
}
}
}
function deselectChildAndSubChild(children, tempSelectedRows) {
children.map((chid) => {
let indx = tempSelectedRows.indexOf(chid);
indx != -1 && tempSelectedRows.splice(indx, 1);
});
}
function deselectResultRows(row, tempSelectedRows) {
if (row.metadata.isRoot) {
deselectChildAndSubChild(row.metadata.subChildren, tempSelectedRows);
deselectChildAndSubChild(row.metadata.children, tempSelectedRows);
let rootIndex = tempSelectedRows.indexOf(`${row.id}`);
rootIndex != -1 && tempSelectedRows.splice(rootIndex, 1);
} else if (row.metadata.subChildren) {
deselectChildAndSubChild(row.metadata.subChildren, tempSelectedRows);
let isChildAllInclude = true;
row.metadata.children.map((id) => {
if (tempSelectedRows.includes(id)) {
isChildAllInclude = false;
}
});
let rootIndex = tempSelectedRows.indexOf(`${row.id}`);
rootIndex != -1 && tempSelectedRows.splice(rootIndex, 1);
let parentIndex = tempSelectedRows.indexOf(`${row.metadata.parentId}`);
(!isChildAllInclude && parentIndex != -1) && tempSelectedRows.splice(parentIndex, 1);
row.metadata.dependencies.map((depid) => {
let depIndex = tempSelectedRows.indexOf(`${depid}`);
depIndex != -1 && tempSelectedRows.splice(depIndex, 1);
});
} else {
let elementIndex = tempSelectedRows.indexOf(`${row.id}`);
elementIndex != -1 && tempSelectedRows.splice(elementIndex, 1);
let parentElIndex = tempSelectedRows.indexOf(`${row.metadata.parentId}`);
parentElIndex != -1 && tempSelectedRows.splice(parentElIndex, 1);
let rootIndex = tempSelectedRows.indexOf(`${row.metadata.rootId}`);
rootIndex != -1 && tempSelectedRows.splice(rootIndex, 1);
row.dependencieRowIds.map((id) => {
let deptRowIndex = tempSelectedRows.indexOf(`${id}`);
deptRowIndex != -1 && tempSelectedRows.splice(deptRowIndex, 1);
});
}
}
function getStyleClassName(row, selectedRowIds, isCellSelected, activeRowId, isCheckbox = false) {
let clsName = null;
if (selectedRowIds.includes(`${row.id}`) || isCellSelected || row.id == activeRowId) {
clsName = isCheckbox ? classes.selectedRowCheckBox : classes.selectedRow;
} else {
if (row.status == FILTER_NAME.DIFFERENT) {
clsName = classes.different;
} else if (row.status == FILTER_NAME.SOURCE_ONLY) {
clsName = classes.source;
} else if (row.status == FILTER_NAME.TARGET_ONLY) {
clsName = classes.target;
} else if (row.status == FILTER_NAME.IDENTICAL) {
clsName = classes.identical;
}
}
return clsName;
}
const columns = [
{
key: 'id',
...SelectColumn,
minWidth: 30,
width: 30,
headerRenderer() {
return (
<InputCheckbox
cid={_.uniqueId('rgc')}
className={classes.headerSelectCell}
value={selectedRows.length == allRowIds.length ? rootSelection : false}
size='small'
onChange={(e) => {
if (e.target.checked) {
setRootSelection(true);
setSelectedRows([...allRowIds]);
selectedRowIds([...allRowIds]);
} else {
setRootSelection(false);
setSelectedRows([]);
selectedRowIds([]);
}
}
}
></InputCheckbox>
);
},
formatter({ row, isCellSelected }) {
isCellSelected && setActiveRow(row.id);
return (
<Box className={!row?.children && clsx(getStyleClassName(row, selectedRows, isCellSelected, activeRow, true), classes.selChBox)}>
<InputCheckbox
className={classes.selectCell}
cid={`${row.id}`}
value={selectedRows.includes(`${row.id}`)}
size='small'
onChange={(e) => {
setSelectedRows((prev) => {
let tempSelectedRows = [...prev];
if (!prev.includes(e.target.id)) {
selectedResultRows(row, tempSelectedRows);
tempSelectedRows.length === allRowIds.length && setRootSelection(true);
} else {
deselectResultRows(row, tempSelectedRows);
}
tempSelectedRows = new Set(tempSelectedRows);
selectedRowIds([...tempSelectedRows]);
return [...tempSelectedRows];
});
}
}
></InputCheckbox>
</Box>
);
}
},
{
key: 'label',
name: 'Objects',
width: '80%',
colSpan(args) {
if (args.type === 'ROW' && 'children' in args.row) {
return 2;
}
return 1;
},
formatter({ row, isCellSelected }) {
const hasChildren = row.children !== undefined;
isCellSelected && setActiveRow(row.id);
return (
<>
{hasChildren && (
<CellExpanderFormatter
row={row}
isCellSelected={isCellSelected}
expanded={row.isExpanded === true}
filterParams={filterParams}
onCellExpand={() => dispatch({ id: row.id, type: 'toggleSubRow', filterParams: filterParams, gridData: gridData, selectedRows: selectedRows })}
classes={classes}
/>
)}
<div className="rdg-cell-value">
{!hasChildren && (
<Box className={clsx(getStyleClassName(row, selectedRows, isCellSelected, activeRow), classes.status)}>
<span className={clsx(classes.recordRow, row.icon)}></span>
{row.label}
</Box>
)}
</div>
</>
);
}
},
{
key: 'status',
name: 'Comparison Result',
formatter({ row, isCellSelected }) {
isCellSelected && setActiveRow(row.id);
return (
<Box className={getStyleClassName(row, selectedRows, isCellSelected, activeRow)}>
{row.status}
</Box>
);
}
},
];
useEffect(() => {
let tempRows = gridData;
tempRows.map((row) => {
dispatch({ id: row.id, type: 'applyFilter', filterParams: filterParams, gridData: gridData, selectedRows: selectedRows });
});
}, [filterParams]);
const eventBus = useContext(SchemaDiffEventsContext);
const rowSelectionTimeoutRef = useRef();
const rowSelection = (rowIdx) => {
if (rowSelectionTimeoutRef.current) {
clearTimeout(rowSelectionTimeoutRef.current);
rowSelectionTimeoutRef.current = null;
}
rowSelectionTimeoutRef.current = setTimeout(()=> {
rowSelectionTimeoutRef.current = null;
const row = rows[rowIdx];
if (row.ddlData != undefined && row.status != FILTER_NAME.IDENTICAL) {
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, row.ddlData);
} else if (row.status == FILTER_NAME.IDENTICAL) {
let url_params = {
'trans_id': transId,
'source_sid': sourceData.sid,
'source_did': sourceData.did,
'source_scid': row.source_scid,
'target_sid': targetData.sid,
'target_did': targetData.did,
'target_scid': row.target_scid,
'comp_status': row.status,
'source_oid': row.source_oid,
'target_oid': row.target_oid,
'node_type': row.itemType,
};
let baseUrl = url_for('schema_diff.ddl_compare', url_params);
schemaDiffToolContext.api.get(baseUrl).then((res) => {
row.ddlData = {
'SQLdiff': res.data.diff_ddl,
'sourceSQL': res.data.source_ddl,
'targetSQL': res.data.target_ddl
};
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, row.ddlData);
}).catch((err) => {
Notifier.alert(err.message);
});
} else {
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, {});
}
}, 250);
};
function rowKeyGetter(row) {
return row.id;
}
return (
<Box className={classes.root} flexGrow="1" minHeight="0" id="schema-diff-grid">
{
gridData ?
<PgReactDataGrid
id="schema-diff-result-grid"
columns={columns} rows={rows}
className={clsx('big-grid', classes.gridPanel, classes.grid)}
treeDepth={2}
enableRowSelect={true}
defaultColumnOptions={{
resizable: true
}}
headerRowHeight={28}
rowHeight={28}
onItemSelect={rowSelection}
enableCellSelect={false}
rowKeyGetter={rowKeyGetter}
direction={'vertical-lr'}
noRowsText={gettext('No difference found')}
noRowsIcon={<InfoIcon className={classes.noRowsIcon} />}
/>
:
<>
{gettext('Loading result grid...')}
</>
}
</Box>
);
}
ResultGridComponent.propTypes = {
gridData: PropTypes.array,
allRowIds: PropTypes.array,
filterParams: PropTypes.array,
selectedRowIds: PropTypes.func,
transId: PropTypes.number,
sourceData: PropTypes.object,
targetData: PropTypes.object,
'sourceData.sid': PropTypes.number,
'sourceData.did': PropTypes.number,
'targetData.sid': PropTypes.number,
'targetData.did': PropTypes.number,
};