1) Port schema diff to React. Fixes #6133

2) Remove SlickGrid.
This commit is contained in:
Nikhil Mohite
2022-09-07 19:20:03 +05:30
committed by Akshay Joshi
parent ad59380676
commit e1942d8c9e
78 changed files with 2794 additions and 7888 deletions

View File

@@ -129,18 +129,7 @@ def panel(trans_id, editor_title):
trans_id=trans_id,
requirejs=True,
basejs=True,
editor_title=editor_title
)
@blueprint.route("/schema_diff.js")
@login_required
def script():
"""render the required javascript"""
return Response(
response=render_template("schema_diff/js/schema_diff.js", _=gettext),
status=200,
mimetype=MIMETYPE_APP_JS
editor_title=editor_title,
)

View File

@@ -1,155 +0,0 @@
.icon-compare:before {
font-size: 1.3em !important;
}
.icon-script {
display: inline-block;
align-content: center;
vertical-align: middle;
height: 18px;
width: 18px;
background-size: 20px !important;
background-repeat: no-repeat;
background-position-x: center;
background-position-y: center;
background-image: url('../img/script.svg') !important;
}
.really-hidden {
display: none !important;
}
#schema-diff-header {
padding: 0.75rem 0.7rem;
}
#schema-diff-header .control-label {
width: 120px !important;
padding: 5px 5px !important;
}
#schema-diff-grid .slick-header-column.ui-state-default {
height: 32px !important;
line-height: 25px !important;
}
#schema-diff-grid .grid-header label {
display: inline-block;
font-weight: bold;
margin: auto auto auto 6px;
}
.grid-header .ui-icon {
margin: 4px 4px auto 6px;
background-color: transparent;
border-color: transparent;
}
.slick-row .cell-actions {
text-align: left;
}
/* Slick.Editors.Text, Slick.Editors.Date */
#schema-diff-grid .slick-header > input.editor-text {
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: transparent;
outline: 0;
padding: 0;
}
/* Slick.Editors.Checkbox */
#schema-diff-grid .slick-header > input.editor-checkbox {
margin: 0;
height: 100%;
padding: 0;
border: 0;
}
.slick-row.selected .cell-selection {
background-color: transparent; /* show default selected row background */
}
#schema-diff-grid .slick-header .ui-state-default,
#schema-diff-grid .slick-header .ui-widget-content.ui-state-default,
#schema-diff-grid .slick-header .ui-widget-header .ui-state-default {
background: none;
}
#schema-diff-grid .slick-header .slick-header-column {
font-weight: bold;
display: block;
}
.slick-group-toggle.collapsed, .slick-group-toggle.expanded {
background: none !important;
width: 20px;
}
.slick-group-toggle {
margin-right: 0px !important;
height: 11px !important;
}
#schema-diff-ddl-comp .badge .caret {
display: inline-block;
margin-left: 2px;
margin-right: 4px;
width: 0.7rem;
}
#schema-diff-ddl-comp .badge {
font-size: inherit;
padding: 7px;
}
#schema-diff-ddl-comp .accordian-group {
padding: 0px;
}
#ddl_comp_fetching_data.pg-sp-container {
height: 100%;
bottom: 10px;
.pg-sp-content {
position: absolute;
width: 100%;
}
}
.ddl-copy {
z-index: 10;
position: absolute;
right: 1px;
top: 1px;
}
#schema-diff-grid .pg-panel-message {
font-size: 0.875rem;
}
#schema-diff-ddl-comp .sql_field_layout {
overflow: auto !important;
height: 100%;
}
#schema-diff-ddl-comp .source_ddl, #schema-diff-ddl-comp .target_ddl, #schema-diff-ddl-comp .diff_ddl {
height: 300px;
overflow: hidden;
}
.target-buttons {
flex-wrap: wrap;
max-width: 40% !important;
}
.slick-cell .ml-2 {
margin-left: 2rem !important;
}
.slick-cell .ml-3 {
margin-left: 3rem !important;
}

View File

@@ -0,0 +1,65 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
export const PANELS = {
SCHEMADIFF: 'id-schema-diff',
RESULTS: 'id-results',
};
export const TYPE = {
SOURCE: 1,
TARGET: 2
};
export const MENUS = {
COMPARE: 'schema-diff-compare',
GENERATE_SCRIPT: 'schema-diff-generate-script',
FILTER: 'schema-diff-filter'
};
export const STYLE_CONSTANT = {
IDENTICAL: 'identical',
DIFFERENT :'different',
SOURCE_ONLY: 'source',
TARGET_ONLY: 'target'
};
export const MENUS_COMPARE_CONSTANT = {
COMPARE_IGNORE_OWNER: 1,
COMPARE_IGNORE_WHITESPACE: 2
};
export const MENUS_FILTER_CONSTANT = {
FILTER_IDENTICAL: 1,
FILTER_DIFFERENT: 2,
FILTER_SOURCE_ONLY: 3,
FILTER_TARGET_ONLY: 4,
};
export const SCHEMA_DIFF_EVENT = {
TRIGGER_SELECT_SERVER: 'TRIGGER_SELECT_SERVER',
TRIGGER_SELECT_DATABASE: 'TRIGGER_SELECT_DATABASE',
TRIGGER_SELECT_SCHEMA: 'TRIGGER_SELECT_DATABASE',
TRIGGER_COMPARE_DIFF: 'TRIGGER_COMPARE_DIFF',
TRIGGER_GENERATE_SCRIPT: 'TRIGGER_GENERATE_SCRIPT',
TRIGGER_CHANGE_FILTER: 'TRIGGER_CHANGE_FILTER',
TRIGGER_CHANGE_RESULT_SQL: 'TRIGGER_CHANGE_RESULT_SQL',
TRIGGER_ROW_SELECT: 'TRIGGER_ROW_SELECT',
};
export const FILTER_NAME = {
IDENTICAL : gettext('Identical'),
DIFFERENT : gettext('Different'),
SOURCE_ONLY: gettext('Source Only'),
TARGET_ONLY: gettext('Target Only')
};

View File

@@ -0,0 +1,165 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import ReactDOM from 'react-dom';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import pgWindow from 'sources/window';
import { registerDetachEvent } from 'sources/utils';
import { _set_dynamic_tab } from '../../../sqleditor/static/js/show_query_tool';
import getApiInstance from '../../../../static/js/api_instance';
import Theme from '../../../../static/js/Theme';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Notify from '../../../../static/js/helpers/Notifier';
import SchemaDiffComponent from './components/SchemaDiffComponent';
import { showRenamePanel } from '../../../../static/js/Dialogs';
export default class SchemaDiff {
static instance;
static getInstance(...args) {
if (!SchemaDiff.instance) {
SchemaDiff.instance = new SchemaDiff(...args);
}
return SchemaDiff.instance;
}
constructor(pgAdmin, pgBrowser) {
this.pgAdmin = pgAdmin;
this.pgBrowser = pgBrowser;
this.wcDocker = window.wcDocker;
this.api = getApiInstance();
}
init() {
let self = this;
if (self.initialized)
return;
self.initialized = true;
// Define the nodes on which the menus to be appear
self.pgBrowser.add_menus([{
name: 'schema_diff',
module: self,
applies: ['tools'],
callback: 'showSchemaDiffTool',
priority: 1,
label: gettext('Schema Diff'),
enable: true,
below: true,
}]);
/* Create and load the new frame required for schema diff panel */
self.frame = new self.pgBrowser.Frame({
name: 'frm_schemadiff',
title: gettext('Schema Diff'),
showTitle: true,
isCloseable: true,
isRenamable: true,
isPrivate: true,
icon: 'pg-font-icon icon-compare',
url: 'about:blank',
});
/* Cache may take time to load for the first time. Keep trying till available */
let cacheIntervalId = setInterval(function () {
if (self.pgBrowser.preference_version() > 0) {
self.preferences = self.pgBrowser.get_preferences_for_module('schema_diff');
clearInterval(cacheIntervalId);
}
}, 0);
self.pgBrowser.onPreferencesChange('schema_diff', function () {
self.preferences = self.pgBrowser.get_preferences_for_module('schema_diff');
});
self.frame.load(self.pgBrowser.docker);
}
showSchemaDiffTool() {
let self = this;
self.api({
url: url_for('schema_diff.initialize', null),
method: 'GET',
})
.then(function (res) {
self.trans_id = res.data.data.schemaDiffTransId;
res.data.data.panel_title = gettext('Schema Diff');
self.launchSchemaDiff(res.data.data);
})
.catch(function (error) {
Notify.error(gettext(`Error in schema diff initialize ${error.response.data}`));
});
}
launchSchemaDiff(data) {
let self = this;
let panelTitle = data.panel_title,
trans_id = data.schemaDiffTransId,
panelTooltip = '';
let url_params = {
'trans_id': trans_id,
'editor_title': panelTitle,
},
baseUrl = url_for('schema_diff.panel', url_params);
let browserPreferences = this.pgBrowser.get_preferences_for_module('browser');
let openInNewTab = browserPreferences.new_browser_tab_open;
if (openInNewTab && openInNewTab.includes('schema_diff')) {
window.open(baseUrl, '_blank');
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function () {
this.pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
this.pgBrowser.Events.once(
'pgadmin-browser:frame:urlloaded:frm_schemadiff',
function (frame) {
frame.openURL(baseUrl);
});
let propertiesPanel = this.pgBrowser.docker.findPanels('properties'),
schemaDiffPanel = this.pgBrowser.docker.addPanel('frm_schemadiff', this.wcDocker.DOCK.STACKED, propertiesPanel[0]);
registerDetachEvent(schemaDiffPanel);
// Panel Rename event
schemaDiffPanel.on(self.wcDocker.EVENT.RENAME, function (panel_data) {
self.panel_rename_event(panel_data, schemaDiffPanel, browserPreferences);
});
_set_dynamic_tab(this.pgBrowser, browserPreferences['dynamic_tabs']);
// Set panel title and icon
schemaDiffPanel.title('<span title="' + panelTooltip + '">' + panelTitle + '</span>');
schemaDiffPanel.icon('pg-font-icon icon-compare');
schemaDiffPanel.focus();
}
}
panel_rename_event(panel_data, panel) {
showRenamePanel(panel_data.$titleText[0].textContent, null, panel);
}
load(container, trans_id) {
ReactDOM.render(
<Theme>
<ModalProvider>
<SchemaDiffComponent params={{ transId: trans_id, pgAdmin: pgWindow.pgAdmin }}></SchemaDiffComponent>
</ModalProvider>
</Theme>,
container
);
}
}

View File

@@ -0,0 +1,143 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { useContext, useState } from 'react';
import { Box, Grid, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/styles';
import { InputSelect } from '../../../../../static/js/components/FormComponents';
import { SchemaDiffEventsContext } from './SchemaDiffComponent';
import { SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
const useStyles = makeStyles(() => ({
root: {
padding: '0rem'
},
spanLabel: {
alignSelf: 'center',
marginRight: '4px',
},
inputLabel: {
padding: '0.3rem',
},
}));
export function InputComponent({ label, serverList, databaseList, schemaList, diff_type, selectedSid = null, selectedDid=null, selectedScid=null }) {
const classes = useStyles();
const [selectedServer, setSelectedServer] = useState(selectedSid);
const [selectedDatabase, setSelectedDatabase] = useState(selectedDid);
const [selectedSchema, setSelectedSchema] = useState(selectedScid);
const eventBus = useContext(SchemaDiffEventsContext);
const [disableDBSelection, setDisableDBSelection] = useState(selectedSid == null ? true : false);
const [disableSchemaSelection, setDisableSchemaSelection] = useState(selectedDid == null ? true : false);
const changeServer = (selectedOption) => {
setDisableDBSelection(false);
setSelectedServer(selectedOption);
// Reset the Database selection if user deselect server from DD
if(selectedOption == null){
setSelectedDatabase(null);
setDisableDBSelection(true);
setSelectedSchema(null);
setDisableSchemaSelection(true);
}
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SERVER, { selectedOption, diff_type, serverList });
};
const changeDatabase = (selectedDB) => {
setSelectedDatabase(selectedDB);
setDisableSchemaSelection(false);
// Reset the Schema selection if user deselect database from DD
if(selectedDB == null){
setSelectedSchema(null);
setDisableSchemaSelection(true);
}
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_SELECT_DATABASE, {selectedServer, selectedDB, diff_type, databaseList});
};
const changeSchema = (selectedSC) => {
setSelectedSchema(selectedSC);
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SCHEMA, { selectedSC, diff_type });
};
return (
<Box className={classes.root}>
<Grid
container
direction="row"
alignItems="center"
key={_.uniqueId('c')}
>
<Grid item lg={2} md={2} sm={2} xs={2} className={classes.inputLabel} key={_.uniqueId('c')}>
<Typography id={label}>{label}</Typography>
</Grid>
<Grid item lg={4} md={4} sm={4} xs={4} className={classes.inputLabel} key={_.uniqueId('c')}>
<InputSelect
options={serverList}
onChange={changeServer}
value={selectedServer}
controlProps={
{
placeholder: 'Select server...'
}
}
key={'server_' + diff_type}
></InputSelect>
</Grid>
<Grid item lg={3} md={3} sm={3} xs={3} className={classes.inputLabel} key={_.uniqueId('c')}>
<InputSelect
options={databaseList}
onChange={changeDatabase}
value={selectedDatabase}
controlProps={
{
placeholder: 'Select Database...'
}
}
key={'database_' + diff_type}
readonly={disableDBSelection}
></InputSelect>
</Grid>
<Grid item lg={3} md={3} sm={3} xs={3} className={classes.inputLabel} key={_.uniqueId('c')}>
<InputSelect
options={schemaList}
onChange={changeSchema}
value={selectedSchema}
controlProps={
{
placeholder: 'Select Schema...'
}
}
key={'schema' + diff_type}
readonly={disableSchemaSelection}
></InputSelect>
</Grid>
</Grid>
</Box >
);
}
InputComponent.propTypes = {
label: PropTypes.string,
serverList: PropTypes.array,
databaseList:PropTypes.array,
schemaList:PropTypes.array,
diff_type:PropTypes.number,
selectedSid: PropTypes.number,
selectedDid: PropTypes.number,
selectedScid:PropTypes.number,
};

View File

