add console support for setting table as enum (close #2767) (#2789)

This commit is contained in:
Rishichandra Wawhal 2019-08-30 18:47:51 +05:30 committed by Rikin Kachhia
parent 2d5d3210b5
commit 5dfe3b86f2
15 changed files with 359 additions and 38 deletions

View File

@ -1356,6 +1356,10 @@ code {
width: 325px; width: 325px;
} }
.cursorNotAllowed {
cursor: not-allowed;
}
/* container height subtracting top header and bottom scroll bar */ /* container height subtracting top header and bottom scroll bar */
$mainContainerHeight: calc(100vh - 50px - 25px); $mainContainerHeight: calc(100vh - 50px - 25px);

View File

@ -14,14 +14,20 @@ const WarningSymbol = ({
return ( return (
<div className={styles.display_inline}> <div className={styles.display_inline}>
<OverlayTrigger placement={tooltipPlacement} overlay={tooltip}> <OverlayTrigger placement={tooltipPlacement} overlay={tooltip}>
<WarningIcon customStyle={customStyle} />
</OverlayTrigger>
</div>
);
};
export const WarningIcon = ({ customStyle }) => {
return (
<i <i
className={`fa fa-exclamation-triangle ${styles.warningSymbol} ${ className={`fa fa-exclamation-triangle ${styles.warningSymbol} ${
customStyle ? customStyle : '' customStyle ? customStyle : ''
}`} }`}
aria-hidden="true" aria-hidden="true"
/> />
</OverlayTrigger>
</div>
); );
}; };

View File

