console: Support custom comments for root fields

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3930
Co-authored-by: Martin Mark <74692114+martin-hasura@users.noreply.github.com>
GitOrigin-RevId: 91c71d8ab2c4886b395f5237ca71cace9ec61d1a
This commit is contained in:
Daniel Chambers 2022-03-24 11:29:41 +11:00 committed by hasura-bot
parent adb648b429
commit cb1722694e
10 changed files with 339 additions and 137 deletions

View File

@ -159,6 +159,7 @@ function:
- console: enable searching tables within a schema
- console: fixed the ability to create updated_at and created_at in the modify page (#8239)
- console: disable search indexing with HTML meta tag
- console: add support for setting comments on the custom root fields of tables/views
- cli: fix inherited roles metadata not being updated when dropping all roles (#7872)
- cli: add support for customization field in sources metadata (#8292)
- ci: ubuntu and centos flavoured graphql-engine images are now available

View File

@ -0,0 +1,79 @@
import React, { ReactText } from 'react';
import clsx from 'clsx';
export type SelectItem = {
label: ReactText;
value: ReactText;
disabled?: boolean;
};
export type SelectInputSplitFieldProps = {
inputType?: 'text' | 'email' | 'password';
selectOptions: SelectItem[];
size?: 'full' | 'medium';
placeholder?: string;
inputDisabled?: boolean;
selectDisabled?: boolean;
inputValue: string;
selectValue: string;
inputOnChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
selectOnChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
};
export const SelectInputSplitField = ({
inputType = 'text',
size = 'full',
selectOptions,
placeholder,
inputDisabled,
selectDisabled,
inputValue,
selectValue,
inputOnChange,
selectOnChange,
}: SelectInputSplitFieldProps) => {
return (
<div
className={clsx('flex rounded', size === 'medium' ? 'w-1/2' : 'w-full')}
>
<select
className="inline-flex form-control"
style={{
width: 'max-content',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderRight: 0,
paddingRight: '1.75rem',
}}
disabled={selectDisabled}
onChange={selectOnChange}
>
{selectOptions.map(({ label, value, disabled = false }) => (
<option
key={value}
selected={selectValue === value}
{...{ value, disabled }}
>
{label}
</option>
))}
</select>
<input
type={inputType}
className={clsx(
'flex-1 min-w-0 form-control',
inputDisabled ? 'disabled' : ''
)}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
placeholder={placeholder}
value={inputValue}
disabled={inputDisabled}
onChange={inputOnChange}
/>
</div>
);
};

View File

@ -0,0 +1,56 @@
import { SelectInputSplitField } from '@/components/Common/SelectInputSplitField/SelectInputSplitField';
import React, { ChangeEvent, useCallback } from 'react';
export type CommentProps = {
value: string | null;
defaultComment: string;
onChange: (comment: string | null) => void;
};
const selectOptions = [
{ label: 'Value', value: 'Value' },
{ label: 'Disabled', value: 'Disabled' },
];
export const CommentInput = ({
value,
defaultComment,
onChange,
}: CommentProps) => {
const inputValue = value ?? '';
const selectValue = value === '' ? 'Disabled' : 'Value';
const placeholder = selectValue === 'Value' ? defaultComment : '';
const inputOnChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (selectValue === 'Disabled') {
onChange('');
} else {
onChange(e.target.value === '' ? null : e.target.value);
}
},
[selectValue, onChange]
);
const selectOnChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === selectValue) return; // No change
const newComment = e.target.value === 'Disabled' ? '' : null;
onChange(newComment);
},
[selectValue, onChange]
);
return (
<SelectInputSplitField
inputValue={inputValue}
inputOnChange={inputOnChange}
inputDisabled={selectValue === 'Disabled'}
selectOptions={selectOptions}
selectValue={selectValue}
selectOnChange={selectOnChange}
placeholder={placeholder}
/>
);
};

View File