@@ -0,0 +1,739 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, 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: {
float: 'inline-end',
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.diffSelectFG,
},
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.palette.primary.light,
},
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
}) {
const classes = useStyles();
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)}>
<span className={classes.countLabel}>{FILTER_NAME.IDENTICAL}:</span> <span className={classes.countStyle}>{row.identicalCount} </span>
<span className={classes.countLabel}>{FILTER_NAME.DIFFERENT}:</span> <span className={classes.countStyle}>{row.differentCount} </span>
<span className={classes.countLabel}>{FILTER_NAME.SOURCE_ONLY}:</span> <span className={classes.countStyle}>{row.sourceOnlyCount} </span>
<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,
};
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;
}
}
function getStyleClassName(row, selectedRowIds, isCellSelected, activeRowId, isCheckbox = false) {
const classes = useStyles();
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;
}
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 i = 0; i < rows.length; i++) {
if (rows[i].id == row.metadata.parentId) {
let isChildInclude = checkAllChildInclude(rows[i], tempSelectedRows);
isChildInclude && tempSelectedRows.push(`${rows[i].metadata.parentId}`);
break;
}
}
}
}
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);
});
}
}
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 })}
/>
)}
<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 rowSelection = (row) => {
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);
});
}
};
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}
onRowClick={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,
};

View File

@@ -0,0 +1,140 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import React, { useContext, useState, useEffect } from 'react';
import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/styles';
import { InputSQL } from '../../../../../static/js/components/FormComponents';
import { SchemaDiffEventsContext } from './SchemaDiffComponent';
import { SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
},
table: {
minWidth: 650,
},
summaryContainer: {
flexGrow: 1,
minHeight: 0,
overflow: 'auto',
},
panelTitle: {
borderBottom: '1px solid ' + theme.otherVars.borderColor,
padding: '0.5rem',
},
editorStyle: {
height: '100%'
},
editor: {
height: '100%',
padding: '0.5rem 0.2rem 2rem 0.5rem',
},
editorLabel: {
padding: '0.3rem 0.6rem 0 0.6rem',
},
header: {
padding: '0.5rem',
borderBottom: '1px solid ' + theme.otherVars.borderColor,
},
sqlContainer: {
display: 'flex',
flexDirection: 'row',
padding: '0rem 0rem 0.5rem',
flexGrow: 1,
overflow: 'hidden'
},
sqldata: {
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
padding: '0.2rem 0.5rem',
width: '33.33%',
},
label: {
flexGrow: 1,
}
}));
export function Results() {
const classes = useStyles();
const [sourceSQL, setSourceSQL] = useState(null);
const [targetSQL, setTargetSQL] = useState(null);
const [sqlDiff, setSqlDiff] = useState(null);
const eventBus = useContext(SchemaDiffEventsContext);
useEffect(() => {
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, triggerUpdateResult);
}, []);
const triggerUpdateResult = (resultData) => {
setSourceSQL(resultData.sourceSQL);
setTargetSQL(resultData.targetSQL);
setSqlDiff(resultData.SQLdiff);
};
return (
<>
<Box className={classes.header}>
<span>{gettext('DDL Comparision')}</span>
</Box>
<Box className={classes.sqlContainer}>
<Box className={classes.sqldata}>
<Box className={classes.label}>{gettext('Source')}</Box>
<InputSQL
onLable={true}
value={sourceSQL}
options={{
readOnly: true,
}}
readonly={true}
/>
</Box>
<Box className={classes.sqldata}>
<Box className={classes.label}>{gettext('Target')}</Box>
<InputSQL
onLable={true}
value={targetSQL}
options={{
readOnly: true,
}}
readonly={true}
/>
</Box>
<Box className={classes.sqldata}>
<Box className={classes.label}>{gettext('Difference')}</Box>
<InputSQL
onLable={true}
value={sqlDiff}
options={{
readOnly: true,
}}
readonly={true}
/>
</Box>
</Box>
</>
);
}
Results.propTypes = {
};

View File

