From 72dbafc3198d6747f56060f347873859a224d80a Mon Sep 17 00:00:00 2001 From: Matt Hardman Date: Thu, 27 Oct 2022 16:49:01 -0500 Subject: [PATCH] console: add permissions form to work with GDC PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6558 Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com> GitOrigin-RevId: 6ec7ca366a9a1aea987ca1a2c14811d4c44e4d60 --- .../gdc/introspection/getTableColumns.ts | 1 - console/src/features/DataSource/index.ts | 2 + .../MetadataAPI/metadataTypes/index.ts | 1 + .../metadataTypes/permissions/index.ts | 1 + .../metadataTypes/permissions/permissions.ts | 51 + .../MetadataAPI/metadataTypes/source/table.ts | 11 + .../PermissionsForm/BulkDelete.stories.tsx | 12 +- .../features/PermissionsForm/BulkDelete.tsx | 17 +- .../PermissionBuilder.module.scss | 32 - .../PermissionBuilder/PermissionBuilder.tsx | 1211 ----------------- .../PermissionBuilder/SelectGroup.tsx | 81 -- .../PermissionBuilder/utils.ts | 357 ----- .../PermissionsForm.stories.tsx | 85 +- .../PermissionsForm/PermissionsForm.tsx | 279 ++-- .../__snapshots__/cache.test.ts.snap | 100 -- .../api/__tests__/cache.test.ts | 47 - .../src/features/PermissionsForm/api/api.ts | 76 +- .../src/features/PermissionsForm/api/cache.ts | 118 -- .../{__tests__ => }/createInsertArgs.test.ts | 33 +- .../src/features/PermissionsForm/api/index.ts | 1 - .../src/features/PermissionsForm/api/utils.ts | 71 +- .../components/Aggregation.tsx | 2 +- .../components/BackendOnly.tsx | 2 +- .../components/RowPermissions.stories.tsx | 22 +- .../components/RowPermissions.tsx | 85 +- .../RowPermissionsBuilder/api/index.ts | 32 - .../api/introspectionQuery.ts | 96 -- .../components/Builder.tsx | 1 - .../components/RenderFormElement.tsx | 1 + .../RowPermissionsBuilder/hooks/index.ts | 19 +- .../RowPermissionsBuilder/index.tsx | 1 + .../utils/createDefaultValues.ts | 7 +- .../hooks/__tests__/useBulkDelete.test.tsx | 46 - .../hooks/__tests__/useDefaultValues.test.tsx | 79 -- .../hooks/__tests__/useFormData.test.tsx | 67 - .../__tests__/useUpdatePermissions.test.tsx | 103 -- .../hooks/dataFetchingHooks/index.ts | 2 +- .../useDefaultValues.stories.tsx | 80 -- .../dataFetchingHooks/useDefaultValues.tsx | 198 --- .../useDefaultValues/index.ts | 1 + .../useDefaultValues/mock/index.ts | 48 + .../useDefaultValues/useDefaultValues.test.ts | 36 + .../useDefaultValues/useDefaultValues.tsx | 141 ++ .../useDefaultValues/utils.ts | 294 ++++ .../dataFetchingHooks/useFormData.stories.tsx | 51 - .../hooks/dataFetchingHooks/useFormData.tsx | 124 -- .../dataFetchingHooks/useFormData/index.ts | 1 + .../useFormData/mock/index.ts | 83 ++ .../useFormData/useFormData.test.ts | 14 + .../useFormData/useFormData.tsx | 139 ++ .../hooks/dataFetchingHooks/utils.ts | 29 + .../hooks/submitHooks/useBulkDelete.tsx | 159 ++- .../hooks/submitHooks/useDeletePermission.tsx | 63 +- .../hooks/submitHooks/useSubmitForm.tsx | 118 +- .../useUpdatePermissions.stories.tsx | 101 -- .../submitHooks/useUpdatePermissions.tsx | 22 +- .../PermissionsForm/mocks/dataStubs.ts | 56 +- .../PermissionsForm/mocks/handlers.mock.ts | 90 +- .../PermissionsForm/utils/formSchema.ts | 8 +- .../PermissionsTab/PermissionsTab.stories.tsx | 115 +- .../PermissionsTab/PermissionsTab.tsx | 29 +- .../PermissionsTable.stories.tsx | 25 +- .../PermissionsTable/PermissionsTable.tsx | 38 +- .../components/Cells.stories.tsx | 2 +- .../PermissionsTable/components/Cells.tsx | 9 +- .../PermissionsTable/hooks/usePermissions.tsx | 380 ++++-- .../hooks/useTableMachine.typegen.ts | 16 +- 67 files changed, 1829 insertions(+), 3793 deletions(-) create mode 100644 console/src/features/MetadataAPI/metadataTypes/permissions/index.ts create mode 100644 console/src/features/MetadataAPI/metadataTypes/permissions/permissions.ts delete mode 100644 console/src/features/PermissionsForm/PermissionBuilder/PermissionBuilder.module.scss delete mode 100644 console/src/features/PermissionsForm/PermissionBuilder/PermissionBuilder.tsx delete mode 100644 console/src/features/PermissionsForm/PermissionBuilder/SelectGroup.tsx delete mode 100644 console/src/features/PermissionsForm/PermissionBuilder/utils.ts delete mode 100644 console/src/features/PermissionsForm/api/__tests__/__snapshots__/cache.test.ts.snap delete mode 100644 console/src/features/PermissionsForm/api/__tests__/cache.test.ts delete mode 100644 console/src/features/PermissionsForm/api/cache.ts rename console/src/features/PermissionsForm/api/{__tests__ => }/createInsertArgs.test.ts (85%) delete mode 100644 console/src/features/PermissionsForm/components/RowPermissionsBuilder/api/index.ts delete mode 100644 console/src/features/PermissionsForm/components/RowPermissionsBuilder/api/introspectionQuery.ts delete mode 100644 console/src/features/PermissionsForm/hooks/__tests__/useBulkDelete.test.tsx delete mode 100644 console/src/features/PermissionsForm/hooks/__tests__/useDefaultValues.test.tsx delete mode 100644 console/src/features/PermissionsForm/hooks/__tests__/useFormData.test.tsx delete mode 100644 console/src/features/PermissionsForm/hooks/__tests__/useUpdatePermissions.test.tsx delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues.stories.tsx delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues.tsx create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/index.ts create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/mock/index.ts create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.test.ts create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.tsx create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/utils.ts delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData.stories.tsx delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData.tsx create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/index.ts create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/mock/index.ts create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.test.ts create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.tsx create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/utils.ts delete mode 100644 console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.stories.tsx diff --git a/console/src/features/DataSource/gdc/introspection/getTableColumns.ts b/console/src/features/DataSource/gdc/introspection/getTableColumns.ts index a31d6482221..e078e80f6e1 100644 --- a/console/src/features/DataSource/gdc/introspection/getTableColumns.ts +++ b/console/src/features/DataSource/gdc/introspection/getTableColumns.ts @@ -51,7 +51,6 @@ export const getTableColumns = async (props: GetTableColumnsProps) => { sourceCustomization: metadataSource?.customization, configuration: metadataTable.configuration, }); - console.log(queryRoot); // eslint-disable-next-line no-underscore-dangle const graphQLFields = introspectionResult.data.__schema.types.find( diff --git a/console/src/features/DataSource/index.ts b/console/src/features/DataSource/index.ts index bef4cfadf29..17b217a2f00 100644 --- a/console/src/features/DataSource/index.ts +++ b/console/src/features/DataSource/index.ts @@ -33,6 +33,7 @@ import { exportMetadata, getDriverPrefix, NetworkArgs, + runIntrospectionQuery, RunSQLResponse, } from './api'; import { getAllSourceKinds } from './common/getAllSourceKinds'; @@ -411,4 +412,5 @@ export { getTableName, RunSQLResponse, getDriverPrefix, + runIntrospectionQuery, }; diff --git a/console/src/features/MetadataAPI/metadataTypes/index.ts b/console/src/features/MetadataAPI/metadataTypes/index.ts index 616fd765cee..dc8aa63d283 100644 --- a/console/src/features/MetadataAPI/metadataTypes/index.ts +++ b/console/src/features/MetadataAPI/metadataTypes/index.ts @@ -10,4 +10,5 @@ export * from './network'; export * from './restEndpoints'; export * from './apiLimits'; export * from './graphqlSchemaIntrospection'; +export * from './permissions'; export * from './metadata'; diff --git a/console/src/features/MetadataAPI/metadataTypes/permissions/index.ts b/console/src/features/MetadataAPI/metadataTypes/permissions/index.ts new file mode 100644 index 00000000000..c85954d3e2b --- /dev/null +++ b/console/src/features/MetadataAPI/metadataTypes/permissions/index.ts @@ -0,0 +1 @@ +export * from './permissions'; diff --git a/console/src/features/MetadataAPI/metadataTypes/permissions/permissions.ts b/console/src/features/MetadataAPI/metadataTypes/permissions/permissions.ts new file mode 100644 index 00000000000..b74b567c3a5 --- /dev/null +++ b/console/src/features/MetadataAPI/metadataTypes/permissions/permissions.ts @@ -0,0 +1,51 @@ +export type Permission = + | InsertPermission + | SelectPermission + | UpdatePermission + | DeletePermission; + +type BasePermission = { + role: string; +}; + +export interface InsertPermission extends BasePermission { + permission: InsertPermissionDefinition; +} +export interface InsertPermissionDefinition { + check?: Record; + set?: Record; + columns?: string[]; + backend_only?: boolean; +} + +export interface SelectPermission extends BasePermission { + permission: SelectPermissionDefinition; +} +export interface SelectPermissionDefinition { + columns?: string[]; + filter?: Record; + allow_aggregations?: boolean; + query_root_fields?: string[]; + subscription_root_fields?: string[]; + limit?: number; +} + +export interface UpdatePermission extends BasePermission { + permission: UpdatePermissionDefinition; +} + +export interface UpdatePermissionDefinition { + columns?: string[]; + filter?: Record; + check?: Record; + set?: Record; + backend_only?: boolean; +} + +export interface DeletePermission extends BasePermission { + permission: DeletePermissionDefinition; +} +export interface DeletePermissionDefinition { + filter?: Record; + backend_only?: boolean; +} diff --git a/console/src/features/MetadataAPI/metadataTypes/source/table.ts b/console/src/features/MetadataAPI/metadataTypes/source/table.ts index 57894aab0a5..c0f3597dee1 100644 --- a/console/src/features/MetadataAPI/metadataTypes/source/table.ts +++ b/console/src/features/MetadataAPI/metadataTypes/source/table.ts @@ -1,3 +1,9 @@ +import { + InsertPermission, + SelectPermission, + UpdatePermission, + DeletePermission, +} from '../permissions'; import { Legacy_SourceToRemoteSchemaRelationship, LocalTableArrayRelationship, @@ -73,4 +79,9 @@ export type MetadataTable = { | ManualArrayRelationship | LocalTableArrayRelationship )[]; + + insert_permissions?: InsertPermission[]; + select_permissions?: SelectPermission[]; + update_permissions?: UpdatePermission[]; + delete_permissions?: DeletePermission[]; }; diff --git a/console/src/features/PermissionsForm/BulkDelete.stories.tsx b/console/src/features/PermissionsForm/BulkDelete.stories.tsx index c8bdf761eb3..ebd35a86b12 100644 --- a/console/src/features/PermissionsForm/BulkDelete.stories.tsx +++ b/console/src/features/PermissionsForm/BulkDelete.stories.tsx @@ -13,20 +13,12 @@ export default { decorators: [ReactQueryDecorator()], } as Meta; -const dataLeaf = { - type: 'schema', - name: 'users', - leaf: { - type: 'table', - name: 'users', - }, -}; - export const Primary: Story = args => { return ; }; Primary.args = { - dataLeaf, + currentSource: 'postgres', + dataSourceName: 'default', roles: ['user'], handleClose: () => {}, }; diff --git a/console/src/features/PermissionsForm/BulkDelete.tsx b/console/src/features/PermissionsForm/BulkDelete.tsx index 7818b6da962..f45276e0056 100644 --- a/console/src/features/PermissionsForm/BulkDelete.tsx +++ b/console/src/features/PermissionsForm/BulkDelete.tsx @@ -2,25 +2,26 @@ import React from 'react'; import { Button } from '@/new-components/Button'; import { useBulkDeletePermissions } from './hooks'; -import { DataLeaf } from '../PermissionsTab/types/types'; -import { useDataSource } from '../PermissionsTab/types/useDataSource'; export interface BulkDeleteProps { + currentSource: string; + dataSourceName: string; roles: string[]; - dataLeaf: DataLeaf; + table: unknown; handleClose: () => void; } export const BulkDelete: React.FC = ({ + currentSource, + dataSourceName, roles, - dataLeaf, + table, handleClose, }) => { - const dataSource = useDataSource(); - const { submit, isLoading, isError } = useBulkDeletePermissions({ - dataSource, - dataLeaf, + currentSource, + dataSourceName, + table, }); const handleDelete = async () => { diff --git a/console/src/features/PermissionsForm/PermissionBuilder/PermissionBuilder.module.scss b/console/src/features/PermissionsForm/PermissionBuilder/PermissionBuilder.module.scss deleted file mode 100644 index f7fc2c3cc51..00000000000 --- a/console/src/features/PermissionsForm/PermissionBuilder/PermissionBuilder.module.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import '../../../components/Common/Common.module'; - -.qb_select, -.qb_input { - border: none; - background-color: transparent; - border-bottom: 2px dotted; - border-radius: 0; - //color: #CB3837 -} - -.qb_select { - cursor: pointer; -} - -.qb_select:focus, -.qb_input:focus { - outline: none; -} - -.qb_container { - // display: inline-block; - min-width: 50%; -} - -.qb_input_suggestion { - margin-left: 10px; - color: #080; - font-size: 10px; - font-weight: bold; - cursor: pointer; -} diff --git a/console/src/features/PermissionsForm/PermissionBuilder/PermissionBuilder.tsx b/console/src/features/PermissionsForm/PermissionBuilder/PermissionBuilder.tsx deleted file mode 100644 index ad2e6346789..00000000000 --- a/console/src/features/PermissionsForm/PermissionBuilder/PermissionBuilder.tsx +++ /dev/null @@ -1,1211 +0,0 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -import React from 'react'; - -import { - currentDriver, - dataSource, - findTable, - generateTableDef, - getQualifiedTableDef, - getRelationshipRefTable, - getSchemaTableNames, - getTableColumn, - getTableColumnNames, - getTableRelationship, - getTableRelationshipNames, - getTrackedTables, - getTableFromRelationshipChain, -} from '@/dataSources'; -import { - getComputedFieldsWithoutArgs, - getComputedFieldFunction, - getGroupedTableComputedFields, -} from '@/dataSources/services/postgresql'; -import { PGFunction } from '@/dataSources/services/postgresql/types'; -import { ComputedField, Table } from '@/dataSources/types'; -import { QualifiedTable } from '@/metadata/types'; -import QueryBuilderJson from '../../../components/Common/QueryBuilderJson/QueryBuilderJson'; -import { - getAllJsonPaths, - isArray, - isJsonString, - isObject, -} from '../../../components/Common/utils/jsUtils'; -import { - addToPrefix, - BoolOperators, - boolOperators, - ColumnOperators, - existOperators, - getOperatorInputType, - getPermissionOperators, - getRootType, - isArrayBoolOperator, - isArrayColumnOperator, - isBoolOperator, - isColumnOperator, - isExistOperator, - TABLE_KEY, - WHERE_KEY, -} from './utils'; -import SelectGroup, { OptGroup, QuotedSelectGroup } from './SelectGroup'; -import { Nullable } from '../../../components/Common/utils/tsUtils'; - -import styles from './PermissionBuilder.module.scss'; - -interface PermissionBuilderProps { - allTableSchemas: Table[]; - allFunctions: PGFunction[]; - schemaList: string[]; - dispatch: () => void; - dispatchFuncSetFilter: (filter: string) => void; - loadSchemasFunc: (schemaNames: string[]) => void; - filter: string; - tableDef: QualifiedTable; -} - -class PermissionBuilder extends React.Component { - override componentDidMount() { - this.loadMissingSchemas(); - } - - override componentDidUpdate(prevProps: PermissionBuilderProps) { - // check for and fetch any missing schemas if - // either permission filter or available table schemas have changed - if ( - this.props.filter !== prevProps.filter || - this.props.allTableSchemas.length !== prevProps.allTableSchemas.length - ) { - this.loadMissingSchemas(); - } - } - - loadMissingSchemas( - tableDef = this.props.tableDef, - filter = this.props.filter - ) { - const { loadSchemasFunc } = this.props; - - const findMissingSchemas = ( - path: Record | string[] | string, - currTable: QualifiedTable - ): string[] => { - let missingSchemas = []; - - let value: any; - if (isObject(path)) { - value = Object.values(path)[0]; - path = Object.keys(path)[0]; - } - - const getNewPath = (newPath: string) => { - return value ? { [newPath]: value } : newPath; - }; - - const pathSplit = (path as string).split('.'); - - const operator = pathSplit[0]; - - if (isArrayBoolOperator(operator as BoolOperators)) { - const newPath = getNewPath(pathSplit.slice(2).join('.')); - missingSchemas = findMissingSchemas(newPath, currTable); - } else if (isBoolOperator(operator as BoolOperators)) { - const newPath = getNewPath(pathSplit.slice(1).join('.')); - missingSchemas = findMissingSchemas(newPath, currTable); - } else if (isExistOperator(operator)) { - const existTableDef = getQualifiedTableDef(value[TABLE_KEY]); - - let existTableSchema; - if (existTableDef) { - existTableSchema = (existTableDef as QualifiedTable).schema; - } - - const existWhere = value[WHERE_KEY] || ''; - - if (existTableSchema) { - const { allTableSchemas } = this.props; - - const allSchemaNames = allTableSchemas.map(t => t.table_schema); - - if (!allSchemaNames?.includes(existTableSchema)) { - missingSchemas.push(existTableSchema); - } - } - - this.loadMissingSchemas( - existTableDef as QualifiedTable, - JSON.stringify(existWhere) - ); - } else if (isColumnOperator(operator as ColumnOperators)) { - // no missing schemas - } else { - const { allTableSchemas } = this.props; - - let tableRelationshipNames: string[] = []; - - const tableSchema = findTable(allTableSchemas, currTable); - - if (tableSchema) { - tableRelationshipNames = getTableRelationshipNames(tableSchema); - } - - if (tableRelationshipNames?.includes(operator)) { - const relationship = getTableRelationship(tableSchema!, operator); - const refTable = getRelationshipRefTable(tableSchema!, relationship!); - - const refTableSchema = findTable(allTableSchemas, refTable!); - if (!refTableSchema) { - missingSchemas.push(refTable!.schema); - } - - const newPath = getNewPath(pathSplit.slice(1).join('.')); - missingSchemas.push(...findMissingSchemas(newPath, refTable!)); - } else { - // no missing schemas - } - } - - return missingSchemas; - }; - - const missingSchemas: string[] = []; - const paths = getAllJsonPaths(JSON.parse(filter || '{}'), existOperators); - - paths.forEach((path: Record | string) => { - const subMissingSchemas = findMissingSchemas(path, tableDef); - missingSchemas.push(...subMissingSchemas); - }); - - if (missingSchemas.length > 0) { - loadSchemasFunc(missingSchemas); - } - } - - override render() { - const wrapDoubleQuotes = (value: JSX.Element) => { - return ( - - "  - {value} -  " - - ); - }; - - /* ****************************** */ - - const getFilter = ( - defaultSchema: string, - conditions: Record, - prefix: string, - value = '' - ) => { - let boolExp = {}; - - const getArrayBoolOperatorFilter = ( - operator: string, - opValue: string, - opConditions: Record[], - opPrefix: string, - isLast: boolean - ) => { - const filter: Record> = {}; - - if (isLast) { - filter[operator] = []; - } else { - const opPrefixSplit = opPrefix.split('.'); - - const position = parseInt(opPrefixSplit[0], 10); - const newPrefix = opPrefixSplit.slice(1).join('.'); - - filter[operator] = opConditions; - filter[operator][position] = getFilter( - defaultSchema, - opConditions[position], - newPrefix, - opValue - ); - if (Object.keys(filter[operator][position]).length === 0) { - filter[operator].splice(position, 1); - } - } - - return filter; - }; - - const getBoolOperatorFilter = ( - operator: string, - opValue: string, - opConditions: Record[], - opPrefix: string, - isLast: boolean - ) => { - const filter: Record> = {}; - - if (isLast) { - filter[operator] = {}; - } else { - filter[operator] = getFilter( - defaultSchema, - opConditions, - opPrefix, - opValue - ); - } - - return filter; - }; - - const getArrayColumnOperatorFilter = ( - operator: string, - opValue: string, - opConditions: string[], - opPrefix: string, - isLast: boolean - ) => { - const filter: Record = {}; - - if (isLast) { - filter[operator] = opValue || []; - } else { - const opPrefixSplit = opPrefix.split('.'); - const position = parseInt(opPrefixSplit[0], 10); - - filter[operator] = opConditions || []; - if (opValue !== '') { - filter[operator][position] = opValue; - } else { - filter[operator].splice(position, 1); - } - } - - return filter; - }; - - const getColumnOperatorFilter = (operator: string, opValue: string) => { - const filter: Record = {}; - filter[operator] = opValue; - return filter; - }; - - const getExistsOperatorFilter = ( - operator: string, - // HACK: justifitied any - opValue: any, - opConditions: Record>, - opPrefix: string, - isLast: boolean - ) => { - const filter = { - [operator]: opConditions, - }; - - if (isLast) { - filter[operator] = { - [TABLE_KEY]: generateTableDef('', defaultSchema), - [WHERE_KEY]: {}, - }; - } else if (opPrefix === TABLE_KEY) { - filter[operator] = { - [TABLE_KEY]: opValue, - [WHERE_KEY]: {}, - }; - } else if (opPrefix === WHERE_KEY) { - filter[operator][WHERE_KEY] = getFilter( - defaultSchema, - opConditions[opPrefix], - opValue.prefix, - opValue.value - ); - } - - return filter; - }; - - const getColumnFilter = ( - operator: string, - opValue: string, - opConditions: Record, - opPrefix: string, - isLast: boolean - ) => { - const filter: Record> = {}; - - if (isLast) { - filter[operator] = {}; - } else { - filter[operator] = getFilter( - defaultSchema, - opConditions, - opPrefix, - opValue - ); - } - - return filter; - }; - - const prefixSplit = prefix.split('.'); - - const operator = prefixSplit[0]; - const newPrefix = prefixSplit.slice(1).join('.'); - - const isLast = prefixSplit.length === 1; - - const opConditions = isLast ? null : conditions[operator]; - - if (operator === '') { - // blank bool exp - } else if (isArrayBoolOperator(operator as BoolOperators)) { - boolExp = getArrayBoolOperatorFilter( - operator, - value, - opConditions, - newPrefix, - isLast - ); - } else if (isBoolOperator(operator as BoolOperators)) { - boolExp = getBoolOperatorFilter( - operator, - value, - opConditions, - newPrefix, - isLast - ); - } else if (isArrayColumnOperator(operator)) { - boolExp = getArrayColumnOperatorFilter( - operator, - value, - opConditions, - newPrefix, - isLast - ); - } else if (isColumnOperator(operator as ColumnOperators)) { - boolExp = getColumnOperatorFilter(operator, value); - } else if (isExistOperator(operator)) { - boolExp = getExistsOperatorFilter( - operator, - value, - opConditions, - newPrefix, - isLast - ); - } else { - boolExp = getColumnFilter( - operator, - value, - opConditions, - newPrefix, - isLast - ); - } - - return boolExp; - }; - - // eslint-disable-next-line no-underscore-dangle - const _dispatchFunc = (data: { prefix: string; value?: string }) => { - const { filter, dispatchFuncSetFilter, tableDef } = this.props; - const newFilter = getFilter( - tableDef.schema, - JSON.parse(filter || '{}'), - data.prefix, - data.value - ); - dispatchFuncSetFilter(JSON.stringify(newFilter)); - }; - - /* ****************************** */ - - const renderSelect = ( - selectDispatchFunc: (val: string) => void, - value: string, - values: string[] | OptGroup[], - prefix = '', - disabledValues: string[] = [] - ) => { - const dispatchSelect = (e: React.ChangeEvent) => { - selectDispatchFunc(e.target.value); - }; - - if (typeof values?.[1] !== 'string') { - return ( - - ); - } - - const selectOptions: JSX.Element[] = []; - [''].concat(values as string[]).forEach((val, i) => { - const optionVal = addToPrefix(prefix, val); - selectOptions.push( - - ); - }); - - const selectedValue = addToPrefix(prefix, value || '--'); - - return ( - - ); - }; - - const renderBoolSelect = ( - selectDispatchFunc: (val: string) => void, - value: string, - prefix = '', - disabledValues = [] - ) => { - const newValue = typeof value === 'boolean' ? `${value}` : ''; - - const values = ['true', 'false']; - - return renderSelect( - selectDispatchFunc, - newValue, - values, - prefix, - disabledValues - ); - }; - - const renderInput = ( - inputDispatchFunc: (val: string) => void, - value: string - ) => { - const dispatchInput = (e: React.ChangeEvent) => { - inputDispatchFunc(e.target.value); - }; - - let inputValue = value; - - if (typeof value === 'object') { - inputValue = JSON.stringify(value); - } - - return ( - - ); - }; - - const renderSuggestion = ( - suggestionDispatchFunc: (val: string) => void, - inputValue: string, - displayValue: string | null = null - ) => { - const dispatchSuggestion = () => { - suggestionDispatchFunc(inputValue); - }; - - return ( - - [{displayValue || inputValue}] - - ); - }; - - /* ****************************** */ - - const renderValue = ( - dispatchFunc: ({ - prefix, - value, - }: { - prefix: string; - value: string | number | boolean; - }) => void, - value: string, - prefix: string, - valueType: string, - tableColumns: string[] | OptGroup[], - showSuggestion = true - ): JSX.Element => { - const currentTypeMap = dataSource.permissionColumnDataTypes; - if (!currentTypeMap) { - // shouldn't happen ideally. check in place for the MySQL `null` - return <>; - } - const dispatchInput = (val: string | number) => { - let newValue: typeof val | boolean = val; - - if (val !== '') { - if ( - currentTypeMap?.boolean && - currentTypeMap.boolean.includes(valueType) - ) { - newValue = val === 'true'; - } else if ( - currentTypeMap?.numeric && - currentTypeMap.numeric.includes(valueType) && - !isNaN(val as number) && - (val as string).substr(-1) !== '.' - ) { - newValue = Number(val); - } else if ( - currentTypeMap?.jsonb && - currentTypeMap.jsonb.includes(valueType) && - isJsonString(val as string) - ) { - newValue = JSON.parse(val as string); - } - } - dispatchFunc({ prefix, value: newValue }); - }; - - const inputBox = () => { - return renderInput(dispatchInput, value); - }; - - const sessionVariableSuggestion = () => { - return renderSuggestion(dispatchInput, 'X-Hasura-User-Id'); - }; - - const jsonSuggestion = () => { - return renderSuggestion(dispatchInput, '{}', 'JSON'); - }; - - let input; - let suggestion; - - if ( - currentTypeMap?.boolean && - currentTypeMap.boolean.includes(valueType) && - currentDriver === 'postgres' - ) { - input = renderBoolSelect(dispatchInput, value); - } else if ( - currentTypeMap?.jsonb && - currentTypeMap.jsonb.includes(valueType) && - currentDriver === 'postgres' - ) { - input = inputBox(); - suggestion = jsonSuggestion(); - } else if (valueType === 'column') { - if (typeof tableColumns?.[0] === 'string') { - input = wrapDoubleQuotes( - renderSelect( - dispatchInput, - value as string, - tableColumns as string[] - ) - ); - } else if (tableColumns?.[0]?.optGroupTitle) { - input = ( - - ); - } - } else { - input = wrapDoubleQuotes(inputBox()); - suggestion = sessionVariableSuggestion(); - } - - return ( - - {input} {showSuggestion ? suggestion : ''} - - ); - }; - - const renderColumnArray = ( - dispatchFunc: (arg: { - prefix: string; - value: Array; - }) => void, - values: string[], - prefix: string, - valueType: string - ) => { - const { tableDef, allTableSchemas } = this.props; - const rootTable = findTable(allTableSchemas, tableDef)!; - let prevTable: Table | null = getTableFromRelationshipChain( - allTableSchemas, - rootTable, - prefix - ); - - const inputArray = (values?.length < 1 ? [''] : []) - .concat(values || []) - .concat(['']) - .map((val, i, arr) => { - const onChange = (v: { - prefix: string; - value: string | number | boolean; - }) => { - dispatchFunc({ - prefix: v.prefix, - value: [...arr.slice(0, i), v.value], - }); - }; - - const options: OptGroup[] = []; - // uncomment options.relationships assignment to enable selection of relationships - if (i === 0) { - 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.push({optGroupTitle: 'relationships', options: getTableRelationshipNames(rootTable)}); - options.push({ - optGroupTitle: 'columns', - options: getTableColumnNames(rootTable), - }); - prevTable = rootTable; - } else if (arr[i - 1]?.length) { - if (prevTable) { - const rel = getTableRelationship(prevTable, arr[i - 1]); - if (rel) { - const def = getRelationshipRefTable(prevTable, rel)!; - prevTable = findTable(allTableSchemas, def)!; - if (prevTable) { - // options.push({optGroupTitle: 'relationships', options: getTableRelationshipNames(prevTable)}); - options.push({ - optGroupTitle: 'columns', - options: getTableColumnNames(prevTable), - }); - } else { - return null; - } - } else { - prevTable = null; - } - } - } - return renderValue(onChange, val, prefix, valueType, options, false); - }); - - const unselectedElements = [(values || []).length]; - - return ( - - - - ); - }; - - const renderValueArray = ( - // TODO: fix any - dispatchFunc: (arg: { prefix: string; value: any }) => void, - values: string[], - prefix: string, - valueType: string, - tableColumns: string[] - ) => { - if (valueType === 'column') { - return renderColumnArray(dispatchFunc, values, prefix, valueType); - } - const dispatchInput = (val: any) => { - dispatchFunc({ prefix, value: val }); - }; - - const sessionVariableSuggestion = () => { - return renderSuggestion(dispatchInput, 'X-Hasura-Allowed-Ids'); - }; - - const inputArray: JSX.Element[] = []; - - (values || []).concat(['']).forEach((val: string, i: number) => { - const input = renderValue( - dispatchFunc, - val, - addToPrefix(prefix, i), - valueType, - tableColumns, - false - ); - inputArray.push(input); - }); - - const unselectedElements = [(values || []).length]; - - const inputArrayJSX = ( - - ); - - const suggestionJSX = sessionVariableSuggestion(); - - return ( - - {inputArrayJSX} {suggestionJSX} - - ); - }; - - const renderOperatorExp = ( - dispatchFunc: (arg0: { prefix: string }) => void, - // HACK: impossible to type - expression: any, - prefix: string, - valueType: string, - tableColumns: string[] - ) => { - const dispatchColumnOperatorSelect = (val: string) => { - dispatchFunc({ prefix: val }); - }; - - // handle shorthand notation for eq - let newExpression = expression; - if (typeof newExpression !== 'object') { - newExpression = { _eq: newExpression }; - } - - const operator = Object.keys(newExpression)[0]; - const operationValue = newExpression[operator]; - - const currentTypeMap = dataSource.permissionColumnDataTypes; - const rootValueType = getRootType(valueType, currentTypeMap); - const operators = ( - getPermissionOperators( - dataSource.supportedColumnOperators as ColumnOperators[], - currentTypeMap - ) as Record - )[rootValueType]; - - const operatorSelect = renderSelect( - dispatchColumnOperatorSelect, - operator, - operators, - prefix - ); - - let valueInput = null; - if (operator) { - const operatorInputType = - getOperatorInputType(operator as ColumnOperators) || valueType; - - if ( - isArrayColumnOperator(operator) && - operationValue instanceof Array - ) { - valueInput = renderValueArray( - dispatchFunc, - operationValue, - addToPrefix(prefix, operator), - operatorInputType, - tableColumns - ); - } else { - valueInput = renderValue( - dispatchFunc, - operationValue, - addToPrefix(prefix, operator), - operatorInputType, - tableColumns - ); - } - } - - const operatorExp = [{ key: operatorSelect, value: valueInput }]; - - const unselectedElements = []; - if (!operator) { - unselectedElements.push(0); - } - - return ( - - ); - }; - - const renderColumnExp = ( - dispatchFunc: (data: { prefix: string; value?: string }) => void, - columnName: string, - expression: Record, - tableDef: QualifiedTable, - tableSchemas: Table[], - schemaList: string[], - prefix: string - ) => { - let tableColumnNames: string[] = []; - let tableRelationshipNames: string[] = []; - let computedFieldFn; - let tableSchema: Table | null = null; - if (tableDef) { - tableSchema = findTable(tableSchemas, tableDef)!; - if (tableSchema) { - tableColumnNames = getTableColumnNames(tableSchema); - tableRelationshipNames = getTableRelationshipNames(tableSchema); - const { allFunctions } = this.props; - const computedFields = getGroupedTableComputedFields( - tableSchema.computed_fields, - allFunctions - ); - const computedField = computedFields.scalar.find( - cs => cs.computed_field_name === columnName - ); - if (computedField) { - computedFieldFn = getComputedFieldFunction( - computedField, - allFunctions - ); - } - } - } - - let columnExp = null; - if (tableRelationshipNames?.includes(columnName)) { - const relationship = getTableRelationship(tableSchema!, columnName); - const refTable = getRelationshipRefTable(tableSchema!, relationship!); - - columnExp = renderBoolExp( - dispatchFunc, - expression, - refTable!, - tableSchemas, - schemaList, - prefix - ); - } else if (computedFieldFn) { - columnExp = renderOperatorExp( - dispatchFunc, - expression, - prefix, - computedFieldFn?.return_type_name, - tableColumnNames - ); - } else { - let columnType = ''; - if (tableSchema && columnName) { - const column = getTableColumn(tableSchema, columnName); - if (column) { - columnType = dataSource.getColumnType(column); - } - } - - columnExp = renderOperatorExp( - dispatchFunc, - expression, - prefix, - columnType, - tableColumnNames - ); - } - - return columnExp; - }; - - const renderTableSelect = ( - dispatchFunc: (tableDef: QualifiedTable) => void, - tableDef: QualifiedTable, - tableSchemas: Table[], - schemaList: string[] | OptGroup[], - defaultSchema: string - ) => { - const selectedSchema = tableDef ? tableDef.schema : defaultSchema; - const selectedTable = tableDef ? tableDef.name : ''; - - const schemaSelectDispatchFunc = (val: Nullable) => { - dispatchFunc(generateTableDef('', val)); - }; - - const tableSelectDispatchFunc = (val: string) => { - dispatchFunc(generateTableDef(val, selectedSchema)); - }; - - const tableNames = getSchemaTableNames(tableSchemas, selectedSchema); - - const schemaSelect = wrapDoubleQuotes( - renderSelect(schemaSelectDispatchFunc, selectedSchema, schemaList) - ); - - const tableSelect = wrapDoubleQuotes( - renderSelect(tableSelectDispatchFunc, selectedTable, tableNames) - ); - - const tableExp = [ - { key: 'schema', value: schemaSelect }, - { key: 'name', value: tableSelect }, - ]; - - return ; - }; - - const renderExistsExp = ( - dispatchFunc: (arg0: { prefix: string; value: any }) => void, - operation: string, - expression: Record, - tableDef: QualifiedTable, - tableSchemas: Table[], - schemaList: string[], - prefix: string - ) => { - const dispatchTableSelect = (val: QualifiedTable) => { - dispatchFunc({ prefix: addToPrefix(prefix, TABLE_KEY), value: val }); - }; - - const dispatchWhereOperatorSelect = (val: { - prefix: string; - value?: string; - }) => { - dispatchFunc({ prefix: addToPrefix(prefix, WHERE_KEY), value: val }); - }; - - const existsOpTable = getQualifiedTableDef( - expression[TABLE_KEY] - ) as QualifiedTable; - const existsOpWhere = expression[WHERE_KEY]; - - const tableSelect = renderTableSelect( - dispatchTableSelect, - existsOpTable, - tableSchemas, - schemaList, - tableDef.schema - ); - - let whereSelect = {}; - if (existsOpTable) { - whereSelect = renderBoolExp( - dispatchWhereOperatorSelect, - existsOpWhere, - existsOpTable, - tableSchemas, - schemaList - ); - } - - const existsArgsJsonObject = { - [TABLE_KEY]: tableSelect, - [WHERE_KEY]: whereSelect, - }; - - const unselectedElements = []; - if (!existsOpTable || !existsOpTable.name) { - unselectedElements.push(WHERE_KEY); - } - - return ( - - ); - }; - - const renderBoolExpArray = ( - dispatchFunc: (data: { - prefix: string; - value?: string | undefined; - }) => void, - expressions: any[], - tableDef: QualifiedTable, - tableSchemas: Table[], - schemaList: string[], - prefix: string - ) => { - const boolExpArray: JSX.Element[] = []; - expressions = isArray(expressions) ? expressions : []; - - expressions.concat([{}]).forEach((expression: any, i: number) => { - const boolExp = renderBoolExp( - dispatchFunc, - expression, - tableDef, - tableSchemas, - schemaList, - addToPrefix(prefix, i) - ); - boolExpArray.push(boolExp); - }); - - const unselectedElements = [expressions.length]; - - return ( - - ); - }; - - const renderBoolExp = ( - dispatchFunc: (data: { prefix: string; value?: string }) => void, - expression: Record, - tableDef: QualifiedTable, - tableSchemas: Table[], - schemaList: string[], - prefix = '' - ) => { - const dispatchOperationSelect = (val: string) => { - dispatchFunc({ prefix: val }); - }; - - let operation = ''; - if (expression) { - operation = Object.keys(expression)[0]; - } - - let tableColumnNames: string[] = []; - let tableRelationshipNames: string[] = []; - let scalarComputedFields: ComputedField[] = []; - if (tableDef) { - const tableSchema = findTable(tableSchemas, tableDef); - if (tableSchema) { - tableColumnNames = getTableColumnNames(tableSchema); - tableRelationshipNames = getTableRelationshipNames(tableSchema); - const { allFunctions } = this.props; - const computedFields = getGroupedTableComputedFields( - tableSchema.computed_fields, - allFunctions - ); - scalarComputedFields = getComputedFieldsWithoutArgs( - computedFields.scalar, - allFunctions, - tableDef.name - ); - } - } - - const computedFieldsOptions = scalarComputedFields.map( - f => f.computed_field_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, - newOperatorOptions, - prefix, - ['---'] - ); - - let boolExpValue = null; - if (operation) { - const newPrefix = addToPrefix(prefix, operation); - if (isArrayBoolOperator(operation as BoolOperators)) { - boolExpValue = renderBoolExpArray( - dispatchFunc, - expression[operation], - tableDef, - tableSchemas, - schemaList, - newPrefix - ); - } else if (isBoolOperator(operation as BoolOperators)) { - boolExpValue = renderBoolExp( - dispatchFunc, - expression[operation], - tableDef, - tableSchemas, - schemaList, - newPrefix - ); - } else if (isExistOperator(operation)) { - boolExpValue = renderExistsExp( - dispatchFunc, - operation, - expression[operation], - tableDef, - tableSchemas, - schemaList, - newPrefix - ); - } else { - boolExpValue = renderColumnExp( - dispatchFunc, - operation, - expression[operation], - tableDef, - tableSchemas, - schemaList, - newPrefix - ); - } - } - - const boolExp = [{ key: boolExpKey, value: boolExpValue }]; - - const unselectedElements = []; - if (!operation) { - unselectedElements.push(0); - } - - return ( - - ); - }; - - /* ****************************** */ - - const showPermissionBuilder = () => { - const { tableDef, filter, allTableSchemas, schemaList } = this.props; - - const trackedTables = getTrackedTables(allTableSchemas); - - return renderBoolExp( - _dispatchFunc, - JSON.parse(filter || '{}'), - tableDef, - trackedTables, - schemaList - ); - }; - - return ( -
-
-
-
- {showPermissionBuilder()} -
-
-
-
- ); - } -} - -export default PermissionBuilder; diff --git a/console/src/features/PermissionsForm/PermissionBuilder/SelectGroup.tsx b/console/src/features/PermissionsForm/PermissionBuilder/SelectGroup.tsx deleted file mode 100644 index 2239e7beda7..00000000000 --- a/console/src/features/PermissionsForm/PermissionBuilder/SelectGroup.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; - -import styles from './PermissionBuilder.module.scss'; -import { addToPrefix } from './utils'; - -export type OptGroup = { optGroupTitle: string; options: string[] }; -interface SelectGroupProps { - selectDispatchFunc: (value: string) => void; - value: 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 = ({ - selectDispatchFunc, - value, - values, - prefix = '', - disabledValues = [], -}) => { - const dispatchSelect = (e: React.ChangeEvent) => { - selectDispatchFunc(e.target.value); - }; - const selectOptions = []; - - selectOptions.push( - - ); - - values.sort(optGroupSortFn).forEach(({ optGroupTitle, options }, i) => { - if (options?.length) { - selectOptions.push( - - {options.map((option, j) => ( - - ))} - - ); - } - }); - const selectedValue = addToPrefix(prefix, value || '--'); - - return ( - - ); -}; - -export default SelectGroup; - -export const QuotedSelectGroup: React.FC = props => { - return ( - - "  - - "  - - ); -}; diff --git a/console/src/features/PermissionsForm/PermissionBuilder/utils.ts b/console/src/features/PermissionsForm/PermissionBuilder/utils.ts deleted file mode 100644 index 3b9581cc65e..00000000000 --- a/console/src/features/PermissionsForm/PermissionBuilder/utils.ts +++ /dev/null @@ -1,357 +0,0 @@ -/* Constants */ - -import { PermissionColumnCategories } from '@/dataSources/types'; - -// TODO: generate using SQL query to handle all types - -const operatorTypeTypesMap = { - comparision: [ - 'boolean', - 'character', - 'dateTime', - 'numeric', - 'uuid', - 'user_defined', - ], - pattern_match: ['character'], - jsonb: ['jsonb'], - geometric: ['geometry'], - geometric_geographic: ['geometry', 'geography'], -}; - -export type BoolOperators = keyof typeof boolOperatorsInfo; -const boolOperatorsInfo = { - _and: { - type: 'bool', - inputStructure: 'array', - }, - _or: { - type: 'bool', - inputStructure: 'array', - }, - _not: { - type: 'bool', - inputStructure: 'object', - }, -}; - -export type ColumnOperators = keyof typeof columnOperatorsInfo; -const columnOperatorsInfo = { - _eq: { - type: 'comparision', - inputStructure: 'object', - inputType: null, - }, - _ne: { - type: 'comparision', - inputStructure: 'object', - inputType: null, - }, - _neq: { - type: 'comparision', - inputStructure: 'object', - inputType: null, - }, - _in: { - type: 'comparision', - inputStructure: 'array', - inputType: null, - }, - _nin: { - type: 'comparision', - inputStructure: 'array', - inputType: null, - }, - _gt: { - type: 'comparision', - inputStructure: 'object', - inputType: null, - }, - _lt: { - type: 'comparision', - inputStructure: 'object', - inputType: null, - }, - _gte: { - type: 'comparision', - inputStructure: 'object', - inputType: null, - }, - _lte: { - type: 'comparision', - inputStructure: 'object', - inputType: null, - }, - _ceq: { - type: 'comparision', - inputStructure: 'array', - inputType: 'column', - }, - _cne: { - type: 'comparision', - inputStructure: 'array', - inputType: 'column', - }, - _cgt: { - type: 'comparision', - inputStructure: 'array', - inputType: 'column', - }, - _clt: { - type: 'comparision', - inputStructure: 'array', - inputType: 'column', - }, - _cgte: { - type: 'comparision', - inputStructure: 'array', - inputType: 'column', - }, - _clte: { - type: 'comparision', - inputStructure: 'array', - inputType: 'column', - }, - _is_null: { - type: 'is_null', - inputStructure: 'object', - inputType: 'boolean', - }, - _like: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _nlike: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _ilike: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _nilike: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _similar: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _nsimilar: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _regex: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _iregex: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _nregex: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _niregex: { - type: 'pattern_match', - inputStructure: 'object', - inputType: null, - }, - _contains: { - type: 'jsonb', - inputStructure: 'object', - inputType: null, - }, - _contained_in: { - type: 'jsonb', - inputStructure: 'object', - inputType: null, - }, - _has_key: { - type: 'jsonb', - inputStructure: 'object', - inputType: 'character', - }, - _has_keys_any: { - type: 'jsonb', - inputStructure: 'array', - inputType: 'character', - }, - _has_keys_all: { - type: 'jsonb', - inputStructure: 'array', - inputType: 'character', - }, - _st_contains: { - type: 'geometric', - inputStructure: 'object', - inputType: null, - }, - _st_crosses: { - type: 'geometric', - inputStructure: 'object', - inputType: 'json', - }, - _st_equals: { - type: 'geometric', - inputStructure: 'object', - inputType: 'json', - }, - _st_overlaps: { - type: 'geometric', - inputStructure: 'object', - inputType: 'json', - }, - _st_touches: { - type: 'geometric', - inputStructure: 'object', - inputType: 'json', - }, - _st_within: { - type: 'geometric', - inputStructure: 'object', - inputType: 'json', - }, - _st_d_within: { - type: 'geometric_geographic', - inputStructure: 'object', - inputType: 'json', - }, - _st_intersects: { - type: 'geometric_geographic', - inputStructure: 'object', - inputType: 'json', - }, -}; - -export const getPermissionOperators = ( - supportedOperators: ColumnOperators[], - typeMap: Partial | null -) => { - let modifiedColumnOperatorsInfo = columnOperatorsInfo; - if (Array.isArray(supportedOperators)) { - modifiedColumnOperatorsInfo = supportedOperators.reduce( - (ops, opName) => ({ - ...ops, - [opName]: columnOperatorsInfo[opName], - }), - {} as typeof columnOperatorsInfo - ); - } - - const operatorMap = { - ...operatorTypeTypesMap, - is_null: Object.keys(typeMap ?? {}), - }; - type OperatorMapKey = keyof typeof operatorMap; - const operators: Partial = {}; - - Object.keys(modifiedColumnOperatorsInfo).forEach(op => { - const key = modifiedColumnOperatorsInfo[op as ColumnOperators].type; - operatorMap[key as OperatorMapKey].forEach(type => { - operators[type as OperatorMapKey] = - operators[type as OperatorMapKey] || []; - operators[type as OperatorMapKey]?.push(op); - }); - }); - - return operators; -}; - -export const boolOperators = Object.keys(boolOperatorsInfo); - -const columnOperators = Object.keys(columnOperatorsInfo); - -export const existOperators = ['_exists']; - -export const allOperators = boolOperators - .concat(columnOperators) - .concat(existOperators); - -export const TABLE_KEY = '_table'; -export const WHERE_KEY = '_where'; - -/* Util functions */ - -export const isBoolOperator = (operator: BoolOperators) => { - return boolOperators.includes(operator); -}; - -export const isExistOperator = (operator: string) => { - return existOperators.includes(operator); -}; - -export const isArrayBoolOperator = (operator: BoolOperators) => { - const arrayBoolOperators = Object.keys(boolOperatorsInfo).filter( - op => boolOperatorsInfo[op as BoolOperators].inputStructure === 'array' - ); - - return arrayBoolOperators.includes(operator); -}; - -export const isColumnOperator = (operator: ColumnOperators) => { - return columnOperators.includes(operator); -}; - -export const isArrayColumnOperator = (operator: string) => { - const arrayColumnOperators = Object.keys(columnOperatorsInfo).filter( - op => columnOperatorsInfo[op as ColumnOperators].inputStructure === 'array' - ); - - return arrayColumnOperators.includes(operator); -}; - -export const getOperatorInputType = (operator: ColumnOperators) => { - return columnOperatorsInfo[operator] - ? columnOperatorsInfo[operator].inputType - : null; -}; - -export const getRootType = ( - type: string, - typeMap: Partial | null -) => { - const typeMapKeys = Object.keys(typeMap ?? {}); - - let rootType = typeMapKeys.find(rType => - typeMap?.[rType as keyof PermissionColumnCategories]?.includes(type) - ); - - if (!rootType) { - rootType = 'user_defined'; - } - - return rootType; -}; - -export function getLegacyOperator(operator: string) { - return operator.replace('_', '$'); -} - -export function addToPrefix(prefix: string, value: string | number) { - let newPrefix; - if (prefix !== null && prefix.toString()) { - if ( - prefix[prefix.length - 1] === '^' || - prefix[prefix.length - 1] === '#' - ) { - newPrefix = prefix + value; - } else { - newPrefix = `${prefix}.${value}`; - } - } else { - newPrefix = value as string; - } - - return newPrefix; -} diff --git a/console/src/features/PermissionsForm/PermissionsForm.stories.tsx b/console/src/features/PermissionsForm/PermissionsForm.stories.tsx index b0d73fc1b42..231a9c2c9aa 100644 --- a/console/src/features/PermissionsForm/PermissionsForm.stories.tsx +++ b/console/src/features/PermissionsForm/PermissionsForm.stories.tsx @@ -16,75 +16,21 @@ export default { const roleName = 'user'; -const dataLeaf = { - type: 'schema', - name: 'users', - leaf: { - type: 'table', - name: 'users', - }, -}; - -export const Showcase: Story = () => { - return ( - <> -

Query Type: Insert

- - {}} - /> - -

