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;
}
.cursorNotAllowed {
cursor: not-allowed;
}
/* container height subtracting top header and bottom scroll bar */
$mainContainerHeight: calc(100vh - 50px - 25px);

View File

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

View File

@ -82,10 +82,6 @@
}
.cursorNotAllowed {
cursor: not-allowed;
}
.apiExplorerWrapper {
display: flex;
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,
perm: '',
},
tableEnum: {
loading: false
},
columnEdit: {},
pkEdit: [''],
pkModify: [''],

View File

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

View File

@ -23,6 +23,7 @@ import {
} from './FilterActions.js';
import { setDefaultQuery, runQuery, setOffset } from './FilterActions';
import Button from '../../../Common/Button/Button';
import ReloadEnumValuesButton from '../Common/ReusableComponents/ReloadEnumValuesButton';
const renderCols = (colName, tableSchema, onChange, usage, key) => {
const columns = tableSchema.columns.map(c => c.column_name);
@ -176,6 +177,7 @@ class FilterQuery extends Component {
render() {
const { dispatch, whereAnd, tableSchema, orderBy } = this.props; // eslint-disable-line no-unused-vars
const styles = require('../../../Common/FilterQuery/FilterQuery.scss');
return (
<div className={styles.add_mar_top}>
<form
@ -209,9 +211,15 @@ class FilterQuery extends Component {
color="yellow"
size="sm"
data-test="run-query"
className={styles.add_mar_right}
>
Run query
</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>
</form>

View File

@ -22,6 +22,9 @@ import {
FETCH_COLUMN_TYPE_CASTS_FAIL,
RESET,
SET_UNIQUE_KEYS,
TOGGLE_ENUM,
TOGGLE_ENUM_SUCCESS,
TOGGLE_ENUM_FAILURE,
} from '../TableModify/ModifyActions';
// TABLE RELATIONSHIPS
@ -586,6 +589,28 @@ const modifyReducer = (tableName, schemas, modifyStateOrig, action) => {
...modifyState,
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:
return modifyState;
}

View File

@ -7,6 +7,7 @@ import { setTable } from '../DataActions';
import JsonInput from '../../../Common/CustomInputTypes/JsonInput';
import TextInput from '../../../Common/CustomInputTypes/TextInput';
import Button from '../../../Common/Button/Button';
import ReloadEnumValuesButton from '../Common/ReusableComponents/ReloadEnumValuesButton';
import { getPlaceholder, BOOLEAN, JSONB, JSONDTYPE, TEXT } from '../utils';
import { NotFoundError } from '../../../Error/PageNotFound';
@ -325,6 +326,10 @@ class InsertItem extends Component {
>
Clear
</Button>
<ReloadEnumValuesButton
dispatch={dispatch}
isEnum={currentTable.is_enum}
/>
</form>
</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 SAVE_UNIQUE_KEY = 'ModifyTable/SAVE_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 toggleEnumSuccess = () => ({
type: TOGGLE_ENUM_SUCCESS,
});
const toggleEnumFailure = () => ({
type: TOGGLE_ENUM_FAILURE,
});
const setForeignKeys = fks => ({
type: SET_FOREIGN_KEYS,
fks,
@ -323,7 +334,7 @@ const saveForeignKeys = (index, tableSchema, columns) => {
.join(', ')})
references "${oldConstraint.ref_table_table_schema}"."${
oldConstraint.ref_table
}"
}"
(${Object.values(oldConstraint.column_mapping)
.map(rc => `"${rc}"`)
.join(', ')})
@ -585,7 +596,7 @@ const deleteTrigger = (trigger, table) => {
downMigrationSql += `CREATE TRIGGER "${triggerName}"
${trigger.action_timing} ${
trigger.event_manipulation
} ON "${tableSchema}"."${tableName}"
} ON "${tableSchema}"."${tableName}"
FOR EACH ${trigger.action_orientation} ${trigger.action_statement};`;
if (trigger.comment) {
@ -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 = (
index,
tableName,
@ -2181,6 +2282,9 @@ export {
SET_FOREIGN_KEYS,
RESET,
SET_UNIQUE_KEYS,
TOGGLE_ENUM,
TOGGLE_ENUM_SUCCESS,
TOGGLE_ENUM_FAILURE,
changeTableOrViewName,
fetchViewDefinition,
handleMigrationErrors,
@ -2211,4 +2315,6 @@ export {
removeUniqueKey,
saveUniqueKey,
deleteTrigger,
toggleEnumSuccess,
toggleEnumFailure,
};

View File

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

View File

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

View File

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

View File

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