@@ -0,0 +1,223 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { useState, useRef, useContext, useEffect } from 'react';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';
import CompareArrowsRoundedIcon from '@material-ui/icons/CompareArrowsRounded';
import FeaturedPlayListRoundedIcon from '@material-ui/icons/FeaturedPlayListRounded';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import { makeStyles } from '@material-ui/styles';
import { DefaultButton, PgButtonGroup, PgIconButton, PrimaryButton } from '../../../../../static/js/components/Buttons';
import { FilterIcon } from '../../../../../static/js/components/ExternalIcon';
import { PgMenu, PgMenuItem, usePgMenuGroup } from '../../../../../static/js/components/Menu';
import { FILTER_NAME, MENUS, MENUS_COMPARE_CONSTANT, SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
import { SchemaDiffContext, SchemaDiffEventsContext } from './SchemaDiffComponent';
const useStyles = makeStyles((theme) => ({
emptyIcon: {
width: '1.5rem'
},
diff_btn: {
marginRight: '1rem'
},
noactionBtn: {
cursor: 'default',
'&:hover': {
backgroundColor: 'inherit',
cursor: 'default'
}
},
scriptBtn: {
display: 'flex',
justifyContent: 'flex-end',
paddingRight: '0.3rem',
[theme.breakpoints.down('sm')]: {
paddingTop: '0.3rem',
flexGrow: 1,
},
},
filterBtn: {
[theme.breakpoints.down('sm')]: {
paddingTop: '0.3rem',
flexGrow: 1,
}
},
compareBtn: {
display: 'flex',
flexGrow: 1,
justifyContent: 'flex-start',
paddingLeft: '1.5rem',
[theme.breakpoints.down('sm')]: {
paddingTop: '0.3rem',
},
}
}));
export function SchemaDiffButtonComponent({ sourceData, targetData, selectedRowIds, rows, compareParams, filterParams = [FILTER_NAME.DIFFERENT, FILTER_NAME.SOURCE_ONLY, FILTER_NAME.TARGET_ONLY] }) {
const classes = useStyles();
const filterRef = useRef(null);
const compareRef = useRef(null);
const eventBus = useContext(SchemaDiffEventsContext);
const schemaDiffCtx = useContext(SchemaDiffContext);
const [selectedFilters, setSelectedFilters] = useState(filterParams);
const [selectedCompare, setSelectedCompare] = useState([]);
const [isDisableCompare, setIsDisableCompare] = useState(true);
const { openMenuName, toggleMenu, onMenuClose } = usePgMenuGroup();
useEffect(() => {
let isDisableComp = true;
if (sourceData.sid != null && sourceData.did != null && targetData.sid != null && targetData.did != null) {
isDisableComp = false;
}
setIsDisableCompare(isDisableComp);
}, [sourceData, targetData]);
useEffect(() => {
let prefCompareOptions = [];
if (!_.isUndefined(compareParams)) {
compareParams.ignoreOwner && prefCompareOptions.push(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER);
compareParams.ignoreWhitespaces && prefCompareOptions.push(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE);
setSelectedCompare(prefCompareOptions);
} else {
schemaDiffCtx?.preferences_schema_diff?.ignore_owner && prefCompareOptions.push(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER);
schemaDiffCtx?.preferences_schema_diff?.ignore_whitespaces && prefCompareOptions.push(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE);
setSelectedCompare(prefCompareOptions);
}
}, [schemaDiffCtx.preferences_schema_diff]);
const selectFilterOption = (option) => {
let newOptions = [];
setSelectedFilters((prev) => {
let newSelectdOptions = [...prev];
let removeIndex = newSelectdOptions.indexOf(option);
if (prev.includes(option)) {
newSelectdOptions.splice(removeIndex, 1);
} else {
newSelectdOptions.push(option);
}
newOptions = [...newSelectdOptions];
return newSelectdOptions;
});
let filterParam = newOptions;
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_FILTER, { filterParams: filterParam });
};
const selectCompareOption = (option) => {
setSelectedCompare((prev) => {
let newSelectdOptions = [...prev];
let removeIndex = newSelectdOptions.indexOf(option);
if (prev.includes(option)) {
newSelectdOptions.splice(removeIndex, 1);
} else {
newSelectdOptions.push(option);
}
return newSelectdOptions;
});
};
const compareDiff = () => {
let compareParam = {
'ignoreOwner': selectedCompare.includes(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER) ? 1 : 0,
'ignoreWhitespaces': selectedCompare.includes(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE) ? 1 : 0,
};
let filterParam = selectedFilters;
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_COMPARE_DIFF, { sourceData, targetData, compareParams: compareParam, filterParams: filterParam });
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, {
sourceSQL: null,
targetSQL: null,
SQLdiff: null,
});
};
const generateScript = () => {
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_GENERATE_SCRIPT, { sid: targetData.sid, did: targetData.did, selectedIds: selectedRowIds, rows: rows });
};
return (
<>
<Box className={classes.compareBtn}>
<PgButtonGroup size="small" disabled={isDisableCompare}>
<PrimaryButton startIcon={<CompareArrowsRoundedIcon />}
onClick={compareDiff}>{gettext('Compare')}</PrimaryButton>
<PgIconButton title={gettext('Compare')} icon={<KeyboardArrowDownIcon />} color={'primary'} splitButton
name={MENUS.COMPARE} ref={compareRef} onClick={toggleMenu} ></PgIconButton>
</PgButtonGroup>
</Box>
<Box className={classes.scriptBtn}>
<PgButtonGroup size="small" disabled={selectedRowIds?.length > 0 ? false : true}>
<DefaultButton startIcon={<FeaturedPlayListRoundedIcon />} onClick={generateScript}>{gettext('Generate Script')}</DefaultButton>
</PgButtonGroup>
</Box>
<Box className={classes.filterBtn}>
<PgButtonGroup size="small" disabled={isDisableCompare} style={{ paddingRight: '0.3rem' }}>
<DefaultButton startIcon={<FilterIcon />} className={classes.noactionBtn}
>{gettext('Filter')}</DefaultButton>
<PgIconButton title={gettext('File')} icon={<KeyboardArrowDownIcon />} splitButton
name={MENUS.FILTER} ref={filterRef} onClick={toggleMenu} ></PgIconButton>
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={compareRef}
open={openMenuName == MENUS.COMPARE}
onClose={onMenuClose}
label={gettext('Compare')}
>
<PgMenuItem onClick={() => { selectCompareOption(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER); }}>
{selectedCompare.includes(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>}{gettext('Ignore owner')}
</PgMenuItem>
<PgMenuItem onClick={() => { selectCompareOption(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE); }}>
{selectedCompare.includes(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>}{gettext('Ignore whitespace')}
</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={filterRef}
open={openMenuName == MENUS.FILTER}
onClose={onMenuClose}
label={gettext('Filter')}
>
<PgMenuItem onClick={() => { selectFilterOption(FILTER_NAME.IDENTICAL); }}>
{selectedFilters.includes(FILTER_NAME.IDENTICAL) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>} {gettext(FILTER_NAME.IDENTICAL)}
</PgMenuItem>
<PgMenuItem onClick={() => { selectFilterOption(FILTER_NAME.DIFFERENT); }}>
{selectedFilters.includes(FILTER_NAME.DIFFERENT) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>} {gettext(FILTER_NAME.DIFFERENT)}
</PgMenuItem>
<PgMenuItem onClick={() => { selectFilterOption(FILTER_NAME.SOURCE_ONLY); }}>
{selectedFilters.includes(FILTER_NAME.SOURCE_ONLY) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>} {gettext(FILTER_NAME.SOURCE_ONLY)}
</PgMenuItem>
<PgMenuItem onClick={() => { selectFilterOption(FILTER_NAME.TARGET_ONLY); }}>
{selectedFilters.includes(FILTER_NAME.TARGET_ONLY) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>} {gettext(FILTER_NAME.TARGET_ONLY)}
</PgMenuItem>
</PgMenu>
</>
);
}
SchemaDiffButtonComponent.propTypes = {
sourceData: PropTypes.object,
targetData: PropTypes.object,
selectedRowIds: PropTypes.array,
rows: PropTypes.array,
compareParams: PropTypes.object,
filterParams: PropTypes.array
};

View File

@@ -0,0 +1,812 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
import { Box, Grid } from '@material-ui/core';
import InfoRoundedIcon from '@material-ui/icons/InfoRounded';
import HelpIcon from '@material-ui/icons/HelpRounded';
import { makeStyles } from '@material-ui/styles';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import Loader from 'sources/components/Loader';
import pgWindow from 'sources/window';
import { PgButtonGroup, PgIconButton } from '../../../../../static/js/components/Buttons';
import Notifier from '../../../../../static/js/helpers/Notifier';
import ConnectServerContent from '../../../../../static/js/Dialogs/ConnectServerContent';
import { generateScript } from '../../../../sqleditor/static/js/show_query_tool';
import { FILTER_NAME, SCHEMA_DIFF_EVENT, TYPE } from '../SchemaDiffConstants';
import { InputComponent } from './InputComponent';
import { SchemaDiffButtonComponent } from './SchemaDiffButtonComponent';
import { SchemaDiffContext, SchemaDiffEventsContext } from './SchemaDiffComponent';
import { ResultGridComponent } from './ResultGridComponent';
const useStyles = makeStyles(() => ({
table: {
minWidth: 650,
},
summaryContainer: {
flexGrow: 1,
minHeight: 0,
overflow: 'auto',
},
note: {
marginTop: '1.2rem',
textAlign: 'center',
},
helpBtn: {
display: 'flex',
flexDirection: 'row-reverse',
paddingRight: '0.3rem'
},
compareComp: {
flexGrow: 1,
},
diffBtn: {
display: 'flex',
justifyContent: 'flex-end'
}
}));
function generateFinalScript(script_array, scriptHeader, script_body) {
_.each(Object.keys(script_array).reverse(), function (s) {
if (script_array[s].length > 0) {
script_body += script_array[s].join('\n') + '\n\n';
}
});
return `${scriptHeader} BEGIN; \n ${script_body} END;`;
}
function checkAndGetSchemaQuery(data, script_array) {
/* Check whether the selected object belongs to source only schema
if yes then we will have to add create schema statement before creating any other object.*/
if (!_.isUndefined(data.source_schema_name) && !_.isNull(data.source_schema_name)) {
let schema_query = '\nCREATE SCHEMA IF NOT EXISTS ' + data.source_schema_name + ';\n';
if (script_array[data.dependLevel].indexOf(schema_query) == -1) {
script_array[data.dependLevel].push(schema_query);
}
}
}
function getGenerateScriptData(rows, selectedIds, script_array) {
for (let selRowVal of rows) {
if (selectedIds.includes(`${selRowVal.id}`)) {
let data = selRowVal;
if (!_.isUndefined(data.diff_ddl)) {
if (!(data.dependLevel in script_array)) script_array[data.dependLevel] = [];
checkAndGetSchemaQuery(data, script_array);
script_array[data.dependLevel].push(data.diff_ddl);
}
}
}
}
function raiseErrorOnFail(alertTitle, xhr) {
try {
if (_.isUndefined(xhr.response.data)) {
Notifier.alert(alertTitle, gettext('Unable to get the response text.'));
} else {
let err = JSON.parse(xhr.response.data);
Notifier.alert(alertTitle, err.errormsg);
}
} catch (e) {
Notifier.alert(alertTitle, gettext(e.message));
}
}
const onHelpClick=()=>{
let url = url_for('help.static', {'filename': 'schema_diff.html'});
window.open(url, 'pgadmin_help');
};
export function SchemaDiffCompare({ params }) {
const classes = useStyles();
const schemaDiffToolContext = useContext(SchemaDiffContext);
const eventBus = useContext(SchemaDiffEventsContext);
const [showResultGrid, setShowResultGrid] = useState(false);
const [selectedSourceSid, setSelectedSourceSid] = useState(null);
const [selectedTargetSid, setSelectedTargetSid] = useState(null);
const [sourceDatabaseList, setSourceDatabaseList] = useState([]);
const [targetDatabaseList, setTargetDatabaseList] = useState([]);
const [selectedSourceDid, setSelectedSourceDid] = useState(null);
const [selectedTargetDid, setSelectedTargetDid] = useState(null);
const [sourceSchemaList, setSourceSchemaList] = useState([]);
const [targetSchemaList, setTargetSchemaList] = useState([]);
const [selectedSourceScid, setSelectedSourceScid] = useState(null);
const [selectedTargetScid, setSelectedTargetScid] = useState(null);
const [sourceGroupServerList, setSourceGroupServerList] = useState([]);
const [gridData, setGridData] = useState([]);
const [allRowIdList, setAllRowIdList] = useState([]);
const [filterOptions, setFilterOptions] = useState([]);
const [compareOptions, setCompareOptions] = useState(undefined);
const [selectedRowIds, setSelectedRowIds] = useState([]);
const [loaderText, setLoaderText] = useState(null);
const [apiResult, setApiResult] = useState([]);
const [rowDep, setRowDep] = useState({});
const [isInit, setIsInit] = useState(true);
useEffect(() => {
schemaDiffToolContext.api.get(url_for('schema_diff.servers')).then((res) => {
let groupedOptions = [];
_.forIn(res.data.data, (val, _key) => {
if (val.lenght == 0) {
return;
}
groupedOptions.push({
label: _key,
options: val
});
});
setSourceGroupServerList(groupedOptions);
}).catch((err) => {
Notifier.alert(err.message);
});
}, []);
useEffect(() => {
// Register all eventes for debugger.
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SERVER, triggerSelectServer);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_SELECT_DATABASE, triggerSelectDatabase);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SCHEMA, triggerSelectSchema);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_COMPARE_DIFF, triggerCompareDiff);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_FILTER, triggerChangeFilter);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_GENERATE_SCRIPT, triggerGenerateScript);
}, []);
function checkAndSetSourceData(diff_type, selectedOption) {
if(selectedOption == null) {
setSelectedRowIds([]);
setGridData([]);
if (diff_type == TYPE.SOURCE) {
setSelectedSourceSid(null);
setSelectedSourceDid(null);
setSelectedSourceScid(null);
} else {
setSelectedTargetSid(null);
setSelectedTargetDid(null);
setSelectedTargetScid(null);
}
}
}
function setSourceTargetSid(diff_type, selectedOption) {
if (diff_type == TYPE.SOURCE) {
setSelectedSourceSid(selectedOption);
} else {
setSelectedTargetSid(selectedOption);
}
}
const triggerSelectServer = ({ selectedOption, diff_type, serverList }) => {
checkAndSetSourceData(diff_type, selectedOption);
for (const group of serverList) {
for (const opt of group.options) {
if (opt.value == selectedOption) {
if (!opt.connected) {
connectServer(selectedOption, diff_type, null, serverList);
break;
} else {
setSourceTargetSid(diff_type, selectedOption);
getDatabaseList(selectedOption, diff_type);
}
}
}
}
setSourceGroupServerList(serverList);
};
const triggerSelectDatabase = ({ selectedServer, selectedDB, diff_type, databaseList }) => {
if(selectedDB == null) {
setGridData([]);
}
if (databaseList) {
for (const opt of databaseList) {
if (opt.value == selectedDB) {
if (!opt.connected) {
connectDatabase(selectedServer, selectedDB, diff_type, databaseList);
break;
} else {
getSchemaList(selectedServer, selectedDB, diff_type);
break;
}
}
}
if (diff_type == TYPE.SOURCE) {
setSelectedSourceDid(selectedDB);
setSourceDatabaseList(databaseList);
} else {
setSelectedTargetDid(selectedDB);
setTargetDatabaseList(databaseList);
}
}
};
const triggerSelectSchema = ({ selectedSC, diff_type }) => {
if (diff_type == TYPE.SOURCE) {
setSelectedSourceScid(selectedSC);
} else {
setSelectedTargetScid(selectedSC);
}
};
const triggerCompareDiff = ({ sourceData, targetData, compareParams, filterParams }) => {
setGridData([]);
setIsInit(false);
if (JSON.stringify(sourceData) === JSON.stringify(targetData)) {
Notifier.alert(gettext('Selection Error'),
gettext('Please select the different source and target.'));
} else {
getCompareStatus();
let schemaDiffPollInterval = setInterval(getCompareStatus, 1000);
setLoaderText('Comparing objects... (this may take a few minutes)...');
let url_params = {
'trans_id': params.transId,
'source_sid': sourceData['sid'],
'source_did': sourceData['did'],
'target_sid': targetData['sid'],
'target_did': targetData['did'],
'ignore_owner': compareParams['ignoreOwner'],
'ignore_whitespaces': compareParams['ignoreWhitespaces'],
};
let baseUrl = url_for('schema_diff.compare_database', url_params);
if (sourceData['scid'] != null && targetData['scid'] != null) {
url_params['source_scid'] = sourceData['scid'];
url_params['target_scid'] = targetData['scid'];
baseUrl = url_for('schema_diff.compare_schema', url_params);
}
setCompareOptions(compareParams);
schemaDiffToolContext.api.get(baseUrl).then((res) => {
setShowResultGrid(true);
setLoaderText(null);
clearInterval(schemaDiffPollInterval);
setFilterOptions(filterParams);
getResultGridData(res.data.data, filterParams);
}).catch((err) => {
setLoaderText(null);
setShowResultGrid(false);
Notifier.error(gettext(err.message));
});
}
};
const triggerChangeFilter = ({ filterParams }) => {
setFilterOptions(filterParams);
};
const triggerGenerateScript = ({ sid, did, selectedIds, rows }) => {
setLoaderText(gettext('Generating script...'));
let generatedScript = undefined, scriptHeader;
scriptHeader = gettext('-- This script was generated by the Schema Diff utility in pgAdmin 4. \n');
scriptHeader += gettext('-- For the circular dependencies, the order in which Schema Diff writes the objects is not very sophisticated \n');
scriptHeader += gettext('-- and may require manual changes to the script to ensure changes are applied in the correct order.\n');
scriptHeader += gettext('-- Please report an issue for any failure with the reproduction steps. \n');
if (selectedIds.length > 0) {
let script_array = { 1: [], 2: [], 3: [], 4: [], 5: [] },
script_body = '';
getGenerateScriptData(rows, selectedIds, script_array);
generatedScript = generateFinalScript(script_array, scriptHeader, script_body);
openQueryTool({ sid: sid, did: did, generatedScript: generatedScript, scriptHeader: scriptHeader });
} else {
openQueryTool({ sid: sid, did: did, scriptHeader: scriptHeader });
}
};
function openQueryTool({ sid, did, generatedScript, scriptHeader }) {
let baseServerUrl = url_for('schema_diff.get_server', { 'sid': sid, 'did': did });
schemaDiffToolContext.api({
url: baseServerUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.then(function (res) {
let data = res.data.data;
let serverData = {};
if (data) {
let sqlId = `schema${params.transId}`;
serverData['sgid'] = data.gid;
serverData['sid'] = data.sid;
serverData['stype'] = data.type;
serverData['server'] = data.name;
serverData['user'] = data.user;
serverData['did'] = did;
serverData['database'] = data.database;
serverData['sql_id'] = sqlId;
if (_.isUndefined(generatedScript)) {
generatedScript = scriptHeader + 'BEGIN;' + '\n' + '' + '\n' + 'END;';
}
localStorage.setItem(sqlId, generatedScript);
generateScript(serverData, pgWindow.pgAdmin.Tools.SQLEditor);
setLoaderText(null);
}
})
.catch(function (xhr) {
setLoaderText(null);
raiseErrorOnFail(gettext('Generate script error'), xhr);
});
}
function generateGridData(record, tempData, allRowIds, filterParams) {
if (record.group_name in tempData && record.label in tempData[record.group_name]['children']) {
let chidId = record.id;
allRowIds.push(`${chidId}`);
tempData[record.group_name]['children'][record.label]['children'].push({
'id': chidId,
'parentId': tempData[record.group_name]['children'][record.label].id,
'label': record.title,
'status': record.status,
'isVisible': filterParams.includes(record.status) ? true : false,
'icon': `icon-${record.type}`,
'isExpanded': false,
'selected': false,
'oid': record.oid,
'itemType': record.type,
'source_oid': record.source_oid,
'target_oid': record.target_oid,
'source_scid': record.source_scid,
'target_scid': record.target_scid,
'dependenciesOid': record.dependencies.map(({ oid }) => oid),
'dependencies': record.dependencies,
'dependencieRowIds': [],
'ddlData': {
'SQLdiff': record.diff_ddl,
'sourceSQL': record.source_ddl,
'targetSQL': record.target_ddl
}
});
} else if (record.group_name in tempData) {
let chidId = Math.floor(Math.random() * 1000000);
allRowIds.push(`${chidId}`);
let subChildId = record.id;
allRowIds.push(`${subChildId}`);
tempData[record.group_name]['children'][record.label] = {
'id': chidId,
'parentId': tempData[record.group_name]['id'],
'label': record.label,
'identicalCount': 0,
'differentCount': 0,
'sourceOnlyCount': 0,
'targetOnlyCount': 0,
'icon': `icon-coll-${record.type}`,
'isExpanded': false,
'selected': false,
'children': [{
'id': subChildId,
'parentId': chidId,
'label': record.title,
'status': record.status,
'isVisible': filterParams.includes(record.status) ? true : false,
'icon': `icon-${record.type}`,
'isExpanded': false,
'selected': false,
'oid': record.oid,
'itemType': record.type,
'source_oid': record.source_oid,
'target_oid': record.target_oid,
'source_scid': record.source_scid,
'target_scid': record.target_scid,
'dependenciesOid': record.dependencies.map(({ oid }) => oid),
'dependencies': record.dependencies,
'dependencieRowIds': [],
'ddlData': {
'SQLdiff': record.diff_ddl,
'sourceSQL': record.source_ddl,
'targetSQL': record.target_ddl
}
}]
};
} else {
let label = record.label;
let _id = Math.floor(Math.random() * 100000);
let _subChildId = Math.floor(Math.random() * 100000);
allRowIds.push(`${_id}`);
allRowIds.push(`${_subChildId}`);
tempData[record.group_name] = {
'id': _id,
'label': record.group_name,
'icon': record.group_name == 'Database Objects' ? 'icon-coll-database' : 'icon-schema',
'groupType': record.group_name,
'isExpanded': false,
'selected': false,
'children': {}
};
let ch_id = record.id;
allRowIds.push(`${ch_id}`);
tempData[record.group_name]['children'][label] = {
'id': _subChildId,
'parentId': _id,
'label': record.label,
'identicalCount': 0,
'differentCount': 0,
'sourceOnlyCount': 0,
'targetOnlyCount': 0,
'selected': false,
'icon': `icon-coll-${record.type}`,
'isExpanded': false,
'children': [{
'id': ch_id,
'parentId': _subChildId,
'label': record.title,
'status': record.status,
'selected': false,
'itemType': record.type,
'isVisible': filterParams.includes(record.status) ? true : false,
'icon': `icon-${record.type}`,
'isExpanded': false,
'oid': record.oid,
'source_oid': record.source_oid,
'target_oid': record.target_oid,
'source_scid': record.source_scid,
'target_scid': record.target_scid,
'dependenciesOid': record.dependencies.map(({ oid }) => oid),
'dependencies': record.dependencies,
'dependencieRowIds': [],
'ddlData': {
'SQLdiff': record.diff_ddl,
'sourceSQL': record.source_ddl,
'targetSQL': record.target_ddl
}
}]
};
}
}
function getResultGridData(gridDataList, filterParams) {
let tempData = {};
let allRowIds = [];
setApiResult(gridDataList);
gridDataList.map((record) => {
generateGridData(record, tempData, allRowIds, filterParams);
});
let keyList = Object.keys(tempData);
let temp = [];
let rowDependencies = {};
for (let i = 0; i < keyList.length; i++) {
tempData[keyList[i]]['children'] = Object.values(tempData[keyList[i]]['children']);
let subChildList = [];
tempData[keyList[i]]['children'].map((ch) => ch.children.map(({ id }) => subChildList.push(`${id}`)));
tempData[keyList[i]]['metadata'] = {
isRoot: true,
children: tempData[keyList[i]]['children'].map(({ id }) => `${id}`),
subChildren: subChildList,
};
tempData[keyList[i]]['children'].map((child) => {
child['metadata'] = {
parentId: tempData[keyList[i]].id,
children: tempData[keyList[i]]['children'].map(({ id }) => `${id}`),
subChildren: child.children.map(({ id }) => `${id}`),
dependencies: [],
};
child.children.map((ch) => {
if (ch.dependenciesOid.length > 0) {
tempData[keyList[i]]['children'].map((el) => {
el.children.map((data) => {
if (ch.dependenciesOid.includes(data.oid)) {
ch.dependencieRowIds.push(`${data.id}`);
}
});
});
}
ch['metadata'] = {
parentId: child.id,
rootId: tempData[keyList[i]].id,
children: child.children.map(({ id }) => `${id}`),
};
child['metadata']['dependencies'].push(...ch.dependencieRowIds);
});
});
temp.push(tempData[keyList[i]]);
}
setRowDep(rowDependencies);
setShowResultGrid(true);
setGridData(temp);
setAllRowIdList([...new Set(allRowIds)]);
}
const getCompareStatus = () => {
let url_params = { 'trans_id': params.transId };
schemaDiffToolContext.api.get(url_for('schema_diff.poll', url_params)).then((res) => {
let msg = res.data.data.compare_msg;
if (res.data.data.diff_percentage != 100) {
msg = msg + gettext(` (this may take a few minutes)... ${res.data.data.diff_percentage} %`);
setLoaderText(msg);
}
})
.catch((err) => {
Notifier.error(gettext(err.message));
});
};
const connectDatabase = (sid, selectedDB, diff_type, databaseList) => {
schemaDiffToolContext.api({
method: 'POST',
url: url_for('schema_diff.connect_database', { 'sid': sid, 'did': selectedDB }),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}).then((res) => {
let dbList = databaseList;
for (const opt of dbList) {
if (opt.value == selectedDB) {
opt.connected = true;
opt.image = res.data.data.icon || 'pg-icon-database';
getSchemaList(sid, selectedDB, diff_type);
if (diff_type == TYPE.SOURCE) {
setSelectedSourceDid(selectedDB);
setSourceDatabaseList(dbList);
} else {
setSelectedTargetDid(selectedDB);
setTargetDatabaseList(dbList);
}
break;
}
}
}).catch((error) => {
Notifier.error(gettext(`Error in connect database ${error.response.data}`));
});
};
const connectServer = (sid, diff_type, formData = null, serverList = []) => {
try {
schemaDiffToolContext.api({
method: 'POST',
url: url_for('schema_diff.connect_server', { 'sid': sid }),
data: formData,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}).then((res) => {
for (const group of serverList) {
for (const opt of group.options) {
if (opt.value == sid) {
opt.connected = true;
opt.image = res.data.data.icon || 'icon-pg';
break;
}
}
}
if (diff_type == TYPE.SOURCE) {
setSelectedSourceSid(sid);
} else {
setSelectedTargetSid(sid);
}
setSourceGroupServerList(serverList);
getDatabaseList(sid, diff_type);
}).catch((error) => {
showConnectServer(error.response?.data.result, sid, diff_type, serverList);
});
} catch (error) {
Notifier.error(gettext(`Error in connect server ${error.response.data}` ));
}
};
function getDatabaseList(sid, diff_type) {
schemaDiffToolContext.api.get(
url_for('schema_diff.databases', { 'sid': sid })
).then((res) => {
res.data.data.map((opt) => {
if (opt.is_maintenance_db) {
if (diff_type == TYPE.SOURCE) {
setSelectedSourceDid(opt.value);
} else {
setSelectedTargetDid(opt.value);
}
getSchemaList(sid, opt.value, diff_type);
}
});
if (diff_type == TYPE.SOURCE) {
setSourceDatabaseList(res.data.data);
} else {
setTargetDatabaseList(res.data.data);
}
});
}
function getSchemaList(sid, did, diff_type) {
schemaDiffToolContext.api.get(
url_for('schema_diff.schemas', { 'sid': sid, 'did': did })
).then((res) => {
if (diff_type == TYPE.SOURCE) {
setSourceSchemaList(res.data.data);
} else {
setTargetSchemaList(res.data.data);
}
});
}
function showConnectServer(result, sid, diff_type, serverList) {
schemaDiffToolContext.modal.showModal(gettext('Connect to server'), (closeModal) => {
return (
<ConnectServerContent
closeModal={() => {
closeModal();
}}
data={result}
onOK={(formData) => {
connectServer(sid, diff_type, formData, serverList);
}}
/>
);
});
}
function getFilterParams() {
let opt = [];
if(isInit && filterOptions.length == 0) {
opt = [FILTER_NAME.DIFFERENT, FILTER_NAME.SOURCE_ONLY, FILTER_NAME.TARGET_ONLY];
} else if(filterOptions.length > 0 ) {
opt = filterOptions;
}
return opt;
}
return (
<>
<Loader message={loaderText} style={{fontWeight: 900}}></Loader>
<Box id='compare-container-schema-diff'>
<Grid
container
direction="row"
alignItems="center"
key={_.uniqueId('c')}
>
<Grid item lg={7} md={7} sm={10} xs={10} key={_.uniqueId('c')}>
<InputComponent
label={gettext('Select Source')}
serverList={sourceGroupServerList}
databaseList={sourceDatabaseList}
schemaList={sourceSchemaList}
selectedSid={selectedSourceSid}
selectedDid={selectedSourceDid}
selectedScid={selectedSourceScid}
diff_type={TYPE.SOURCE}
></InputComponent>
</Grid>
<Grid item lg={5} md={5} sm={2} xs={2} key={_.uniqueId('c')} className={classes.helpBtn}>
<PgButtonGroup size="small">
<PgIconButton data-test='schema-diff-help' title={gettext('Help')} icon={<HelpIcon />} onClick={onHelpClick} />
</PgButtonGroup>
</Grid>
</Grid>
<Grid
container
direction="row"
alignItems="center"
key={_.uniqueId('c')}
>
<Grid item lg={7} md={7} sm={10} xs={10} key={_.uniqueId('c')}>
<InputComponent
label={gettext('Select Target')}
serverList={sourceGroupServerList}
databaseList={targetDatabaseList}
schemaList={targetSchemaList}
selectedSid={selectedTargetSid}
selectedDid={selectedTargetDid}
selectedScid={selectedTargetScid}
diff_type={TYPE.TARGET}
></InputComponent>
</Grid>
<Grid item lg={5} md={5} sm={12} xs={12} key={_.uniqueId('c')} className={classes.diffBtn}>
<SchemaDiffButtonComponent
sourceData={{
'sid': selectedSourceSid,
'did': selectedSourceDid,
'scid': selectedSourceScid,
}}
selectedRowIds={selectedRowIds}
rows={apiResult}
targetData={{
'sid': selectedTargetSid,
'did': selectedTargetDid,
'scid': selectedTargetScid,
}}
filterParams={getFilterParams()}
compareParams={compareOptions}
></SchemaDiffButtonComponent>
</Grid>
</Grid>
</Box>
{showResultGrid && gridData.length > 0 && selectedTargetDid && selectedSourceDid ?
<ResultGridComponent
gridData={gridData}
allRowIds={allRowIdList}
filterParams={filterOptions}
selectedRowIds={(rows) => { setSelectedRowIds(rows); }}
rowDependencies={rowDep}
transId={params.transId}
sourceData={{
'sid': selectedSourceSid,
'did': selectedSourceDid,
'scid': selectedSourceScid,
}}
targetData={{
'sid': selectedTargetSid,
'did': selectedTargetDid,
'scid': selectedTargetScid,
}}
></ResultGridComponent>
:
<Box className={classes.note}>
<InfoRoundedIcon style={{ fontSize: '1.2rem' }} />
{gettext(' Source and Target database server must be of the same major version.')}<br />
<strong>{gettext(' Database Compare:')}</strong>
{gettext(' Select the server and database for the source and target and Click')} <strong>{gettext('Compare.')}</strong>
<br />
<strong>{gettext('Schema Compare:')}</strong>
{gettext(' Select the server, database and schema for the source and target and Click')} <strong>{gettext('Compare.')}</strong>
<br />
<strong>{gettext('Note:')}</strong> {gettext('The dependencies will not be resolved in the Schema comparison.')}
</Box>
}
</>
);
}
SchemaDiffCompare.propTypes = {
params: PropTypes.object,
'params.transId': PropTypes.number,
};

View File

@@ -0,0 +1,124 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import {DividerBox} from 'rc-dock';
import url_for from 'sources/url_for';
import { Box, makeStyles } from '@material-ui/core';
import { Results } from './Results';
import { SchemaDiffCompare } from './SchemaDiffCompare';
import EventBus from '../../../../../static/js/helpers/EventBus';
import getApiInstance from '../../../../../static/js/api_instance';
import { useModal } from '../../../../../static/js/helpers/ModalProvider';
export const SchemaDiffEventsContext = createContext();
export const SchemaDiffContext = createContext();
const useStyles = makeStyles((theme) => ({
resetRoot: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
flexWrap: 'wrap',
...theme.mixins.panelBorder.bottom,
'& #id-schema-diff': {
overflow: 'auto'
},
'& #id-results': {
overflow: 'auto'
}
},
resultPanle: {
backgroundColor: theme.palette.default.main,
zIndex: 5,
border: '1px solid ' + theme.otherVars.borderColor,
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
height: '50%',
minHeight: 150,
overflow: 'hidden',
},
comparePanel:{
overflow: 'hidden',
border: '1px solid ' + theme.otherVars.borderColor,
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minHeight: 150,
height: '50%',
}
}));
export default function SchemaDiffComponent({params}) {
const classes = useStyles();
const eventBus = useRef(new EventBus());
const containerRef = React.useRef(null);
const api = getApiInstance();
const modal = useModal();
const [schemaDiffState, setSchemaDiffState] = useState({
preferences: null
});
const schemaDiffContextValue = useMemo(()=> ({
api: api,
modal: modal,
preferences_schema_diff: schemaDiffState.preferences
}), [schemaDiffState.preferences]);
registerUnload();
useEffect(() => {
reflectPreferences();
params.pgAdmin.Browser.onPreferencesChange('schema_diff', function () {
reflectPreferences();
});
}, []);
const reflectPreferences = useCallback(() => {
setSchemaDiffState({
preferences: params.pgAdmin.Browser.get_preferences_for_module('schema_diff')
});
}, []);
function registerUnload() {
window.addEventListener('unload', ()=>{
api.delete(url_for('schema_diff.close', {
trans_id: params.transId
}));
});
}
return (
<SchemaDiffContext.Provider value={schemaDiffContextValue}>
<SchemaDiffEventsContext.Provider value={eventBus.current}>
<Box display="flex" flexDirection="column" flexGrow="1" tabIndex="0" style={{overflowY: 'auto', minHeight: 80}}>
<DividerBox mode='vertical' style={{flexGrow: 1}}>
<div className={classes.comparePanel} id="schema-diff-compare-container" ref={containerRef}><SchemaDiffCompare params={params} /></div>
<div className={classes.resultPanle} id="schema-diff-result-container">
<Results />
</div>
</DividerBox>
</Box>
</SchemaDiffEventsContext.Provider>
</SchemaDiffContext.Provider>
);
}
SchemaDiffComponent.propTypes = {
params: PropTypes.object
};