@ -82,10 +82,6 @@
} }
.cursorNotAllowed {
cursor: not-allowed;
}
.apiExplorerWrapper { .apiExplorerWrapper {
display: flex; display: flex;
height: $mainContainerHeight; height: $mainContainerHeight;

View File

@ -0,0 +1,72 @@
import React from 'react';
import Toggle from 'react-toggle';
import styles from '../../../../Common/Common.scss';
const enumCompatibilityDocsUrl =
'https://docs.hasura.io/1.0/graphql/manual/schema/enums.html#create-an-enum-table';
export const EnumTableModifyWarning = ({ isEnum }) => {
if (!isEnum) {
return null;
}
return (
<div className={styles.add_mar_bottom}>
<i>
* This table is set as an enum. Modifying it may cause your Hasura
metadata to become inconsistent.
<br />
<a
href={enumCompatibilityDocsUrl}
target="_blank"
rel="noopener noreferrer"
>
See enum table requirements.
</a>
</i>
</div>
);
};
const EnumsSection = ({ isEnum, toggleEnum, loading }) => {
let title;
if (loading) {
title = 'Please wait...';
}
const getCompatibilityNote = () => {
return (
<div>
<i>
* The table must meet some requirements for you to set it as an enum.{' '}
<a
href={enumCompatibilityDocsUrl}
target="_blank"
rel="noopener noreferrer"
>
See requirements.
</a>
</i>
</div>
);
};
return (
<div>
<h4 className={`${styles.subheading_text}`}>Set table as enum</h4>
<div
className={`${styles.display_flex} ${styles.add_mar_bottom}`}
title={title}
data-toggle="tooltip"
>
<span className={styles.add_mar_right_mid}>
Expose the table values as GraphQL enums
</span>
<Toggle checked={isEnum} icons={false} onChange={toggleEnum} />
</div>
{getCompatibilityNote()}
</div>
);
};
export default EnumsSection;

View File

@ -0,0 +1,32 @@
import React from 'react';
import ReloadEnumMetadata from '../../../Metadata/MetadataOptions/ReloadMetadata';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import styles from '../../../../Common/Common.scss';
const ReloadEnumValuesButton = ({ isEnum, dispatch, tooltipStyle }) => {
if (!isEnum) return null;
const tooltip = (
<Tooltip id="tooltip-reload-enum-metadata">
Reload enum values in your GraphQL schema after inserting, updating or
deleting enum values
</Tooltip>
);
return (
<React.Fragment>
<ReloadEnumMetadata buttonText="Reload enum values" dispatch={dispatch} />
<OverlayTrigger overlay={tooltip} placement="right">
<i
className={`fa fa-info-circle ${
styles.cursorPointer
} ${tooltipStyle || ''}`}
aria-hidden="true"
/>
</OverlayTrigger>
</React.Fragment>
);
};
export default ReloadEnumValuesButton;

View File

@ -90,6 +90,9 @@ const defaultModifyState = {
rel: null, rel: null,
perm: '', perm: '',
}, },
tableEnum: {
loading: false
},
columnEdit: {}, columnEdit: {},
pkEdit: [''], pkEdit: [''],
pkModify: [''], pkModify: [''],

View File

@ -8,6 +8,8 @@ import JsonInput from '../../../Common/CustomInputTypes/JsonInput';
import TextInput from '../../../Common/CustomInputTypes/TextInput'; import TextInput from '../../../Common/CustomInputTypes/TextInput';
import Button from '../../../Common/Button/Button'; import Button from '../../../Common/Button/Button';
import ReloadEnumValuesButton from '../Common/ReusableComponents/ReloadEnumValuesButton';
import { import {
getPlaceholder, getPlaceholder,
INTEGER, INTEGER,
@ -69,9 +71,12 @@ class EditItem extends Component {
} }
const styles = require('../../../Common/TableCommon/Table.scss'); const styles = require('../../../Common/TableCommon/Table.scss');
const columns = schemas.find(
const currentTable = schemas.find(
x => x.table_name === tableName && x.table_schema === currentSchema x => x.table_name === tableName && x.table_schema === currentSchema
).columns; );
const columns = currentTable.columns;
const refs = {}; const refs = {};
const elements = columns.map((col, i) => { const elements = columns.map((col, i) => {
@ -246,6 +251,10 @@ class EditItem extends Component {
> >
{buttonText} {buttonText}
</Button> </Button>
<ReloadEnumValuesButton
dispatch={dispatch}
isEnum={currentTable.is_enum}
/>
</form> </form>
</div> </div>
<div className="col-xs-3">{alert}</div> <div className="col-xs-3">{alert}</div>

View File

@ -23,6 +23,7 @@ import {
} from './FilterActions.js'; } from './FilterActions.js';
import { setDefaultQuery, runQuery, setOffset } from './FilterActions'; import { setDefaultQuery, runQuery, setOffset } from './FilterActions';
import Button from '../../../Common/Button/Button'; import Button from '../../../Common/Button/Button';
import ReloadEnumValuesButton from '../Common/ReusableComponents/ReloadEnumValuesButton';
const renderCols = (colName, tableSchema, onChange, usage, key) => { const renderCols = (colName, tableSchema, onChange, usage, key) => {
const columns = tableSchema.columns.map(c => c.column_name); const columns = tableSchema.columns.map(c => c.column_name);
@ -176,6 +177,7 @@ class FilterQuery extends Component {
render() { render() {
const { dispatch, whereAnd, tableSchema, orderBy } = this.props; // eslint-disable-line no-unused-vars const { dispatch, whereAnd, tableSchema, orderBy } = this.props; // eslint-disable-line no-unused-vars
const styles = require('../../../Common/FilterQuery/FilterQuery.scss'); const styles = require('../../../Common/FilterQuery/FilterQuery.scss');
return ( return (
<div className={styles.add_mar_top}> <div className={styles.add_mar_top}>
<form <form
@ -209,9 +211,15 @@ class FilterQuery extends Component {
color="yellow" color="yellow"
size="sm" size="sm"
data-test="run-query" data-test="run-query"
className={styles.add_mar_right}
> >
Run query Run query
</Button> </Button>
<ReloadEnumValuesButton
dispatch={dispatch}
isEnum={tableSchema.is_enum}
tooltipStyle={styles.add_mar_left_mid}
/>
{/* <div className={styles.count + ' alert alert-info'}><i>Total <b>{tableName}</b> rows in the database for current query: {count} </i></div> */} {/* <div className={styles.count + ' alert alert-info'}><i>Total <b>{tableName}</b> rows in the database for current query: {count} </i></div> */}
</div> </div>
</form> </form>

View File

@ -22,6 +22,9 @@ import {
FETCH_COLUMN_TYPE_CASTS_FAIL, FETCH_COLUMN_TYPE_CASTS_FAIL,
RESET, RESET,
SET_UNIQUE_KEYS, SET_UNIQUE_KEYS,
TOGGLE_ENUM,
TOGGLE_ENUM_SUCCESS,
TOGGLE_ENUM_FAILURE,
} from '../TableModify/ModifyActions'; } from '../TableModify/ModifyActions';
// TABLE RELATIONSHIPS // TABLE RELATIONSHIPS
@ -586,6 +589,28 @@ const modifyReducer = (tableName, schemas, modifyStateOrig, action) => {
...modifyState, ...modifyState,
uniqueKeyModify: action.keys, uniqueKeyModify: action.keys,
}; };
case TOGGLE_ENUM:
return {
...modifyState,
tableEnum: {
loading: true,
},
};
case TOGGLE_ENUM_FAILURE:
return {
...modifyState,
tableEnum: {
loading: false,
error: action.error,
},
};
case TOGGLE_ENUM_SUCCESS:
return {
...modifyState,
tableEnum: {
loading: false,
},
};
default: default:
return modifyState; return modifyState;
} }

View File

@ -7,6 +7,7 @@ import { setTable } from '../DataActions';
import JsonInput from '../../../Common/CustomInputTypes/JsonInput'; import JsonInput from '../../../Common/CustomInputTypes/JsonInput';
import TextInput from '../../../Common/CustomInputTypes/TextInput'; import TextInput from '../../../Common/CustomInputTypes/TextInput';
import Button from '../../../Common/Button/Button'; import Button from '../../../Common/Button/Button';
import ReloadEnumValuesButton from '../Common/ReusableComponents/ReloadEnumValuesButton';
import { getPlaceholder, BOOLEAN, JSONB, JSONDTYPE, TEXT } from '../utils'; import { getPlaceholder, BOOLEAN, JSONB, JSONDTYPE, TEXT } from '../utils';
import { NotFoundError } from '../../../Error/PageNotFound'; import { NotFoundError } from '../../../Error/PageNotFound';
@ -325,6 +326,10 @@ class InsertItem extends Component {
> >
Clear Clear
</Button> </Button>
<ReloadEnumValuesButton
dispatch={dispatch}
isEnum={currentTable.is_enum}
/>
</form> </form>
</div> </div>
<div className="col-xs-3">{alert}</div> <div className="col-xs-3">{alert}</div>

View File

@ -65,9 +65,20 @@ const FETCH_COLUMN_TYPE_CASTS_FAIL = 'ModifyTable/FETCH_COLUMN_TYPE_CASTS_FAIL';
const SET_UNIQUE_KEYS = 'ModifyTable/SET_UNIQUE_KEYS'; const SET_UNIQUE_KEYS = 'ModifyTable/SET_UNIQUE_KEYS';
const SAVE_UNIQUE_KEY = 'ModifyTable/SAVE_UNIQUE_KEY'; const SAVE_UNIQUE_KEY = 'ModifyTable/SAVE_UNIQUE_KEY';
const REMOVE_UNIQUE_KEY = 'ModifyTable/REMOVE_UNIQUE_KEY'; const REMOVE_UNIQUE_KEY = 'ModifyTable/REMOVE_UNIQUE_KEY';
const TOGGLE_ENUM = 'ModifyTable/TOGGLE_ENUM';
const TOGGLE_ENUM_SUCCESS = 'ModifyTable/TOGGLE_ENUM_SUCCESS';
const TOGGLE_ENUM_FAILURE = 'ModifyTable/TOGGLE_ENUM_FAILURE';
const RESET = 'ModifyTable/RESET'; const RESET = 'ModifyTable/RESET';
const toggleEnumSuccess = () => ({
type: TOGGLE_ENUM_SUCCESS,
});
const toggleEnumFailure = () => ({
type: TOGGLE_ENUM_FAILURE,
});
const setForeignKeys = fks => ({ const setForeignKeys = fks => ({
type: SET_FOREIGN_KEYS, type: SET_FOREIGN_KEYS,
fks, fks,
@ -2055,6 +2066,96 @@ const removeUniqueKey = (index, tableName, existingConstraints, callback) => {
}; };
}; };
export const toggleTableAsEnum = (isEnum, successCallback, failureCallback) => (
dispatch,
getState
) => {
const isOk = window.confirm(
`Are you sure you want to ${isEnum ? 'un' : ''}set this table as an enum?`
);
if (!isOk) {
return;
}
dispatch({ type: TOGGLE_ENUM });
const { currentTable, currentSchema } = getState().tables;
const { allSchemas } = getState().tables;
const getEnumQuery = is_enum => ({
type: 'set_table_is_enum',
args: {
table: {
schema: currentSchema,
name: currentTable,
},
is_enum,
},
});
const upQuery = [getEnumQuery(!isEnum)];
const downQuery = [getEnumQuery(isEnum)];
const migrationName =
'alter_table_' +
currentSchema +
'_' +
currentTable +
'_set_enum_' +
!isEnum;
const action = !isEnum ? 'Setting' : 'Unsetting';
const requestMsg = `${action} table as enum...`;
const successMsg = `${action} table as enum successful`;
const errorMsg = `${action} table as enum failed`;
const customOnSuccess = () => {
// success callback
if (successCallback) {
successCallback();
}
dispatch(toggleEnumSuccess());
const newAllSchemas = allSchemas.map(schema => {
if (
schema.table_name === currentTable &&
schema.table_schema === currentSchema
) {
return {
...schema,
is_enum: !isEnum,
};
}
return schema;
});
dispatch({ type: LOAD_SCHEMA, allSchemas: newAllSchemas });
};
const customOnError = () => {
dispatch(toggleEnumFailure());
if (failureCallback) {
failureCallback();
}
};
makeMigrationCall(
dispatch,
getState,
upQuery,
downQuery,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
);
};
const saveUniqueKey = ( const saveUniqueKey = (
index, index,
tableName, tableName,
@ -2181,6 +2282,9 @@ export {
SET_FOREIGN_KEYS, SET_FOREIGN_KEYS,
RESET, RESET,
SET_UNIQUE_KEYS, SET_UNIQUE_KEYS,
TOGGLE_ENUM,
TOGGLE_ENUM_SUCCESS,
TOGGLE_ENUM_FAILURE,
changeTableOrViewName, changeTableOrViewName,
fetchViewDefinition, fetchViewDefinition,
handleMigrationErrors, handleMigrationErrors,
@ -2211,4 +2315,6 @@ export {
removeUniqueKey, removeUniqueKey,
saveUniqueKey, saveUniqueKey,
deleteTrigger, deleteTrigger,
toggleEnumSuccess,
toggleEnumFailure,
}; };

View File

@ -4,11 +4,15 @@ import TableHeader from '../TableCommon/TableHeader';
import { getAllDataTypeMap } from '../Common/utils'; import { getAllDataTypeMap } from '../Common/utils';
import { TABLE_ENUMS_SUPPORT } from '../../../../helpers/versionUtils';
import globals from '../../../../Globals';
import { import {
deleteTableSql, deleteTableSql,
untrackTableSql, untrackTableSql,
RESET, RESET,
setUniqueKeys, setUniqueKeys,
toggleTableAsEnum,
} from '../TableModify/ModifyActions'; } from '../TableModify/ModifyActions';
import { import {
setTable, setTable,
@ -20,6 +24,9 @@ import ColumnEditorList from './ColumnEditorList';
import ColumnCreator from './ColumnCreator'; import ColumnCreator from './ColumnCreator';
import PrimaryKeyEditor from './PrimaryKeyEditor'; import PrimaryKeyEditor from './PrimaryKeyEditor';
import TableCommentEditor from './TableCommentEditor'; import TableCommentEditor from './TableCommentEditor';
import EnumsSection, {
EnumTableModifyWarning,
} from '../Common/ReusableComponents/EnumsSection';
import ForeignKeyEditor from './ForeignKeyEditor'; import ForeignKeyEditor from './ForeignKeyEditor';
import UniqueKeyEditor from './UniqueKeyEditor'; import UniqueKeyEditor from './UniqueKeyEditor';
import TriggerEditorList from './TriggerEditorList'; import TriggerEditorList from './TriggerEditorList';
@ -54,6 +61,7 @@ class ModifyTable extends React.Component {
uniqueKeyModify, uniqueKeyModify,
columnDefaultFunctions, columnDefaultFunctions,
schemaList, schemaList,
tableEnum,
} = this.props; } = this.props;
const dataTypeIndexMap = getAllDataTypeMap(dataTypes); const dataTypeIndexMap = getAllDataTypeMap(dataTypes);
@ -74,7 +82,7 @@ class ModifyTable extends React.Component {
color="white" color="white"
size="sm" size="sm"
onClick={() => { onClick={() => {
const isOk = confirm('Are you sure to untrack?'); const isOk = confirm('Are you sure?');
if (isOk) { if (isOk) {
dispatch(untrackTableSql(tableName)); dispatch(untrackTableSql(tableName));
} }
@ -102,6 +110,26 @@ class ModifyTable extends React.Component {
</Button> </Button>
); );
const getEnumsSection = () => {
const supportEnums =
globals.featuresCompatibility &&
globals.featuresCompatibility[TABLE_ENUMS_SUPPORT];
if (!supportEnums) return null;
const toggleEnum = () => dispatch(toggleTableAsEnum(tableSchema.is_enum));
return (
<React.Fragment>
<EnumsSection
isEnum={tableSchema.is_enum}
toggleEnum={toggleEnum}
loading={tableEnum.loading}
/>
<hr />
</React.Fragment>
);
};
// if (tableSchema.primary_key.columns > 0) {} // if (tableSchema.primary_key.columns > 0) {}
return ( return (
<div className={`${styles.container} container-fluid`}> <div className={`${styles.container} container-fluid`}>
@ -127,6 +155,7 @@ class ModifyTable extends React.Component {
isTable isTable
dispatch={dispatch} dispatch={dispatch}
/> />
<EnumTableModifyWarning isEnum={tableSchema.is_enum} />
<h4 className={styles.subheading_text}>Columns</h4> <h4 className={styles.subheading_text}>Columns</h4>
<ColumnEditorList <ColumnEditorList
validTypeCasts={validTypeCasts} validTypeCasts={validTypeCasts}
@ -178,6 +207,7 @@ class ModifyTable extends React.Component {
<h4 className={styles.subheading_text}>Triggers</h4> <h4 className={styles.subheading_text}>Triggers</h4>
<TriggerEditorList tableSchema={tableSchema} dispatch={dispatch} /> <TriggerEditorList tableSchema={tableSchema} dispatch={dispatch} />
<hr /> <hr />
{getEnumsSection()}
{untrackBtn} {untrackBtn}
{deleteBtn} {deleteBtn}
<br /> <br />

View File

@ -1,3 +1,6 @@
import globals from '../../../Globals';
import { TABLE_ENUMS_SUPPORT } from '../../../helpers/versionUtils';
export const INTEGER = 'integer'; export const INTEGER = 'integer';
export const SERIAL = 'serial'; export const SERIAL = 'serial';
export const BIGINT = 'bigint'; export const BIGINT = 'bigint';
@ -228,6 +231,14 @@ export const fetchTrackedTableListQuery = options => {
order_by: [{ column: 'table_name', type: 'asc' }], order_by: [{ column: 'table_name', type: 'asc' }],
}, },
}; };
const supportEnums =
globals.featuresCompatibility &&
globals.featuresCompatibility[TABLE_ENUMS_SUPPORT];
if (supportEnums) {
query.args.columns.push('is_enum');
}
if ( if (
(options.schemas && options.schemas.length !== 0) || (options.schemas && options.schemas.length !== 0) ||
(options.tables && options.tables.length !== 0) (options.tables && options.tables.length !== 0)
@ -468,12 +479,14 @@ export const mergeLoadSchemaData = (
let _uniqueConstraints = []; let _uniqueConstraints = [];
let _fkConstraints = []; let _fkConstraints = [];
let _refFkConstraints = []; let _refFkConstraints = [];
let _isEnum = false;
if (_isTableTracked) { if (_isTableTracked) {
_primaryKey = trackedTableInfo.primary_key; _primaryKey = trackedTableInfo.primary_key;
_relationships = trackedTableInfo.relationships; _relationships = trackedTableInfo.relationships;
_permissions = trackedTableInfo.permissions; _permissions = trackedTableInfo.permissions;
_uniqueConstraints = trackedTableInfo.unique_constraints; _uniqueConstraints = trackedTableInfo.unique_constraints;
_isEnum = trackedTableInfo.is_enum;
_fkConstraints = fkData.filter( _fkConstraints = fkData.filter(
fk => fk.table_schema === _tableSchema && fk.table_name === _tableName fk => fk.table_schema === _tableSchema && fk.table_name === _tableName
@ -501,6 +514,7 @@ export const mergeLoadSchemaData = (
foreign_key_constraints: _fkConstraints, foreign_key_constraints: _fkConstraints,
opp_foreign_key_constraints: _refFkConstraints, opp_foreign_key_constraints: _refFkConstraints,
view_info: _viewInfo, view_info: _viewInfo,
is_enum: _isEnum,
}; };
_mergedTableData.push(_mergedInfo); _mergedTableData.push(_mergedInfo);

View File

@ -11,15 +11,23 @@ import {
class ReloadMetadata extends Component { class ReloadMetadata extends Component {
constructor() { constructor() {
super(); super();
this.state = {};
this.state.isReloading = false; this.state = {
isReloading: false,
};
} }
render() { render() {
const { dispatch } = this.props; const { dispatch } = this.props;
const { isReloading } = this.state; const { isReloading } = this.state;
const metaDataStyles = require('../Metadata.scss'); const metaDataStyles = require('../Metadata.scss');
const reloadMetadataAndLoadInconsistentMetadata = () => {
const reloadMetadataAndLoadInconsistentMetadata = e => {
e.preventDefault();
this.setState({ isReloading: true }); this.setState({ isReloading: true });
dispatch( dispatch(
reloadMetadata( reloadMetadata(
() => { () => {
@ -42,9 +50,10 @@ class ReloadMetadata extends Component {
data-test="data-reload-metadata" data-test="data-reload-metadata"
color="white" color="white"
size="sm" size="sm"
disabled={this.state.isReloading}
onClick={reloadMetadataAndLoadInconsistentMetadata} onClick={reloadMetadataAndLoadInconsistentMetadata}
> >
{buttonText} {this.props.buttonText || buttonText}
</Button> </Button>
</div> </div>
); );
@ -53,7 +62,7 @@ class ReloadMetadata extends Component {
ReloadMetadata.propTypes = { ReloadMetadata.propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
dataHeaders: PropTypes.object.isRequired, buttonText: PropTypes.string,
}; };
export default ReloadMetadata; export default ReloadMetadata;

View File

@ -4,6 +4,7 @@ export const FT_JWT_ANALYZER = 'JWTAnalyzer';
export const RELOAD_METADATA_API_CHANGE = 'reloadMetaDataApiChange'; export const RELOAD_METADATA_API_CHANGE = 'reloadMetaDataApiChange';
export const REMOTE_SCHEMA_TIMEOUT_CONF_SUPPORT = export const REMOTE_SCHEMA_TIMEOUT_CONF_SUPPORT =
'remoteSchemaTimeoutConfSupport'; 'remoteSchemaTimeoutConfSupport';
export const TABLE_ENUMS_SUPPORT = 'tableEnumsSupport';
// list of feature launch versions // list of feature launch versions
const featureLaunchVersions = { const featureLaunchVersions = {
@ -11,6 +12,7 @@ const featureLaunchVersions = {
[RELOAD_METADATA_API_CHANGE]: 'v1.0.0-beta.3', [RELOAD_METADATA_API_CHANGE]: 'v1.0.0-beta.3',
[FT_JWT_ANALYZER]: 'v1.0.0-beta.3', [FT_JWT_ANALYZER]: 'v1.0.0-beta.3',
[REMOTE_SCHEMA_TIMEOUT_CONF_SUPPORT]: 'v1.0.0-beta.5', [REMOTE_SCHEMA_TIMEOUT_CONF_SUPPORT]: 'v1.0.0-beta.5',
[TABLE_ENUMS_SUPPORT]: 'v1.0.0-beta.6'
}; };
export const getFeaturesCompatibility = serverVersion => { export const getFeaturesCompatibility = serverVersion => {