///////////////////////////////////////////////////////////// // // 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': { 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 (
{expanded ? : } {row.label} { 'identicalCount' in row ? { filterParams.includes(FILTER_NAME.IDENTICAL) && <>{FILTER_NAME.IDENTICAL}: {row.identicalCount} } { filterParams.includes(FILTER_NAME.DIFFERENT) && <>{FILTER_NAME.DIFFERENT}: {row.differentCount} } { filterParams.includes(FILTER_NAME.SOURCE_ONLY) && <>{FILTER_NAME.SOURCE_ONLY}: {row.sourceOnlyCount} } { filterParams.includes(FILTER_NAME.TARGET_ONLY) && <>{FILTER_NAME.TARGET_ONLY}: {row.targetOnlyCount} } : null }
); } 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 ( { if (e.target.checked) { setRootSelection(true); setSelectedRows([...allRowIds]); selectedRowIds([...allRowIds]); } else { setRootSelection(false); setSelectedRows([]); selectedRowIds([]); } } } > ); }, formatter({ row, isCellSelected }) { isCellSelected && setActiveRow(row.id); return ( { 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]; }); } } > ); } }, { 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 && ( dispatch({ id: row.id, type: 'toggleSubRow', filterParams: filterParams, gridData: gridData, selectedRows: selectedRows })} classes={classes} /> )}
{!hasChildren && ( {row.label} )}
); } }, { key: 'status', name: 'Comparison Result', formatter({ row, isCellSelected }) { isCellSelected && setActiveRow(row.id); return ( {row.status} ); } }, ]; 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 ( { gridData ? } /> : <> {gettext('Loading result grid...')} } ); } 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, };