View File

@@ -0,0 +1,22 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'top/browser/static/js/browser';
import SchemaDiff from './SchemaDiffModule';
if (!pgAdmin.Tools) {
pgAdmin.Tools = {};
}
pgAdmin.Tools.SchemaDiff = SchemaDiff.getInstance(pgAdmin, pgBrowser);
module.exports = {
SchemaDiff: SchemaDiff,
};

View File

@@ -1,543 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import $ from 'jquery';
import Backbone from 'backbone';
import Backform from 'pgadmin.backform';
import gettext from 'sources/gettext';
import clipboard from 'sources/selection/clipboard';
var formatNode = function (opt) {
if (!opt.id) {
return opt.text;
}
var optimage = $(opt.element).data('image');
if (!optimage) {
return opt.text;
} else {
return $('<span></span>').append(
$('<span></span>', {
class: 'wcTabIcon ' + optimage,
})
).append($('<span></span>').text(opt.text));
}
};
let SchemaDiffSqlControl =
Backform.SqlFieldControl.extend({
defaults: {
label: '',
extraClasses: [], // Add default control height
helpMessage: null,
maxlength: 4096,
rows: undefined,
copyRequired: false,
},
template: _.template([
'<% if (copyRequired) { %><button class="btn btn-secondary ddl-copy d-none">' + gettext('Copy') + '</button> <% } %>',
'<div class="pgadmin-controls pg-el-9 pg-el-12 sql_field_layout <%=extraClasses.join(\' \')%>">',
' <textarea ',
' class="<%=Backform.controlClassName%> " name="<%=name%>" aria-label="<%=name%>"',
' maxlength="<%=maxlength%>" placeholder="<%-placeholder%>" <%=disabled ? "disabled" : ""%>',
' rows=<%=rows%>',
' <%=required ? "required" : ""%>><%-value%></textarea>',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>',
].join('\n')),
initialize: function() {
Backform.TextareaControl.prototype.initialize.apply(this, arguments);
this.sqlCtrl = null;
_.bindAll(this, 'onFocus', 'onBlur', 'refreshTextArea', 'copyData');
},
render: function() {
let obj = Backform.SqlFieldControl.prototype.render.apply(this, arguments);
obj.sqlCtrl.setOption('readOnly', true);
if(this.$el.find('.ddl-copy')) this.$el.find('.ddl-copy').on('click', this.copyData);
return obj;
},
copyData() {
event.stopPropagation();
clipboard.copyTextToClipboard(this.model.get('diff_ddl'));
this.$el.find('.ddl-copy').text(gettext('Copied!'));
var self = this;
setTimeout(function() {
let $copy = self.$el.find('.ddl-copy');
if (!$copy.hasClass('d-none')) $copy.addClass('d-none');
$copy.text(gettext('Copy'));
}, 3000);
return false;
},
onFocus: function() {
let $ctrl = this.$el.find('.pgadmin-controls').first(),
$copy = this.$el.find('.ddl-copy');
if (!$ctrl.hasClass('focused')) $ctrl.addClass('focused');
if ($copy.hasClass('d-none')) $copy.removeClass('d-none');
},
});
let SchemaDiffSelect2Control =
Backform.Select2Control.extend({
defaults: _.extend(Backform.Select2Control.prototype.defaults, {
url: undefined,
transform: undefined,
url_with_id: false,
select2: {
allowClear: true,
placeholder: gettext('Select an item...'),
width: 'style',
templateResult: formatNode,
templateSelection: formatNode,
},
controlsClassName: 'pgadmin-controls pg-el-sm-11 pg-el-12',
}),
className: function() {
return 'pgadmin-controls pg-el-sm-4';
},
events: {
'focus select': 'clearInvalid',
'keydown :input': 'processTab',
'select2:select': 'onSelect',
'select2:selecting': 'beforeSelect',
'select2:clear': 'onChange',
},
template: _.template([
'<% if(label == false) {} else {%>',
' <label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<% }%>',
'<div class="<%=controlsClassName%>">',
' <select class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>"',
' name="<%=name%>" aria-label="<%=name%>" value="<%-value%>" <%=disabled ? "disabled" : ""%>',
' <%=required ? "required" : ""%><%= select2.multiple ? " multiple>" : ">" %>',
' <%=select2.first_empty ? " <option></option>" : ""%>',
' <% for (var i=0; i < options.length; i++) {%>',
' <% if (options[i].group) { %>',
' <% var group = options[i].group; %>',
' <% if (options[i].optval) { %> <% var option_length = options[i].optval.length; %>',
' <optgroup label="<%=group%>">',
' <% for (var subindex=0; subindex < option_length; subindex++) {%>',
' <% var option = options[i].optval[subindex]; %>',
' <option ',
' <% if (option.image) { %> data-image=<%=option.image%> <%}%>',
' <% if (option.connected) { %> data-connected=connected <%}%>',
' value=<%- formatter.fromRaw(option.value) %>',
' <% if (option.selected) {%>selected="selected"<%} else {%>',
' <% if (!select2.multiple && option.value === rawValue) {%>selected="selected"<%}%>',
' <% if (select2.multiple && rawValue && rawValue.indexOf(option.value) != -1){%>selected="selected" data-index="rawValue.indexOf(option.value)"<%}%>',
' <%}%>',
' <%= disabled ? "disabled" : ""%>><%-option.label%></option>',
' <%}%>',
' </optgroup>',
' <%}%>',
' <%} else {%>',
' <% var option = options[i]; %>',
' <option ',
' <% if (option.image) { %> data-image=<%=option.image%> <%}%>',
' <% if (option.connected) { %> data-connected=connected <%}%>',
' value=<%- formatter.fromRaw(option.value) %>',
' <% if (option.selected) {%>selected="selected"<%} else {%>',
' <% if (!select2.multiple && option.value === rawValue) {%>selected="selected"<%}%>',
' <% if (select2.multiple && rawValue && rawValue.indexOf(option.value) != -1){%>selected="selected" data-index="rawValue.indexOf(option.value)"<%}%>',
' <%}%>',
' <%= disabled ? "disabled" : ""%>><%-option.label%></option>',
' <%}%>',
' <%}%>',
' </select>',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>',
].join('\n')),
beforeSelect: function() {
var selVal = arguments[0].params.args.data.id;
if(this.field.get('connect') && this.$el.find('option[value="'+selVal+'"]').attr('data-connected') !== 'connected') {
this.field.get('connect').apply(this, [selVal, this.changeIcon.bind(this)]);
} else {
$(this.$sel).trigger('change');
setTimeout(function(){ this.onChange.apply(this); }.bind(this), 200);
}
},
changeIcon: function(data) {
let span = this.$el.find('.select2-selection .select2-selection__rendered span.wcTabIcon'),
selSpan = this.$el.find('option:selected');
if (span.hasClass('icon-server-not-connected') || span.hasClass('icon-shared-server-not-connected')) {
let icon = (data.icon) ? data.icon : 'icon-pg';
span.removeClass('icon-server-not-connected');
span.addClass(icon);
span.attr('data-connected', 'connected');
selSpan.data().image = icon;
selSpan.attr('data-connected', 'connected');
this.onChange.apply(this);
}
else if (span.hasClass('icon-database-not-connected')) {
let icon = (data.icon) ? data.icon : 'pg-icon-database';
span.removeClass('icon-database-not-connected');
span.addClass(icon);
span.attr('data-connected', 'connected');
selSpan.removeClass('icon-database-not-connected');
selSpan.data().image = icon;
selSpan.attr('data-connected', 'connected');
this.onChange.apply(this);
}
},
onChange: function() {
var model = this.model,
attrArr = this.field.get('name').split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
value = this.getValueFromDOM(),
changes = {},
that = this;
if (this.model.errorModel instanceof Backbone.Model) {
if (_.isEmpty(path)) {
this.model.errorModel.unset(name);
} else {
var nestedError = this.model.errorModel.get(name);
if (nestedError) {
this.keyPathSetter(nestedError, path, null);
this.model.errorModel.set(name, nestedError);
}
}
}
changes[name] = _.isEmpty(path) ? value : _.clone(model.get(name)) || {};
if (!_.isEmpty(path)) that.keyPathSetter(changes[name], path, value);
that.stopListening(that.model, 'change:' + name, that.render);
model.set(changes);
that.listenTo(that.model, 'change:' + name, that.render);
},
render: function() {
/*
* Initialization from the original control.
*/
this.fetchData();
return Backform.Select2Control.prototype.render.apply(this, arguments);
},
fetchData: function() {
/*
* We're about to fetch the options required for this control.
*/
var self = this,
url = self.field.get('url'),
m = self.model;
url = _.isFunction(url) ? url.apply(m) : url;
if (url && self.field.get('deps')) {
url = url.replace('sid', m.get(self.field.get('deps')[0]));
}
// Hmm - we found the url option.
// That means - we needs to fetch the options from that node.
if (url) {
var data;
m.trigger('pgadmin:view:fetching', m, self.field);
$.ajax({
async: false,
url: url,
})
.done(function(res) {
/*
* We will cache this data for short period of time for avoiding
* same calls.
*/
data = res.data;
})
.fail(function() {
m.trigger('pgadmin:view:fetch:error', m, self.field);
});
m.trigger('pgadmin:view:fetched', m, self.field);
// To fetch only options from cache, we do not need time from 'at'
// attribute but only options.
//
/*
* Transform the data
*/
var transform = this.field.get('transform') || self.defaults.transform;
if (transform && _.isFunction(transform)) {
// We will transform the data later, when rendering.
// It will allow us to generate different data based on the
// dependencies.
self.field.set('options', transform.bind(self, data));
} else {
self.field.set('options', data);
}
}
},
});
let SchemaDiffHeaderView = Backform.Form.extend({
label: '',
className: function() {
return 'pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-12';
},
tabPanelClassName: function() {
return Backform.tabClassName;
},
tabIndex: 0,
initialize: function(opts) {
this.label = opts.label;
Backform.Form.prototype.initialize.apply(this, arguments);
},
template: _.template(`
<div class="row pgadmin-control-group">
<div class="col-1 control-label">` + gettext('Select Source') + `</div>
<div class="col-6 source row"></div>
</div>
<div class="row pgadmin-control-group">
<div class="col-1 control-label">` + gettext('Select Target') + `</div>
<div class="col-6 target row"></div>
<div class="col-5 target-buttons">
<div class="action-btns d-flex">
<div class="btn-group mr-auto" role="group" aria-label="">
<button class="btn btn-primary"><span class="pg-font-icon icon-compare sql-icon-lg"></span>&nbsp;` + gettext('Compare') + `</button>
<button id="btn-ignore-dropdown" type="button" class="btn btn-primary-icon dropdown-toggle dropdown-toggle-split mr-auto"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="ignore"
title=""
tabindex="0">
</button>` +
[
'<ul class="dropdown-menu ignore">',
'<li>',
'<a class="dropdown-item" id="btn-ignore-owner" href="#" tabindex="0">',
'<i class="fa fa-check visibility-hidden" aria-hidden="true"></i>',
'<span> ' + gettext('Ignore owner') + ' </span>',
'</a>',
'</li>',
'<li>',
'<a class="dropdown-item" id="btn-ignore-whitespaces" href="#" tabindex="0">',
'<i class="fa fa-check visibility-hidden" aria-hidden="true"></i>',
'<span> ' + gettext('Ignore whitespace') + ' </span>',
'</a>',
'</li>',
'</ul>',
].join('\n') + `</div>
<button id="generate-script" class="btn btn-primary-icon mr-1" disabled><i class="fa fa-file-code sql-icon-lg"></i>&nbsp;` + gettext('Generate Script') + `</button>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-filter" type="button" class="btn btn-primary-icon"
title=""
tabindex="0"
style="pointer-events: none;">
<i class="fa fa-filter sql-icon-lg" aria-hidden="true"></i>&nbsp;` + gettext('Filter') + `
</button>
<button id="btn-filter-dropdown" type="button" class="btn btn-primary-icon dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="filter"
title=""
tabindex="0">
</button>` +
[
'<ul class="dropdown-menu filter">',
'<li>',
'<a class="dropdown-item" id="btn-identical" href="#" tabindex="0">',
'<i class="identical fa fa-check visibility-hidden" aria-hidden="true"></i>',
'<span> ' + gettext('Identical') + ' </span>',
'</a>',
'</li>',
'<li>',
'<a class="dropdown-item" id="btn-differentt" href="#" tabindex="0">',
'<i class="different fa fa-check" aria-hidden="true"></i>',
'<span> ' + gettext('Different') + ' </span>',
'</a>',
'</li>',
'<li>',
'<a class="dropdown-item" id="btn-source-only" href="#" tabindex="0">',
'<i class="source-only fa fa-check" aria-hidden="true"></i>',
'<span> ' + gettext('Source Only') + ' </span>',
'</a>',
'</li>',
'<li>',
'<a class="dropdown-item" id="btn-target-only" href="#" tabindex="0">',
'<i class="target-only fa fa-check" aria-hidden="true"></i>',
'<span> ' + gettext('Target Only') + ' </span>',
'</a>',
'</li>',
'</ul>',
'</div>',
'</div>',
'</div>',
'</div>',
].join('\n')
),
render: function() {
this.cleanup();
var controls = this.controls,
m = this.model,
self = this,
idx = (this.tabIndex * 100);
this.$el.empty();
$(this.template()).appendTo(this.$el);
this.fields.each(function(f) {
var cntr = new(f.get('control'))({
field: f,
model: m,
dialog: self,
tabIndex: idx,
});
if (f.get('group') && f.get('group') == 'source') {
self.$el.find('.source').append(cntr.render().$el);
}
else {
self.$el.find('.target').append(cntr.render().$el);
}
controls.push(cntr);
});
return this;
},
remove: function(opts) {
if (opts && opts.data) {
if (this.model) {
if (this.model.reset) {
this.model.reset({
validate: false,
silent: true,
stop: true,
});
}
this.model.clear({
validate: false,
silent: true,
stop: true,
});
delete(this.model);
}
if (this.errorModel) {
this.errorModel.clear({
validate: false,
silent: true,
stop: true,
});
delete(this.errorModel);
}
}
this.cleanup();
Backform.Form.prototype.remove.apply(this, arguments);
},
});
let SchemaDiffFooterView = Backform.Form.extend({
className: function() {
return 'set-group pg-el-12';
},
tabPanelClassName: function() {
return Backform.tabClassName;
},
legendClass: 'badge',
contentClass: Backform.accordianContentClassName,
template: {
'content': _.template(`
<div class="pg-el-sm-12 row <%=contentClass%>">
<div class="pg-el-sm-4 ddl-source">` + gettext('Source') + `</div>
<div class="pg-el-sm-4 ddl-target">` + gettext('Target') + `</div>
<div class="pg-el-sm-4 ddl-diff">` + gettext('Difference') + `
</div>
</div>
</div>
`),
},
initialize: function(opts) {
this.label = opts.label;
Backform.Form.prototype.initialize.apply(this, arguments);
},
render: function() {
this.cleanup();
let m = this.model,
$el = this.$el,
tmpl = this.template,
controls = this.controls,
data = {
'className': _.result(this, 'className'),
'legendClass': _.result(this, 'legendClass'),
'contentClass': _.result(this, 'contentClass'),
'collapse': _.result(this, 'collapse'),
},
idx = (this.tabIndex * 100);
this.$el.empty();
let el = $((tmpl['content'])(data)).appendTo($el);
this.fields.each(function(f) {
let cntr = new(f.get('control'))({
field: f,
model: m,
dialog: self,
tabIndex: idx,
name: f.get('name'),
});
if (f.get('group') && f.get('group') == 'ddl-source') {
el.find('.ddl-source').append(cntr.render().$el);
}
else if (f.get('group') && f.get('group') == 'ddl-target') {
el.find('.ddl-target').append(cntr.render().$el);
}
else {
el.find('.ddl-diff').append(cntr.render().$el);
}
controls.push(cntr);
});
$('div.CodeMirror div textarea').attr('aria-label', 'textarea');
let $diff_sc = this.$el.find('.source_ddl'),
$diff_tr = this.$el.find('.target_ddl'),
$diff = this.$el.find('.diff_ddl'),
footer_height = this.$el.parent().height() - 50;
$diff_sc.height(footer_height);
$diff_sc.css({
'height': footer_height + 'px',
});
$diff_tr.height(footer_height);
$diff_tr.css({
'height': footer_height + 'px',
});
$diff.height(footer_height);
$diff.css({
'height': footer_height + 'px',
});
return this;
},
});
export {
SchemaDiffSelect2Control,
SchemaDiffHeaderView,
SchemaDiffFooterView,
SchemaDiffSqlControl,
};

