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 = () => {
cy.wait(10000);
cy.get(getElementFromAlias('modify-table-edit-column-0')).click();
cy.get(getElementFromAlias('edit-col-name'))
.clear()

View File

@ -12669,6 +12669,26 @@
"integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=",
"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": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz",
@ -12953,6 +12973,21 @@
"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": {
"version": "4.0.2",
"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": {
"version": "5.5.1",
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",

View File

@ -79,6 +79,7 @@
"query-string": "^6.1.0",
"react": "16.8.2",
"react-ace": "^6.1.1",
"react-autosuggest": "^9.4.3",
"react-bootstrap": "^0.32.1",
"react-copy-to-clipboard": "^5.0.0",
"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,
bsClass,
styleOverrides,
placeholder,
filterOption,
}) => {
/* Select element style customization */
@ -68,7 +69,7 @@ const SearchableSelectBox = ({
isSearchable
components={{ Option: CustomOption }}
classNamePrefix={`${bsClass}`}
placeholder="column_type"
placeholder={placeholder}
options={options}
onChange={onChange}
value={value}

View File

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

View File

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

View File

@ -3,11 +3,9 @@ import PropTypes from 'prop-types';
import SearchableSelectBox from '../../../Common/SearchableSelect/SearchableSelect';
import { commonDataTypes } from '../utils';
import {
getDataOptions,
getPlaceholder,
getDefaultValue,
} from '../Common/utils';
import { getDataOptions, inferDefaultValues } from '../Common/utils';
import TableColumnDefault from './TableColumnDefault';
/* Custom style object for searchable select box */
const customSelectBoxStyles = {
@ -17,6 +15,9 @@ const customSelectBoxStyles = {
singleValue: {
color: '#555555',
},
valueContainer: {
padding: '0px 12px',
},
};
const TableColumn = props => {
@ -32,6 +33,8 @@ const TableColumn = props => {
onColNullableChange,
onColUniqueChange,
dataTypes: restTypes,
columnDefaultFunctions,
columnTypeCasts,
uniqueKeys,
} = props;
@ -56,6 +59,7 @@ const TableColumn = props => {
restTypes,
i
);
const getRemoveIcon = colLen => {
let removeIcon;
if (i + 1 === colLen) {
@ -71,6 +75,16 @@ const TableColumn = props => {
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 (
<div key={i} className={`${styles.display_flex} form-group`}>
<input
@ -92,8 +106,19 @@ const TableColumn = props => {
bsClass={`col-type-${i} add_table_column_selector`}
styleOverrides={customSelectBoxStyles}
filterOption={'prefix'}
placeholder="column_type"
/>
</span>
<span className={`${styles.inputDefault} ${styles.defaultWidth}`}>
<TableColumnDefault
onChange={setColDefaultValue}
colIndex={i}
testId={`col-default-${i}`}
column={column}
colDefaultFunctions={defaultFunctions}
/>
</span>
{/*
<input
placeholder={getPlaceholder(column)}
type="text"
@ -107,7 +132,8 @@ const TableColumn = props => {
column.nullable || false
)}
data-test={`col-default-${i}`}
/>{' '}
/>
*/}{' '}
<input
className={`${styles.inputCheckbox} form-control `}
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';
const commonlyUsedFunctions = ['now', 'gen_random_uuid', 'random'];
const getParanthesized = name => {
return `${name}()`;
};
const splitDbRow = row => {
/* Splits comma seperated type names
* Splits comma seperated type display names
* Splits comma seperated type user friendly type names
* Splits comma seperated type descriptions
* */
return {
@ -59,6 +65,39 @@ const getDataTypeInfo = (row, categoryInfo, colId, cached = {}) => {
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:
* dataTypes -> Frequently used types
@ -136,10 +175,41 @@ const getDefaultValue = column => {
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 {
getDataOptions,
getPlaceholder,
getDefaultValue,
getDataTypeInfo,
getAllDataTypeMap,
getDefaultFunctionsOptions,
inferDefaultValues,
};

View File

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

View File

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

View File

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

View File

@ -4,8 +4,13 @@ import gqlPattern, { gqlColumnErrorNotif } from '../Common/GraphQLValidation';
import { commonDataTypes } from '../utils';
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 { addColSql } from '../TableModify/ModifyActions';
@ -90,15 +95,22 @@ const useColumnEditor = (dispatch, tableName) => {
},
colDefault: {
value: colDefault,
onChange: e => {
setColumnState({ ...columnState, colDefault: e.target.value });
onChange: (e, data) => {
const { newValue } = data;
setColumnState({ ...columnState, colDefault: newValue });
},
},
onSubmit,
};
};
const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
const ColumnCreator = ({
dispatch,
tableName,
dataTypes: restTypes = [],
validTypeCasts,
columnDefaultFunctions,
}) => {
const {
colName,
colType,
@ -108,6 +120,37 @@ const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
onSubmit,
} = 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(
commonDataTypes,
restTypes,
@ -153,6 +196,7 @@ const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
bsClass={`col-type-${0} modify_select`}
styleOverrides={customSelectBoxStyles}
filterOption={'prefix'}
placeholder="column_type"
/>
</span>
<input
@ -170,7 +214,8 @@ const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
data-test="unique-checkbox"
/>
<label className={styles.nullLabel}>Unique</label>
{getDefaultInput()}
{/*
<input
placeholder="default value"
type="text"
@ -180,6 +225,7 @@ const ColumnCreator = ({ dispatch, tableName, dataTypes: restTypes = [] }) => {
{...colDefault}
data-test="default-value"
/>
*/}
<Button
type="submit"

View File

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

View File

@ -12,6 +12,11 @@ import {
import { ordinalColSort } from '../utils';
import { defaultDataTypeToCast } from '../constants';
import {
getDefaultFunctionsOptions,
inferDefaultValues,
} from '../Common/utils';
import styles from './ModifyTable.scss';
const ColumnEditorList = ({
@ -21,6 +26,7 @@ const ColumnEditorList = ({
dispatch,
validTypeCasts,
dataTypeIndexMap,
columnDefaultFunctions,
}) => {
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 lowerUdtName = udtName.toLowerCase();
if (lowerUdtName in validTypeCasts) {
return validTypeCasts[lowerUdtName];
}
return [
...dataTypeIndexMap[lowerUdtName],
...dataTypeIndexMap[defaultDataTypeToCast],
];
if (dataTypeIndexMap && Object.keys(dataTypeIndexMap).length > 0) {
return [
...dataTypeIndexMap[lowerUdtName],
...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 = () => {
return (
<ColumnEditor
alterTypeOptions={getValidTypeCasts(col.udt_name)}
defaultOptions={getValidDefaultTypes(col.udt_name)}
column={col}
onSubmit={onSubmit}
onDelete={safeOnDelete}

View File

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

View File

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

View File

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

View File

@ -91,17 +91,17 @@ const ManualRelationshipSelector = ({
disabled={!relAdd.relType || !relAdd.relName}
>
{// default unselected option
relAdd.rSchema === '' && (
<option value={''} disabled>
{'-- reference schema --'}
</option>
)}
relAdd.rSchema === '' && (
<option value={''} disabled>
{'-- reference schema --'}
</option>
)}
{// all reference schema options
schemaList.map((rs, j) => (
<option key={j} value={rs}>
{rs}
</option>
))}
schemaList.map((rs, j) => (
<option key={j} value={rs}>
{rs}
</option>
))}
</select>
</div>
);

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.typcategory != 'P'
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,
allowedQueries: [
...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;
}
.add_table_column_selector__control {
.add_table_column_selector__control, .add_table_default_value_selector__control {
min-height: 34px !important;
max-height: 34px !important;
}