suggest column default values (#2352)

This commit is contained in:
Karthik Venkateswaran 2019-06-18 23:03:54 +05:30 committed by Rikin Kachhia
parent 00e911e3cd
commit 93a7c2c734
26 changed files with 662 additions and 83 deletions

View File

@ -62,6 +62,7 @@ export const passMTRenameTable = () => {
}; };
export const passMTRenameColumn = () => { export const passMTRenameColumn = () => {
cy.wait(10000);
cy.get(getElementFromAlias('modify-table-edit-column-0')).click(); cy.get(getElementFromAlias('modify-table-edit-column-0')).click();
cy.get(getElementFromAlias('edit-col-name')) cy.get(getElementFromAlias('edit-col-name'))
.clear() .clear()

View File

@ -12669,6 +12669,26 @@
"integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=", "integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=",
"dev": true "dev": true
}, },
"react-autosuggest": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-9.4.3.tgz",
"integrity": "sha512-wFbp5QpgFQRfw9cwKvcgLR8theikOUkv8PFsuLYqI2PUgVlx186Cz8MYt5bLxculi+jxGGUUVt+h0esaBZZouw==",
"requires": {
"prop-types": "^15.5.10",
"react-autowhatever": "^10.1.2",
"shallow-equal": "^1.0.0"
}
},
"react-autowhatever": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-autowhatever/-/react-autowhatever-10.2.0.tgz",
"integrity": "sha512-dqHH4uqiJldPMbL8hl/i2HV4E8FMTDEdVlOIbRqYnJi0kTpWseF9fJslk/KS9pGDnm80JkYzVI+nzFjnOG/u+g==",
"requires": {
"prop-types": "^15.5.8",
"react-themeable": "^1.1.0",
"section-iterator": "^2.0.0"
}
},
"react-base16-styling": { "react-base16-styling": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz", "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz",
@ -12953,6 +12973,21 @@
"prop-types": "^15.5.0" "prop-types": "^15.5.0"
} }
}, },
"react-themeable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz",
"integrity": "sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4=",
"requires": {
"object-assign": "^3.0.0"
},
"dependencies": {
"object-assign": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
"integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I="
}
}
},
"react-toggle": { "react-toggle": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.0.2.tgz", "resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.0.2.tgz",
@ -14185,6 +14220,11 @@
} }
} }
}, },
"section-iterator": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz",
"integrity": "sha1-v0RNev7rlK1Dw5rS+yYVFifMuio="
},
"semver": { "semver": {
"version": "5.5.1", "version": "5.5.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
@ -14304,6 +14344,11 @@
} }
} }
}, },
"shallow-equal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.1.0.tgz",
"integrity": "sha512-0SW1nWo1hnabO62SEeHsl8nmTVVEzguVWZCj5gaQrgWAxz/BaCja4OWdJBWLVPDxdtE/WU7c98uUCCXyPHSCvw=="
},
"shallowequal": { "shallowequal": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",

View File

@ -79,6 +79,7 @@
"query-string": "^6.1.0", "query-string": "^6.1.0",
"react": "16.8.2", "react": "16.8.2",
"react-ace": "^6.1.1", "react-ace": "^6.1.1",
"react-autosuggest": "^9.4.3",
"react-bootstrap": "^0.32.1", "react-bootstrap": "^0.32.1",
"react-copy-to-clipboard": "^5.0.0", "react-copy-to-clipboard": "^5.0.0",
"react-dom": "16.8.2", "react-dom": "16.8.2",

View File

@ -0,0 +1,65 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Autosuggest from 'react-autosuggest';
const CustomInputAutoSuggest = props => {
const [suggestions, setSuggestions] = useState([]);
const { options, theme = require('./Theme.scss') } = props;
const getSuggestions = value => {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
const filterResults = () => {
return options.map(option => {
return {
title: option.title,
suggestions: option.suggestions.filter(
op => op.value.toLowerCase().slice(0, inputLength) === inputValue
),
};
});
};
return inputLength === 0 ? [...options] : filterResults();
};
const onSuggestionsFetchRequested = ob => {
const { value } = ob;
setSuggestions(getSuggestions(value));
};
const getSuggestionValue = suggestion => suggestion.value;
const onSuggestionsClearRequested = () => {
setSuggestions([]);
};
const renderSuggestion = suggestion => <div>{suggestion.value}</div>;
/* Don't render the section when there are no suggestions in it */
const renderSectionTitle = section => {
return section.suggestions.length > 0 ? section.title : null;
};
const getSectionSuggestions = section => {
return section.suggestions;
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={{ ...props }}
theme={theme}
multiSection
renderSectionTitle={renderSectionTitle}
shouldRenderSuggestions={() => true}
getSectionSuggestions={getSectionSuggestions}
/>
);
};
CustomInputAutoSuggest.propTypes = {
options: PropTypes.array.isRequired,
};
export default CustomInputAutoSuggest;

View File

@ -0,0 +1,11 @@
@import '../Theme.scss';
.suggestionsContainerOpen {
top: 30px;
width: 280px;
left: 5px;
}
.suggestion {
padding: 6px 12px;
}

View File

@ -0,0 +1,10 @@
@import '../Theme.scss';
.suggestionsContainerOpen {
top: 30px;
width: 100%;
}
.suggestion {
padding: 6px 12px;
}

View File

@ -0,0 +1,91 @@
$suggestion-width: 280px;
$set-top: 34px;
$suggestion-padding: 6px 12px;
.container {
position: relative;
}
.input {
border: 1px solid #aaa;
border-radius: 4px;
}
.inputFocussed {
outline: none;
}
.inputOpen {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.suggestionsContainer {
display: none;
}
.suggestionsContainerOpen {
display: block;
position: absolute;
top: $set-top;
min-width: 100%;
width: $suggestion-width;
border: 1px solid #aaa;
background-color: #fff;
border-radius: 4px;
z-index: 4;
}
.suggestionsList {
margin: 0;
padding: 0;
list-style-type: none;
}
.suggestion {
cursor: pointer;
word-break: break-word;
// padding: $suggestion-padding;
background-color: transparent;
color: inherit;
cursor: default;
display: block;
font-size: inherit;
padding: 8px 12px;
width: 100%;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
box-sizing: border-box;
&:hover {
background-color: #DEEBFF;
}
}
.suggestionHighlighted {
background-color: #DEEBFF;
}
.sectionTitle {
color: #999;
cursor: default;
display: block;
font-size: 75%;
font-weight: 500;
margin-bottom: 0.25em;
padding-left: 12px;
padding-right: 12px;
text-transform: uppercase;
box-sizing: border-box;
}
.sectionContainer {
margin-bottom: 5px;
}
.sectionContainerFirst {
margin-top: 10px;
}

View File

@ -38,6 +38,7 @@ const SearchableSelectBox = ({
value, value,
bsClass, bsClass,
styleOverrides, styleOverrides,
placeholder,
filterOption, filterOption,
}) => { }) => {
/* Select element style customization */ /* Select element style customization */
@ -68,7 +69,7 @@ const SearchableSelectBox = ({
isSearchable isSearchable
components={{ Option: CustomOption }} components={{ Option: CustomOption }}
classNamePrefix={`${bsClass}`} classNamePrefix={`${bsClass}`}
placeholder="column_type" placeholder={placeholder}
options={options} options={options}
onChange={onChange} onChange={onChange}
value={value} value={value}

View File

@ -9,6 +9,8 @@ import {
import { UPDATE_MIGRATION_STATUS_ERROR } from '../../../Main/Actions'; import { UPDATE_MIGRATION_STATUS_ERROR } from '../../../Main/Actions';
import { setTable } from '../DataActions.js'; import { setTable } from '../DataActions.js';
import { isPostgresFunction } from '../utils';
const SET_DEFAULTS = 'AddTable/SET_DEFAULTS'; const SET_DEFAULTS = 'AddTable/SET_DEFAULTS';
const SET_TABLENAME = 'AddTable/SET_TABLENAME'; const SET_TABLENAME = 'AddTable/SET_TABLENAME';
const SET_TABLECOMMENT = 'AddTable/SET_TABLECOMMENT'; const SET_TABLECOMMENT = 'AddTable/SET_TABLECOMMENT';
@ -121,7 +123,14 @@ const createTableSql = () => {
) { ) {
if (currentCols[i].type === 'text') { if (currentCols[i].type === 'text') {
// if a column type is text and if it has a default value, add a single quote by default // if a column type is text and if it has a default value, add a single quote by default
const checkIfFunctionFormat = isPostgresFunction(
currentCols[i].default.value
);
if (!checkIfFunctionFormat) {
tableColumns += " DEFAULT '" + currentCols[i].default.value + "'"; tableColumns += " DEFAULT '" + currentCols[i].default.value + "'";
} else {
tableColumns += ' DEFAULT ' + currentCols[i].default.value;
}
} else { } else {
if (currentCols[i].type === 'uuid') { if (currentCols[i].type === 'uuid') {
isUUIDDefault = true; isUUIDDefault = true;

View File

@ -29,7 +29,7 @@ import {
setUniqueKeys, setUniqueKeys,
} from './AddActions'; } from './AddActions';
import { fetchColumnTypes, RESET_COLUMN_TYPE_LIST } from '../DataActions'; import { fetchColumnTypeInfo, RESET_COLUMN_TYPE_INFO } from '../DataActions';
import { setDefaults, setPk, createTableSql } from './AddActions'; import { setDefaults, setPk, createTableSql } from './AddActions';
import { validationError, resetValidation } from './AddActions'; import { validationError, resetValidation } from './AddActions';
@ -71,12 +71,12 @@ class AddTable extends Component {
this.setColDefaultValue = this.setColDefaultValue.bind(this); this.setColDefaultValue = this.setColDefaultValue.bind(this);
} }
componentDidMount() { componentDidMount() {
this.props.dispatch(fetchColumnTypes()); this.props.dispatch(fetchColumnTypeInfo());
} }
componentWillUnmount() { componentWillUnmount() {
this.props.dispatch(setDefaults()); this.props.dispatch(setDefaults());
this.props.dispatch({ this.props.dispatch({
type: RESET_COLUMN_TYPE_LIST, type: RESET_COLUMN_TYPE_INFO,
}); });
} }
onTableNameChange = e => { onTableNameChange = e => {
@ -123,9 +123,9 @@ class AddTable extends Component {
} }
}; };
setColDefaultValue = (i, isNullableChecked, e) => { setColDefaultValue = (i, isNullableChecked, value) => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(setColDefault(e.target.value, i, isNullableChecked)); dispatch(setColDefault(value, i, isNullableChecked));
}; };
columnValidation() { columnValidation() {
@ -285,6 +285,8 @@ class AddTable extends Component {
internalError, internalError,
dataTypes, dataTypes,
schemaList, schemaList,
columnDefaultFunctions,
columnTypeCasts,
} = this.props; } = this.props;
const styles = require('../../../Common/TableCommon/Table.scss'); const styles = require('../../../Common/TableCommon/Table.scss');
const getCreateBtnText = () => { const getCreateBtnText = () => {
@ -322,6 +324,8 @@ class AddTable extends Component {
<TableColumns <TableColumns
uniqueKeys={uniqueKeys} uniqueKeys={uniqueKeys}
dataTypes={dataTypes} dataTypes={dataTypes}
columnDefaultFunctions={columnDefaultFunctions}
columnTypeCasts={columnTypeCasts}
columns={columns} columns={columns}
onRemoveColumn={this.onRemoveColumn} onRemoveColumn={this.onRemoveColumn}
onColumnChange={this.onColumnNameChange} onColumnChange={this.onColumnNameChange}
@ -435,6 +439,8 @@ const mapStateToProps = state => ({
allSchemas: state.tables.allSchemas, allSchemas: state.tables.allSchemas,
currentSchema: state.tables.currentSchema, currentSchema: state.tables.currentSchema,
dataTypes: state.tables.columnDataTypes, dataTypes: state.tables.columnDataTypes,
columnDefaultFunctions: state.tables.columnDefaultFunctions,
columnTypeCasts: state.tables.columnTypeCasts,
columnDataTypeFetchErr: state.tables.columnDataTypeFetchErr, columnDataTypeFetchErr: state.tables.columnDataTypeFetchErr,
schemaList: state.tables.schemaList, schemaList: state.tables.schemaList,
}); });

View File

@ -3,11 +3,9 @@ import PropTypes from 'prop-types';
import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect'; import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect';
import { commonDataTypes } from '../utils'; import { commonDataTypes } from '../utils';
import { import { getDataOptions, inferDefaultValues } from '../Common/utils';
getDataOptions,
getPlaceholder, import TableColumnDefault from './TableColumnDefault';
getDefaultValue,
} from '../Common/utils';
/* Custom style object for searchable select box */ /* Custom style object for searchable select box */
const customSelectBoxStyles = { const customSelectBoxStyles = {
@ -17,6 +15,9 @@ const customSelectBoxStyles = {
singleValue: { singleValue: {
color: '#555555', color: '#555555',
}, },
valueContainer: {
padding: '0px 12px',
},
}; };
const TableColumn = props => { const TableColumn = props => {
@ -32,6 +33,8 @@ const TableColumn = props => {
onColNullableChange, onColNullableChange,
onColUniqueChange, onColUniqueChange,
dataTypes: restTypes, dataTypes: restTypes,
columnDefaultFunctions,
columnTypeCasts,
uniqueKeys, uniqueKeys,
} = props; } = props;
@ -56,6 +59,7 @@ const TableColumn = props => {
restTypes, restTypes,
i i
); );
const getRemoveIcon = colLen => { const getRemoveIcon = colLen => {
let removeIcon; let removeIcon;
if (i + 1 === colLen) { if (i + 1 === colLen) {
@ -71,6 +75,16 @@ const TableColumn = props => {
return removeIcon; return removeIcon;
}; };
/* Collect list of relevant default values if the type doesn't have any default values
* */
const getInferredDefaultValues = () =>
inferDefaultValues(columnDefaultFunctions, columnTypeCasts)(column.type);
const defaultFunctions =
column.type in columnDefaultFunctions
? columnDefaultFunctions[column.type]
: getInferredDefaultValues();
return ( return (
<div key={i} className={`${styles.display_flex} form-group`}> <div key={i} className={`${styles.display_flex} form-group`}>
<input <input
@ -92,8 +106,19 @@ const TableColumn = props => {
bsClass={`col-type-${i} add_table_column_selector`} bsClass={`col-type-${i} add_table_column_selector`}
styleOverrides={customSelectBoxStyles} styleOverrides={customSelectBoxStyles}
filterOption={'prefix'} filterOption={'prefix'}
placeholder="column_type"
/> />
</span> </span>
<span className={`${styles.inputDefault} ${styles.defaultWidth}`}>
<TableColumnDefault
onChange={setColDefaultValue}
colIndex={i}
testId={`col-default-${i}`}
column={column}
colDefaultFunctions={defaultFunctions}
/>
</span>
{/*
<input <input
placeholder={getPlaceholder(column)} placeholder={getPlaceholder(column)}
type="text" type="text"
@ -107,7 +132,8 @@ const TableColumn = props => {
column.nullable || false column.nullable || false
)} )}
data-test={`col-default-${i}`} data-test={`col-default-${i}`}
/>{' '} />
*/}{' '}
<input <input
className={`${styles.inputCheckbox} form-control `} className={`${styles.inputCheckbox} form-control `}
checked={column.nullable} checked={column.nullable}

View File

@ -0,0 +1,43 @@
import React from 'react';
import CustomInputAutoSuggest from '../../../Common/CustomInputAutoSuggest/CustomInputAutoSuggest';
import {
getPlaceholder,
getDefaultValue,
getDefaultFunctionsOptions,
} from '../Common/utils';
const TableColumnDefault = ({
column,
colDefaultFunctions,
onChange,
testId,
colIndex: i,
}) => {
// const styles = require('../../../Common/TableCommon/Table.scss');
const handleColDefaultValueChange = (e, data) => {
const { newValue } = data;
onChange(i, column.nullable || false, newValue);
};
const renderTableColumnDefaultHtml = () => {
const dfVal = getDefaultValue(column);
/* Collect direct default functions and the indirect default functions */
const defaultValues = getDefaultFunctionsOptions(colDefaultFunctions, i);
return (
<CustomInputAutoSuggest
options={defaultValues}
onChange={handleColDefaultValueChange}
value={dfVal}
className={`col-default-value-${i} add_table_default_value_selector form-control`}
placeholder={getPlaceholder(column)}
id={`col-default-value-${i}`}
data-test={testId}
/>
);
};
return renderTableColumnDefaultHtml();
};
export default TableColumnDefault;

View File

@ -1,8 +1,14 @@
import { aggCategory, pgCategoryCode } from './PgInfo'; import { aggCategory, pgCategoryCode } from './PgInfo';
const commonlyUsedFunctions = ['now', 'gen_random_uuid', 'random'];
const getParanthesized = name => {
return `${name}()`;
};
const splitDbRow = row => { const splitDbRow = row => {
/* Splits comma seperated type names /* Splits comma seperated type names
* Splits comma seperated type display names * Splits comma seperated type user friendly type names
* Splits comma seperated type descriptions * Splits comma seperated type descriptions
* */ * */
return { return {
@ -59,6 +65,39 @@ const getDataTypeInfo = (row, categoryInfo, colId, cached = {}) => {
return { typInfo: currTypeObj, typValueMap: columnTypeValueMap }; return { typInfo: currTypeObj, typValueMap: columnTypeValueMap };
}; };
const getDefaultFunctionsOptions = (funcs, identifier) => {
const defaultValues = [
{
title: 'All Functions',
suggestions: [],
},
];
funcs.forEach((f, i) => {
const funcVal = getParanthesized(f);
const suggestionObj = {
value: funcVal,
label: funcVal,
description: funcVal,
key: i,
colIdentifier: identifier,
title: 'All Functions',
};
if (commonlyUsedFunctions.indexOf(f) !== -1) {
if (defaultValues.length === 1) {
defaultValues.push({
title: 'Frequently Used Functions',
suggestions: [],
});
}
defaultValues[1].suggestions.push(suggestionObj);
} else {
defaultValues[0].suggestions.push(suggestionObj);
}
});
/* Reversing the array just so that if frequently used types were present, they come first */
return defaultValues.reverse();
};
/* /*
* Input arguments: * Input arguments:
* dataTypes -> Frequently used types * dataTypes -> Frequently used types
@ -136,10 +175,41 @@ const getDefaultValue = column => {
return ('default' in column && column.default.value) || ''; return ('default' in column && column.default.value) || '';
}; };
const getRecommendedTypeCasts = (dataType, typeCasts) => {
return (dataType in typeCasts && typeCasts[dataType][3].split(',')) || [];
};
const inferDefaultValues = (defaultFuncs, typeCasts) => {
let defaultValues = [];
const visitedType = {};
/* Current type is the type for which default values needs to be computed
* Algorithm:
* Look for the types which the current type can be casted to
* Try to find the default values for the right type and accumulate it to an array
* */
const computeDefaultValues = currentType => {
visitedType[currentType] = true;
/* Retrieve the recommended type casts for the current type */
const validRightCasts = getRecommendedTypeCasts(currentType, typeCasts);
validRightCasts.forEach(v => {
if (!visitedType[v]) {
if (v in defaultFuncs) {
visitedType[v] = true;
defaultValues = [...defaultValues, ...defaultFuncs[v]];
}
}
});
return defaultValues;
};
return computeDefaultValues;
};
export { export {
getDataOptions, getDataOptions,
getPlaceholder, getPlaceholder,
getDefaultValue, getDefaultValue,
getDataTypeInfo, getDataTypeInfo,
getAllDataTypeMap, getAllDataTypeMap,
getDefaultFunctionsOptions,
inferDefaultValues,
}; };

View File

@ -29,7 +29,10 @@ import {
fetchTrackedTableListQuery, fetchTrackedTableListQuery,
mergeLoadSchemaData, mergeLoadSchemaData,
} from './utils'; } from './utils';
import { fetchColumnTypesQuery } from './utils';
import { fetchColumnTypesQuery, fetchColumnDefaultFunctions } from './utils';
import { fetchColumnCastsQuery, convertArrayToJson } from './TableModify/utils';
import { SERVER_CONSOLE_MODE } from '../../../constants'; import { SERVER_CONSOLE_MODE } from '../../../constants';
@ -47,9 +50,9 @@ const UPDATE_REMOTE_SCHEMA_MANUAL_REL = 'Data/UPDATE_SCHEMA_MANUAL_REL';
const SET_CONSISTENT_SCHEMA = 'Data/SET_CONSISTENT_SCHEMA'; const SET_CONSISTENT_SCHEMA = 'Data/SET_CONSISTENT_SCHEMA';
const SET_CONSISTENT_FUNCTIONS = 'Data/SET_CONSISTENT_FUNCTIONS'; const SET_CONSISTENT_FUNCTIONS = 'Data/SET_CONSISTENT_FUNCTIONS';
const FETCH_COLUMN_TYPE_LIST = 'Data/FETCH_COLUMN_TYPE_LIST'; const FETCH_COLUMN_TYPE_INFO = 'Data/FETCH_COLUMN_TYPE_INFO';
const FETCH_COLUMN_TYPE_LIST_FAIL = 'Data/FETCH_COLUMN_TYPE_LIST_FAIL'; const FETCH_COLUMN_TYPE_INFO_FAIL = 'Data/FETCH_COLUMN_TYPE_INFO_FAIL';
const RESET_COLUMN_TYPE_LIST = 'Data/RESET_COLUMN_TYPE_LIST'; const RESET_COLUMN_TYPE_INFO = 'Data/RESET_COLUMN_TYPE_INFO';
const MAKE_REQUEST = 'ModifyTable/MAKE_REQUEST'; const MAKE_REQUEST = 'ModifyTable/MAKE_REQUEST';
const REQUEST_SUCCESS = 'ModifyTable/REQUEST_SUCCESS'; const REQUEST_SUCCESS = 'ModifyTable/REQUEST_SUCCESS';
@ -535,15 +538,39 @@ const makeMigrationCall = (
); );
}; };
const fetchColumnTypes = () => { const getBulkColumnInfoFetchQuery = schema => {
return (dispatch, getState) => { const fetchColumnTypes = {
const url = Endpoints.getSchema;
const reqQuery = {
type: 'run_sql', type: 'run_sql',
args: { args: {
sql: fetchColumnTypesQuery, sql: fetchColumnTypesQuery,
}, },
}; };
const fetchTypeDefaultValues = {
type: 'run_sql',
args: {
sql: fetchColumnDefaultFunctions(schema),
},
};
const fetchValidTypeCasts = {
type: 'run_sql',
args: {
sql: fetchColumnCastsQuery,
},
};
return {
type: 'bulk',
args: [fetchColumnTypes, fetchTypeDefaultValues, fetchValidTypeCasts],
};
};
const fetchColumnTypeInfo = () => {
return (dispatch, getState) => {
const url = Endpoints.getSchema;
const currState = getState();
const { currentSchema } = currState.tables;
const reqQuery = getBulkColumnInfoFetchQuery(currentSchema);
const options = { const options = {
credentials: globalCookiePolicy, credentials: globalCookiePolicy,
method: 'POST', method: 'POST',
@ -552,9 +579,20 @@ const fetchColumnTypes = () => {
}; };
return dispatch(requestAction(url, options)).then( return dispatch(requestAction(url, options)).then(
data => { data => {
const resultData = data[1].result.slice(1);
const typeFuncsMap = {};
resultData.forEach(r => {
typeFuncsMap[r[1]] = r[0].split(',');
});
const columnDataTypeInfo = {
columnDataTypes: data[0].result.slice(1),
columnTypeDefaultValues: typeFuncsMap,
columnTypeCasts: convertArrayToJson(data[2].result.slice(1)),
};
return dispatch({ return dispatch({
type: FETCH_COLUMN_TYPE_LIST, type: FETCH_COLUMN_TYPE_INFO,
data: data.result.slice(1), data: columnDataTypeInfo,
}); });
}, },
error => { error => {
@ -567,7 +605,7 @@ const fetchColumnTypes = () => {
) )
); );
return dispatch({ return dispatch({
type: FETCH_COLUMN_TYPE_LIST_FAIL, type: FETCH_COLUMN_TYPE_INFO_FAIL,
data: error, data: error,
}); });
} }
@ -680,24 +718,30 @@ const dataReducer = (state = defaultState, action) => {
}, },
}, },
}; };
case FETCH_COLUMN_TYPE_LIST: case FETCH_COLUMN_TYPE_INFO:
return { return {
...state, ...state,
columnDataTypes: action.data, columnDataTypes: action.data.columnDataTypes,
columnDataTypeFetchErr: defaultState.columnDataTypeFetchErr, columnDefaultFunctions: action.data.columnTypeDefaultValues,
columnDataTypeInfoErr: null,
columnTypeCasts: action.data.columnTypeCasts,
}; };
case FETCH_COLUMN_TYPE_LIST_FAIL: case FETCH_COLUMN_TYPE_INFO_FAIL:
return { return {
...state, ...state,
columnDataTypes: [], columnDataTypes: [],
columnDataTypeFetchErr: action.data, columnDefaultFunctions: {},
columnTypeCasts: {},
columnDataTypeInfoErr: action.data,
}; };
case RESET_COLUMN_TYPE_LIST: case RESET_COLUMN_TYPE_INFO:
return { return {
...state, ...state,
columnDataTypes: [...defaultState.columnDataTypes], columnDataTypes: [...defaultState.columnDataTypes],
columnDataTypeFetchErr: defaultState.columnDataTypeFetchErr, columnDefaultFunctions: { ...defaultState.columnDefaultFunctions },
columnTypeCasts: { ...defaultState.columnTypeCasts },
columnDataTypeInfoErr: defaultState.columnDataTypeInfoErr,
}; };
default: default:
return state; return state;
@ -727,7 +771,7 @@ export {
LOAD_SCHEMA, LOAD_SCHEMA,
setConsistentSchema, setConsistentSchema,
setConsistentFunctions, setConsistentFunctions,
fetchColumnTypes, fetchColumnTypeInfo,
RESET_COLUMN_TYPE_LIST, RESET_COLUMN_TYPE_INFO,
setUntrackedRelations, setUntrackedRelations,
}; };

View File

@ -136,7 +136,9 @@ const defaultModifyState = {
const defaultState = { const defaultState = {
columnDataTypes: [], // To store list of column types supported by postgres columnDataTypes: [], // To store list of column types supported by postgres
columnDataTypeFetchErr: null, columnDataTypeInfoErr: null,
columnDefaultFunctions: {},
columnTypeCasts: {},
currentTable: null, currentTable: null,
view: { ...defaultViewState }, view: { ...defaultViewState },
modify: { ...defaultModifyState }, modify: { ...defaultModifyState },

View File

@ -129,7 +129,8 @@ class InsertItem extends Component {
type: 'text', type: 'text',
}; };
const colType = col.data_type; // const colType = col.data_type;
const colType = col.udt_name;
const placeHolder = hasDefault const placeHolder = hasDefault
? col.column_default ? col.column_default
: getPlaceholder(colType); : getPlaceholder(colType);

View File

@ -4,8 +4,13 @@ import gqlPattern, { gqlColumnErrorNotif } from '../Common/GraphQLValidation';
import { commonDataTypes } from '../utils'; import { commonDataTypes } from '../utils';
import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect'; import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect';
import CustomInputAutoSuggest from '../../../Common/CustomInputAutoSuggest/CustomInputAutoSuggest';
import { getDataOptions } from '../Common/utils'; import {
getDataOptions,
getDefaultFunctionsOptions,
inferDefaultValues,
} from '../Common/utils';
import Button from '../../../Common/Button/Button'; import Button from '../../../Common/Button/Button';
import { addColSql } from '../TableModify/ModifyActions'; import { addColSql } from '../TableModify/ModifyActions';
@ -90,15 +95,22 @@ const useColumnEditor = (dispatch, tableName) => {
}, },
colDefault: { colDefault: {
value: colDefault, value: colDefault,
onChange: e => { onChange: (e, data) => {
setColumnState({ ...columnState, colDefault: e.target.value }); const { newValue } = data;
setColumnState({ ...columnState, colDefault: newValue });
}, },
}, },
onSubmit, onSubmit,
}; };
}; };
const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => { const ColumnCreator = ({
dispatch,
tableName,
dataTypes: restTypes = [],
validTypeCasts,
columnDefaultFunctions,
}) => {
const { const {
colName, colName,
colType, colType,
@ -108,6 +120,37 @@ const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
onSubmit, onSubmit,
} = useColumnEditor(dispatch, tableName); } = useColumnEditor(dispatch, tableName);
let defaultOptions = [];
const getInferredDefaultValues = () =>
inferDefaultValues(columnDefaultFunctions, validTypeCasts)(colType.value);
const colDefaultFunctions =
colType.value in columnDefaultFunctions
? columnDefaultFunctions[colType.value]
: getInferredDefaultValues();
if (colDefaultFunctions && colDefaultFunctions.length > 0) {
defaultOptions = getDefaultFunctionsOptions(colDefaultFunctions, 0);
}
const getDefaultInput = () => {
const theme = require('../../../Common/CustomInputAutoSuggest/CustomThemes/AddColumnDefault.scss');
return (
<CustomInputAutoSuggest
placeholder="default value"
options={defaultOptions}
className={`${styles.input}
${styles.defaultInput}
input-sm form-control`}
{...colDefault}
data-test="default-value"
theme={theme}
/>
);
};
const { columnDataTypes, columnTypeValueMap } = getDataOptions( const { columnDataTypes, columnTypeValueMap } = getDataOptions(
commonDataTypes, commonDataTypes,
restTypes, restTypes,
@ -153,6 +196,7 @@ const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
bsClass={`col-type-${0} modify_select`} bsClass={`col-type-${0} modify_select`}
styleOverrides={customSelectBoxStyles} styleOverrides={customSelectBoxStyles}
filterOption={'prefix'} filterOption={'prefix'}
placeholder="column_type"
/> />
</span> </span>
<input <input
@ -170,7 +214,8 @@ const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
data-test="unique-checkbox" data-test="unique-checkbox"
/> />
<label className={styles.nullLabel}>Unique</label> <label className={styles.nullLabel}>Unique</label>
{getDefaultInput()}
{/*
<input <input
placeholder="default value" placeholder="default value"
type="text" type="text"
@ -180,6 +225,7 @@ const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
{...colDefault} {...colDefault}
data-test="default-value" data-test="default-value"
/> />
*/}
<Button <Button
type="submit" type="submit"

View File

@ -1,6 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect'; import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect';
import CustomInputAutoSuggest from '../../../Common/CustomInputAutoSuggest/CustomInputAutoSuggest';
import { getValidAlterOptions } from './utils'; import { getValidAlterOptions } from './utils';
@ -12,6 +13,7 @@ const ColumnEditor = ({
selectedProperties, selectedProperties,
editColumn, editColumn,
alterTypeOptions, alterTypeOptions,
defaultOptions,
}) => { }) => {
const colName = columnProperties.name; const colName = columnProperties.name;
@ -62,8 +64,9 @@ const ColumnEditor = ({
const updateColumnType = selected => { const updateColumnType = selected => {
dispatch(editColumn(colName, 'type', selected.value)); dispatch(editColumn(colName, 'type', selected.value));
}; };
const updateColumnDef = e => { const updateColumnDef = (e, data) => {
dispatch(editColumn(colName, 'default', e.target.value)); const { newValue } = data;
dispatch(editColumn(colName, 'default', newValue));
}; };
const updateColumnComment = e => { const updateColumnComment = e => {
dispatch(editColumn(colName, 'comment', e.target.value)); dispatch(editColumn(colName, 'comment', e.target.value));
@ -75,6 +78,23 @@ const ColumnEditor = ({
dispatch(editColumn(colName, 'isUnique', e.target.value === 'true')); dispatch(editColumn(colName, 'isUnique', e.target.value === 'true'));
}; };
const getDefaultInput = () => {
const theme = require('../../../Common/CustomInputAutoSuggest/CustomThemes/EditColumnDefault.scss');
return (
<CustomInputAutoSuggest
options={defaultOptions}
className="input-sm form-control"
value={selectedProperties[colName].default || ''}
onChange={updateColumnDef}
type="text"
disabled={columnProperties.pkConstraint}
data-test="edit-col-default"
theme={theme}
/>
);
};
return ( return (
<div className={`${styles.colEditor} container-fluid`}> <div className={`${styles.colEditor} container-fluid`}>
<form className="form-horizontal" onSubmit={onSubmit}> <form className="form-horizontal" onSubmit={onSubmit}>
@ -100,6 +120,7 @@ const ColumnEditor = ({
bsClass={`col-type-${0} modify_select`} bsClass={`col-type-${0} modify_select`}
styleOverrides={customSelectBoxStyles} styleOverrides={customSelectBoxStyles}
filterOption={'prefix'} filterOption={'prefix'}
placeholder="column_type"
/> />
</div> </div>
</div> </div>
@ -136,6 +157,8 @@ const ColumnEditor = ({
<div className={`${styles.display_flex} form-group`}> <div className={`${styles.display_flex} form-group`}>
<label className="col-xs-2">Default</label> <label className="col-xs-2">Default</label>
<div className="col-xs-6"> <div className="col-xs-6">
{getDefaultInput()}
{/*
<input <input
className="input-sm form-control" className="input-sm form-control"
value={selectedProperties[colName].default || ''} value={selectedProperties[colName].default || ''}
@ -144,6 +167,7 @@ const ColumnEditor = ({
disabled={columnProperties.pkConstraint} disabled={columnProperties.pkConstraint}
data-test="edit-col-default" data-test="edit-col-default"
/> />
*/}
</div> </div>
</div> </div>
<div className={`${styles.display_flex} form-group`}> <div className={`${styles.display_flex} form-group`}>

View File

@ -12,6 +12,11 @@ import {
import { ordinalColSort } from '../utils'; import { ordinalColSort } from '../utils';
import { defaultDataTypeToCast } from '../constants'; import { defaultDataTypeToCast } from '../constants';
import {
getDefaultFunctionsOptions,
inferDefaultValues,
} from '../Common/utils';
import styles from './ModifyTable.scss'; import styles from './ModifyTable.scss';
const ColumnEditorList = ({ const ColumnEditorList = ({
@ -21,6 +26,7 @@ const ColumnEditorList = ({
dispatch, dispatch,
validTypeCasts, validTypeCasts,
dataTypeIndexMap, dataTypeIndexMap,
columnDefaultFunctions,
}) => { }) => {
const tableName = tableSchema.table_name; const tableName = tableSchema.table_name;
@ -131,21 +137,55 @@ const ColumnEditorList = ({
); );
}; };
/* If the dataTypeIndexMap is not loaded, then just load the current type information
* */
const getValidTypeCasts = udtName => { const getValidTypeCasts = udtName => {
const lowerUdtName = udtName.toLowerCase(); const lowerUdtName = udtName.toLowerCase();
if (lowerUdtName in validTypeCasts) { if (lowerUdtName in validTypeCasts) {
return validTypeCasts[lowerUdtName]; return validTypeCasts[lowerUdtName];
} }
if (dataTypeIndexMap && Object.keys(dataTypeIndexMap).length > 0) {
return [ return [
...dataTypeIndexMap[lowerUdtName], ...dataTypeIndexMap[lowerUdtName],
...dataTypeIndexMap[defaultDataTypeToCast], ...dataTypeIndexMap[defaultDataTypeToCast],
]; ];
}
return [lowerUdtName, lowerUdtName, ''];
}; };
const getValidDefaultTypes = udtName => {
const lowerUdtName = udtName.toLowerCase();
let defaultOptions = [];
if (lowerUdtName in columnDefaultFunctions) {
defaultOptions = columnDefaultFunctions[lowerUdtName];
} else {
defaultOptions = inferDefaultValues(
columnDefaultFunctions,
validTypeCasts
)(lowerUdtName);
}
return getDefaultFunctionsOptions(defaultOptions);
};
/*
* Alter type options contains a list of items and its valid castable types
* [
* "Data type",
* "User friendly name of the data type",
* "Description of the data type",
* "Comma seperated castable data types",
* "Comma seperated user friendly names of the castable data types",
* "Colon seperated user friendly description of the castable data types"
* ]
* */
const colEditorExpanded = () => { const colEditorExpanded = () => {
return ( return (
<ColumnEditor <ColumnEditor
alterTypeOptions={getValidTypeCasts(col.udt_name)} alterTypeOptions={getValidTypeCasts(col.udt_name)}
defaultOptions={getValidDefaultTypes(col.udt_name)}
column={col} column={col}
onSubmit={onSubmit} onSubmit={onSubmit}
onDelete={safeOnDelete} onDelete={safeOnDelete}

View File

@ -25,6 +25,8 @@ import {
getUniqueConstraintName, getUniqueConstraintName,
} from '../Common/ReusableComponents/utils'; } from '../Common/ReusableComponents/utils';
import { isPostgresFunction } from '../utils';
import { import {
fetchColumnCastsQuery, fetchColumnCastsQuery,
convertArrayToJson, convertArrayToJson,
@ -881,7 +883,9 @@ const addColSql = (
callback callback
) => { ) => {
let defWithQuotes = "''"; let defWithQuotes = "''";
if (colType === 'text' && colDefault !== '') {
const checkIfFunctionFormat = isPostgresFunction(colDefault);
if (colType === 'text' && colDefault !== '' && !checkIfFunctionFormat) {
defWithQuotes = "'" + colDefault + "'"; defWithQuotes = "'" + colDefault + "'";
} else { } else {
defWithQuotes = colDefault; defWithQuotes = colDefault;
@ -1167,9 +1171,10 @@ const saveColumnChangesSql = (colName, column) => {
const comment = columnEdit.comment || ''; const comment = columnEdit.comment || '';
const newName = columnEdit.name; const newName = columnEdit.name;
const currentSchema = columnEdit.schemaName; const currentSchema = columnEdit.schemaName;
const checkIfFunctionFormat = isPostgresFunction(def);
// ALTER TABLE <table> ALTER COLUMN <column> TYPE <column_type>; // ALTER TABLE <table> ALTER COLUMN <column> TYPE <column_type>;
let defWithQuotes; let defWithQuotes;
if (colType === 'text') { if (colType === 'text' && !checkIfFunctionFormat) {
defWithQuotes = `'${def}'`; defWithQuotes = `'${def}'`;
} else { } else {
defWithQuotes = def; defWithQuotes = def;

View File

@ -8,13 +8,12 @@ import {
deleteTableSql, deleteTableSql,
untrackTableSql, untrackTableSql,
RESET, RESET,
fetchColumnCasts,
setUniqueKeys, setUniqueKeys,
} from '../TableModify/ModifyActions'; } from '../TableModify/ModifyActions';
import { import {
setTable, setTable,
fetchColumnTypes, fetchColumnTypeInfo,
RESET_COLUMN_TYPE_LIST, RESET_COLUMN_TYPE_INFO,
} from '../DataActions'; } from '../DataActions';
import Button from '../../../Common/Button/Button'; import Button from '../../../Common/Button/Button';
import ColumnEditorList from './ColumnEditorList'; import ColumnEditorList from './ColumnEditorList';
@ -31,12 +30,11 @@ class ModifyTable extends React.Component {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch({ type: RESET }); dispatch({ type: RESET });
dispatch(setTable(this.props.tableName)); dispatch(setTable(this.props.tableName));
dispatch(fetchColumnTypes()); dispatch(fetchColumnTypeInfo());
dispatch(fetchColumnCasts());
} }
componentWillUnmount() { componentWillUnmount() {
this.props.dispatch({ this.props.dispatch({
type: RESET_COLUMN_TYPE_LIST, type: RESET_COLUMN_TYPE_INFO,
}); });
} }
render() { render() {
@ -53,6 +51,7 @@ class ModifyTable extends React.Component {
dataTypes, dataTypes,
validTypeCasts, validTypeCasts,
uniqueKeyModify, uniqueKeyModify,
columnDefaultFunctions,
schemaList, schemaList,
} = this.props; } = this.props;
@ -135,6 +134,7 @@ class ModifyTable extends React.Component {
columnEdit={columnEdit} columnEdit={columnEdit}
dispatch={dispatch} dispatch={dispatch}
currentSchema={currentSchema} currentSchema={currentSchema}
columnDefaultFunctions={columnDefaultFunctions}
/> />
<hr /> <hr />
<h4 className={styles.subheading_text}>Add a new column</h4> <h4 className={styles.subheading_text}>Add a new column</h4>
@ -142,6 +142,8 @@ class ModifyTable extends React.Component {
dispatch={dispatch} dispatch={dispatch}
tableName={tableName} tableName={tableName}
dataTypes={dataTypes} dataTypes={dataTypes}
validTypeCasts={validTypeCasts}
columnDefaultFunctions={columnDefaultFunctions}
/> />
<hr /> <hr />
<h4 className={styles.subheading_text}>Primary Key</h4> <h4 className={styles.subheading_text}>Primary Key</h4>
@ -212,7 +214,8 @@ const mapStateToProps = (state, ownProps) => ({
pkModify: state.tables.modify.pkModify, pkModify: state.tables.modify.pkModify,
fkModify: state.tables.modify.fkModify, fkModify: state.tables.modify.fkModify,
dataTypes: state.tables.columnDataTypes, dataTypes: state.tables.columnDataTypes,
validTypeCasts: state.tables.modify.alterColumnOptions, columnDefaultFunctions: state.tables.columnDefaultFunctions,
validTypeCasts: state.tables.columnTypeCasts,
columnDataTypeFetchErr: state.tables.columnDataTypeFetchErr, columnDataTypeFetchErr: state.tables.columnDataTypeFetchErr,
schemaList: state.tables.schemaList, schemaList: state.tables.schemaList,
...state.tables.modify, ...state.tables.modify,

View File

@ -14,22 +14,31 @@ const getValidAlterOptions = (alterTypeOptions, colName) => {
colName, colName,
0 0
); );
/*
* alterTypeOptions can also only contain only three elements
*/
let allInfo = [...currentInfo];
let allOptionsMap = {
...currentMap,
};
if (alterTypeOptions.length > 3) {
const { const {
typInfo: validOptions, typInfo: validOptions,
typValueMap: validOptionsMap, typValueMap: validOptionsMap,
} = getDataTypeInfo(alterTypeOptions.slice(3, 6), colName, 0); } = getDataTypeInfo(alterTypeOptions.slice(3, 6), colName, 0);
const _allInfo = [...currentInfo, ...validOptions]; allInfo = allInfo.concat(validOptions);
// const allInfo = [...currentInfo, ...validOptions];
const _allOptionsMap = { allOptionsMap = {
...validOptionsMap, ...validOptionsMap,
...currentMap, ...currentMap,
}; };
}
return { return {
alterOptions: _allInfo, alterOptions: allInfo,
alterOptionsValueMap: _allOptionsMap, alterOptionsValueMap: allOptionsMap,
}; };
}; };

View File

@ -587,3 +587,29 @@ WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHER
AND t.typname != 'unknown' AND t.typname != 'unknown'
AND t.typcategory != 'P' AND t.typcategory != 'P'
GROUP BY t.typcategory;`; GROUP BY t.typcategory;`;
export const fetchColumnDefaultFunctions = (schema = 'public') => `
SELECT string_agg(pgp.proname, ','),
t.typname as "Type"
from pg_proc pgp
JOIN pg_type t
ON pgp.prorettype = t.oid
JOIN pg_namespace pgn
ON pgn.oid = pgp.pronamespace
WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))
AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)
AND pg_catalog.pg_type_is_visible(t.oid)
AND t.typname != 'unknown'
AND t.typcategory != 'P'
AND (array_length(pgp.proargtypes, 1) = 0)
AND ( pgn.nspname = '${schema}' OR pgn.nspname = 'pg_catalog' )
AND pgp.proretset=false
AND pgp.prokind='f'
GROUP BY t.typname
ORDER BY t.typname ASC;
`;
const postgresFunctionTester = /.*\(\)$/gm;
export const isPostgresFunction = str =>
new RegExp(postgresFunctionTester).test(str);

View File

@ -473,7 +473,7 @@ export const metadataReducer = (state = defaultState, action) => {
...state, ...state,
allowedQueries: [ allowedQueries: [
...state.allowedQueries.map(q => ...state.allowedQueries.map(q =>
q.name === action.data.queryName ? action.data.newQuery : q (q.name === action.data.queryName ? action.data.newQuery : q)
), ),
], ],
}; };

View File

@ -13,7 +13,7 @@
max-height: 30px !important; max-height: 30px !important;
} }
.add_table_column_selector__control { .add_table_column_selector__control, .add_table_default_value_selector__control {
min-height: 34px !important; min-height: 34px !important;
max-height: 34px !important; max-height: 34px !important;
} }