View File

@@ -1,171 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define('pgadmin.schemadiff', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'sources/csrf', 'pgadmin.alertifyjs', 'sources/utils', 'pgadmin.browser.node',
], function(
gettext, url_for, $, _, pgAdmin, csrfToken, Alertify, commonUtils,
) {
var wcDocker = window.wcDocker,
pgBrowser = pgAdmin.Browser;
/* Return back, this has been called more than once */
if (pgBrowser.SchemaDiff)
return pgBrowser.SchemaDiff;
// Create an Object Restore of pgBrowser class
pgBrowser.SchemaDiff = {
init: function() {
if (this.initialized)
return;
this.initialized = true;
csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
// Define the nodes on which the menus to be appear
var menus = [{
name: 'schema_diff',
module: this,
applies: ['tools'],
callback: 'show_schema_diff_tool',
priority: 1,
label: gettext('Schema Diff'),
enable: true,
below: true,
}];
pgBrowser.add_menus(menus);
// Creating a new pgBrowser frame to show the data.
var schemaDiffFrameType = new pgBrowser.Frame({
name: 'frm_schemadiff',
showTitle: true,
isCloseable: true,
isPrivate: true,
url: 'about:blank',
});
let self = this;
/* Cache may take time to load for the first time
* Keep trying till available
*/
let cacheIntervalId = setInterval(function() {
if(pgBrowser.preference_version() > 0) {
self.preferences = pgBrowser.get_preferences_for_module('schema_diff');
clearInterval(cacheIntervalId);
}
},0);
pgBrowser.onPreferencesChange('schema_diff', function() {
self.preferences = pgBrowser.get_preferences_for_module('schema_diff');
});
// Load the newly created frame
schemaDiffFrameType.load(pgBrowser.docker);
return this;
},
// Callback to draw schema diff for objects
show_schema_diff_tool: function() {
var self = this,
baseUrl = url_for('schema_diff.initialize', null);
$.ajax({
url: baseUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.done(function(res) {
self.trans_id = res.data.schemaDiffTransId;
res.data.panel_title = gettext('Schema Diff'); //TODO: Set the panel title
// TODO: Following function is used to test the fetching of the
// databases this should be moved to server selection event later.
self.launch_schema_diff(res.data);
})
.fail(function(xhr) {
self.raise_error_on_fail(gettext('Schema Diff initialize error') , xhr);
});
},
launch_schema_diff: function(data) {
var panel_title = data.panel_title,
trans_id = data.schemaDiffTransId,
panel_tooltip = '';
var url_params = {
'trans_id': trans_id,
'editor_title': panel_title,
},
baseUrl = url_for('schema_diff.panel', url_params);
var browser_preferences = pgBrowser.get_preferences_for_module('browser');
var open_new_tab = browser_preferences.new_browser_tab_open;
if (open_new_tab && open_new_tab.includes('schema_diff')) {
window.open(baseUrl, '_blank');
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function() {
pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
var propertiesPanel = pgBrowser.docker.findPanels('properties'),
schemaDiffPanel = pgBrowser.docker.addPanel('frm_schemadiff', wcDocker.DOCK.STACKED, propertiesPanel[0]);
commonUtils.registerDetachEvent(schemaDiffPanel);
// Rename schema diff tab
schemaDiffPanel.on(wcDocker.EVENT.RENAME, function(panel_data) {
Alertify.prompt('', panel_data.$titleText[0].textContent,
// We will execute this function when user clicks on the OK button
function(evt, value) {
if(value) {
// Remove the leading and trailing white spaces.
value = value.trim();
schemaDiffPanel.title('<span>'+ _.escape(value) +'</span>');
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function(evt) { evt.cancel = false; }
).set({'title': gettext('Rename Panel')});
});
// Set panel title and icon
schemaDiffPanel.title('<span title="'+panel_tooltip+'">'+panel_title+'</span>');
schemaDiffPanel.icon('pg-font-icon icon-compare');
schemaDiffPanel.focus();
var openSchemaDiffURL = function(j) {
// add spinner element
$(j).data('embeddedFrame').$container.append(pgBrowser.SchemaDiff.spinner_el);
setTimeout(function() {
var frameInitialized = $(j).data('frameInitialized');
if (frameInitialized) {
var frame = $(j).data('embeddedFrame');
if (frame) {
frame.openURL(baseUrl);
frame.$container.find('.pg-sp-container').delay(1000).hide(1);
}
} else {
openSchemaDiffURL(j);
}
}, 100);
};
openSchemaDiffURL(schemaDiffPanel);
}
},
};
return pgBrowser.SchemaDiff;
});

View File

@@ -1,243 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
function handleDependencies() {
event.stopPropagation();
let isChecked = event.target.checked || (event.target.checked === undefined &&
event.target.className && event.target.className.indexOf('unchecked') == -1);
let isHeaderSelected = false;
if (event.target.id !== undefined) isHeaderSelected = event.target.id.includes('header-selector');
if (this.gridContext && this.gridContext.rowIndex && _.isUndefined(this.gridContext.row.rows)) {
// Single Row Selection
let rowData = this.grid.getData().getItem(this.gridContext.rowIndex);
this.gridContext = {};
if (rowData.status) {
let depRows = this.selectDependencies(rowData, isChecked);
this.selectedRowCount = this.grid.getSelectedRows().length;
if (isChecked && depRows.length > 0)
this.grid.setSelectedRows(depRows);
else if (!isChecked)
this.grid.setSelectedRows(this.grid.getSelectedRows().filter(x => !depRows.includes(x)));
this.ddlCompare(rowData);
}
} else if((this.gridContext && this.gridContext.row && !_.isUndefined(this.gridContext.row.rows)) ||
this.selectedRowCount != this.grid.getSelectedRows().length) {
// Group / All Rows Selection
this.selectedRowCount = this.grid.getSelectedRows().length;
if (this.gridContext.row && this.gridContext.row.__group) {
let context = this.gridContext;
this.gridContext = {};
this.selectDependenciesForGroup(isChecked, context);
} else {
this.gridContext = {};
this.selectDependenciesForAll(isChecked, isHeaderSelected);
}
}
if (this.grid.getSelectedRows().length > 0) {
this.header.$el.find('button#generate-script').removeAttr('disabled');
} else {
this.header.$el.find('button#generate-script').attr('disabled', true);
}
}
function selectDependenciesForGroup(isChecked, context) {
let self = this,
finalRows = [];
if (!isChecked) {
_.each(context.row.rows, function(row) {
if (row && row.status && row.status.toLowerCase() != 'identical' ) {
let d = self.selectDependencies(row, isChecked);
finalRows = finalRows.concat(d);
}
});
}
else {
_.each(self.grid.getSelectedRows(), function(row) {
let data = self.grid.getData().getItem(row);
if (data.status && data.status.toLowerCase() != 'identical') {
finalRows = finalRows.concat(self.selectDependencies(data, isChecked));
}
});
}
finalRows = [...new Set(finalRows)];
if (isChecked)
self.grid.setSelectedRows(finalRows);
else {
let filterRows = [];
filterRows = self.grid.getSelectedRows().filter(x => !finalRows.includes(x));
self.selectedRowCount = filterRows.length;
self.grid.setSelectedRows(filterRows);
}
}
function selectDependenciesForAll(isChecked, isHeaderSelected) {
let self = this,
finalRows = [];
if(!isChecked && isHeaderSelected) {
self.dataView.getItems().map(function(el) {
el.dependentCount = [];
el.dependLevel = 1;
});
self.selectedRowCount = 0;
return;
}
_.each(self.grid.getSelectedRows(), function(row) {
let data = self.grid.getData().getItem(row);
if (data.status) {
finalRows = finalRows.concat(self.selectDependencies(data, isChecked));
}
});
finalRows = [...new Set(finalRows)];
if (isChecked && finalRows.length > 0)
self.grid.setSelectedRows(finalRows);
else if (!isChecked) {
let filterRows = [];
filterRows = self.grid.getSelectedRows().filter(x => !finalRows.includes(x));
self.selectedRowCount = filterRows.length;
self.grid.setSelectedRows(filterRows);
}
}
function selectDependencies(data, isChecked) {
let self = this,
rows = [],
setDependencies = undefined,
setOrigDependencies = undefined,
finalRows = [];
if (!data.dependLevel || !isChecked) data.dependLevel = 1;
if (!data.dependentCount || !isChecked) data.dependentCount = [];
if (data.status && data.status.toLowerCase() == 'identical') {
self.selectedRowCount = self.grid.getSelectedRows().length;
return [];
}
setDependencies = function(rowData, dependencies, is_checked) {
// Special handling for extension, if extension is present in the
// dependency list then iterate and select only extension node.
let extensions = dependencies.filter(item => item.type == 'extension');
if (extensions.length > 0) dependencies = extensions;
_.each(dependencies, function(dependency) {
if (dependency.length == 0) return;
let dependencyData = [];
dependencyData = self.dataView.getItems().filter(item => item.type == dependency.type && item.oid == dependency.oid);
if (dependencyData.length > 0) {
dependencyData = dependencyData[0];
if (!dependencyData.dependentCount) dependencyData.dependentCount = [];
let groupData = [];
if (dependencyData.status && dependencyData.status.toLowerCase() != 'identical') {
groupData = self.dataView.getGroups().find(
(item) => { if (dependencyData.group_name == item.groupingKey) return item.groups; }
);
if (groupData && groupData.groups) {
groupData = groupData.groups.find(
(item) => { return item.groupingKey == dependencyData.group_name + ':|:' + dependencyData.type; }
);
if (groupData && groupData.collapsed == 1)
self.dataView.expandGroup(dependencyData.group_name + ':|:' + dependencyData.type);
}
if (is_checked || _.isUndefined(is_checked)) {
dependencyData.dependLevel = rowData.dependLevel + 1;
if (dependencyData.dependentCount.indexOf(rowData.oid) === -1)
dependencyData.dependentCount.push(rowData.oid);
rows[rows.length] = dependencyData;
} else {
dependencyData.dependentCount.splice(dependencyData.dependentCount.indexOf(rowData.oid), 1);
if (dependencyData.dependentCount.length == 0) {
rows[rows.length] = dependencyData;
dependencyData.dependLevel = 1;
}
}
}
if (Object.keys(dependencyData.dependencies).length > 0) {
if (dependencyData.dependentCount.indexOf(rowData.oid) !== -1 ) {
let depCirRows = dependencyData.dependencies.filter(x => x.oid !== rowData.oid);
if (!dependencyData.orig_dependencies)
dependencyData.orig_dependencies = Object.assign([], dependencyData.dependencies);
dependencyData.dependencies = depCirRows;
}
setDependencies(dependencyData, dependencyData.dependencies, is_checked);
}
}
});
};
setDependencies(data, data.dependencies, isChecked);
setOrigDependencies = function(dependencies) {
_.each(dependencies, function(dependency) {
if (dependency.length == 0) return;
let dependencyData = [];
dependencyData = self.dataView.getItems().filter(item => item.type == dependency.type && item.oid == dependency.oid);
if (dependencyData.length > 0) {
dependencyData = dependencyData[0];
if (dependencyData.orig_dependencies && Object.keys(dependencyData.orig_dependencies).length > 0) {
if (!dependencyData.dependentCount) dependencyData.dependentCount = [];
if (dependencyData.status && dependencyData.status.toLowerCase() != 'identical') {
dependencyData.dependencies = dependencyData.orig_dependencies;
dependencyData.orig_dependencies = [];
}
if (dependencyData.dependencies.length > 0) {
setOrigDependencies(dependencyData.dependencies);
}
}
}
});
};
setOrigDependencies(data.dependencies);
if (isChecked) finalRows = self.grid.getSelectedRows();
_.each(rows, function(row) {
let r = self.grid.getData().getRowByItem(row);
if(!_.isUndefined(r) && finalRows.indexOf(r) === -1 ) {
finalRows.push(self.grid.getData().getRowByItem(row));
}
});
self.selectedRowCount = finalRows.length;
return finalRows;
}
export {
handleDependencies,
selectDependenciesForGroup,
selectDependenciesForAll,
selectDependencies,
};

View File

@@ -1,38 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'sources/url_for', 'jquery',
'sources/pgadmin', 'pgadmin.tools.schema_diff_ui',
], function(
url_for, $, pgAdmin, SchemaDiffUIModule
) {
var pgTools = pgAdmin.Tools = pgAdmin.Tools || {};
var SchemaDiffUI = SchemaDiffUIModule.default;
/* Return back, this has been called more than once */
if (pgTools.SchemaDiffHook)
return pgTools.SchemaDiffHook;
pgTools.SchemaDiffHook = {
load: function(trans_id) {
window.onbeforeunload = function() {
$.ajax({
url: url_for('schema_diff.close', {'trans_id': trans_id}),
method: 'DELETE',
});
};
let schemaUi = new SchemaDiffUI($('#schema-diff-container'), trans_id);
schemaUi.render();
},
};
return pgTools.SchemaDiffHook;
});

View File

@@ -1,967 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import url_for from 'sources/url_for';
import $ from 'jquery';
import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs';
import Backbone from 'backbone';
import Slick from 'sources/../bundle/slickgrid';
import pgAdmin from 'sources/pgadmin';
import {setPGCSRFToken} from 'sources/csrf';
import 'pgadmin.tools.sqleditor';
import pgWindow from 'sources/window';
import _ from 'underscore';
import Notify from '../../../../static/js/helpers/Notifier';
import { showSchemaDiffServerPassword } from '../../../../static/js/Dialogs/index';
import { SchemaDiffSelect2Control, SchemaDiffHeaderView,
SchemaDiffFooterView, SchemaDiffSqlControl} from './schema_diff.backform';
import { handleDependencies, selectDependenciesForGroup,
selectDependenciesForAll, selectDependencies } from './schema_diff_dependency';
import { generateScript } from '../../../sqleditor/static/js/show_query_tool';
var wcDocker = window.wcDocker;
export default class SchemaDiffUI {
constructor(container, trans_id) {
var self = this;
this.$container = container;
this.header = null;
this.trans_id = trans_id;
this.filters = ['Identical', 'Different', 'Source Only', 'Target Only'];
this.sel_filters = ['Different', 'Source Only', 'Target Only'];
this.ignore_filters = ['owner', 'whitespaces'];
this.ignore_whitespaces = 0;
this.ignore_owner = 0;
this.dataView = null;
this.grid = null;
this.selection = {};
this.model = new Backbone.Model({
source_sid: undefined,
source_did: undefined,
source_scid: undefined,
target_sid: undefined,
target_did: undefined,
target_scid: undefined,
source_ddl: undefined,
target_ddl: undefined,
diff_ddl: undefined,
});
setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
this.docker = new wcDocker(
this.$container, {
allowContextMenu: false,
allowCollapse: false,
loadingClass: 'pg-sp-icon',
themePath: url_for('static', {
'filename': 'css',
}),
theme: 'webcabin.overrides.css',
}
);
this.header_panel = new pgAdmin.Browser.Panel({
name: 'schema_diff_header_panel',
showTitle: false,
isCloseable: false,
isPrivate: true,
content: '<div id="schema-diff-header" class="pg-el-container" el="sm"></div><div id="schema-diff-grid" class="pg-el-container" el="sm"></div>',
elContainer: true,
});
this.footer_panel = new pgAdmin.Browser.Panel({
name: 'schema_diff_footer_panel',
title: gettext('DDL Comparison'),
isCloseable: false,
isPrivate: true,
height: '60',
content: `<div id="schema-diff-ddl-comp" class="pg-el-container" el="sm">
<div id="ddl_comp_fetching_data" class="pg-sp-container schema-diff-busy-fetching d-none">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
<div class="row"><div class="col-12 pg-sp-text">` + gettext('Comparing objects...') + `</div></div>
</div>
</div></div>`,
});
this.header_panel.load(this.docker);
this.footer_panel.load(this.docker);
this.panel_obj = this.docker.addPanel('schema_diff_header_panel', wcDocker.DOCK.TOP, {w:'95%', h:'50%'});
this.footer_panel_obj = this.docker.addPanel('schema_diff_footer_panel', wcDocker.DOCK.BOTTOM, this.panel_obj, {w:'95%', h:'50%'});
self.footer_panel_obj.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() {
setTimeout(function() {
this.resize_grid();
}.bind(self), 200);
});
self.footer_panel_obj.on(wcDocker.EVENT.RESIZE_ENDED, function() {
setTimeout(function() {
this.resize_panels();
}.bind(self), 200);
});
}
raise_error_on_fail(alert_title, xhr) {
try {
if (_.isUndefined(xhr.responseText)) {
Notify.alert(alert_title, gettext('Unable to get the response text.'));
} else {
var err = JSON.parse(xhr.responseText);
Notify.alert(alert_title, err.errormsg);
}
} catch (e) {
Notify.alert(alert_title, gettext(e.message));
}
}
resize_panels() {
let $src_ddl = $('#schema-diff-ddl-comp .source_ddl'),
$tar_ddl = $('#schema-diff-ddl-comp .target_ddl'),
$diff_ddl = $('#schema-diff-ddl-comp .diff_ddl'),
footer_height = $('#schema-diff-ddl-comp').height() - 50;
$src_ddl.height(footer_height);
$src_ddl.css({
'height': footer_height + 'px',
});
$tar_ddl.height(footer_height);
$tar_ddl.css({
'height': footer_height + 'px',
});
$diff_ddl.height(footer_height);
$diff_ddl.css({
'height': footer_height + 'px',
});
this.resize_grid();
}
compare_schemas() {
var self = this,
url_params = self.model.toJSON();
if (url_params['source_sid'] == '' || _.isUndefined(url_params['source_sid']) ||
url_params['source_did'] == '' || _.isUndefined(url_params['source_did']) ||
url_params['target_sid'] == '' || _.isUndefined(url_params['target_sid']) ||
url_params['target_did'] == '' || _.isUndefined(url_params['target_did'])
) {
Notify.alert(gettext('Selection Error'), gettext('Please select source and target.'));
return false;
}
// Check if user has selected the same options for comparison on the GUI
let opts = [['source_sid', 'target_sid'], ['source_did', 'target_did'], ['source_scid', 'target_scid']];
let isSameOptsSelected = true;
for (let opt of opts) {
if (url_params[opt[0]] && url_params[opt[1]] &&
(parseInt(url_params[opt[0]]) !== parseInt(url_params[opt[1]]))) {
isSameOptsSelected = false;
}
}
if (isSameOptsSelected) {
Notify.alert(gettext('Selection Error'), gettext('Please select the different source and target.'));
return false;
}
this.selection = JSON.parse(JSON.stringify(url_params));
url_params['trans_id'] = self.trans_id;
url_params['ignore_owner'] = self.ignore_owner;
url_params['ignore_whitespaces'] = self.ignore_whitespaces;
_.each(url_params, function(key, val) {
url_params[key] = parseInt(val, 10);
});
var baseUrl = url_for('schema_diff.compare_database', url_params);
// If compare two schema then change the base url
if (url_params['source_scid'] != '' && !_.isUndefined(url_params['source_scid']) &&
url_params['target_scid'] != '' && !_.isUndefined(url_params['target_scid'])) {
baseUrl = url_for('schema_diff.compare_schema', url_params);
}
self.model.set({
'source_ddl': undefined,
'target_ddl': undefined,
'diff_ddl': undefined,
});
self.render_grid([]);
self.footer.render();
self.startDiffPoller();
return $.ajax({
url: baseUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.done(function (res) {
self.stopDiffPoller();
self.render_grid(res.data);
})
.fail(function (xhr) {
self.raise_error_on_fail(gettext('Schema compare error'), xhr);
self.stopDiffPoller();
});
}
generate_script() {
var self = this,
baseServerUrl = url_for('schema_diff.get_server', {'sid': self.selection['target_sid'],
'did': self.selection['target_did']}),
sel_rows = self.grid ? self.grid.getSelectedRows() : [],
url_params = self.selection,
generated_script = undefined,
open_query_tool,
script_header;
script_header = gettext('-- This script was generated by the Schema Diff utility in pgAdmin 4. \n');
script_header += gettext('-- For the circular dependencies, the order in which Schema Diff writes the objects is not very sophisticated \n');
script_header += gettext('-- and may require manual changes to the script to ensure changes are applied in the correct order.\n');
script_header += gettext('-- Please report an issue for any failure with the reproduction steps. \n');
_.each(url_params, function(key, val) {
url_params[key] = parseInt(val, 10);
});
$('#diff_fetching_data').removeClass('d-none');
$('#diff_fetching_data').find('.schema-diff-busy-text').text('Generating script...');
open_query_tool = function get_server_details() {
$.ajax({
url: baseServerUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.done(function (res) {
let data = res.data;
let server_data = {};
if (data) {
let sqlId = `schema${self.trans_id}`;
server_data['sgid'] = data.gid;
server_data['sid'] = data.sid;
server_data['stype'] = data.type;
server_data['server'] = data.name;
server_data['user'] = data.user;
server_data['did'] = self.model.get('target_did');
server_data['database'] = data.database;
server_data['sql_id'] = sqlId;
if (_.isUndefined(generated_script)) {
generated_script = script_header + 'BEGIN;' + '\n' + self.model.get('diff_ddl') + '\n' + 'END;';
}
localStorage.setItem(sqlId, generated_script);
generateScript(server_data, pgWindow.pgAdmin.Tools.SQLEditor, Alertify);
}
$('#diff_fetching_data').find('.schema-diff-busy-text').text('');
$('#diff_fetching_data').addClass('d-none');
})
.fail(function (xhr) {
self.raise_error_on_fail(gettext('Generate script error'), xhr);
$('#diff_fetching_data').find('.schema-diff-busy-text').text('');
$('#diff_fetching_data').addClass('d-none');
});
};
if (sel_rows.length > 0) {
let script_array = {1: [], 2: [], 3: [], 4: [], 5: []},
script_body = '';
for (let sel_row_val of sel_rows) {
let data = self.grid.getData().getItem(sel_row_val);
if(!_.isUndefined(data.diff_ddl)) {
if (!(data.dependLevel in script_array)) script_array[data.dependLevel] = [];
// Check whether the selected object belongs to source only schema
// if yes then we will have to add create schema statement before
// creating any other object.
if (!_.isUndefined(data.source_schema_name) && !_.isNull(data.source_schema_name)) {
let schema_query = '\nCREATE SCHEMA IF NOT EXISTS ' + data.source_schema_name + ';\n';
if (script_array[data.dependLevel].indexOf(schema_query) == -1) {
script_array[data.dependLevel].push(schema_query);
}
}
script_array[data.dependLevel].push(data.diff_ddl);
}
}
_.each(Object.keys(script_array).reverse(), function(s) {
if (script_array[s].length > 0) {
script_body += script_array[s].join('\n') + '\n\n';
}
});
generated_script = script_header + 'BEGIN;' + '\n' + script_body + 'END;';
open_query_tool();
} else if (!_.isUndefined(self.model.get('diff_ddl'))) {
open_query_tool();
}
return false;
}
check_empty_diff() {
var self = this;
this.panel_obj.$container.find('#schema-diff-grid .slick-viewport .pg-panel-message').remove();
if (self.dataView.getFilteredItems().length == 0) {
let msg = gettext('No difference found');
// Make the height to 0px to avoid extra scroll bar, height will be calculated automatically.
this.panel_obj.$container.find('#schema-diff-grid .slick-viewport .grid-canvas')[0].style.height = '0px';
this.panel_obj.$container.find('#schema-diff-grid .slick-viewport'
).prepend('<div class="pg-panel-message">'+ msg +'</div>');
}
}
render_grid(data) {
var self = this;
var grid;
if (self.grid) {
// Only render the data
self.render_grid_data(data);
self.check_empty_diff();
return;
}
// Checkbox Column
var checkboxSelector = new Slick.CheckboxSelectColumn({
cssClass: 'slick-cell-checkboxsel',
minWidth: 30,
});
// Format Schema object title with appropriate icon
var formatColumnTitle = function (row, cell, value, columnDef, dataContext) {
let icon = 'icon-' + dataContext.type;
return '<i class="ml-2 wcTabIcon '+ icon +'"></i><span>' + value + '</span>';
};
// Grid Columns
var grid_width = (self.grid_width - 47) / 2 ;
var columns = [
checkboxSelector.getColumnDefinition(),
{id: 'title', name: gettext('Objects'), field: 'title', minWidth: grid_width, formatter: formatColumnTitle},
{id: 'status', name: gettext('Comparison Result'), field: 'status', minWidth: grid_width},
{id: 'label', name: gettext('Objects'), field: 'label', width: 0, minWidth: 0, maxWidth: 0,
cssClass: 'really-hidden', headerCssClass: 'really-hidden'},
{id: 'type', name: gettext('Objects'), field: 'type', width: 0, minWidth: 0, maxWidth: 0,
cssClass: 'really-hidden', headerCssClass: 'really-hidden'},
{id: 'id', name: 'id', field: 'id', width: 0, minWidth: 0, maxWidth: 0,
cssClass: 'really-hidden', headerCssClass: 'really-hidden' },
];
// Grid Options
var options = {
enableCellNavigation: true,
enableColumnReorder: false,
enableRowSelection: true,
};
// Grouping by Schema Object
self.groupBySchemaObject = function() {
self.dataView.setGrouping([{
getter: 'group_name',
formatter: function (g) {
let icon = 'icon-schema';
if (g.rows[0].group_name == 'Database Objects'){
icon = 'icon-coll-database';
}
return '<i class="wcTabIcon '+ icon +'"></i><span>' + _.escape(g.rows[0].group_name);
},
aggregateCollapsed: true,
lazyTotalsCalculation: true,
}, {
getter: 'type',
formatter: function (g) {
let icon = 'icon-coll-' + g.value;
let identical=0, different=0, source_only=0, target_only=0;
for (let row_val of g.rows) {
if (row_val['status'] == self.filters[0]) identical++;
else if (row_val['status'] == self.filters[1]) different++;
else if (row_val['status'] == self.filters[2]) source_only++;
else if (row_val['status'] == self.filters[3]) target_only++;
}
return '<i class="wcTabIcon '+ icon +'"></i><span>' + g.rows[0].label + ' - ' + gettext('Identical') + ': <strong>' + identical + '</strong>&nbsp;&nbsp;' + gettext('Different') + ': <strong>' + different + '</strong>&nbsp;&nbsp;' + gettext('Source Only') + ': <strong>' + source_only + '</strong>&nbsp;&nbsp;' + gettext('Target Only') + ': <strong>' + target_only + '</strong></span>';
},
aggregateCollapsed: true,
collapsed: true,
lazyTotalsCalculation: true,
}]);
};
var groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider({ checkboxSelect: true,
checkboxSelectPlugin: checkboxSelector });
// Dataview for grid
self.dataView = new Slick.Data.DataView({
groupItemMetadataProvider: groupItemMetadataProvider,
inlineFilters: false,
});
// Wire up model events to drive the grid
self.dataView.onRowCountChanged.subscribe(function () {
grid.updateRowCount();
grid.render();
self.accessibility_error();
});
self.dataView.onRowsChanged.subscribe(function (e, args) {
grid.invalidateRows(args.rows);
grid.render();
self.accessibility_error();
});
// Change Row css on the basis of item status
self.dataView.getItemMetadata = function(row) {
let item = self.dataView.getItem(row),
group_item = groupItemMetadataProvider.getGroupRowMetadata(item);
if (item.__group) {
return group_item;
}
if(item.status === 'Different') {
return { cssClasses: 'different' };
} else if (item.status === 'Source Only') {
return { cssClasses: 'source' };
} else if (item.status === 'Target Only') {
return { cssClasses: 'target' };
}
return null;
};
// Grid filter
self.filter = function (item) {
let self_local = this;
if (self_local.sel_filters.indexOf(item.status) !== -1) return true;
return false;
};
let $data_grid = $('#schema-diff-grid');
grid = this.grid = new Slick.Grid($data_grid, self.dataView, columns, options);
$('label[for='+ columns[0].name.split('\'')[1] +']').append('<span style="display:none">checkbox</span>');
grid.registerPlugin(groupItemMetadataProvider);
grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false}));
grid.registerPlugin(checkboxSelector);
self.dataView.syncGridSelection(grid, true, true);
grid.onMouseEnter.subscribe(function (evt) {
var cell = grid.getCellFromEvent(evt);
self.gridContext = {};
self.gridContext.rowIndex = cell.row;
self.gridContext.row = grid.getDataItem(cell.row);
}.bind(self));
grid.onMouseLeave.subscribe(function () {
self.gridContext = {};
});
grid.onClick.subscribe(function(e, args) {
if (args.row) {
data = args.grid.getData().getItem(args.row);
if (data.status) this.ddlCompare(data);
}
}.bind(self));
grid.onSelectedRowsChanged.subscribe(self.handleDependencies.bind(this));
self.model.on('change:diff_ddl', function(event) {
self.handleDependencies.bind(event, self);
});
$('#schema-diff-grid').on('keyup', function() {
if ((event.keyCode == 38 || event.keyCode ==40) && this.grid.getActiveCell().row) {
data = this.grid.getData().getItem(this.grid.getActiveCell().row);
this.ddlCompare(data);
}
}.bind(self));
self.render_grid_data(data);
}
render_grid_data(data) {
var self = this;
self.grid.setSelectedRows([]);
self.selected_row_count = self.grid.getSelectedRows().length;
data.sort((a, b) => (a.label > b.label) ? 1 : (a.label === b.label) ? ((a.title > b.title) ? 1 : -1) : -1);
self.dataView.beginUpdate();
self.dataView.setItems(data);
self.dataView.setFilter(self.filter.bind(self));
self.groupBySchemaObject();
self.dataView.endUpdate();
self.dataView.refresh();
self.resize_grid();
self.accessibility_error();
}
accessibility_error() {
$('.slick-viewport').scroll(function() {
setTimeout(function() {
$('span.slick-column-name label').append('<span style="display:none">checkbox</span>');
$('div.slick-cell.l0 label').each(function(inx, el) {
$(el).append('<span style="display:none">checkbox</span>');
});
}, 0);
});
}
handle_generate_button(){
if (this.grid.getSelectedRows().length > 0 || (this.model.get('diff_ddl') != '' && !_.isUndefined(this.model.get('diff_ddl')))) {
this.header.$el.find('button#generate-script').removeAttr('disabled');
} else {
this.header.$el.find('button#generate-script').attr('disabled', true);
}
}
resize_grid() {
let $data_grid = $('#schema-diff-grid'),
grid_height = (this.panel_obj.height() > 0) ? this.panel_obj.height() - 100 : this.grid_height - 100;
$data_grid.height(grid_height);
$data_grid.css({
'height': grid_height + 'px',
});
if (this.grid) this.grid.resizeCanvas();
}
getCompareStatus() {
var self = this,
url_params = {'trans_id': self.trans_id},
baseUrl = url_for('schema_diff.poll', url_params);
$.ajax({
url: baseUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.done(function (res) {
let msg = _.escape(res.data.compare_msg);
if (res.data.diff_percentage != 100) {
msg = msg + gettext(' (this may take a few minutes)...');
}
msg = msg + '<br>' + gettext('%s completed.', res.data.diff_percentage + '%');
$('#diff_fetching_data').find('.schema-diff-busy-text').html(msg);
})
.fail(function (xhr) {
self.raise_error_on_fail(gettext('Poll error'), xhr);
self.stopDiffPoller('fail');
});
}
startDiffPoller() {
$('#ddl_comp_fetching_data').addClass('d-none');
$('#diff_fetching_data').removeClass('d-none');
/* Execute once for the first time as setInterval will not do */
this.getCompareStatus();
this.diff_poller_int_id = setInterval(this.getCompareStatus.bind(this), 1000);
}
stopDiffPoller(status) {
clearInterval(this.diff_poller_int_id);
// The last polling for comparison
if (status !== 'fail') this.getCompareStatus();
$('#diff_fetching_data').find('.schema-diff-busy-text').text('');
$('#diff_fetching_data').addClass('d-none');
}
ddlCompare(data) {
var self = this,
node_type = data.type,
source_oid = data.oid,
target_oid;
self.model.set({
'source_ddl': undefined,
'target_ddl': undefined,
'diff_ddl': undefined,
}, {silent: true});
if(data.status && data.status.toLowerCase() == 'identical') {
var url_params = self.selection;
target_oid = data.target_oid;
url_params['trans_id'] = self.trans_id;
url_params['source_scid'] = data.source_scid;
url_params['target_scid'] = data.target_scid;
url_params['source_oid'] = source_oid;
url_params['target_oid'] = target_oid;
url_params['comp_status'] = data.status;
url_params['node_type'] = node_type;
_.each(url_params, function(key, val) {
url_params[key] = parseInt(val, 10);
});
$('#ddl_comp_fetching_data').removeClass('d-none');
var baseUrl = url_for('schema_diff.ddl_compare', url_params);
self.model.url = baseUrl;
self.model.fetch({
success: function() {
self.footer.render();
$('#ddl_comp_fetching_data').addClass('d-none');
},
error: function() {
self.footer.render();
$('#ddl_comp_fetching_data').addClass('d-none');
},
});
} else {
self.model.set({
'source_ddl': data.source_ddl,
'target_ddl': data.target_ddl,
'diff_ddl': data.diff_ddl,
}, {silent: true});
self.footer.render();
}
}
transformFunc(data) {
let group_template_options = [];
for (let key in data) {
if (data.hasOwnProperty(key)) {
group_template_options.push({'group': key, 'optval': data[key]});
}
}
return group_template_options;
}
render() {
let self = this;
let panel = self.docker.findPanels('schema_diff_header_panel')[0];
var header = panel.$container.find('#schema-diff-header');
self.header = new SchemaDiffHeaderView({
el: header,
model: this.model,
fields: [{
name: 'source_sid', label: false,
control: SchemaDiffSelect2Control,
transform: function(data) {
return self.transformFunc(data);
},
url: url_for('schema_diff.servers'),
select2: {
allowClear: true,
placeholder: gettext('Select server...'),
},
connect: function() {
self.connect_server(arguments[0], arguments[1]);
},
group: 'source',
disabled: function() {
return false;
},
}, {
name: 'source_did',
group: 'source',
deps: ['source_sid'],
control: SchemaDiffSelect2Control,
url: function() {
if (this.get('source_sid'))
return url_for('schema_diff.databases', {'sid': this.get('source_sid')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select database...'),
},
disabled: function(m) {
let self_local = this;
if (!_.isUndefined(m.get('source_sid')) && !_.isNull(m.get('source_sid'))
&& m.get('source_sid') !== '') {
setTimeout(function() {
for (let opt_val of self_local.options) {
if (opt_val.is_maintenance_db) {
m.set('source_did', opt_val.value);
}
}
}, 10);
return false;
}
setTimeout(function() {
m.set('source_did', undefined);
}, 10);
return true;
},
connect: function() {
self.connect_database(this.model.get('source_sid'), arguments[0], arguments[1]);
},
}, {
name: 'source_scid',
control: SchemaDiffSelect2Control,
group: 'source',
deps: ['source_sid', 'source_did'],
url: function() {
if (this.get('source_sid') && this.get('source_did'))
return url_for('schema_diff.schemas', {'sid': this.get('source_sid'), 'did': this.get('source_did')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select schema...'),
},
disabled: function(m) {
if (!_.isUndefined(m.get('source_did')) && !_.isNull(m.get('source_did'))
&& m.get('source_did') !== '') {
return false;
}
setTimeout(function() {
m.set('source_scid', undefined);
}, 10);
return true;
},
}, {
name: 'target_sid', label: false,
control: SchemaDiffSelect2Control,
transform: function(data) {
return self.transformFunc(data);
},
group: 'target',
url: url_for('schema_diff.servers'),
select2: {
allowClear: true,
placeholder: gettext('Select server...'),
},
disabled: function() {
return false;
},
connect: function() {
self.connect_server(arguments[0], arguments[1]);
},
}, {
name: 'target_did',
control: SchemaDiffSelect2Control,
group: 'target',
deps: ['target_sid'],
url: function() {
if (this.get('target_sid'))
return url_for('schema_diff.databases', {'sid': this.get('target_sid')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select database...'),
},
disabled: function(m) {
let self_local = this;
if (!_.isUndefined(m.get('target_sid')) && !_.isNull(m.get('target_sid'))
&& m.get('target_sid') !== '') {
setTimeout(function() {
for (let opt_val of self_local.options) {
if (opt_val.is_maintenance_db) {
m.set('target_did', opt_val.value);
}
}
}, 10);
return false;
}
setTimeout(function() {
m.set('target_did', undefined);
}, 10);
return true;
},
connect: function() {
self.connect_database(this.model.get('target_sid'), arguments[0], arguments[1]);
},
}, {
name: 'target_scid',
control: SchemaDiffSelect2Control,
group: 'target',
deps: ['target_sid', 'target_did'],
url: function() {
if (this.get('target_sid') && this.get('target_did'))
return url_for('schema_diff.schemas', {'sid': this.get('target_sid'), 'did': this.get('target_did')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select schema...'),
},
disabled: function(m) {
if (!_.isUndefined(m.get('target_did')) && !_.isNull(m.get('target_did'))
&& m.get('target_did') !== '') {
return false;
}
setTimeout(function() {
m.set('target_scid', undefined);
}, 10);
return true;
},
}],
});
self.footer = new SchemaDiffFooterView({
model: this.model,
fields: [{
name: 'source_ddl', label: false,
control: SchemaDiffSqlControl,
group: 'ddl-source',
}, {
name: 'target_ddl', label: false,
control: SchemaDiffSqlControl,
group: 'ddl-target',
}, {
name: 'diff_ddl', label: false,
control: SchemaDiffSqlControl,
group: 'ddl-diff', copyRequired: true,
}],
});
self.header.render();
self.header.$el.find('button.btn-primary').on('click', self.compare_schemas.bind(self));
self.header.$el.find('button#generate-script').on('click', self.generate_script.bind(self));
self.header.$el.find('ul.filter a.dropdown-item').on('click', self.refresh_filters.bind(self));
self.header.$el.find('ul.ignore a.dropdown-item').on('click', self.refresh_ignore_settings.bind(self));
/* Set the default value for 'ignore owner' and 'ignore whitespace' */
let pref = pgWindow.pgAdmin.Browser.get_preferences_for_module('schema_diff');
if (pref.ignore_owner) self.header.$el.find('ul.ignore a.dropdown-item#btn-ignore-owner').click();
if (pref.ignore_whitespaces) self.header.$el.find('ul.ignore a.dropdown-item#btn-ignore-whitespaces').click();
let footer_panel = self.docker.findPanels('schema_diff_footer_panel')[0],
header_panel = self.docker.findPanels('schema_diff_header_panel')[0];
footer_panel.$container.find('#schema-diff-ddl-comp').append(self.footer.render().$el);
$('div.CodeMirror div textarea').attr('aria-label', 'textarea');
header_panel.$container.find('#schema-diff-grid').append(`<div class='obj_properties container-fluid'>
<div class='pg-panel-message'>` + gettext('<strong>Database Compare:</strong> Select the server and database for the source and target and Click <strong>Compare</strong>.') +
gettext('</br><strong>Schema Compare:</strong> Select the server, database and schema for the source and target and Click <strong>Compare</strong>.') +
gettext('</br><strong>Note:</strong> The dependencies will not be resolved in the Schema comparison.') + '</div></div>');
self.grid_width = $('#schema-diff-grid').width();
self.grid_height = this.panel_obj.height();
}
refresh_filters(event) {
let self = this;
_.each(self.filters, function(filter) {
let index = self.sel_filters.indexOf(filter);
let filter_class = '.' + filter.replace(' ', '-').toLowerCase();
if ($(event.currentTarget).find(filter_class).length == 1) {
if ($(filter_class).hasClass('visibility-hidden') === true) {
$(filter_class).removeClass('visibility-hidden');
if (index === -1) self.sel_filters.push(filter);
} else {
$(filter_class).addClass('visibility-hidden');
if(index !== -1 ) self.sel_filters.splice(index, 1);
}
}
});
// Check whether comparison data is loaded or not
if(!_.isUndefined(self.dataView) && !_.isNull(self.dataView)) {
// Refresh the grid
self.dataView.refresh();
self.check_empty_diff();
}
}
refresh_ignore_settings(event) {
let self = this,
element = $(event.currentTarget).find('.fa-check');
if (element.length == 1) {
if (element.hasClass('visibility-hidden') === true) {
element.removeClass('visibility-hidden');
if (event.currentTarget.id === 'btn-ignore-owner') self.ignore_owner = 1;
if (event.currentTarget.id === 'btn-ignore-whitespaces') self.ignore_whitespaces = 1;
} else {
element.addClass('visibility-hidden');
if (event.currentTarget.id === 'btn-ignore-owner') self.ignore_owner = 0;
if (event.currentTarget.id === 'btn-ignore-whitespaces') self.ignore_whitespaces = 0;
}
}
}
connect_database(server_id, db_id, callback) {
var url = url_for('schema_diff.connect_database', {'sid': server_id, 'did': db_id});
$.post(url)
.done(function(res) {
if (res.success && res.data) {
callback(res.data);
}
})
.fail(function(xhr, error) {
Notify.pgNotifier(error, xhr, gettext('Failed to connect the database.'));
});
}
connect_server(server_id, callback) {
let self = this;
var onFailure = function(
xhr, status, error, sid, err_callback
) {
Notify.pgNotifier('error', xhr, error, function(msg) {
setTimeout(function() {
showSchemaDiffServerPassword(
self.docker,
gettext('Connect to Server'),
msg,
sid,
err_callback,
onSuccess,
onFailure
);
}, 100);
});
},
onSuccess = function(res, suc_callback) {
if (res && res.data) {
// We're not reconnecting
suc_callback(res.data);
}
};
var url = url_for('schema_diff.connect_server', {'sid': server_id});
$.post(url)
.done(function(res) {
if (res.success == 1) {
return onSuccess(res, callback);
}
})
.fail(function(xhr, status, error) {
return onFailure(
xhr, status, error, server_id, callback
);
});
}
}
SchemaDiffUI.prototype.handleDependencies = handleDependencies;
SchemaDiffUI.prototype.selectDependenciesForGroup = selectDependenciesForGroup;
SchemaDiffUI.prototype.selectDependenciesForAll = selectDependenciesForAll;
SchemaDiffUI.prototype.selectDependencies = selectDependencies;

View File

@@ -1,145 +0,0 @@
#schema-diff-container {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: $color-gray-light;
}
#schema-diff-grid .slick-header .slick-header-columns {
background: $color-bg;
height: 40px;
border-bottom: none;
}
#schema-diff-grid .slick-header .slick-header-column.ui-state-default {
padding: 4px 0 3px 6px;
border-bottom: $panel-border;
border-right: $panel-border;
}
#schema-diff-grid {
font-family: $font-family-primary;
font-size: $tree-font-size;
.ui-widget-content {
background-color: $input-bg;
color: $input-color;
}
.ui-state-default {
color: $color-fg;
}
}
.slick-row:hover .slick-cell{
border-top: $table-hover-border;
border-bottom: $table-hover-border;
background-color: $table-hover-bg-color;
}
#schema-diff-grid .slick-header .slick-header-column.selected {
background-color: $color-primary;
color: $color-primary-fg;
}
.slick-row .slick-cell {
border-bottom: $panel-border;
border-right: $panel-border;
z-index: 0;
}
#schema-diff-grid .slick-row .slick-cell.l0.r0.selected {
background-color: inherit;
}
#schema-diff-grid .slick-row > .slick-cell.selected {
background-color: $table-hover-bg-color !important;
border-top: $table-hover-border;
border-bottom: $table-hover-border;
}
#schema-diff-grid div.slick-header.ui-state-default {
background: $color-bg;
border-bottom: none;
border-right: none;
border-top: $panel-border;
}
#schema-diff-grid .different {
background-color: $schemadiff-diff-row-color !important;
color: $schema-diff-color-fg;
}
#schema-diff-grid .source {
background-color: $schemadiff-source-row-color;
color: $schema-diff-color-fg;
}
#schema-diff-grid .target {
background-color: $schemadiff-target-row-color !important;
color: $schema-diff-color-fg;
}
#schema-diff-grid .slick-row.active {
background-color: $table-bg-selected !important;
color: $schema-diff-color-fg;
}
#schema-diff-ddl-comp {
height: 100%;
bottom: 10px;
background-color: $color-bg !important;
overflow-y: hidden;
}
#schema-diff-grid .slick-group-select-checkbox {
width: 13px;
height: 13px;
margin-right: 1rem;
vertical-align: middle;
display: inline-block;
}
.slick-group-toggle.collapsed::before {
font-family: $font-family-icon;
content: "\f054";
font-size: 0.6rem;
border: none;
font-weight: 900;
}
.slick-group-toggle.expanded::before {
font-family: $font-family-icon;
content: "\f078";
font-size: 0.6rem;
margin-left: 0rem;
font-weight: 900;
}
#schema-diff-ddl-comp .badge .caret::before {
font-family: $font-family-icon;
content: "\f078";
font-size: 0.7rem;
margin-left: 0rem;
font-weight: 900;
}
.slick-group {
color: $input-color !important;
}
.slick-group:hover {
color: $schema-diff-color-fg !important;
}
.slick-group.active {
color: $schema-diff-color-fg !important;
}
.select2-selection__placeholder {
color: $select2-placeholder !important;
}
#btn-ignore-dropdown {
color: $btn-primary-color !important;
background-color: $color-primary !important;
border-color: $color-primary !important;
}

