mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: add create "delete" permissions
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8071 GitOrigin-RevId: efb545e134908b61eef71f79a477061753a4bb1c
This commit is contained in:
parent
318cf1b692
commit
938423ec54
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -43,7 +43,7 @@ export const useDeletePermission = ({
|
||||
driver,
|
||||
dataSourceName,
|
||||
table,
|
||||
roleName,
|
||||
role: roleName,
|
||||
resourceVersion,
|
||||
queries,
|
||||
});
|
||||
|
@ -96,7 +96,7 @@ export const useSubmitForm = (args: UseSubmitFormArgs) => {
|
||||
driver: metadataSource.kind,
|
||||
table,
|
||||
tables,
|
||||
roleName,
|
||||
role: roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
resourceVersion: resource_version,
|
||||
|
@ -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;
|
@ -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']],
|
||||
};
|
@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
|
||||
import { ReactQueryDecorator } from '../../../storybook/decorators/react-query';
|
||||
|
@ -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 (
|
||||
|
Loading…
Reference in New Issue
Block a user