mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-11-10 10:29:12 +03:00
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:
parent
5de2ef7d31
commit
64a5c52904
@ -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
|
||||
|
||||
|
@ -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,
|
||||
['---']
|
||||
);
|
||||
|
@ -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>
|
||||
"
|
||||
<select
|
||||
value={selectedValue}
|
||||
name={prefix}
|
||||
onChange={dispatchSelect}
|
||||
className={styles.qb_select}
|
||||
data-test="qb-select"
|
||||
>
|
||||
{selectOptions}
|
||||
</select>
|
||||
"
|
||||
</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>
|
||||
"
|
||||
<SelectGroup {...props} />
|
||||
"
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user