@ -1,13 +1,13 @@
import React from 'react';
import {
CustomRootField,
CustomRootFields,
} from '../../../../../dataSources/types';
import { CustomRootFields } from '../../../../../dataSources/types';
import CollapsibleToggle from '../../../../Common/CollapsibleToggle/CollapsibleToggle';
import { getRootFieldLabel } from './utils';
import { Nullable } from '../../../../../components/Common/utils/tsUtils';
import { getTableCustomRootFieldName } from '../../../../../dataSources';
import {
getTableCustomRootFieldComment,
getTableCustomRootFieldName,
} from '../../../../../dataSources';
import { CommentInput } from './CommentInput';
interface RootFieldEditorProps {
rootFields: CustomRootFields;
@ -15,7 +15,7 @@ interface RootFieldEditorProps {
tableName: string;
tableSchema: string;
customName?: string;
customNameOnChange: ChangeHandler;
customNameOnChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
selectOnChange: ChangeHandler;
selectByPkOnChange: ChangeHandler;
selectAggOnChange: ChangeHandler;
@ -27,7 +27,22 @@ interface RootFieldEditorProps {
deleteByPkOnChange: ChangeHandler;
}
type ChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => void;
interface ChangeHandler {
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onCommentChange: (comment: string | null) => void;
}
export const rootFieldLabels: Record<keyof CustomRootFields, string> = {
select: 'Select',
select_by_pk: 'Select by PK',
select_aggregate: 'Select Aggregate',
insert: 'Insert',
insert_one: 'Insert One',
update: 'Update',
update_by_pk: 'Update by PK',
delete: 'Delete',
delete_by_pk: 'Delete by PK',
};
const RootFieldEditor: React.FC<RootFieldEditorProps> = ({
rootFields,
@ -46,17 +61,19 @@ const RootFieldEditor: React.FC<RootFieldEditorProps> = ({
customName,
tableSchema,
}) => {
const {
select,
select_by_pk: selectByPk,
select_aggregate: selectAgg,
insert,
insert_one: insertOne,
update,
update_by_pk: updateByPk,
delete: _delete,
delete_by_pk: deleteByPk,
} = rootFields;
const qualifiedTableName =
tableSchema === '' ? `"${tableName}"` : `"${tableSchema}.${tableName}"`;
const rootFieldDefaultComments: Record<keyof CustomRootFields, string> = {
select: `fetch data from the table: ${qualifiedTableName}`,
select_by_pk: `fetch data from the table: ${qualifiedTableName} using primary key columns`,
select_aggregate: `fetch aggregated fields from the table: ${qualifiedTableName}`,
insert: `insert data into the table: ${qualifiedTableName}`,
insert_one: `insert a single row into the table: ${qualifiedTableName}`,
update: `update data of the table: ${qualifiedTableName}`,
update_by_pk: `update single row of the table: ${qualifiedTableName}`,
delete: `delete data from the table: ${qualifiedTableName}`,
delete_by_pk: `delete single row from the table: ${qualifiedTableName}`,
};
const getRootField = () => {
if (customName) {
@ -85,27 +102,53 @@ const RootFieldEditor: React.FC<RootFieldEditorProps> = ({
return `${rfType}_${rootField}`;
};
const getRow = (
rfType: string,
value: Nullable<string> | CustomRootField,
onChange: ChangeHandler
const getCustomNameRow = (
value: Nullable<string>,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
) => (
<div className="flex items-center">
<div className="text-gray-600">{getRootFieldLabel(rfType)}</div>
<div className="ml-auto w-6/12">
<div className="flex items-center space-x-4 ">
<div className="text-gray-600 w-2/12">Custom Table Name</div>
<div className="w-4/12">
<input
type="text"
value={getTableCustomRootFieldName(value) || ''}
placeholder={`${getDefaultRootField(rfType)} (default)`}
value={value || ''}
placeholder={`${getDefaultRootField('custom_name')} (default)`}
className="form-control"
onChange={onChange}
disabled={disabled}
/>
</div>
<div className="w-6/12" />
</div>
);
const getSection = (rfType: string) => {
const getRootFieldRow = (
rfType: keyof CustomRootFields,
onChange: ChangeHandler
) => (
<div className="flex items-center space-x-4">
<div className="text-gray-600 w-2/12">{rootFieldLabels[rfType]}</div>
<div className="w-4/12">
<input
type="text"
value={getTableCustomRootFieldName(rootFields[rfType]) || ''}
placeholder={`${getDefaultRootField(rfType)} (default)`}
className="form-control"
onChange={onChange.onNameChange}
disabled={disabled}
/>
</div>
<div className="w-6/12">
<CommentInput
value={getTableCustomRootFieldComment(rootFields[rfType])}
defaultComment={rootFieldDefaultComments[rfType]}
onChange={onChange.onCommentChange}
/>
</div>
</div>
);
const getSection = (rfType: 'query' | 'mutation') => {
return (
<div>
<CollapsibleToggle
@ -113,33 +156,37 @@ const RootFieldEditor: React.FC<RootFieldEditorProps> = ({
useDefaultTitleStyle
isOpen
>
<div className="flex items-center space-x-4 pb-2">
<div className="text-gray-600 w-2/12" />
<div className="text-gray-600 w-4/12">Field Name</div>
<div className="text-gray-600 w-6/12">Comment </div>
</div>
{rfType === 'query' && (
<div className="space-y-md mb-md">
{getRow('select', select, selectOnChange)}
{getRow('select_by_pk', selectByPk, selectByPkOnChange)}
{getRow('select_aggregate', selectAgg, selectAggOnChange)}
{getRootFieldRow('select', selectOnChange)}
{getRootFieldRow('select_by_pk', selectByPkOnChange)}
{getRootFieldRow('select_aggregate', selectAggOnChange)}
</div>
)}
{rfType === 'mutation' && (
<div className="space-y-md mb-md">
{getRow('insert', insert, insertOnChange)}
{getRow('insert_one', insertOne, insertOneOnChange)}
{getRow('update', update, updateOnChange)}
{getRow('update_by_pk', updateByPk, updateByPkOnChange)}
{getRow('delete', _delete, deleteOnChange)}
{getRow('delete_by_pk', deleteByPk, deleteByPkOnChange)}
{getRootFieldRow('insert', insertOnChange)}
{getRootFieldRow('insert_one', insertOneOnChange)}
{getRootFieldRow('update', updateOnChange)}
{getRootFieldRow('update_by_pk', updateByPkOnChange)}
{getRootFieldRow('delete', deleteOnChange)}
{getRootFieldRow('delete_by_pk', deleteByPkOnChange)}
</div>
)}
</CollapsibleToggle>
</div>
);
};
return (
<div>
<div>
<div className="mb-md">
{getRow('custom_name', customName, customNameOnChange)}
{getCustomNameRow(customName, customNameOnChange)}
</div>
{getSection('query')}
{getSection('mutation')}

View File

@ -110,19 +110,3 @@ export const getKeyDef = (config, constraintName) => {
</div>
);
};
export const getRootFieldLabel = rfType => {
const labels = {
custom_name: 'Custom Table Name',
select: 'Select',
select_by_pk: 'Select by PK',
select_aggregate: 'Select Aggregate',
insert: 'Insert',
insert_one: 'Insert One',
update: 'Update',
update_by_pk: 'Update by PK',
delete: 'Delete',
delete_by_pk: 'Delete by PK',
};
return labels[rfType];
};

View File

@ -145,15 +145,17 @@ const ColumnEditorList = ({
const keyPropertiesString = propertiesList.join(', ');
propertiesDisplay.push(
<span className="ml-xs mr-2 font-normal" key={'props'}>
<span className="ml-xs font-normal" key={'props'}>
{keyPropertiesString}
</span>
);
propertiesDisplay.push(
<span key={'comment'} className={styles.text_gray}>
{columnProperties.comment && `${columnProperties.comment}`}
</span>
<div>
<span key={'comment'} className="text-gray-600 text-sm">
{columnProperties.comment && `${columnProperties.comment}`}
</span>
</div>
);
return propertiesDisplay;
@ -161,15 +163,13 @@ const ColumnEditorList = ({
const collapsedLabel = () => {
return (
<div className="flex items-center" key={colName}>
<div>
<span className="font-semibold">{colName}</span>
<span className="ml-xs font-semibold">
{columnProperties.customFieldName &&
`${columnProperties.customFieldName}`}
</span>
</div>{' '}
{gqlCompatibilityWarning()} - {keyProperties()}
<div key={colName}>
<span className="font-semibold">{colName}</span>
<span className="mr-xs">
{columnProperties.customFieldName &&
`${columnProperties.customFieldName}`}
</span>
- {gqlCompatibilityWarning()} {keyProperties()}
</div>
);
};

View File

@ -217,7 +217,7 @@ class ModifyTable extends React.Component {
</>
)}
<div className="w-full sm:w-6/12">
<div className="w-full sm:w-full">
<h3 className="text-sm tracking-widest text-gray-400 uppercase font-semibold mb-sm">
Configure Fields
</h3>

View File

@ -8,9 +8,14 @@ import {
} from './ModifyActions';
import { Dispatch } from '../../../../types';
import { CustomRootFields } from '../../../../dataSources/types';
import {
CustomRootField,
CustomRootFields,
} from '../../../../dataSources/types';
import {
getTableCustomRootFieldComment,
getTableCustomRootFieldName,
setTableCustomRootFieldComment,
setTableCustomRootFieldName,
} from '../../../../dataSources';
@ -37,56 +42,69 @@ const RootFieldsEditor = ({
dispatch(modifyRootFields(rf));
};
const onChange = (field: keyof CustomRootFields, customField: string) => {
const newRootFields = {
...rootFieldsEdit,
[field]: setTableCustomRootFieldName(rootFieldsEdit[field], customField),
};
dispatch(modifyRootFields(newRootFields));
};
const onRootFieldChange = (field: keyof CustomRootFields) => ({
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => {
const newRootFields = {
...rootFieldsEdit,
[field]: setTableCustomRootFieldName(
rootFieldsEdit[field],
e.target.value
),
};
dispatch(modifyRootFields(newRootFields));
},
onCommentChange: (comment: string | null) => {
const newRootFields = {
...rootFieldsEdit,
[field]: setTableCustomRootFieldComment(rootFieldsEdit[field], comment),
};
dispatch(modifyRootFields(newRootFields));
},
});
const onChangeCustomName = (newName: string) => {
dispatch(modifyTableCustomName(newName));
};
const getRootFieldNames = (
rootFields: CustomRootFields
): Record<string, string> => {
const rootFieldNames = Object.entries(rootFields).map(([key, value]) =>
value ? { [key]: getTableCustomRootFieldName(value) } : {}
const renderCustomRootFieldLabel = (
rootFieldName: string,
rootFieldConfig: string | CustomRootField
) => {
const rfCustomName = getTableCustomRootFieldName(rootFieldConfig);
const rfComment = getTableCustomRootFieldComment(rootFieldConfig);
return (
<div className="mb-xs" key={rootFieldName}>
<span className="flex items-center">
<span className="font-semibold mr-xs">{rootFieldName}</span>
{rfCustomName ? (
<>
<span className="mr-xs">&rarr;</span>
<span>{rfCustomName}</span>
</>
) : null}
</span>
<span className="text-gray-600 text-sm">{rfComment}</span>
</div>
);
return Object.assign({}, ...rootFieldNames);
};
const collapsedLabel = () => {
const customRootFieldLabels: React.ReactNode[] = [];
const customNameLabel = existingCustomName
? [renderCustomRootFieldLabel('custom_table_name', existingCustomName)]
: [];
if (existingCustomName) {
customRootFieldLabels.push(
<span className="flex items-center" key={existingCustomName}>
<span className="font-semibold mr-xs">custom_table_name</span>{' '}
<span className="mr-xs">&rarr;</span>{' '}
<span>{existingCustomName}</span>
</span>
);
}
Object.entries(getRootFieldNames(existingRootFields)).forEach(
([rootField, customRootField]) => {
customRootFieldLabels.push(
<>
<span className="flex items-center" key={rootField}>
<span className="font-semibold mr-xs">{rootField}</span>
<span className="mr-xs">&rarr;</span>
<span>{customRootField}</span>
</span>
</>
);
}
const existingRootFieldLabels = Object.entries(
existingRootFields
).map(([rootField, customRootField]) =>
customRootField === null
? []
: [renderCustomRootFieldLabel(rootField, customRootField)]
);
return <div>{customRootFieldLabels}</div>;
const allLabels = customNameLabel.concat(...existingRootFieldLabels);
return <div>{allLabels}</div>;
};
const editorExpanded = () => (
@ -99,33 +117,15 @@ const RootFieldsEditor = ({
customNameOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChangeCustomName(e.target.value);
}}
selectOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('select', e.target.value);
}}
selectByPkOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('select_by_pk', e.target.value);
}}
selectAggOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('select_aggregate', e.target.value);
}}
insertOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('insert', e.target.value);
}}
insertOneOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('insert_one', e.target.value);
}}
updateOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('update', e.target.value);
}}
updateByPkOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('update_by_pk', e.target.value);
}}
deleteOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('delete', e.target.value);
}}
deleteByPkOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange('delete_by_pk', e.target.value);
}}
selectOnChange={onRootFieldChange('select')}
selectByPkOnChange={onRootFieldChange('select_by_pk')}
selectAggOnChange={onRootFieldChange('select_aggregate')}
insertOnChange={onRootFieldChange('insert')}
insertOneOnChange={onRootFieldChange('insert_one')}
updateOnChange={onRootFieldChange('update')}
updateByPkOnChange={onRootFieldChange('update_by_pk')}
deleteOnChange={onRootFieldChange('delete')}
deleteByPkOnChange={onRootFieldChange('delete_by_pk')}
/>
);

View File

@ -57,7 +57,8 @@ export const sanitiseRootFields = rootFields => {
rootField = rootField.trim() || null;
} else if (rootField) {
rootField.name = rootField.name ? rootField.name.trim() : null;
rootField.comment = rootField.comment ? rootField.comment.trim() : null;
rootField.comment =
typeof rootField.comment === 'string' ? rootField.comment.trim() : null;
}
santisedRootFields[rootFieldType] = rootField;
});

View File

@ -312,6 +312,40 @@ export const setTableCustomRootFieldName = (
return newName;
};
export const getTableCustomRootFieldComment = (
rootFieldValue: Nullable<string> | CustomRootField
): string | null => {
if (rootFieldValue) {
if (
typeof rootFieldValue === 'string' ||
rootFieldValue.comment === undefined
) {
return null;
}
return rootFieldValue.comment;
}
return null;
};
export const setTableCustomRootFieldComment = (
existingRootFieldValue: Nullable<string> | CustomRootField,
newComment: string | null
): Nullable<string> | CustomRootField => {
if (typeof existingRootFieldValue === 'string') {
return {
name: existingRootFieldValue,
comment: newComment,
};
} else if (typeof existingRootFieldValue === 'object') {
return {
...existingRootFieldValue,
comment: newComment,
};
}
return { comment: newComment };
};
export const getTableColumnConfig = (table: NormalizedTable) =>
table?.configuration?.column_config || {};