console: add create "delete" permissions

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8071
GitOrigin-RevId: efb545e134908b61eef71f79a477061753a4bb1c
This commit is contained in:
Erik Magnusson 2023-02-24 14:19:08 +01:00 committed by hasura-bot
parent 318cf1b692
commit 938423ec54
11 changed files with 294 additions and 108 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { useConsoleForm } from '../../../new-components/Form';
import { Button } from '../../../new-components/Button';
import { IndicatorCard } from '../../../new-components/IndicatorCard';
@ -21,11 +21,11 @@ import {
RowPermissionsSectionWrapper,
} from './components';
import { ReturnValue, useFormData, useUpdatePermissions } from './hooks';
import { useFormData, useUpdatePermissions } from './hooks';
import ColumnRootFieldPermissions from './components/RootFieldPermissions/RootFieldPermissions';
import { useListAllTableColumns } from '../../Data';
import { useMetadataSource } from '../../MetadataAPI';
import { omit } from 'lodash';
import useScrollIntoView from './hooks/useScrollIntoView';
export interface ComponentProps {
dataSourceName: string;
@ -37,17 +37,6 @@ export interface ComponentProps {
data: ReturnType<typeof useFormData>['data'];
}
const getCanSave = (
defaultValues: ReturnValue['defaultValues'],
newValues: Record<string, any>
) => {
const cloneWithoutClonePermissions = omit(newValues, 'clonePermissions');
return (
JSON.stringify(cloneWithoutClonePermissions) ===
JSON.stringify(defaultValues)
);
};
const Component = (props: ComponentProps) => {
const {
dataSourceName,
@ -58,6 +47,8 @@ const Component = (props: ComponentProps) => {
handleClose,
data,
} = props;
const permissionSectionRef = useRef(null);
useScrollIntoView(permissionSectionRef, [roleName], { behavior: 'smooth' });
const { data: metadataTables } = useMetadata(
MetadataSelector.getTables(dataSourceName)
@ -199,7 +190,12 @@ const Component = (props: ComponentProps) => {
tables={tables}
roles={roles}
/>
<div className="pt-2 flex gap-2">
<div
ref={permissionSectionRef}
className="pt-2 flex gap-2"
id="form-buttons-container"
>
<Button
type="submit"
mode="primary"
@ -208,7 +204,6 @@ const Component = (props: ComponentProps) => {
? 'You must select an option for row permissions'
: 'Submit'
}
disabled={getCanSave(defaultValues, getValues())}
isLoading={updatePermissions.isLoading}
>
Save Permissions

View File

@ -8,7 +8,7 @@ import { Table } from '../../../hasura-metadata-types';
interface CreateBodyArgs {
dataSourceName: string;
table: Table;
roleName: string;
role: string;
resourceVersion: number;
}
@ -21,7 +21,7 @@ const createDeleteBody = ({
driver,
dataSourceName,
table,
roleName,
role,
resourceVersion,
queries,
}: CreateDeleteBodyArgs): {
@ -34,7 +34,7 @@ const createDeleteBody = ({
type: `${driver}_drop_${queryType}_permission` as allowedMetadataTypes,
args: {
table,
role: roleName,
role,
source: dataSourceName,
},
}));
@ -107,6 +107,9 @@ interface CreateInsertBodyArgs extends CreateBodyArgs {
existingPermissions: ExistingPermission[];
driver: string;
tables: Table[];
dataSourceName: string;
table: Table;
role: string;
}
export interface InsertBodyResult {
@ -119,7 +122,7 @@ const createInsertBody = ({
dataSourceName,
table,
queryType,
roleName,
role,
formData,
accessType,
resourceVersion,
@ -132,7 +135,7 @@ const createInsertBody = ({
dataSourceName,
table,
queryType,
role: roleName,
role,
formData,
accessType,
existingPermissions,

View File

@ -1,74 +1,78 @@
import { CreateInsertArgs, createInsertArgs } from './utils';
const selectArgs: CreateInsertArgs = {
driver: 'postgres',
dataSourceName: 'default',
accessType: 'fullAccess',
table: 'users',
queryType: 'insert',
role: 'user',
tables: [],
formData: {
queryType: 'select',
filterType: 'none',
query_root_fields: null,
subscription_root_fields: null,
filter: {},
rowCount: '0',
columns: {
id: false,
email: true,
name: false,
type: true,
username: false,
},
aggregationEnabled: false,
clonePermissions: [
{
tableName: '',
queryType: '',
roleName: '',
},
],
},
existingPermissions: [
{
table: 'users',
role: 'user',
queryType: 'insert',
},
{
table: 'users',
role: 'user',
queryType: 'select',
},
],
};
import { createInsertArgs } from './utils';
import {
selectArgs,
deleteArgs,
insertArgs,
} from '../mocks/createPermissionsData.mock';
test('create select args object from form data', () => {
const result = createInsertArgs(selectArgs);
expect(result).toEqual([
{
args: {
role: 'user',
source: 'default',
table: 'users',
},
type: 'postgres_drop_insert_permission',
type: 'sqlagent_drop_select_permission',
args: { table: ['Album'], role: 'user', source: 'Chinook' },
},
{
type: 'sqlagent_create_select_permission',
args: {
permission: {
allow_aggregations: false,
columns: ['email', 'type'],
filter: {},
set: [],
},
table: ['Album'],
role: 'user',
source: 'default',
table: 'users',
permission: {
columns: ['AlbumId', 'Title', 'ArtistId'],
filter: { _not: { AlbumId: { _eq: 'X-Hasura-User-Id' } } },
set: [],
allow_aggregations: false,
},
source: 'Chinook',
},
},
]);
});
test('create delete args object from form data', () => {
const result = createInsertArgs(deleteArgs);
expect(result).toEqual([
{
type: 'sqlagent_drop_delete_permission',
args: { table: ['Album'], role: 'user', source: 'Chinook' },
},
{
type: 'sqlagent_create_delete_permission',
args: {
table: ['Album'],
role: 'user',
permission: { backend_only: false, filter: { Title: { _eq: 'Test' } } },
source: 'Chinook',
},
},
]);
});
test('create insert args object from form data', () => {
const result = createInsertArgs(insertArgs);
expect(result).toEqual([
{
type: 'sqlagent_create_insert_permission',
args: {
table: ['Album'],
role: 'user',
permission: {
columns: [],
check: {
_and: [
{},
{ AlbumId: { _eq: '1337' } },
{ _not: { ArtistId: { _eq: '1338' } } },
],
},
allow_upsert: true,
set: {},
backend_only: false,
},
source: 'Chinook',
},
type: 'postgres_create_insert_permission',
},
]);
});

View File

@ -3,11 +3,29 @@ import produce from 'immer';
import { allowedMetadataTypes } from '../../../MetadataAPI';
import { AccessType } from '../../types';
import { PermissionsSchema } from '../../schema';
import { PermissionsSchema, Presets } from '../../schema';
import { areTablesEqual } from '../../../hasura-metadata-api';
import { Table } from '../../../hasura-metadata-types';
import { getTableDisplayName } from '../../../DatabaseRelationships';
const formatFilterValues = (formFilter: Record<string, any>[]) => {
return Object.entries(formFilter).reduce<Record<string, any>>(
(acc, [operator, value]) => {
if (operator === '_and' || operator === '_or') {
const filteredEmptyObjects = (value as any[]).filter(
p => Object.keys(p).length !== 0
);
acc[operator] = filteredEmptyObjects;
return acc;
}
acc[operator] = value;
return acc;
},
{}
);
};
type SelectPermissionMetadata = {
columns: string[];
set: Record<string, any>;
@ -24,24 +42,7 @@ const createSelectObject = (input: PermissionsSchema) => {
.filter(({ 1: value }) => value)
.map(([key]) => key);
// in row permissions builder an extra input is rendered automatically
// this will always be empty and needs to be removed
const filter = Object.entries(input.filter).reduce<Record<string, any>>(
(acc, [operator, value]) => {
if (operator === '_and' || operator === '_or') {
const filteredEmptyObjects = (value as any[]).filter(
p => Object.keys(p).length !== 0
);
acc[operator] = filteredEmptyObjects;
return acc;
}
acc[operator] = value;
return acc;
},
{}
);
const filter = formatFilterValues(input.filter);
const permissionObject: SelectPermissionMetadata = {
columns,
@ -102,6 +103,28 @@ const createInsertObject = (input: PermissionsSchema) => {
throw new Error('Case not handled');
};
export type DeletePermissionMetadata = {
columns?: string[];
set?: Record<string, any>;
backend_only: boolean;
filter: Record<string, any>;
};
const createDeleteObject = (input: PermissionsSchema) => {
if (input.queryType === 'delete') {
const filter = formatFilterValues(input.filter);
const permissionObject: DeletePermissionMetadata = {
backend_only: input.backendOnly || false,
filter,
};
return permissionObject;
}
throw new Error('Case not handled');
};
/**
* creates the permissions object for the server
*/
@ -114,7 +137,7 @@ const createPermission = (formData: PermissionsSchema) => {
case 'update':
throw new Error('Case not handled');
case 'delete':
throw new Error('Case not handled');
return createDeleteObject(formData);
default:
throw new Error('Case not handled');
}

View File

@ -237,7 +237,7 @@ export const createPermission = {
};
},
delete: (permission: DeletePermissionDefinition) => {
const filter = JSON.stringify(permission?.filter) || '';
const filter = permission?.filter || {};
const filterType = getCheckType(permission?.filter);
const presets = getPresets({
currentQueryPermissions: permission,

View File

@ -43,7 +43,7 @@ export const useDeletePermission = ({
driver,
dataSourceName,
table,
roleName,
role: roleName,
resourceVersion,
queries,
});

View File

@ -96,7 +96,7 @@ export const useSubmitForm = (args: UseSubmitFormArgs) => {
driver: metadataSource.kind,
table,
tables,
roleName,
role: roleName,
queryType,
accessType,
resourceVersion: resource_version,

View File

@ -0,0 +1,40 @@
import { useEffect } from 'react';
export enum ScrollIntoViewBehavior {
auto = 'auto',
smooth = 'smooth',
}
export enum ScrollIntoViewBlock {
start = 'start',
center = 'center',
end = 'end',
nearest = 'nearest',
}
export enum ScrollIntoViewInline {
start = 'start',
center = 'center',
end = 'end',
nearest = 'nearest',
}
export type ScrollIntoViewOptions = {
behavior?: keyof typeof ScrollIntoViewBehavior;
block?: keyof typeof ScrollIntoViewBlock;
inline?: keyof typeof ScrollIntoViewInline;
};
const useScrollIntoView = (
ref: React.RefObject<any>,
deps: string[],
options: ScrollIntoViewOptions | boolean = {}
) => {
useEffect(() => {
if (ref && ref?.current?.scrollIntoView) {
ref.current.scrollIntoView(options);
}
}, [...deps, ref]);
};
export default useScrollIntoView;

View File

@ -0,0 +1,122 @@
import { CreateInsertArgs } from '../api/utils';
export const selectArgs: CreateInsertArgs = {
driver: 'sqlagent',
dataSourceName: 'Chinook',
table: ['Album'],
queryType: 'select',
role: 'user',
formData: {
queryType: 'select',
filterType: 'custom',
filter: { _not: { AlbumId: { _eq: 'X-Hasura-User-Id' } } },
columns: { AlbumId: true, Title: true, ArtistId: true },
presets: [],
rowCount: '0',
aggregationEnabled: false,
clonePermissions: [{ tableName: '', queryType: '', roleName: '' }],
query_root_fields: null,
subscription_root_fields: null,
supportedOperators: [
{ name: 'equals', value: '_eq' },
{ name: 'not equals', value: '_neq' },
{ name: '>', value: '_gt' },
{ name: '<', value: '_lt' },
{ name: '>=', value: '_gte' },
{ name: '<=', value: '_lte' },
],
},
accessType: 'partialAccess',
existingPermissions: [
{ role: 'asdf', queryType: 'select', table: ['Album'] },
{ role: 'new', queryType: 'select', table: ['Album'] },
{ role: 'sdfsf', queryType: 'select', table: ['Album'] },
{ role: 'testrole', queryType: 'select', table: ['Album'] },
{ role: 'user', queryType: 'select', table: ['Album'] },
{ role: 'user', queryType: 'delete', table: ['Album'] },
{ role: 'asdf', queryType: 'select', table: ['Artist'] },
{ role: 'testrole', queryType: 'select', table: ['Artist'] },
{ role: 'user', queryType: 'select', table: ['Artist'] },
],
tables: [['Album'], ['Artist']],
};
export const deleteArgs: CreateInsertArgs = {
driver: 'sqlagent',
dataSourceName: 'Chinook',
table: ['Album'],
queryType: 'delete',
role: 'user',
formData: {
queryType: 'delete',
filterType: 'custom',
filter: { Title: { _eq: 'Test' } },
supportedOperators: [
{ name: 'equals', value: '_eq' },
{ name: 'not equals', value: '_neq' },
{ name: '>', value: '_gt' },
{ name: '<', value: '_lt' },
{ name: '>=', value: '_gte' },
{ name: '<=', value: '_lte' },
],
clonePermissions: [{ tableName: '', queryType: '', roleName: '' }],
},
accessType: 'partialAccess',
existingPermissions: [
{ role: 'asdf', queryType: 'select', table: ['Album'] },
{ role: 'new', queryType: 'select', table: ['Album'] },
{ role: 'sdfsf', queryType: 'select', table: ['Album'] },
{ role: 'testrole', queryType: 'select', table: ['Album'] },
{ role: 'user', queryType: 'select', table: ['Album'] },
{ role: 'user', queryType: 'delete', table: ['Album'] },
{ role: 'asdf', queryType: 'select', table: ['Artist'] },
{ role: 'testrole', queryType: 'select', table: ['Artist'] },
{ role: 'user', queryType: 'select', table: ['Artist'] },
],
tables: [['Album'], ['Artist']],
};
export const insertArgs: CreateInsertArgs = {
driver: 'sqlagent',
dataSourceName: 'Chinook',
table: ['Album'],
queryType: 'insert',
role: 'user',
formData: {
queryType: 'insert',
checkType: 'custom',
filterType: 'none',
check: {
_and: [
{},
{ AlbumId: { _eq: '1337' } },
{ _not: { ArtistId: { _eq: '1338' } } },
],
},
columns: { AlbumId: false, Title: false, ArtistId: false },
presets: [{ columnName: 'default', presetType: 'static', columnValue: '' }],
backendOnly: false,
supportedOperators: [
{ name: 'equals', value: '_eq' },
{ name: 'not equals', value: '_neq' },
{ name: '>', value: '_gt' },
{ name: '<', value: '_lt' },
{ name: '>=', value: '_gte' },
{ name: '<=', value: '_lte' },
],
clonePermissions: [{ tableName: '', queryType: '', roleName: '' }],
},
accessType: 'noAccess',
existingPermissions: [
{ role: 'asdf', queryType: 'select', table: ['Album'] },
{ role: 'new', queryType: 'select', table: ['Album'] },
{ role: 'sdfsf', queryType: 'select', table: ['Album'] },
{ role: 'testrole', queryType: 'select', table: ['Album'] },
{ role: 'user', queryType: 'select', table: ['Album'] },
{ role: 'user', queryType: 'delete', table: ['Album'] },
{ role: 'asdf', queryType: 'select', table: ['Artist'] },
{ role: 'testrole', queryType: 'select', table: ['Artist'] },
{ role: 'user', queryType: 'select', table: ['Artist'] },
],
tables: [['Album'], ['Artist']],
};

View File

@ -1,5 +1,3 @@
import React from 'react';
import { Story, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '../../../storybook/decorators/react-query';

View File

@ -121,7 +121,8 @@ export const PermissionsTable: React.FC<PermissionsTableProps> = ({
// only select is possible on GDC as mutations are not available yet
const isEditable =
(roleName !== 'admin' && permissionType === 'select') ||
(roleName !== 'admin' && permissionType === 'insert');
(roleName !== 'admin' && permissionType === 'insert') ||
(roleName !== 'admin' && permissionType === 'delete');
if (isNewRole) {
return (