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 = () => ( +
+
+
+
+ {rel_name} +   +
+
+
+
+ ); + + const expandedLabel = () => { + return ( +
+
+
+
+ {rel_name} +   +
+
+
+
+ ); + }; + + 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 ( +
+ {remoteRelContent} + +
+ ); +}; + +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) + ); + } + ); + }; +};