console: support computed fields in permission builder

>

### Description
>

### Changelog

- [x] `CHANGELOG.md` is updated with user-facing content relevant to this PR. If no changelog is required, then add the `no-changelog-required` label.

### Affected components

- [x] Console

### Related Issues
closes [#7336](https://github.com/hasura/graphql-engine/issues/7336)

### Steps to test and verify

- Add a scalar computed field with only table row as argument, to the table you want to use for testing
- Then head to table's permission tab, and try to use permission builder to set permissions for any role
- confirm that computed fields are listed as options along table columns

https://github.com/hasura/graphql-engine-mono/pull/2056

GitOrigin-RevId: 2cd16ca4a0e6a6288d4b62549ebe1aaaf841952b
This commit is contained in:
Ikechukwu Eze 2021-08-16 13:26:40 +01:00 committed by hasura-bot
parent 5de2ef7d31
commit 64a5c52904
7 changed files with 176 additions and 86 deletions

View File

@ -9,6 +9,7 @@
- server: fix GraphQL type for single-row returning functions (close #7109)
- console: add support for creation of indexes for Postgres data sources
- console: allow same named queries and unnamed queries on allowlist file upload
- console: support computed fields in permission builder
## v2.0.6

View File

@ -26,7 +26,6 @@ import {
isJsonString,
isObject,
} from '../../../../Common/utils/jsUtils';
// import { ColumnArray } from './ColumnArray';
import {
addToPrefix,
boolOperators,
@ -42,11 +41,16 @@ import {
TABLE_KEY,
WHERE_KEY,
} from './utils';
import SelectGroup from './SelectGroup';
import SelectGroup, { QuotedSelectGroup } from './SelectGroup';
import {
getComputedFieldFunction,
getGroupedTableComputedFields,
} from '../../../../../dataSources/services/postgresql';
class PermissionBuilder extends React.Component {
static propTypes = {
allTableSchemas: PropTypes.array.isRequired,
allFunctions: PropTypes.array.isRequired,
schemaList: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired,
dispatchFuncSetFilter: PropTypes.func.isRequired,
@ -433,6 +437,18 @@ class PermissionBuilder extends React.Component {
selectDispatchFunc(e.target.value);
};
if (typeof values?.[1] !== 'string') {
return (
<SelectGroup
selectDispatchFunc={selectDispatchFunc}
value={value}
values={values}
prefix={prefix}
disabledValues={disabledValues}
/>
);
}
const _selectOptions = [];
[''].concat(values).forEach((val, i) => {
const optionVal = addToPrefix(prefix, val);
@ -574,13 +590,13 @@ class PermissionBuilder extends React.Component {
input = inputBox();
suggestion = jsonSuggestion();
} else if (valueType === 'column') {
if (Array.isArray(tableColumns)) {
if (typeof tableColumns?.[0] === 'string') {
input = wrapDoubleQuotes(
renderSelect(dispatchInput, value, tableColumns)
);
} else if (tableColumns.relationships || tableColumns.columns) {
} else if (tableColumns?.[0]?.optGroupTitle) {
input = (
<SelectGroup
<QuotedSelectGroup
selectDispatchFunc={dispatchInput}
value={value}
values={tableColumns}
@ -619,15 +635,21 @@ class PermissionBuilder extends React.Component {
});
};
const options = {};
const options = [];
// uncomment options.relationships assignment to enable selection of relationships
if (i === 0) {
options.root = ['$'];
// options.relationships = getTableRelationshipNames(prevTable);
options.columns = getTableColumnNames(prevTable);
options.push({ optGroupTitle: 'root', options: ['$'] });
// options.push({optGroupTitle: 'relationships', options: getTableRelationshipNames(prevTable)});
options.push({
optGroupTitle: 'columns',
options: getTableColumnNames(prevTable),
});
} else if (arr[i - 1] === '$') {
// options.relationships = getTableRelationshipNames(rootTable);
options.columns = getTableColumnNames(rootTable);
// options.push({optGroupTitle: 'relationships', options: getTableRelationshipNames(rootTable)});
options.push({
optGroupTitle: 'columns',
options: getTableColumnNames(rootTable),
});
prevTable = rootTable;
} else if (arr[i - 1]?.length) {
if (prevTable) {
@ -636,8 +658,12 @@ class PermissionBuilder extends React.Component {
const def = getRelationshipRefTable(prevTable, rel);
prevTable = findTable(allTableSchemas, def);
if (prevTable) {
// options.relationships = getTableRelationshipNames(prevTable);
// options.push({optGroupTitle: 'relationships', options: getTableRelationshipNames(prevTable)});
options.columns = getTableColumnNames(prevTable);
options.push({
optGroupTitle: 'columns',
options: getTableColumnNames(prevTable),
});
} else {
return null;
}
@ -797,12 +823,27 @@ class PermissionBuilder extends React.Component {
) => {
let tableColumnNames = [];
let tableRelationshipNames = [];
let computedFieldFn;
let tableSchema;
if (tableDef) {
tableSchema = findTable(tableSchemas, tableDef);
if (tableSchema) {
tableColumnNames = getTableColumnNames(tableSchema);
tableRelationshipNames = getTableRelationshipNames(tableSchema);
const { allFunctions } = this.props;
const computedFields = getGroupedTableComputedFields(
tableSchema,
allFunctions
);
const computedField = computedFields.scalar.find(
cs => cs.name === columnName
);
if (computedField) {
computedFieldFn = getComputedFieldFunction(
computedField,
allFunctions
);
}
}
}
@ -819,6 +860,14 @@ class PermissionBuilder extends React.Component {
schemaList,
prefix
);
} else if (computedFieldFn) {
_columnExp = renderOperatorExp(
dispatchFunc,
expression,
prefix,
computedFieldFn?.return_type_name,
tableColumnNames
);
} else {
let columnType = '';
if (tableSchema && columnName) {
@ -985,26 +1034,40 @@ class PermissionBuilder extends React.Component {
let tableColumnNames = [];
let tableRelationshipNames = [];
let scalarComputedFields = [];
if (tableDef) {
const tableSchema = findTable(tableSchemas, tableDef);
if (tableSchema) {
tableColumnNames = getTableColumnNames(tableSchema);
tableRelationshipNames = getTableRelationshipNames(tableSchema);
const { allFunctions } = this.props;
const computedFields = getGroupedTableComputedFields(
tableSchema,
allFunctions
);
scalarComputedFields = computedFields.scalar.filter(sc => {
const cFn = getComputedFieldFunction(sc, allFunctions)
?.input_arg_types;
// Only the computed fields that do not require extra arguments other than the table row
// are currenlty supported by the server https://github.com/hasura/graphql-engine/issues/7336
return cFn?.length === 1 && cFn[0].name === tableDef.name;
});
}
}
const columnOptions = tableColumnNames.concat(tableRelationshipNames);
const operatorOptions = boolOperators
.concat(['---'])
.concat(existOperators)
.concat(['---'])
.concat(columnOptions);
const computedFieldsOptions = scalarComputedFields.map(f => f.name);
const newOperatorOptions = [
{ optGroupTitle: 'bool operators', options: boolOperators },
{ optGroupTitle: 'exist operators', options: existOperators },
{ optGroupTitle: 'columns', options: tableColumnNames },
{ optGroupTitle: 'relationships', options: tableRelationshipNames },
{ optGroupTitle: 'computed fields', options: computedFieldsOptions },
];
const _boolExpKey = renderSelect(
dispatchOperationSelect,
operation,
operatorOptions,
newOperatorOptions,
prefix,
['---']
);

View File

@ -3,14 +3,21 @@ import React from 'react';
import styles from './PermissionBuilder.scss';
import { addToPrefix } from './utils';
type OptGroup = { optGroupTitle: string; options: string[] };
interface SelectGroupProps {
selectDispatchFunc: (value: string) => void;
value: string;
values: Record<string, string[]>;
values: OptGroup[];
prefix?: string;
disabledValues?: string[];
}
const optGroupSortFn = (a: OptGroup, b: OptGroup) => {
if (a.optGroupTitle === 'root') return 1;
if (b.optGroupTitle === 'root') return -1;
return 0;
};
const SelectGroup: React.FC<SelectGroupProps> = ({
selectDispatchFunc,
value,
@ -21,47 +28,54 @@ const SelectGroup: React.FC<SelectGroupProps> = ({
const dispatchSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
selectDispatchFunc(e.target.value);
};
const selectOptions = [];
selectOptions.push(<option value="">--</option>);
selectOptions.push(
<option value={addToPrefix(prefix, '--')} key={0} disabled>
--
</option>
);
Object.entries(values)
.sort((a, b) => (a[0] > b[0] ? 1 : -1))
.forEach(([key, val]) => {
if (val?.length) {
selectOptions.push(
<optgroup label={key}>
{val.map((v, i) => (
<option
value={addToPrefix(prefix, v)}
key={i}
disabled={disabledValues.includes(v)}
>
{v || '--'}
</option>
))}
</optgroup>
);
}
});
values.sort(optGroupSortFn).forEach(({ optGroupTitle, options }, i) => {
if (options?.length) {
selectOptions.push(
<optgroup label={optGroupTitle} key={i + 1}>
{options.map((option, j) => (
<option
value={addToPrefix(prefix, option)}
key={j}
disabled={disabledValues.includes(option)}
>
{option || '--'}
</option>
))}
</optgroup>
);
}
});
const selectedValue = addToPrefix(prefix, value || '--');
return (
<span>
&quot;&nbsp;
<select
value={selectedValue}
name={prefix}
onChange={dispatchSelect}
className={styles.qb_select}
data-test="qb-select"
>
{selectOptions}
</select>
&nbsp;&quot;
</span>
<select
value={selectedValue}
name={prefix}
onChange={dispatchSelect}
className={styles.qb_select}
data-test="qb-select"
>
{selectOptions}
</select>
);
};
export default SelectGroup;
export const QuotedSelectGroup: React.FC<SelectGroupProps> = props => {
return (
<span>
&quot;&nbsp;
<SelectGroup {...props} />
&quot;&nbsp;
</span>
);
};

View File

@ -815,6 +815,7 @@ class Permissions extends Component {
loadSchemasFunc={loadSchemasFunc}
tableDef={generateTableDef(tableName, currentSchema)}
allTableSchemas={allSchemas}
allFunctions={allFunctions}
schemaList={schemaList}
filter={filterString[filterType]}
dispatch={dispatch}

View File

@ -241,6 +241,18 @@ export const getGroupedTableComputedFields = (
return groupedComputedFields;
};
export const getComputedFieldFunction = (
computedField: ComputedField,
allFunctions: PGFunction[]
) => {
const computedFieldFnDef = computedField.definition.function;
return findFunction(
allFunctions,
computedFieldFnDef.name,
computedFieldFnDef.schema
);
};
const schemaListSql = (
schemas?: string[]
) => `SELECT schema_name FROM information_schema.schemata WHERE

View File

@ -15,6 +15,7 @@ export type PGFunction = {
function_schema: string;
function_definition: string;
return_type_type: string;
return_type_name: string;
function_type: string;
input_arg_types?: PGInputArgType[];
returns_set: boolean;

View File

@ -6,24 +6,22 @@
* This is where we have **TEMPORARY** fixes while we migrate the codebase to tailwind
*/
/* Redefine bootstrap form control to take precedence over tailwind */
.form-control {
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857;
color: #555555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0 0 0, 0.08);
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857;
color: #555555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0 0 0, 0.08);
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
/* reset some more bootstrap styles */
@ -42,31 +40,31 @@ input[data-test^="unique-"],
/* Browse row checkbox */
input[type="checkbox"][data-test^="row-checkbox"],
input[type="checkbox"][data-test="select-all-rows"] {
appearance: revert;
height: unset;
width: unset;
appearance: revert;
height: unset;
width: unset;
}
select[data-test="qb-select"], select.form-control {
font-size: unset;
line-height: unset;
border-radius: 4px;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
select[data-test='qb-select'],
select.form-control {
font-size: unset;
line-height: unset;
border-radius: 4px;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
}
/* By default, tailwind reset everything to block, but this breaks most of our SVGs and images, so this fix it */
svg, img {
display: inline;
svg,
img {
display: inline;
}
/* Add a blue color by default to all a without classes or with an empty class */
a:not([class]), a[class=""] {
color: #337ab7;
a:not([class]),
a[class=''] {
color: #337ab7;
}
.dropdown button {
background-color: rgb(239, 239, 239);
background-color: rgb(239, 239, 239);
}