Query Type: Select

- - {}} - /> - -

Query Type: Update

- - {}} - /> - -

Query Type: Delete

- - {}} - /> - - ); -}; - export const Insert: Story = args => ( ); Insert.args = { - dataLeaf, - roleName, - accessType: 'partialAccess', + currentSource: 'postgres', + dataSourceName: 'default', + queryType: 'insert', + table: { + schema: 'public', + name: 'user', + }, + roleName, handleClose: () => {}, }; -Insert.parameters = { - // Disable storybook for Insert stories - chromatic: { disableSnapshot: true }, -}; export const Select: Story = args => ( @@ -95,13 +41,25 @@ Select.args = { }; Select.parameters = Insert.parameters; +export const GDCSelect: Story = args => ( + +); +GDCSelect.args = { + currentSource: 'sqlite', + dataSourceName: 'sqlite', + queryType: 'select', + table: ['Artist'], + roleName, + handleClose: () => {}, +}; +GDCSelect.parameters = Insert.parameters; + export const Update: Story = args => ( ); Update.args = { ...Insert.args, queryType: 'update', - accessType: 'noAccess', }; Update.parameters = Insert.parameters; @@ -111,6 +69,5 @@ export const Delete: Story = args => ( Delete.args = { ...Insert.args, queryType: 'delete', - accessType: 'noAccess', }; Delete.parameters = Insert.parameters; diff --git a/console/src/features/PermissionsForm/PermissionsForm.tsx b/console/src/features/PermissionsForm/PermissionsForm.tsx index c971465e5cf..cdcc53bdc39 100644 --- a/console/src/features/PermissionsForm/PermissionsForm.tsx +++ b/console/src/features/PermissionsForm/PermissionsForm.tsx @@ -1,64 +1,43 @@ import React from 'react'; -import { FieldValues, useFormContext, UseFormProps } from 'react-hook-form'; import { Form } from '@/new-components/Form'; import { Button } from '@/new-components/Button'; -import { - RowPermissionsSectionWrapper, - RowPermissionsSection, - ColumnPermissionsSection, - ColumnPresetsSection, - AggregationSection, - BackendOnlySection, - ClonePermissionsSection, -} from './components'; -import { DataLeaf } from '../PermissionsTab/types/types'; -import { useDataSource } from '../PermissionsTab/types/useDataSource'; - -import { useDefaultValues, useFormData, useUpdatePermissions } from './hooks'; import { schema } from './utils/formSchema'; import { AccessType, FormOutput, QueryType } from './types'; +import { + AggregationSection, + BackendOnlySection, + ColumnPermissionsSection, + ColumnPresetsSection, + RowPermissionsSection, + RowPermissionsSectionWrapper, +} from './components'; + +import { useFormData, useDefaultValues, useUpdatePermissions } from './hooks'; export interface PermissionsFormProps { - dataLeaf: DataLeaf; + currentSource: string; + dataSourceName: string; + table: unknown; queryType: QueryType; roleName: string; accessType: AccessType; handleClose: () => void; } -interface ResetterProps { - defaultValues: FormOutput; -} +export const PermissionsForm = (props: PermissionsFormProps) => { + const { + currentSource, + dataSourceName, + table, + queryType, + roleName, + accessType, + handleClose, + } = props; -// required to update the default values when the form switches between query types -// for example from update to select -const Resetter: React.FC = ({ defaultValues }) => { - const { reset } = useFormContext(); - - const initialRender = React.useRef(true); - - React.useEffect(() => { - if (initialRender.current) { - initialRender.current = false; - } else { - reset(defaultValues); - } - }, [defaultValues, reset]); - - return null; -}; - -export const PermissionsForm: React.FC = ({ - dataLeaf, - queryType, - roleName, - accessType, - handleClose, -}) => { - const dataSource = useDataSource(); // loads all information about selected table // e.g. column names, supported queries etc. const { @@ -66,41 +45,36 @@ export const PermissionsForm: React.FC = ({ isLoading: loadingFormData, isError: formDataError, } = useFormData({ - dataTarget: { - dataSource, - dataLeaf, - }, + dataSourceName, + table, queryType, roleName, }); // loads any existing permissions from the metadata const { - data: defaults, + data: defaultValues, isLoading: defaultValuesLoading, isError: defaultValuesError, } = useDefaultValues({ - dataTarget: { - dataSource, - dataLeaf, - }, + dataSourceName, + table, roleName, queryType, }); // functions fired when the form is submitted const { updatePermissions, deletePermissions } = useUpdatePermissions({ - dataTarget: { - dataSource, - dataLeaf, - }, + currentSource, + dataSourceName, + table, queryType, roleName, accessType, }); const handleSubmit = async (formData: Record) => { - updatePermissions.submit(formData as FormOutput); + await updatePermissions.submit(formData as FormOutput); handleClose(); }; @@ -116,28 +90,14 @@ export const PermissionsForm: React.FC = ({ const isLoading = loadingFormData || defaultValuesLoading; - const { allFunctions, roles, tables, tableNames, supportedQueries, columns } = - data; - // allRowChecks relates to other queries and is for duplicating from others - // therefore it shouldn't be passed to the form as a default value - const { allRowChecks, ...defaultValues } = defaults; + const allRowChecks = defaultValues?.allRowChecks; // for update it is possible to set pre update and post update row checks const rowPermissions = queryType === 'update' ? ['pre', 'post'] : [queryType]; - // add new role to list of roles for clone permissions - const allRoles = React.useMemo(() => { - if (roles) { - return !roles.includes(roleName) ? [...roles, roleName] : roles; - } - - return [roleName]; - }, [roleName, roles]); - - // these will be replaced by components once spec is decided if (isSubmittingError) { - return
Error submitting form data
; + return
Error submitting form
; } // these will be replaced by components once spec is decided @@ -152,110 +112,113 @@ export const PermissionsForm: React.FC = ({ return (
} - className="p-4" + options={{ defaultValues }} > - {() => ( -
-
- -

- Role: {roleName} Action:{' '} - {queryType} -

-
- + {options => { + console.log('form values---->', options.getValues()); + console.log('form errors---->', options.formState.errors); + return ( +
+
+ +

+ Role: {roleName} Action:{' '} + {queryType} +

+
- - {rowPermissions.map(permissionName => ( - - {/* if queryType is update 2 row permissions sections are rendered (pre and post) */} - {/* therefore they need titles */} - {queryType === 'update' && ( -

- - {permissionName === 'pre' ? 'Pre-update' : 'Post-update'} -   check - -   - {permissionName === 'Post-update' && '(optional)'} -

- )} - -
- ))} -
- - {queryType !== 'delete' && ( - - )} + defaultOpen + > + {rowPermissions.map(permissionName => ( + + {queryType === 'update' && ( +

+ + {permissionName === 'pre' + ? 'Pre-update' + : 'Post-update'} +   check + +   + {permissionName === 'Post-update' && '(optional)'} +

+ )} + +
+ ))} + - {['insert', 'update'].includes(queryType) && ( - - )} + {queryType !== 'delete' && ( + + )} - {queryType === 'select' && ( - - )} + {['insert', 'update'].includes(queryType) && ( + + )} - {['insert', 'update', 'delete'].includes(queryType) && ( - - )} + {queryType === 'select' && ( + + )} -
+ {['insert', 'update', 'delete'].includes(queryType) && ( + + )} - {!!tableNames?.length && ( +
+ + {/* {!!tableNames?.length && ( - )} + )} */} -
-
- +
+ - + +
-
- )} + ); + }} ); }; diff --git a/console/src/features/PermissionsForm/api/__tests__/__snapshots__/cache.test.ts.snap b/console/src/features/PermissionsForm/api/__tests__/__snapshots__/cache.test.ts.snap deleted file mode 100644 index 5a67a9a6933..00000000000 --- a/console/src/features/PermissionsForm/api/__tests__/__snapshots__/cache.test.ts.snap +++ /dev/null @@ -1,100 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`update metadata in cache 1`] = ` -Object { - "metadata": Object { - "inherited_roles": Array [], - "sources": Array [ - Object { - "configuration": Object { - "connection_info": Object { - "database_url": Object { - "from_env": "HASURA_GRAPHQL_DATABASE_URL", - }, - "isolation_level": "read-committed", - "pool_settings": Object { - "connection_lifetime": 600, - "idle_timeout": 180, - "max_connections": 50, - "retries": 1, - }, - "use_prepared_statements": true, - }, - }, - "functions": Array [ - Object { - "function": Object { - "name": "search_user2", - "schema": "public", - }, - }, - ], - "kind": "postgres", - "name": "default", - "tables": Array [ - Object { - "table": Object { - "name": "a_table", - "schema": "public", - }, - }, - Object { - "insert_permissions": Array [ - Object { - "permission": Object { - "allow_aggregations": false, - "backend_only": false, - "check": Object { - "id": Object { - "_eq": 1, - }, - }, - "columns": Array [ - "email", - "type", - ], - "computed_fields": Array [], - "filter": Object {}, - "limit": 0, - "presets": Object {}, - }, - "role": "user", - "source": "default", - "table": Object { - "name": "users", - "schema": "public", - }, - }, - ], - "select_permissions": Array [ - Object { - "permission": Object { - "allow_aggregations": true, - "columns": Array [ - "email", - "id", - "type", - ], - "filter": Object { - "id": Object { - "_eq": 1, - }, - }, - "limit": 5, - }, - "role": "user", - }, - ], - "table": Object { - "name": "users", - "schema": "public", - }, - }, - ], - }, - ], - "version": 3, - }, - "resource_version": 30, -} -`; diff --git a/console/src/features/PermissionsForm/api/__tests__/cache.test.ts b/console/src/features/PermissionsForm/api/__tests__/cache.test.ts deleted file mode 100644 index c82683fa51a..00000000000 --- a/console/src/features/PermissionsForm/api/__tests__/cache.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { InsertBodyResult } from '../api'; -import { updateTablePermission } from '../cache'; -import { metadata } from '../../mocks/dataStubs'; - -const data: InsertBodyResult = { - type: 'bulk', - resource_version: 30, - args: [ - { - type: 'pg_create_insert_permission', - args: { - table: { - name: 'users', - schema: 'public', - }, - role: 'user', - permission: { - columns: ['email', 'type'], - presets: {}, - computed_fields: [], - backend_only: false, - limit: 0, - allow_aggregations: false, - check: { - id: { - _eq: 1, - }, - }, - filter: {}, - }, - source: 'default', - }, - }, - ], -}; - -test('update metadata in cache', () => { - const result = updateTablePermission({ - key: 'insert_permissions', - tableName: 'users', - roleName: 'user', - metadata, - data, - }); - - expect(result).toMatchSnapshot(); -}); diff --git a/console/src/features/PermissionsForm/api/api.ts b/console/src/features/PermissionsForm/api/api.ts index aec9752b470..2854f9afbbf 100644 --- a/console/src/features/PermissionsForm/api/api.ts +++ b/console/src/features/PermissionsForm/api/api.ts @@ -1,11 +1,12 @@ import { allowedMetadataTypes } from '@/features/MetadataAPI'; -import { NewDataTarget } from '../../PermissionsTab/types/types'; import { AccessType, FormOutput, QueryType } from '../types'; -import { createInsertArgs, driverPrefixes } from './utils'; +import { createInsertArgs } from './utils'; interface CreateBodyArgs { - dataTarget: NewDataTarget; + currentSource: string; + dataSourceName: string; + table: unknown; roleName: string; resourceVersion: number; } @@ -15,7 +16,9 @@ interface CreateDeleteBodyArgs extends CreateBodyArgs { } const createDeleteBody = ({ - dataTarget, + currentSource, + dataSourceName, + table, roleName, resourceVersion, queries, @@ -25,18 +28,16 @@ const createDeleteBody = ({ resource_version: number; args: BulkArgs[]; } => { - const driverPrefix = driverPrefixes[dataTarget.dataSource.driver]; - - if (!['postgres', 'mssql'].includes(dataTarget.dataSource.driver)) { - throw new Error(`${dataTarget.dataSource.driver} not supported`); - } + // if (!['postgres', 'mssql'].includes(currentSource)) { + // throw new Error(`${currentSource} not supported`); + // } const args = queries.map(queryType => ({ - type: `${driverPrefix}_drop_${queryType}_permission` as allowedMetadataTypes, + type: `${currentSource}_drop_${queryType}_permission` as allowedMetadataTypes, args: { - table: dataTarget.dataLeaf.leaf?.name || '', + table, role: roleName, - source: dataTarget.dataSource.database, + source: dataSourceName, }, })); @@ -50,17 +51,23 @@ const createDeleteBody = ({ return body; }; -interface CreateBulkDeleteBodyArgs extends CreateBodyArgs { - roleList?: Array<{ roleName: string; queries: QueryType[] }>; +interface CreateBulkDeleteBodyArgs { + source: string; + dataSourceName: string; + table: unknown; + resourceVersion: number; + roleList?: Array<{ roleName: string; queries: string[] }>; } interface BulkArgs { type: allowedMetadataTypes; - args: Record; + args: Record; } const createBulkDeleteBody = ({ - dataTarget, + source, + dataSourceName, + table, resourceVersion, roleList, }: CreateBulkDeleteBodyArgs): { @@ -69,21 +76,19 @@ const createBulkDeleteBody = ({ resource_version: number; args: BulkArgs[]; } => { - const driverPrefix = driverPrefixes[dataTarget.dataSource.driver]; - - if (!['postgres', 'mssql'].includes(dataTarget.dataSource.driver)) { - throw new Error(`${dataTarget.dataSource.driver} not supported`); - } + // if (!['postgres', 'mssql'].includes(source)) { + // throw new Error(`${dataSourceName} not supported`); + // } const args = roleList?.reduce((acc, role) => { role.queries.forEach(queryType => { acc.push({ - type: `${driverPrefix}_drop_${queryType}_permission` as allowedMetadataTypes, + type: `${source}_drop_${queryType}_permission` as allowedMetadataTypes, args: { - table: dataTarget.dataLeaf.leaf?.name || '', + table, role: role.roleName, - source: dataTarget.dataSource.database, + source: dataSourceName, }, }); }); @@ -93,7 +98,7 @@ const createBulkDeleteBody = ({ const body = { type: 'bulk' as allowedMetadataTypes, - source: dataTarget.dataSource.database, + source: dataSourceName, resource_version: resourceVersion, args: args ?? [], }; @@ -115,27 +120,28 @@ export interface InsertBodyResult { } const createInsertBody = ({ - dataTarget, + currentSource, + dataSourceName, + table, queryType, roleName, formData, - // accessType, + accessType, resourceVersion, existingPermissions, }: CreateInsertBodyArgs): InsertBodyResult => { - const driverPrefix = driverPrefixes[dataTarget.dataSource.driver]; - - if (!['postgres', 'mssql'].includes(dataTarget.dataSource.driver)) { - throw new Error(`${dataTarget.dataSource.driver} not supported`); - } + // if (!['postgres', 'mssql'].includes(currentSource)) { + // throw new Error(`${currentSource} not supported`); + // } const args = createInsertArgs({ - driverPrefix, - database: dataTarget.dataSource.database, - table: dataTarget.dataLeaf.leaf?.name || '', + currentSource, + dataSourceName, + table, queryType, role: roleName, formData, + accessType, existingPermissions, }); diff --git a/console/src/features/PermissionsForm/api/cache.ts b/console/src/features/PermissionsForm/api/cache.ts deleted file mode 100644 index 2179cfa684b..00000000000 --- a/console/src/features/PermissionsForm/api/cache.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { useQueryClient } from 'react-query'; -import produce from 'immer'; - -import { - DeletePermissionEntry, - InsertPermissionEntry, - SelectPermissionEntry, - UpdatePermissionEntry, -} from '@/metadata/types'; - -import { MetadataResponse } from '../../MetadataAPI'; -import { api } from './api'; -import { AccessType, QueryType } from '../types'; - -type PermissionEntry = - | InsertPermissionEntry - | SelectPermissionEntry - | UpdatePermissionEntry - | DeletePermissionEntry; - -type MetadataKeys = - | 'insert_permissions' - | 'select_permissions' - | 'update_permissions' - | 'delete_permissions'; - -interface UpdateTablePermissionArgs { - key: MetadataKeys; - tableName: string; - roleName: string; - metadata: MetadataResponse; - data: ReturnType; -} - -export const updateTablePermission = ({ - key, - tableName, - roleName, - metadata, - data, -}: UpdateTablePermissionArgs) => { - // find the arg - const newMetadataItem = data.args.find(arg => arg.type.includes('create')); - - // find and update the relevant piece of metadata that needs updating - const nextState = produce(metadata, draft => { - // find the table that is being edited - const selectedTable = draft.metadata.sources[0].tables.find( - ({ table }) => table.name === tableName - ); - - // find the queryType that is being edited - const selectedPermission = selectedTable?.[key]; - - // find the index of the role that is being edited - const selectedRolePermissionIndex = selectedPermission?.findIndex( - (permission: PermissionEntry) => permission.role === roleName - ); - - // if the selected permission already exists replace it - if ( - selectedRolePermissionIndex !== undefined && - selectedPermission && - newMetadataItem - ) { - selectedPermission[selectedRolePermissionIndex] = newMetadataItem?.args; - } else if (newMetadataItem) { - selectedPermission?.push(newMetadataItem.args); - } - }); - - return nextState; -}; - -interface HandleUpdateArgs { - args: { - tableName: string; - schemaName: string; - roleName: string; - queryType: QueryType; - accessType: AccessType; - }; - response: { - headers: any; - body: ReturnType; - }; -} - -const useUpdateTablePermissionCache = () => { - const client = useQueryClient(); - - const handleUpdate = ({ args, response }: HandleUpdateArgs) => { - const metadata = client.getQueryData(['metadata']); - - const { tableName, roleName, queryType } = args; - - if (metadata) { - // update cache - const result = updateTablePermission({ - key: `${queryType}_permissions`, - tableName, - roleName, - metadata, - data: response.body, - }); - - client.setQueryData('metadata', result); - } - - return { metadata }; - }; - - return { handleUpdate }; -}; - -export const cache = { - useUpdateTablePermissionCache, -}; diff --git a/console/src/features/PermissionsForm/api/__tests__/createInsertArgs.test.ts b/console/src/features/PermissionsForm/api/createInsertArgs.test.ts similarity index 85% rename from console/src/features/PermissionsForm/api/__tests__/createInsertArgs.test.ts rename to console/src/features/PermissionsForm/api/createInsertArgs.test.ts index d631fa31d28..4cd5df5afba 100644 --- a/console/src/features/PermissionsForm/api/__tests__/createInsertArgs.test.ts +++ b/console/src/features/PermissionsForm/api/createInsertArgs.test.ts @@ -1,16 +1,17 @@ -import { CreateInsertArgs, createInsertArgs } from '../utils'; +import { CreateInsertArgs, createInsertArgs } from './utils'; const insertArgs: CreateInsertArgs = { - driverPrefix: 'pg' as const, - database: 'default', + currentSource: 'postgres', + dataSourceName: 'default', + accessType: 'fullAccess', table: 'users', queryType: 'insert', role: 'user', formData: { checkType: 'custom', filterType: 'none', - check: '{"id":{"_eq":1}}', - filter: '', + check: { id: { _eq: 1 } }, + filter: {}, rowCount: '0', columns: { id: false, @@ -55,7 +56,7 @@ test('create insert args object from form data', () => { expect(result).toEqual([ { - type: 'pg_drop_insert_permission', + type: 'postgres_drop_insert_permission', args: { table: 'users', role: 'user', @@ -63,7 +64,7 @@ test('create insert args object from form data', () => { }, }, { - type: 'pg_create_insert_permission', + type: 'postgres_create_insert_permission', args: { table: 'users', role: 'user', @@ -72,7 +73,6 @@ test('create insert args object from form data', () => { presets: {}, computed_fields: [], backend_only: false, - limit: 0, allow_aggregations: false, check: { id: { @@ -88,16 +88,17 @@ test('create insert args object from form data', () => { }); const insertArgsWithClonePermissions: CreateInsertArgs = { - driverPrefix: 'pg' as const, - database: 'default', + currentSource: 'postgres', + dataSourceName: 'default', + accessType: 'fullAccess', table: 'users', queryType: 'insert', role: 'user', formData: { checkType: 'custom', filterType: 'none', - check: '{"id":{"_eq":1}}', - filter: '', + check: { id: { _eq: 1 } }, + filter: {}, rowCount: '0', columns: { id: false, @@ -147,7 +148,7 @@ test('create insert args object from form data with clone permissions', () => { expect(result).toEqual([ { - type: 'pg_drop_insert_permission', + type: 'postgres_drop_insert_permission', args: { table: 'users', role: 'user', @@ -155,7 +156,7 @@ test('create insert args object from form data with clone permissions', () => { }, }, { - type: 'pg_create_insert_permission', + type: 'postgres_create_insert_permission', args: { table: 'users', role: 'user', @@ -164,7 +165,6 @@ test('create insert args object from form data with clone permissions', () => { presets: {}, computed_fields: [], backend_only: false, - limit: 0, allow_aggregations: false, check: { id: { @@ -177,7 +177,7 @@ test('create insert args object from form data with clone permissions', () => { }, }, { - type: 'pg_create_select_permission', + type: 'postgres_create_select_permission', args: { table: 'a_table', role: 'user', @@ -186,7 +186,6 @@ test('create insert args object from form data with clone permissions', () => { presets: {}, computed_fields: [], backend_only: false, - limit: 0, allow_aggregations: false, check: { id: { diff --git a/console/src/features/PermissionsForm/api/index.ts b/console/src/features/PermissionsForm/api/index.ts index e743cad373e..b1c13e73406 100644 --- a/console/src/features/PermissionsForm/api/index.ts +++ b/console/src/features/PermissionsForm/api/index.ts @@ -1,2 +1 @@ export * from './api'; -export * from './cache'; diff --git a/console/src/features/PermissionsForm/api/utils.ts b/console/src/features/PermissionsForm/api/utils.ts index 576ca4e3638..4de5a66af93 100644 --- a/console/src/features/PermissionsForm/api/utils.ts +++ b/console/src/features/PermissionsForm/api/utils.ts @@ -2,19 +2,18 @@ import produce from 'immer'; import { allowedMetadataTypes } from '@/features/MetadataAPI'; -import { FormOutput } from '../types'; +import { AccessType, FormOutput } from '../types'; -export const driverPrefixes = { - postgres: 'pg', - mysql: 'mysql', - mssql: 'mssql', - bigquery: 'bigquery', - citus: 'citus', - cockroach: 'cockroach', -} as const; - -type DriverPrefixKeys = keyof typeof driverPrefixes; -type DriverPrefixValues = typeof driverPrefixes[DriverPrefixKeys]; +interface PermissionArgs { + columns: string[]; + presets?: Record; + computed_fields: string[]; + backend_only: boolean; + allow_aggregations: boolean; + check: Record; + filter: Record; + limit?: number; +} /** * creates the permissions object for the server @@ -35,42 +34,47 @@ const createPermission = (formData: FormOutput) => { .map(([key]) => key); // return permissions object for args - return { + const permissionObject: PermissionArgs = { columns, presets, computed_fields: [], backend_only: formData.backendOnly, - limit: parseInt(formData.rowCount, 10), allow_aggregations: formData.aggregationEnabled, - check: JSON.parse(formData.check || '{}'), - filter: JSON.parse(formData.filter || '{}'), + check: formData.check, + filter: formData.filter, }; + + if (formData.rowCount && formData.rowCount !== '0') { + permissionObject.limit = parseInt(formData.rowCount, 10); + } + + return permissionObject; }; export interface CreateInsertArgs { - driverPrefix: DriverPrefixValues; - database: string; - table: string; + currentSource: string; + dataSourceName: string; + table: unknown; queryType: string; role: string; + accessType: AccessType; formData: FormOutput; existingPermissions: ExistingPermission[]; } interface ExistingPermission { - table: string; + table: unknown; role: string; queryType: string; } - /** * creates the insert arguments to update permissions * adds cloned permissions * and creates drop arguments where permissions already exist */ export const createInsertArgs = ({ - driverPrefix, - database, + currentSource, + dataSourceName, table, queryType, role, @@ -82,12 +86,12 @@ export const createInsertArgs = ({ // create args object with args from form const initialArgs = [ { - type: `${driverPrefix}_create_${queryType}_permission` as allowedMetadataTypes, + type: `${currentSource}_create_${queryType}_permission` as allowedMetadataTypes, args: { table, role, permission, - source: database, + source: dataSourceName, }, }, ]; @@ -96,7 +100,7 @@ export const createInsertArgs = ({ // determine if args from form already exist const permissionExists = existingPermissions.find( existingPermission => - existingPermission.table === table && + JSON.stringify(existingPermission.table) === JSON.stringify(table) && existingPermission.role === role && existingPermission.queryType === queryType ); @@ -104,11 +108,11 @@ export const createInsertArgs = ({ // if the permission already exists it needs to be dropped if (permissionExists) { draft.unshift({ - type: `${driverPrefix}_drop_${queryType}_permission` as allowedMetadataTypes, + type: `${currentSource}_drop_${queryType}_permission` as allowedMetadataTypes, args: { table, role, - source: database, + source: dataSourceName, }, } as typeof initialArgs[0]); } @@ -133,19 +137,20 @@ export const createInsertArgs = ({ ); // add each closed permission to args draft.push({ - type: `${driverPrefix}_create_${clonedPermission.queryType}_permission` as allowedMetadataTypes, + type: `${currentSource}_create_${clonedPermission.queryType}_permission` as allowedMetadataTypes, args: { table: clonedPermission.tableName || '', role: clonedPermission.roleName || '', permission: permissionWithColumnsAndPresetsRemoved, - source: database, + source: dataSourceName, }, }); // determined if the cloned permission already exists const clonedPermissionExists = existingPermissions.find( existingPermission => - existingPermission.table === clonedPermission.tableName && + JSON.stringify(existingPermission.table) === + JSON.stringify(clonedPermission.tableName) && existingPermission.role === clonedPermission.roleName && existingPermission.queryType === clonedPermission.queryType ); @@ -153,11 +158,11 @@ export const createInsertArgs = ({ // if it already exists drop it if (clonedPermissionExists) { draft.unshift({ - type: `${driverPrefix}_drop_${clonedPermission.queryType}_permission` as allowedMetadataTypes, + type: `${currentSource}_drop_${clonedPermission.queryType}_permission` as allowedMetadataTypes, args: { table: clonedPermission.tableName, role: clonedPermission.roleName, - source: database, + source: dataSourceName, }, } as typeof initialArgs[0]); } diff --git a/console/src/features/PermissionsForm/components/Aggregation.tsx b/console/src/features/PermissionsForm/components/Aggregation.tsx index cef8e52239a..65dbe1ed4ca 100644 --- a/console/src/features/PermissionsForm/components/Aggregation.tsx +++ b/console/src/features/PermissionsForm/components/Aggregation.tsx @@ -37,7 +37,7 @@ export const AggregationSection: React.FC = ({ status={enabled ? 'Enabled' : 'Disabled'} data-test="toggle-agg-permission" disabled={disabled} - defaultOpen={defaultOpen} + defaultOpen={defaultOpen || enabled} >
diff --git a/console/src/features/PermissionsForm/components/BackendOnly.tsx b/console/src/features/PermissionsForm/components/BackendOnly.tsx index f30d7ecc5d1..a71d93b260c 100644 --- a/console/src/features/PermissionsForm/components/BackendOnly.tsx +++ b/console/src/features/PermissionsForm/components/BackendOnly.tsx @@ -18,7 +18,7 @@ export const BackendOnlySection: React.FC = ({ const enabled = watch('backendOnly'); return ( - + ( -
{}} className="p-4"> + {}}> {() => } ), @@ -28,15 +28,6 @@ export default { const roleName = 'two'; -const dataLeaf = { - type: 'schema', - name: 'users', - leaf: { - type: 'table', - name: 'users', - }, -}; - // this will be moved into a utils folder const allRowChecks = [ { @@ -62,11 +53,14 @@ export const Insert: Story = args => ( Insert.args = { wrapper: { roleName, queryType: 'insert', defaultOpen: true }, section: { - dataLeaf, + table: { + schema: 'public', + name: 'user', + }, queryType: 'delete', allRowChecks, - allSchemas, - allFunctions, + // allSchemas, + // allFunctions, }, }; diff --git a/console/src/features/PermissionsForm/components/RowPermissions.tsx b/console/src/features/PermissionsForm/components/RowPermissions.tsx index af11c8a0327..f8993216788 100644 --- a/console/src/features/PermissionsForm/components/RowPermissions.tsx +++ b/console/src/features/PermissionsForm/components/RowPermissions.tsx @@ -1,22 +1,18 @@ import React from 'react'; -import { useFormContext, Controller } from 'react-hook-form'; +import { useFormContext } from 'react-hook-form'; import 'brace/mode/json'; import 'brace/theme/github'; -import { NormalizedTable, Table } from '@/dataSources/types'; -import { PGFunction } from '@/dataSources/services/postgresql/types'; -import { generateTableDef } from '@/dataSources'; import { InputField } from '@/new-components/Form'; import { IconTooltip } from '@/new-components/Tooltip'; import { Collapse } from '@/new-components/deprecated'; import { getIngForm } from '../../../components/Services/Data/utils'; import JSONEditor from './JSONEditor'; -import PermissionBuilder from '../PermissionBuilder/PermissionBuilder'; +import { RowPermissionBuilder } from './RowPermissionsBuilder'; import { QueryType } from '../types'; -import { DataLeaf } from '../../PermissionsTab/types/types'; const NoChecksLabel = () => ( @@ -37,13 +33,10 @@ const CustomLabel = () => ( ); export interface RowPermissionsProps { - dataLeaf: DataLeaf; + table: unknown; queryType: QueryType; subQueryType?: string; - allRowChecks: Array<{ queryType: QueryType; value: string }>; - allSchemas?: NormalizedTable[]; - allFunctions?: PGFunction[]; } enum SelectedSection { @@ -87,15 +80,36 @@ const getRowPermissionCheckType = ( return 'filterType'; }; +const isGDCTable = (table: unknown): table is string[] => { + return Array.isArray(table); +}; + +const hasTableName = (table: unknown): table is { name: string } => { + return typeof table === 'object' && 'name' in (table || {}); +}; + +const getTableName = (table: unknown) => { + const gdcTable = isGDCTable(table); + if (gdcTable) { + return table[table.length - 1]; + } + + const tableName = hasTableName(table); + if (tableName) { + return table.name; + } + + throw new Error('cannot read table'); +}; + export const RowPermissionsSection: React.FC = ({ + table, queryType, subQueryType, - dataLeaf, allRowChecks, - allSchemas, - allFunctions, }) => { - const { control, register, watch, setValue } = useFormContext(); + const tableName = getTableName(table); + const { register, watch, setValue } = useFormContext(); // determines whether the inputs should be pointed at `check` or `filter` const rowPermissions = getRowPermission(queryType, subQueryType); // determines whether the check type should be pointer at `checkType` or `filterType` @@ -108,15 +122,10 @@ export const RowPermissionsSection: React.FC = ({ const disabled = queryType === 'update' && subQueryType === 'post' && !watch('check'); - const schemaList = React.useMemo( - () => allSchemas?.map(({ table_schema }) => table_schema), - [allSchemas] - ); - const selectedSection = watch(rowPermissionsCheckType); return ( -
+
diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/api/index.ts b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/api/index.ts deleted file mode 100644 index cade3d7b404..00000000000 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/api/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AxiosInstance } from 'axios'; -import { introspectionQuery } from './introspectionQuery'; - -export interface NetworkArgs { - httpClient: AxiosInstance; -} - -type Args = { operationName: string; query: string } & NetworkArgs; - -export const runGraphQL = async ({ - operationName, - query, - httpClient, -}: Args) => { - try { - const result = await httpClient.post('v1/graphql', { - query, - operationName, - }); - return result.data; - } catch (err) { - throw err; - } -}; - -export const runIntrospectionQuery = async ({ httpClient }: NetworkArgs) => { - return runGraphQL({ - operationName: 'IntrospectionQuery', - query: introspectionQuery, - httpClient, - }); -}; diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/api/introspectionQuery.ts b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/api/introspectionQuery.ts deleted file mode 100644 index 2ba089544e6..00000000000 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/api/introspectionQuery.ts +++ /dev/null @@ -1,96 +0,0 @@ -export const introspectionQuery = `query IntrospectionQuery { - __schema { - queryType { - name - } - mutationType { - name - } - subscriptionType { - name - } - types { - ...FullType - } - directives { - name - description - locations - args { - ...InputValue - } - } - } - } - fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } - } - fragment InputValue on __InputValue { - name - description - type { - ...TypeRef - } - defaultValue - } - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } - }`; diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/Builder.tsx b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/Builder.tsx index 17a8450776c..44506d48f94 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/Builder.tsx +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/Builder.tsx @@ -155,7 +155,6 @@ export const Builder = (props: Props) => { if (dropDownState?.name === '_and' || dropDownState?.name === '_or') { setValue(permissionsKey, undefined); } - // when the dropdown changes both the permissions object // and operators object need to be unregistered below this level unregister(permissionsKey); diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/RenderFormElement.tsx b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/RenderFormElement.tsx index 2ed44c12844..b391127780f 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/RenderFormElement.tsx +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/RenderFormElement.tsx @@ -95,6 +95,7 @@ export const RenderFormElement = (props: Props) => { })} /> - -
-
- {!!updatePermissions.data && } - - {!!deletePermissions.data && } -
-
- ); -}; - -export default { - title: 'Features/Permissions Form/hooks/useUpdatePermissions', - component: UseUpdatePermissionsComponent, - decorators: [ReactQueryDecorator()], - parameters: { - msw: handlers, - chromatic: { disableSnapshot: true }, - }, -} as Meta; - -const roleName = 'user'; - -export const Primary: Story = args => ( - -); -Primary.args = { - dataTarget: { - dataSource: { - driver: 'postgres', - database: 'default', - }, - dataLeaf, - }, - roleName, - queryType: 'insert', - accessType: 'fullAccess', -}; diff --git a/console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.tsx b/console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.tsx index 7877215b9af..e397fb43444 100644 --- a/console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.tsx +++ b/console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.tsx @@ -2,32 +2,42 @@ import { useSubmitForm } from './useSubmitForm'; import { useDeletePermission } from './useDeletePermission'; import { AccessType, QueryType } from '../../types'; -import { NewDataTarget } from '../../../PermissionsTab/types/types'; export interface UseUpdatePermissionsArgs { - dataTarget: NewDataTarget; + currentSource: string; + dataSourceName: string; + table: unknown; roleName: string; queryType: QueryType; accessType: AccessType; } export const useUpdatePermissions = ({ - dataTarget, + currentSource, + dataSourceName, + table, roleName, queryType, accessType, }: UseUpdatePermissionsArgs) => { const updatePermissions = useSubmitForm({ - dataTarget, + currentSource, + dataSourceName, + table, roleName, queryType, accessType, }); const deletePermissions = useDeletePermission({ - dataTarget, + currentSource, + dataSourceName, + table, roleName, }); - return { updatePermissions, deletePermissions }; + return { + updatePermissions, + deletePermissions, + }; }; diff --git a/console/src/features/PermissionsForm/mocks/dataStubs.ts b/console/src/features/PermissionsForm/mocks/dataStubs.ts index 68916bb96d1..6ec2509086e 100644 --- a/console/src/features/PermissionsForm/mocks/dataStubs.ts +++ b/console/src/features/PermissionsForm/mocks/dataStubs.ts @@ -1,4 +1,4 @@ -import { MetadataResponse } from '../../MetadataAPI'; +import { Metadata } from '@/features/MetadataAPI'; export const schemaList = { result_type: 'TuplesOk', @@ -15,7 +15,26 @@ export const query = { ], }; -export const metadata: MetadataResponse = { +export const metadataTable = { + name: ['Artist'], + columns: [ + { + name: 'ArtistId', + type: 'number', + nullable: false, + }, + { + name: 'Name', + type: 'string', + nullable: true, + }, + ], + primary_key: ['ArtistId'], + description: + 'CREATE TABLE [Artist]\n(\n [ArtistId] INTEGER NOT NULL,\n [Name] NVARCHAR(120),\n CONSTRAINT [PK_Artist] PRIMARY KEY ([ArtistId])\n)', +}; + +export const metadata: Metadata = { resource_version: 30, metadata: { inherited_roles: [], @@ -27,7 +46,7 @@ export const metadata: MetadataResponse = { tables: [ { table: { schema: 'public', name: 'a_table' } }, { - table: { schema: 'public', name: 'users' }, + table: { schema: 'public', name: 'user' }, insert_permissions: [ { role: 'user', @@ -53,6 +72,7 @@ export const metadata: MetadataResponse = { }, ], functions: [{ function: { schema: 'public', name: 'search_user2' } }], + configuration: { connection_info: { use_prepared_statements: true, @@ -67,6 +87,36 @@ export const metadata: MetadataResponse = { }, }, }, + { + name: 'sqlite', + kind: 'sqlite', + tables: [ + { + table: ['Album'], + }, + { + table: ['Artist'], + select_permissions: [ + { + role: 'user', + permission: { + columns: ['ArtistId', 'Name'], + filter: {}, + allow_aggregations: true, + }, + }, + ], + }, + ], + configuration: { + template: null, + timeout: null, + value: { + db: '/chinook.db', + include_sqlite_meta_tables: false, + }, + }, + }, ], }, }; diff --git a/console/src/features/PermissionsForm/mocks/handlers.mock.ts b/console/src/features/PermissionsForm/mocks/handlers.mock.ts index be775b1c057..3fb023e4564 100644 --- a/console/src/features/PermissionsForm/mocks/handlers.mock.ts +++ b/console/src/features/PermissionsForm/mocks/handlers.mock.ts @@ -1,12 +1,15 @@ import { rest } from 'msw'; +import { results } from '../components/RowPermissionsBuilder/mocks'; +import { metadata, metadataTable } from './dataStubs'; const baseUrl = 'http://localhost:8080'; export const handlers = (url = baseUrl) => [ - rest.post(`${url}/v2/query`, (req, res, ctx) => { - const body = req.body as Record; + rest.post(`${url}/v2/query`, async (req, res, ctx) => { + const body = (await req.json()) as Record; const isUseSchemaList = body?.args?.sql?.includes('SELECT schema_name'); + const isColumnsQuery = body?.args?.sql?.includes('column_name'); if (isUseSchemaList) { return res( @@ -17,6 +20,20 @@ export const handlers = (url = baseUrl) => [ ); } + if (isColumnsQuery) { + return res( + ctx.json({ + result_type: 'TuplesOk', + result: [ + ['column_name', 'data_type'], + ['id', 'integer'], + ['name', 'text'], + ['email', 'text'], + ], + }) + ); + } + return res( ctx.json({ result_type: 'TuplesOk', @@ -29,70 +46,21 @@ export const handlers = (url = baseUrl) => [ }) ); }), + rest.post(`${url}/v1/metadata`, async (req, res, ctx) => { + const body = (await req.json()) as Record; - rest.post(`${url}/v1/metadata`, (req, res, ctx) => { - const body = req.body as Record; + const isGetTableInfo = body.type === 'get_table_info'; + if (isGetTableInfo) { + return res(ctx.json(metadataTable)); + } if (body.type === 'export_metadata') { - return res( - ctx.json({ - resource_version: 30, - metadata: { - version: 3, - sources: [ - { - name: 'default', - kind: 'postgres', - tables: [ - { table: { schema: 'public', name: 'a_table' } }, - { - table: { schema: 'public', name: 'users' }, - insert_permissions: [ - { - role: 'user', - permission: { - check: { id: { _eq: 1 } }, - columns: ['email', 'type'], - backend_only: false, - }, - }, - ], - select_permissions: [ - { - role: 'user', - permission: { - columns: ['email', 'id', 'type'], - filter: { id: { _eq: 1 } }, - limit: 5, - allow_aggregations: true, - }, - }, - ], - }, - ], - functions: [ - { function: { schema: 'public', name: 'search_user2' } }, - ], - configuration: { - connection_info: { - use_prepared_statements: true, - database_url: { from_env: 'HASURA_GRAPHQL_DATABASE_URL' }, - isolation_level: 'read-committed', - pool_settings: { - connection_lifetime: 600, - retries: 1, - idle_timeout: 180, - max_connections: 50, - }, - }, - }, - }, - ], - }, - }) - ); + return res(ctx.json(metadata)); } return res(ctx.json([{ message: 'success' }])); }), + rest.post(`${url}/v1/graphql`, (req, res, ctx) => { + return res(ctx.json(results)); + }), ]; diff --git a/console/src/features/PermissionsForm/utils/formSchema.ts b/console/src/features/PermissionsForm/utils/formSchema.ts index 8a2b86ec579..55040f045f2 100644 --- a/console/src/features/PermissionsForm/utils/formSchema.ts +++ b/console/src/features/PermissionsForm/utils/formSchema.ts @@ -3,9 +3,9 @@ import * as z from 'zod'; export const schema = z.object({ checkType: z.string(), filterType: z.string(), - check: z.string(), - filter: z.string(), - rowCount: z.string(), + check: z.any(), + filter: z.any(), + rowCount: z.string().optional(), columns: z.record(z.optional(z.boolean())), presets: z.optional( z.array( @@ -28,3 +28,5 @@ export const schema = z.object({ ) ), }); + +export type PermissionsSchema = z.infer; diff --git a/console/src/features/PermissionsTab/PermissionsTab.stories.tsx b/console/src/features/PermissionsTab/PermissionsTab.stories.tsx index 5b8087de458..8fa762b60ce 100644 --- a/console/src/features/PermissionsTab/PermissionsTab.stories.tsx +++ b/console/src/features/PermissionsTab/PermissionsTab.stories.tsx @@ -1,7 +1,5 @@ import React from 'react'; import { Story, Meta } from '@storybook/react'; -import { within, waitFor, userEvent } from '@storybook/testing-library'; -import { expect } from '@storybook/jest'; import { ReactQueryDecorator } from '@/storybook/decorators/react-query'; @@ -18,105 +16,32 @@ export const Primary: Story = args => ( ); Primary.args = { - tableType: 'table', - dataLeaf: { - type: 'schema', - name: 'public', - leaf: { type: 'table', name: 'users' }, + currentSource: 'postgres', + dataSourceName: 'default', + table: { + name: 'user', + schema: 'public', }, }; -Primary.parameters = { +export const GDC: Story = args => ( + +); +GDC.args = { + currentSource: 'sqlite', + dataSourceName: 'sqlite', + table: ['Artist'], +}; + +GDC.parameters = { msw: handlers(), }; -export const BasicInteraction: Story = args => ( +export const GDCNoMocks: Story = args => ( ); - -BasicInteraction.args = Primary.args; -BasicInteraction.parameters = Primary.parameters; - -BasicInteraction.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - // open user insert section - const userInsertButton = await canvas.findByLabelText('user-insert'); - userEvent.click(userInsertButton); - - // change input of row section - const input: HTMLInputElement = await waitFor(() => - canvas.findByDisplayValue('1') - ); - userEvent.clear(input); - userEvent.type(input, '2'); - - // change selections of selected columns - const nameCheckbox: HTMLInputElement = await canvas.findByLabelText('name'); - userEvent.click(nameCheckbox); - - // toggle all options on - const toggleAllBtn = await canvas.getByRole('button', { name: 'Toggle All' }); - userEvent.click(toggleAllBtn); - - // open backend only section and select - const backendOnly = await canvas.findByText('Backend only'); - userEvent.click(backendOnly); - const backendOnlyCheckbox: HTMLInputElement = await canvas.findByLabelText( - 'Allow from backends only' - ); - userEvent.click(backendOnlyCheckbox); - - // check interactions were successful - expect(input.value).toEqual('2'); - expect(backendOnlyCheckbox.checked).toBe(true); - expect(nameCheckbox.checked).toBe(true); -}; - -export const MultipleInteractions: Story = args => ( - -); - -MultipleInteractions.args = Primary.args; -MultipleInteractions.parameters = Primary.parameters; - -MultipleInteractions.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - // click on new role item - const newRoleInsertButton = await canvas.findByLabelText('-insert'); - userEvent.click(newRoleInsertButton); - await new Promise(r => setTimeout(r, 200)); - - // check role input becomes focused - const newRoleInput: HTMLInputElement = await canvas.findByPlaceholderText( - 'Create new role...' - ); - expect(document.activeElement).toEqual(newRoleInput); - - userEvent.type(newRoleInput, 'new_role'); - userEvent.click(newRoleInsertButton); - - await new Promise(r => setTimeout(r, 200)); - - // expect form to now be open - const closeBtn = canvas.getByRole('button', { name: 'Close' }); - expect(closeBtn).toBeTruthy(); - - const noChecksLabel = await canvas.findByLabelText('Without any checks'); - userEvent.click(noChecksLabel); - - // click on bulk update - const userCheckBox = await canvas.findByLabelText('user'); - userEvent.click(userCheckBox); - - // expect new role name to be cleared - expect(newRoleInput.value).toEqual(''); - - // expect bulk update to be open - const removePermissionsBtn = canvas.getByRole('button', { - name: 'Remove All Permissions', - }); - expect(removePermissionsBtn).toBeTruthy(); - - // expect main for to be closed - expect(canvas.queryByText('Close')).toBeFalsy(); +GDCNoMocks.args = { + currentSource: 'sqlite', + dataSourceName: 'sqlite', + table: ['Artist'], }; diff --git a/console/src/features/PermissionsTab/PermissionsTab.tsx b/console/src/features/PermissionsTab/PermissionsTab.tsx index 7d3767223da..b8d39d2ac21 100644 --- a/console/src/features/PermissionsTab/PermissionsTab.tsx +++ b/console/src/features/PermissionsTab/PermissionsTab.tsx @@ -1,35 +1,48 @@ import React from 'react'; import { useTableMachine, PermissionsTable } from '../PermissionsTable'; -import { PermissionsForm, BulkDelete } from '../PermissionsForm'; +import { BulkDelete } from '../PermissionsForm'; +import { PermissionsForm } from '../PermissionsForm/PermissionsForm'; import { AccessType } from '../PermissionsForm/types'; -import { DataLeaf } from './types/types'; export interface PermissionsTabProps { - dataLeaf: DataLeaf; - tableType: 'table' | 'view'; + currentSource: string; + dataSourceName: string; + table: unknown; } -export const PermissionsTab: React.FC = ({ dataLeaf }) => { +export const PermissionsTab: React.FC = ({ + currentSource, + dataSourceName, + table, +}) => { const machine = useTableMachine(); const [state, send] = machine; return (
- + {state.value === 'bulkOpen' && !!state.context.bulkSelections.length && ( send('CLOSE')} /> )} {state.value === 'formOpen' && ( = args => { @@ -19,16 +22,20 @@ export const Default: Story = args => { }; Default.args = { - dataLeaf: { - type: 'schema', - name: 'public', - leaf: { - type: 'table', - name: 'users', - }, + dataSourceName: 'default', + table: { + schema: 'public', + name: 'user', }, }; -Default.parameters = { - msw: handlers(), +export const GDCTable: Story = args => { + const machine = useTableMachine(); + + return ; +}; + +GDCTable.args = { + dataSourceName: 'sqlite', + table: ['Artist'], }; diff --git a/console/src/features/PermissionsTable/PermissionsTable.tsx b/console/src/features/PermissionsTable/PermissionsTable.tsx index 3e6647a8c2d..f35702e6ecf 100644 --- a/console/src/features/PermissionsTable/PermissionsTable.tsx +++ b/console/src/features/PermissionsTable/PermissionsTable.tsx @@ -1,22 +1,17 @@ import React from 'react'; import { FaInfo } from 'react-icons/fa'; -import { QUERY_TYPES, Operations } from '@/dataSources'; - -import { arrayDiff } from '../../components/Common/utils/jsUtils'; - import { useRolePermissions } from './hooks/usePermissions'; import { PermissionsLegend } from './components/PermissionsLegend'; import { EditableCell, InputCell } from './components/Cells'; import { TableMachine } from './hooks'; -import { DataLeaf } from '../PermissionsTab/types/types'; -import { useDataSource } from '../PermissionsTab/types/useDataSource'; type QueryType = 'insert' | 'select' | 'update' | 'delete'; +const queryType = ['insert', 'select', 'update', 'delete'] as const; interface ViewPermissionsNoteProps { viewsSupported: boolean; - supportedQueryTypes: Operations[]; + supportedQueryTypes: QueryType[]; } export const ViewPermissionsNote: React.FC = ({ @@ -27,7 +22,9 @@ export const ViewPermissionsNote: React.FC = ({ return null; } - const unsupportedQueryTypes = arrayDiff(QUERY_TYPES, supportedQueryTypes); + const unsupportedQueryTypes = queryType.filter( + query => !supportedQueryTypes.includes(query) + ); if (unsupportedQueryTypes.length) { return ( @@ -42,7 +39,8 @@ export const ViewPermissionsNote: React.FC = ({ }; export interface PermissionsTableProps { - dataLeaf: DataLeaf; + dataSourceName: string; + table: unknown; machine: ReturnType; } @@ -54,16 +52,22 @@ export interface Selection { } export const PermissionsTable: React.FC = ({ - dataLeaf, + dataSourceName, + table, machine, }) => { + const { data } = useRolePermissions({ + dataSourceName, + table, + }); + const [state, send] = machine; - const dataSource = useDataSource(); - const { supportedQueries, rolePermissions } = useRolePermissions({ - dataSource, - dataLeaf, - }); + if (!data) { + return null; + } + + const { supportedQueries, rolePermissions } = data; return ( <> @@ -100,7 +104,9 @@ export const PermissionsTable: React.FC = ({ /> {permissionTypes.map(({ permissionType, access }) => { - const isEditable = roleName !== 'admin'; + // only select is possible on GDC as mutations are not available yet + const isEditable = + roleName !== 'admin' && permissionType === 'select'; if (isNewRole) { return ( diff --git a/console/src/features/PermissionsTable/components/Cells.stories.tsx b/console/src/features/PermissionsTable/components/Cells.stories.tsx index 547a85656cd..da6d4c26210 100644 --- a/console/src/features/PermissionsTable/components/Cells.stories.tsx +++ b/console/src/features/PermissionsTable/components/Cells.stories.tsx @@ -16,7 +16,7 @@ export default { component: InputCell, decorators: [ (StoryComponent: React.FC) => ( -
{}} className="p-4"> + {}}> {() => } ), diff --git a/console/src/features/PermissionsTable/components/Cells.tsx b/console/src/features/PermissionsTable/components/Cells.tsx index 47de529dc34..9052dace69f 100644 --- a/console/src/features/PermissionsTable/components/Cells.tsx +++ b/console/src/features/PermissionsTable/components/Cells.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Button } from '@/new-components/Button'; import { TableMachine } from '../hooks'; import { PermissionsIcon } from './PermissionsIcons'; @@ -82,7 +81,7 @@ export const EditableCell: React.FC = ({ }) => { if (!isEditable) { return ( - + ); @@ -90,15 +89,15 @@ export const EditableCell: React.FC = ({ return ( - + ); }; diff --git a/console/src/features/PermissionsTable/hooks/usePermissions.tsx b/console/src/features/PermissionsTable/hooks/usePermissions.tsx index 4573df9d00b..c8573be4cdc 100644 --- a/console/src/features/PermissionsTable/hooks/usePermissions.tsx +++ b/console/src/features/PermissionsTable/hooks/usePermissions.tsx @@ -1,23 +1,42 @@ -import { dataSource, Operations } from '@/dataSources'; -import { ComputedField, TableColumn } from '@/dataSources/types'; -import { - useMetadataTableComputedFields, - useMetadataTablePermissions, -} from '@/features/MetadataAPI'; -import { useAllFunctions, useSchemaList, useSingleTable } from '@/hooks'; +import { AxiosInstance } from 'axios'; +import isEqual from 'lodash.isequal'; +import { DataSource, exportMetadata } from '@/features/DataSource'; +import type { TableColumn } from '@/features/DataSource'; -import { NewDataTarget } from '../../PermissionsTab/types/types'; +import { useQuery } from 'react-query'; +import { useHttpClient } from '@/features/Network'; -export type RolePermissions = { - [role: string]: { - [query in 'insert' | 'select' | 'update' | 'delete']: { - columns: (string | '*')[]; - computed_fields: (string | '*')[]; - } & { - [key in 'check' | 'filter']: Record; - }; +interface RolePermission { + roleName: string; + isNewRole: boolean; + permissionTypes: { + permissionType: QueryType; + access: Access; + }[]; + bulkSelect: { + isSelectable: boolean; + isDisabled: boolean; }; -}; +} + +const metadataPermissionKeys = [ + 'insert_permissions', + 'select_permissions', + 'update_permissions', + 'delete_permissions', +] as const; + +export const keyToPermission = { + insert_permissions: 'insert', + select_permissions: 'select', + update_permissions: 'update', + delete_permissions: 'delete', +} as const; + +type QueryType = 'insert' | 'select' | 'update' | 'delete'; +type Access = 'fullAccess' | 'partialAccess' | 'noAccess'; + +const supportedQueries: QueryType[] = ['insert', 'select', 'update', 'delete']; export const getAllowedFilterKeys = ( query: 'insert' | 'select' | 'update' | 'delete' @@ -32,154 +51,233 @@ export const getAllowedFilterKeys = ( } }; -export const getRolePermission = ( - role: 'admin' | string, - rolePermissions: RolePermissions, - query: 'insert' | 'select' | 'update' | 'delete', - schemaColumns: TableColumn[], - computedFields: { scalar: ComputedField[] } -): 'fullAccess' | 'partialAccess' | 'noAccess' => { - if (role === 'admin') { - return 'fullAccess'; - } +type GetAccessTypeArgs = { + QueryType: QueryType; + permission: any; + // permission: Permission['permission']; + tableColumns: TableColumn[]; +}; - if (!rolePermissions[role]) { - return 'noAccess'; - } +const getAccessType = ({ + QueryType, + permission, + tableColumns, +}: GetAccessTypeArgs): Access => { + const filterKeys = getAllowedFilterKeys(QueryType); + const checkColumns = QueryType !== 'delete'; + // const checkComputedFields = QueryType === 'select'; - const permissions = rolePermissions[role][query]; - if (!permissions) { - return 'noAccess'; - } - - const filterKeys = getAllowedFilterKeys(query); - const checkColumns = query !== 'delete'; - const checkComputedFields = query === 'select'; - - if (!filterKeys.every(key => JSON.stringify(permissions[key]) === '{}')) { + // if any permissions are set for any of the filter keys then + // the user only has partial access to that QueryType + const hasRowPermissionsSet = !filterKeys.every( + key => JSON.stringify(permission[key]) === '{}' + ); + if (hasRowPermissionsSet) { return 'partialAccess'; } - if ( - checkColumns && - (!permissions.columns || - (!permissions.columns.includes('*') && - permissions.columns.length !== schemaColumns.length)) - ) { - return 'partialAccess'; - } + // unless all columns are selected + // the user only has partial access to that QueryType + const noColumnsChecked = !permission.columns; + const allColumnsChecked = + permission.columns?.includes('*') || + permission.columns?.length === tableColumns.length; - if ( - checkComputedFields && - computedFields.scalar.length && - (!permissions.computed_fields || - (permissions.computed_fields.includes('*') && - permissions.computed_fields.length !== computedFields.scalar.length)) - ) { + const hasLimitedAccessToColumns = + checkColumns && (noColumnsChecked || !allColumnsChecked); + if (hasLimitedAccessToColumns) { return 'partialAccess'; } return 'fullAccess'; }; -interface RolePermission { - roleName: string; - isNewRole: boolean; - permissionTypes: { - permissionType: Operations; - access: 'fullAccess' | 'partialAccess' | 'noAccess'; - }[]; - bulkSelect: { - isSelectable: boolean; - isDisabled: boolean; - }; -} +type GetMetadataTableArgs = { + dataSourceName: string; + table: unknown; + httpClient: AxiosInstance; +}; -export const useRolePermissions = (dataTarget: NewDataTarget) => { - const table = { - name: dataTarget.dataLeaf.leaf?.name || '', - schema: dataTarget.dataLeaf.name, - }; +const getMetadataTable = async ({ + httpClient, + dataSourceName, + table, +}: GetMetadataTableArgs) => { + // get all metadata + const { metadata } = await exportMetadata({ httpClient }); - const { data: schemas } = useSchemaList({ - source: dataTarget.dataSource.database, - driver: dataTarget.dataSource.driver, - }); - const { data: currentTableSchema } = useSingleTable({ - table, - source: dataTarget.dataSource.database, - driver: dataTarget.dataSource.driver, - }); - const { data: permissions } = useMetadataTablePermissions( - table, - dataTarget.dataSource.database - ); - const { data: computedFields } = useMetadataTableComputedFields( - table, - dataTarget.dataSource.database - ); - const { data: allFunctions } = useAllFunctions( - { - schemas: schemas!, - driver: dataTarget.dataSource.driver, - source: dataTarget.dataSource.database, - }, - { enabled: !!schemas } + // find current source + const currentMetadataSource = metadata?.sources?.find( + source => source.name === dataSourceName ); - if (!permissions || !allFunctions) { - return { supportedQueries: [], rolePermissions: [] }; - } + if (!currentMetadataSource) + throw Error(`useRolePermissions.metadataSource not found`); - const currentRolePermissions = permissions.reduce((acc, p) => { - // only add the role if it exists on the current table - if (p.table_name === table.name) { - acc[p.role_name] = p.permissions; - } + const trackedTables = currentMetadataSource.tables; + + // find selected table + return trackedTables.find(trackedTable => isEqual(trackedTable.table, table)); +}; + +type SupportedQueriesObject = Partial>; + +const createSupportedQueryObject = (access: Access) => + supportedQueries.reduce((acc, supportedQuery) => { + acc[supportedQuery] = access; return acc; - }, {} as Record); + }, {}); - let supportedQueries: Operations[] = []; - if (currentTableSchema) { - supportedQueries = dataSource.getTableSupportedQueries(currentTableSchema); - } +const isPermission = (props: { + key: string; + value: any; +}): props is { + key: typeof metadataPermissionKeys[number]; + value: any[]; + // value: Permission[]; +} => props.key in keyToPermission; - const groupedComputedFields = dataSource.getGroupedTableComputedFields( - computedFields ?? [], - allFunctions +type CreateRoleTableDataArgs = { + metadataTable: any; + tableColumns?: TableColumn[]; +}; + +type RoleToPermissionsMap = Record>>; + +const createRoleTableData = async ({ + metadataTable, + tableColumns, +}: CreateRoleTableDataArgs): Promise => { + if (!metadataTable) return []; + // create object with key of role + // and value describing permissions attached to that role + const roleToPermissionsMap = Object.entries( + metadataTable + ).reduce((acc, [key, value]) => { + const props = { key, value }; + // check if metadata key is related to permissions + if (isPermission(props)) { + const QueryType = keyToPermission[props.key]; + + props.value.forEach(permissionObject => { + if (!acc[permissionObject.role]) { + // add all supported queries to the object + acc[permissionObject.role] = createSupportedQueryObject('noAccess'); + } + // if permission exists on metadata for a particular QueryType + // find out the access type for that QueryType + // and replace the access type on the object + acc[permissionObject.role][QueryType] = getAccessType({ + QueryType, + permission: permissionObject.permission, + tableColumns: tableColumns || [], + }); + }); + } + + return acc; + }, {}); + + // create the array that has the relevant information for each row of the table + const permissions = Object.entries(roleToPermissionsMap).map( + ([roleName, permission]) => { + const permissionEntries = Object.entries(permission) as [ + QueryType, + Access + ][]; + const permissionTypes = permissionEntries.map(([key, value]) => ({ + permissionType: key, + access: value, + })); + + const isNewRole = roleName === 'newRole'; + + return { + roleName: isNewRole ? '' : roleName, + isNewRole, + permissionTypes, + bulkSelect: { + isSelectable: roleName !== 'admin' && !isNewRole, + isDisabled: false, + }, + }; + } ); - const currentRoles = Object.keys(currentRolePermissions).map(roleName => ({ - roleName, - isNewRole: false, - })); - - const roleList = [ - { roleName: 'admin', isNewRole: false }, - ...currentRoles, - { roleName: '', isNewRole: true }, - ]; - - const rolePermissions: RolePermission[] = roleList.map( - ({ roleName, isNewRole }) => ({ - roleName, - isNewRole, - permissionTypes: supportedQueries.map(queryType => ({ - permissionType: queryType, - access: getRolePermission( - roleName, - currentRolePermissions, - queryType, - currentTableSchema?.columns || [], - groupedComputedFields - ), + // add admin row + // and row for adding a new role + const finalPermissions = [ + { + roleName: 'admin', + isNewRole: false, + permissionTypes: Object.entries( + createSupportedQueryObject('fullAccess') + ).map(([key, value]) => ({ + permissionType: key as QueryType, + access: value, })), bulkSelect: { - isSelectable: roleName !== 'admin' && !isNewRole, - isDisabled: !Object.keys(currentRolePermissions).includes(roleName), + isSelectable: false, + isDisabled: false, }, - }) - ); + }, + ...permissions, + { + roleName: 'newRole', + isNewRole: true, + permissionTypes: Object.entries( + createSupportedQueryObject('noAccess') + ).map(([key, value]) => ({ + permissionType: key as QueryType, + access: value, + })), + bulkSelect: { + isSelectable: true, + isDisabled: false, + }, + }, + ]; - return { supportedQueries, rolePermissions }; + return finalPermissions; +}; + +type UseRolePermissionsArgs = { + dataSourceName: string; + table: unknown; +}; + +export const useRolePermissions = ({ + dataSourceName, + table, +}: UseRolePermissionsArgs) => { + const httpClient = useHttpClient(); + return useQuery< + { supportedQueries: QueryType[]; rolePermissions: RolePermission[] }, + Error + >({ + queryKey: [dataSourceName, 'permissionsTable'], + queryFn: async () => { + // find the specific metadata table + const metadataTable = await getMetadataTable({ + httpClient, + dataSourceName, + table, + }); + + // get table columns for metadata table from db introspection + const tableColumns = await DataSource(httpClient).getTableColumns({ + dataSourceName, + table, + }); + + // // extract the permissions data in the format required for the table + const rolePermissions = await createRoleTableData({ + metadataTable, + tableColumns, + }); + + return { rolePermissions, supportedQueries }; + }, + refetchOnWindowFocus: false, + }); }; diff --git a/console/src/features/PermissionsTable/hooks/useTableMachine.typegen.ts b/console/src/features/PermissionsTable/hooks/useTableMachine.typegen.ts index 691f4d7467d..ab64c5eb618 100644 --- a/console/src/features/PermissionsTable/hooks/useTableMachine.typegen.ts +++ b/console/src/features/PermissionsTable/hooks/useTableMachine.typegen.ts @@ -2,12 +2,6 @@ export interface Typegen0 { '@@xstate/typegen': true; - eventsCausingActions: { - formCloseEffect: 'CLOSE' | ''; - formOpenEffect: 'FORM_OPEN'; - bulkUpdateEffect: 'BULK_OPEN'; - updateRoleNameEffect: 'NEW_ROLE_NAME' | ''; - }; internalEvents: { '': { type: '' }; 'xstate.init': { type: 'xstate.init' }; @@ -19,12 +13,18 @@ export interface Typegen0 { guards: never; delays: never; }; + eventsCausingActions: { + bulkUpdateEffect: 'BULK_OPEN'; + formCloseEffect: '' | 'CLOSE' | 'xstate.init'; + formOpenEffect: 'FORM_OPEN'; + updateRoleNameEffect: '' | 'NEW_ROLE_NAME'; + }; eventsCausingServices: {}; eventsCausingGuards: { - newRoleEmpty: ''; bulkIsEmpty: ''; + newRoleEmpty: ''; }; eventsCausingDelays: {}; - matchesStates: 'closed' | 'formOpen' | 'bulkOpen' | 'updateRoleName'; + matchesStates: 'bulkOpen' | 'closed' | 'formOpen' | 'updateRoleName'; tags: never; }