diff --git a/console/src/components/ApiExplorer/GraphiQLWrapper.js b/console/src/components/ApiExplorer/GraphiQLWrapper.js
index c124504e6528b..cd1fecaa263a5 100644
--- a/console/src/components/ApiExplorer/GraphiQLWrapper.js
+++ b/console/src/components/ApiExplorer/GraphiQLWrapper.js
@@ -97,6 +97,7 @@ class GraphiQLWrapper extends Component {
const { supportAnalyze, analyzeApiChange, headerFocus } = this.state;
const { numberOfTables, queryParams } = this.props;
+
const graphqlNetworkData = this.props.data;
const graphQLFetcher = graphQLParams => {
diff --git a/console/src/components/Common/Common.scss b/console/src/components/Common/Common.scss
index 0c9c72b348433..68475d532cdbe 100644
--- a/console/src/components/Common/Common.scss
+++ b/console/src/components/Common/Common.scss
@@ -384,6 +384,9 @@ input {
.remove_margin_bottom {
margin-bottom: 0 !important;
}
+.remove_margin_right {
+ margin-right: 0 !important;
+}
.remove_margin {
margin: 0 !important;
@@ -488,6 +491,10 @@ input {
margin-bottom: 10px;
}
+.add_mar_bottom_small {
+ margin-bottom: 5px;
+}
+
.add_mar_right {
margin-right: 20px !important;
}
@@ -1248,9 +1255,22 @@ code {
width: 100% !important;
}
+.wd150px {
+ width: 150px;
+}
+
+.cursorPointer {
+ cursor: pointer;
+}
+
+.display_flex_centered {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
/* container height subtracting top header and bottom scroll bar */
$mainContainerHeight: calc(100vh - 50px - 25px);
/* Min container width below which horizontal scroll will appear */
$minContainerWidth: 1200px;
-
diff --git a/console/src/components/Services/Data/DataState.js b/console/src/components/Services/Data/DataState.js
index dba4098be5755..242f5e45f4191 100644
--- a/console/src/components/Services/Data/DataState.js
+++ b/console/src/components/Services/Data/DataState.js
@@ -113,6 +113,7 @@ const defaultModifyState = {
tables: [],
},
},
+ remoteRelationships: [],
permissionsState: { ...defaultPermissionsState },
prevPermissionState: { ...defaultPermissionsState },
ongoingRequest: false,
diff --git a/console/src/components/Services/Data/TableCommon/TableReducer.js b/console/src/components/Services/Data/TableCommon/TableReducer.js
index 86fe94dac44aa..2e9c04a0de825 100644
--- a/console/src/components/Services/Data/TableCommon/TableReducer.js
+++ b/console/src/components/Services/Data/TableCommon/TableReducer.js
@@ -34,6 +34,7 @@ import {
REL_ADD_NEW_CLICKED,
REL_SET_MANUAL_COLUMNS,
REL_ADD_MANUAL_CLICKED,
+ LOAD_REMOTE_RELATIONSHIPS,
} from '../TableRelationships/Actions';
// TABLE PERMISSIONS
@@ -198,6 +199,11 @@ const modifyReducer = (tableName, schemas, modifyStateOrig, action) => {
manualColumns: action.data,
},
};
+ case LOAD_REMOTE_RELATIONSHIPS:
+ return {
+ ...modifyState,
+ remoteRelationships: action.data,
+ };
case TABLE_COMMENT_EDIT:
return {
...modifyState,
diff --git a/console/src/components/Services/Data/TablePermissions/Permissions.js b/console/src/components/Services/Data/TablePermissions/Permissions.js
index 5c3ceea036a0b..2e2089002af58 100644
--- a/console/src/components/Services/Data/TablePermissions/Permissions.js
+++ b/console/src/components/Services/Data/TablePermissions/Permissions.js
@@ -773,7 +773,6 @@ class Permissions extends Component {
queryLabel
)
);
-
if (isSelected) {
_filterOptionsSection.push(selectedValue);
}
@@ -1422,7 +1421,6 @@ class Permissions extends Component {
let _deleteBtn;
const presetType = getPresetValueType(preset);
-
if (presetType) {
_deleteBtn = (
);
}
-
return _deleteBtn;
};
diff --git a/console/src/components/Services/Data/TableRelationships/Actions.js b/console/src/components/Services/Data/TableRelationships/Actions.js
index 5f00cfb6d0d10..f420ece77f744 100644
--- a/console/src/components/Services/Data/TableRelationships/Actions.js
+++ b/console/src/components/Services/Data/TableRelationships/Actions.js
@@ -21,6 +21,9 @@ export const REL_ADD_NEW_CLICKED = 'ModifyTable/REL_ADD_NEW_CLICKED';
export const REL_SET_MANUAL_COLUMNS = 'ModifyTable/REL_SET_MANUAL_COLUMNS';
export const REL_ADD_MANUAL_CLICKED = 'ModifyTable/REL_ADD_MANUAL_CLICKED';
+export const LOAD_REMOTE_RELATIONSHIPS =
+ 'ModifyTable/LOAD_REMOTE_RELATIONSHIPS';
+
const resetRelationshipForm = () => ({ type: REL_RESET });
const addNewRelClicked = () => ({ type: REL_ADD_NEW_CLICKED });
const relManualAddClicked = () => ({ type: REL_ADD_MANUAL_CLICKED });
diff --git a/console/src/components/Services/Data/TableRelationships/Relationships.js b/console/src/components/Services/Data/TableRelationships/Relationships.js
index a1e9f667663f9..652ee9c33a365 100644
--- a/console/src/components/Services/Data/TableRelationships/Relationships.js
+++ b/console/src/components/Services/Data/TableRelationships/Relationships.js
@@ -20,6 +20,7 @@ import { getRelDef, getObjArrRelList } from './utils';
import Button from '../../../Common/Button/Button';
import AddManualRelationship from './AddManualRelationship';
+import RemoteRelationships from './RemoteRelationships/Wrapper';
import suggestedRelationshipsRaw from './autoRelations';
import RelationshipEditor from './RelationshipEditor';
import semverCheck from '../../../../helpers/semver';
@@ -153,8 +154,8 @@ const AddRelationship = ({
);
@@ -354,6 +355,7 @@ class Relationships extends Component {
currentSchema,
migrationMode,
schemaList,
+ remoteRelationships,
} = this.props;
const styles = require('../TableModify/ModifyTable.scss');
const tableStyles = require('../../../Common/TableCommon/TableStyles.scss');
@@ -466,7 +468,7 @@ class Relationships extends Component {
-
Relationships
+ Table Relationships
{addedRelationshipsView}
{relAdd.isActive ? (
@@ -514,7 +516,7 @@ class Relationships extends Component {
)}
+ Remote Relationships
+
{alert}
@@ -541,6 +549,7 @@ Relationships.propTypes = {
relAdd: PropTypes.object.isRequired,
migrationMode: PropTypes.bool.isRequired,
ongoingRequest: PropTypes.bool.isRequired,
+ remoteRelationships: PropTypes.array,
lastError: PropTypes.object,
lastFormError: PropTypes.object,
lastSuccess: PropTypes.bool,
diff --git a/console/src/components/Services/Data/TableRelationships/RemoteRelationships/AddRemoteRelationship.js b/console/src/components/Services/Data/TableRelationships/RemoteRelationships/AddRemoteRelationship.js
new file mode 100644
index 0000000000000..21d0bc44ac441
--- /dev/null
+++ b/console/src/components/Services/Data/TableRelationships/RemoteRelationships/AddRemoteRelationship.js
@@ -0,0 +1,304 @@
+import React from 'react';
+import ExpandableEditor from '../../../../Common/Layout/ExpandableEditor/Editor';
+import styles from '../../TableModify/ModifyTable.scss';
+import { showErrorNotification } from '../../Notification';
+import {
+ useRemoteSchemasEdit,
+ useRemoteSchemas,
+ saveRemoteRelQuery,
+} from './remoteRelationshipUtils';
+
+const AddRemoteRelationship = ({ dispatch, tableSchema }) => {
+ const schemaInfo = useRemoteSchemas(dispatch).schemas || [];
+ const {
+ relName,
+ setRelName,
+ schemaName,
+ setSchemaName,
+ fieldNamePath,
+ setFieldNamePath,
+ inputField,
+ setInputField,
+ tableColumn,
+ setTableColumn,
+ reset,
+ nested,
+ setNested,
+ } = useRemoteSchemasEdit();
+ const remoteSchemas = schemaInfo
+ .filter(s => s.schema_name !== 'hasura')
+ .map(s => s.schema_name);
+ let schema = {};
+
+ let fields = [];
+ if (schemaName) {
+ schema = schemaInfo.find(s => s.schema_name === schemaName);
+ fields = schema.fields.map(f => f.name);
+ }
+
+ let selectedField;
+ let hasChildren;
+ let inputFields = [];
+ let childrenFields = [];
+ const fieldPathLength = fieldNamePath.length;
+ if (fieldPathLength > 0) {
+ selectedField = fieldNamePath[0];
+ const parentField = schema.fields.find(f => f.name === fieldNamePath[0]);
+ if (parentField.selection_fields.length > 0) {
+ hasChildren = true;
+ childrenFields = parentField.selection_fields.map(f => f.name);
+ }
+ if (fieldPathLength === 1) {
+ inputFields = Object.keys(parentField.input_types);
+ } else {
+ const selectedChildField = parentField.selection_fields.find(
+ sf => sf.name === fieldNamePath[1]
+ );
+ inputFields = Object.keys(selectedChildField.input_types);
+ }
+ }
+
+ const setFieldNameInFieldPath = (name, i = 0) => {
+ if (i === 0) {
+ setFieldNamePath([name]);
+ } else {
+ setFieldNamePath([...fieldNamePath.slice(0, i), name]);
+ }
+ };
+
+ const columns = tableSchema.columns.map(c => c.column_name);
+ const expanded = () => {
+ const getNestedOptions = () => {
+ let addNestingButton = null;
+ let nestingDropdown = null;
+ let removeNestingButton = null;
+ if (hasChildren) {
+ addNestingButton = (
+ {
+ setNested(true);
+ }}
+ >
+
+
+ );
+ }
+ if (nested && hasChildren) {
+ addNestingButton = (
+
+ .
+
+ );
+ nestingDropdown = (
+
+
+
+ );
+ removeNestingButton = (
+ {
+ setNested(false);
+ }}
+ >
+
+
+ );
+ }
+ return [addNestingButton, nestingDropdown, removeNestingButton];
+ };
+
+ return (
+
+
+
+ Relationship name
+
+
+
+
+
+
+
+ Remote schema
+
+
+
+
+
+
+
+ Field name
+
+
+
+
+
+ {getNestedOptions()}
+
+
+
+
+ Mapping
+ (from table column to input field)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+ const expandButtonText = '+ Add a remote relationship';
+ const saveFunc = toggle => {
+ if (!schemaName) {
+ return dispatch(showErrorNotification('Please select a remote schema'));
+ }
+ dispatch(
+ saveRemoteRelQuery(
+ relName,
+ tableSchema,
+ fieldNamePath,
+ inputField,
+ tableColumn,
+ () => {
+ reset();
+ setNested(false);
+ toggle();
+ }
+ )
+ );
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default AddRemoteRelationship;
diff --git a/console/src/components/Services/Data/TableRelationships/RemoteRelationships/ListRemoteRelationships.js b/console/src/components/Services/Data/TableRelationships/RemoteRelationships/ListRemoteRelationships.js
new file mode 100644
index 0000000000000..b313ea1303510
--- /dev/null
+++ b/console/src/components/Services/Data/TableRelationships/RemoteRelationships/ListRemoteRelationships.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import styles from '../../TableModify/ModifyTable.scss';
+import ExpandableEditor from '../../../../Common/Layout/ExpandableEditor/Editor';
+import {
+ getRemoteRelDef,
+ deleteRemoteRelationship,
+} from './remoteRelationshipUtils';
+
+const ListRemoteRelationships = props => {
+ const { tableSchema, remoteRels, dispatch } = props;
+ return remoteRels.map(rel => {
+ const { rel_def, rel_name } = rel;
+
+ const collapsedLabel = () => (
+
+ );
+
+ const expandedLabel = () => {
+ return (
+
+ );
+ };
+
+ const expanded = () => {
+ return (
+
+ {getRemoteRelDef(rel_def)}
+
+
+ );
+ };
+
+ const expandButtonText = 'View';
+
+ const removeFunc = () => {
+ const isOk = window.confirm('Are you sure?');
+ if (isOk) {
+ dispatch(deleteRemoteRelationship(tableSchema, rel_name));
+ }
+ };
+
+ return (
+
+
+
+ );
+ });
+};
+
+export default ListRemoteRelationships;
diff --git a/console/src/components/Services/Data/TableRelationships/RemoteRelationships/Wrapper.js b/console/src/components/Services/Data/TableRelationships/RemoteRelationships/Wrapper.js
new file mode 100644
index 0000000000000..ec3b3a4b65c15
--- /dev/null
+++ b/console/src/components/Services/Data/TableRelationships/RemoteRelationships/Wrapper.js
@@ -0,0 +1,37 @@
+import React, { useEffect } from 'react';
+import AddRemoteRelationship from './AddRemoteRelationship';
+import ListRemoteRelationships from './ListRemoteRelationships';
+import styles from '../../TableModify/ModifyTable.scss';
+import { loadRemoteRelationships } from './remoteRelationshipUtils';
+
+const RemoteRelationships = props => {
+ // const remoteRels = props.remoteRels;
+ useEffect(() => {
+ props.dispatch(loadRemoteRelationships(props.tableSchema.table_name));
+ }, []);
+ const noRemoteRelsMessage = (
+
+
+
+
+
+ );
+
+ const remoteRelList = ;
+
+ const { remoteRels } = props;
+ const remoteRelContent =
+ remoteRels.length > 0 ? remoteRelList : noRemoteRelsMessage;
+
+ return (
+
+ );
+};
+
+export default RemoteRelationships;
diff --git a/console/src/components/Services/Data/TableRelationships/RemoteRelationships/remoteRelationshipUtils.js b/console/src/components/Services/Data/TableRelationships/RemoteRelationships/remoteRelationshipUtils.js
new file mode 100644
index 0000000000000..5d8ea381027d5
--- /dev/null
+++ b/console/src/components/Services/Data/TableRelationships/RemoteRelationships/remoteRelationshipUtils.js
@@ -0,0 +1,322 @@
+import { useState, useEffect } from 'react';
+import Endpoints from '../../../../../Endpoints';
+import requestAction from '../../../../../utils/requestAction';
+import {
+ showSuccessNotification,
+ showErrorNotification,
+} from '../../Notification';
+import gqlPattern, { gqlRelErrorNotif } from '../../Common/GraphQLValidation';
+import { LOAD_REMOTE_RELATIONSHIPS } from '../Actions';
+
+const genLoadRemoteRelationshipsQuery = tableName => {
+ return {
+ type: 'select',
+ args: {
+ table: {
+ schema: 'hdb_catalog',
+ name: 'hdb_remote_relationship',
+ },
+ columns: ['table_name', 'rel_name', 'rel_def'],
+ where: {
+ table_name: tableName,
+ },
+ },
+ };
+};
+
+const genDropRelationship = (tableName, schemaName, relName) => {
+ return {
+ type: 'drop_remote_relationship',
+ args: {
+ table: {
+ name: tableName,
+ schema: schemaName,
+ },
+ name: relName,
+ },
+ };
+};
+
+const loadRemoteSchemasQuery = {
+ type: 'get_remote_schema_info',
+ args: {},
+};
+
+const generateCreateRemoteRelationshipQuery = (
+ name,
+ tableName,
+ fieldNamePath,
+ inputField,
+ columnName,
+ schemaName,
+ isNameSpaced
+) => {
+ const query = {
+ type: 'create_remote_relationship',
+ args: {
+ name,
+ table: {
+ name: tableName,
+ schema: schemaName,
+ },
+ using: {
+ table: tableName,
+ remote_field: isNameSpaced ? fieldNamePath[1] : fieldNamePath[0],
+ input_field: inputField,
+ column: columnName,
+ },
+ },
+ };
+ if (isNameSpaced) {
+ query.args.using.namespace = fieldNamePath[0];
+ }
+ return query;
+};
+
+export const loadRemoteRelationships = tableName => {
+ return dispatch => {
+ return dispatch(
+ requestAction(Endpoints.query, {
+ method: 'POST',
+ body: JSON.stringify(genLoadRemoteRelationshipsQuery(tableName)),
+ })
+ ).then(
+ data => {
+ dispatch({
+ type: LOAD_REMOTE_RELATIONSHIPS,
+ data,
+ });
+ },
+ error => {
+ console.error(error);
+ }
+ );
+ };
+};
+
+const loadRemoteSchemas = cb => {
+ return dispatch => {
+ return dispatch(
+ requestAction(Endpoints.query, {
+ method: 'POST',
+ body: JSON.stringify(loadRemoteSchemasQuery),
+ })
+ ).then(
+ data => {
+ cb({
+ schemas: data,
+ });
+ },
+ error => {
+ console.error(error);
+ cb({
+ error: error,
+ });
+ }
+ );
+ };
+};
+
+export const useRemoteSchemas = dispatch => {
+ const [remoteSchemas, setRemoteSchemas] = useState({});
+ useEffect(() => {
+ dispatch(loadRemoteSchemas(r => setRemoteSchemas(r)));
+ }, []);
+ return remoteSchemas;
+};
+
+export const useRemoteSchemasEdit = () => {
+ const defaultState = {
+ relName: '',
+ schemaName: '',
+ fieldNamePath: [],
+ inputField: '',
+ tableColumn: '',
+ nested: false,
+ };
+ const [rsState, setRsState] = useState(defaultState);
+
+ const {
+ relName,
+ schemaName,
+ fieldNamePath,
+ inputField,
+ tableColumn,
+ nested,
+ } = rsState;
+ const setRelName = e => {
+ setRsState({
+ ...rsState,
+ relName: e.target.value,
+ });
+ };
+ const setSchemaName = e => {
+ setRsState({
+ ...defaultState,
+ relName: rsState.relName,
+ schemaName: e.target.value,
+ });
+ };
+ const setFieldNamePath = list => {
+ setRsState({
+ ...rsState,
+ inputField: '',
+ tableColumn: '',
+ fieldNamePath: list,
+ });
+ };
+ const setInputField = e => {
+ setRsState({
+ ...rsState,
+ inputField: e.target.value,
+ });
+ };
+ const setTableColumn = e => {
+ setRsState({
+ ...rsState,
+ tableColumn: e.target.value,
+ });
+ };
+
+ const setNested = value => {
+ const fnp = rsState.fieldNamePath;
+ setRsState({
+ ...rsState,
+ fieldNamePath: fnp[0] ? [fnp[0]] : [],
+ nested: value,
+ });
+ };
+
+ const reset = () => {
+ setRsState({
+ ...defaultState,
+ });
+ };
+
+ return {
+ relName,
+ setRelName,
+ schemaName,
+ setSchemaName,
+ fieldNamePath,
+ setFieldNamePath,
+ inputField,
+ setInputField,
+ tableColumn,
+ setTableColumn,
+ reset,
+ nested,
+ setNested,
+ };
+};
+
+export const saveRemoteRelQuery = (
+ name,
+ tableSchema,
+ fieldNamePath,
+ inputField,
+ columnName,
+ successCb,
+ errorCb
+) => {
+ return dispatch => {
+ const tableName = tableSchema.table_name;
+ const schemaName = tableSchema.schema_name;
+ if (!name) {
+ return dispatch(
+ showErrorNotification('Relationship name cannot be empty')
+ );
+ }
+ if (!gqlPattern.test(name)) {
+ return dispatch(
+ showErrorNotification(
+ gqlRelErrorNotif[0],
+ gqlRelErrorNotif[1],
+ gqlRelErrorNotif[2],
+ gqlRelErrorNotif[3]
+ )
+ );
+ }
+ if (fieldNamePath.length === 0) {
+ return dispatch(showErrorNotification('Please select a field'));
+ }
+ if (!inputField) {
+ return dispatch(showErrorNotification('Please select an input field'));
+ }
+ if (!columnName) {
+ return dispatch(showErrorNotification('Please select a table column'));
+ }
+ dispatch(
+ requestAction(Endpoints.query, {
+ method: 'POST',
+ body: JSON.stringify(
+ generateCreateRemoteRelationshipQuery(
+ name,
+ tableName,
+ fieldNamePath,
+ inputField,
+ columnName,
+ schemaName,
+ fieldNamePath.length === 2
+ )
+ ),
+ })
+ ).then(
+ () => {
+ if (successCb) {
+ successCb();
+ }
+ dispatch(loadRemoteRelationships(tableName));
+ dispatch(showSuccessNotification('Remote relationship created'));
+ },
+ err => {
+ console.error(err);
+ if (errorCb) {
+ errorCb();
+ }
+ dispatch(
+ showErrorNotification(
+ 'Failed creating remote relationship',
+ err.error
+ )
+ );
+ }
+ );
+ };
+};
+
+export const getRemoteRelDef = relDef => {
+ const { table, column, input_field, remote_field, namespace } = relDef;
+ if (namespace) {
+ return ` ${table} . ${column} → ${namespace} { ${remote_field} ( ${input_field} ) }`;
+ }
+ return ` ${table} . ${column} → ${remote_field} ( ${input_field} )`;
+};
+
+export const deleteRemoteRelationship = (tableSchema, name) => {
+ return dispatch => {
+ return dispatch(
+ requestAction(Endpoints.query, {
+ method: 'POST',
+ body: JSON.stringify(
+ genDropRelationship(
+ tableSchema.table_name,
+ tableSchema.schema_name,
+ name
+ )
+ ),
+ })
+ ).then(
+ () => {
+ dispatch(loadRemoteRelationships(tableSchema.table_name));
+ dispatch(showSuccessNotification('Remote relationship deleted'));
+ },
+ e => {
+ console.error(e);
+ dispatch(
+ showErrorNotification('Failed deleting remote relationship', e.error)
+ );
+ }
+ );
+ };
+};