View File

@@ -2,14 +2,10 @@
{% block init_script %}
try {
require(
['sources/generated/slickgrid', 'sources/generated/codemirror', 'sources/generated/browser_nodes'],
['sources/generated/codemirror', 'sources/generated/browser_nodes', 'sources/generated/schema_diff'],
function() {
require(['sources/generated/schema_diff'], function(pgSchemaDiffHook) {
var pgSchemaDiffHook = pgSchemaDiffHook || pgAdmin.Tools.SchemaDiffHook;
pgSchemaDiffHook.load({{trans_id}});
}, function() {
console.log(arguments);
});
var pgSchemaDiff = window.pgAdmin.Tools.SchemaDiff;
pgSchemaDiff.load(document.getElementById('schema-diff-main-container'),{{trans_id}});
},
function() {
console.log(arguments);
@@ -19,18 +15,28 @@ try {
}
{% endblock %}
{% block css_link %}
<style>
#schema-diff-main-container {
display: flex;
flex-direction: column;
height: 100%;
}
#schema-diff-main-container:not(:empty) + .pg-sp-container {
display: none;
}
</style>
<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
{% endblock %}
{% block title %}{{editor_title}}{% endblock %}
{% block body %}
<div id="schema-diff-container">
<div id="diff_fetching_data" class="pg-sp-container schema-diff-busy-fetching d-none">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
<div class="row"><div class="col-12 pg-sp-text schema-diff-busy-text"></div></div>
<div id="schema-diff-main-container" tabindex="0">
<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>
</div>
{% endblock %}