mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
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
This commit is contained in:
parent
b30dc55321
commit
72dbafc319
@ -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(
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -10,4 +10,5 @@ export * from './network';
|
||||
export * from './restEndpoints';
|
||||
export * from './apiLimits';
|
||||
export * from './graphqlSchemaIntrospection';
|
||||
export * from './permissions';
|
||||
export * from './metadata';
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './permissions';
|
@ -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<string, unknown>;
|
||||
set?: Record<string, unknown>;
|
||||
columns?: string[];
|
||||
backend_only?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectPermission extends BasePermission {
|
||||
permission: SelectPermissionDefinition;
|
||||
}
|
||||
export interface SelectPermissionDefinition {
|
||||
columns?: string[];
|
||||
filter?: Record<string, unknown>;
|
||||
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<string, unknown>;
|
||||
check?: Record<string, unknown>;
|
||||
set?: Record<string, unknown>;
|
||||
backend_only?: boolean;
|
||||
}
|
||||
|
||||
export interface DeletePermission extends BasePermission {
|
||||
permission: DeletePermissionDefinition;
|
||||
}
|
||||
export interface DeletePermissionDefinition {
|
||||
filter?: Record<string, unknown>;
|
||||
backend_only?: boolean;
|
||||
}
|
@ -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[];
|
||||
};
|
||||
|
@ -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<BulkDeleteProps> = args => {
|
||||
return <BulkDelete {...args} />;
|
||||
};
|
||||
Primary.args = {
|
||||
dataLeaf,
|
||||
currentSource: 'postgres',
|
||||
dataSourceName: 'default',
|
||||
roles: ['user'],
|
||||
handleClose: () => {},
|
||||
};
|
||||
|
@ -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<BulkDeleteProps> = ({
|
||||
currentSource,
|
||||
dataSourceName,
|
||||
roles,
|
||||
dataLeaf,
|
||||
table,
|
||||
handleClose,
|
||||
}) => {
|
||||
const dataSource = useDataSource();
|
||||
|
||||
const { submit, isLoading, isError } = useBulkDeletePermissions({
|
||||
dataSource,
|
||||
dataLeaf,
|
||||
currentSource,
|
||||
dataSourceName,
|
||||
table,
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
|
@ -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;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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<SelectGroupProps> = ({
|
||||
selectDispatchFunc,
|
||||
value,
|
||||
values,
|
||||
prefix = '',
|
||||
disabledValues = [],
|
||||
}) => {
|
||||
const dispatchSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
selectDispatchFunc(e.target.value);
|
||||
};
|
||||
const selectOptions = [];
|
||||
|
||||
selectOptions.push(
|
||||
<option value={addToPrefix(prefix, '--')} key={0} disabled>
|
||||
--
|
||||
</option>
|
||||
);
|
||||
|
||||
values.sort(optGroupSortFn).forEach(({ optGroupTitle, options }, i) => {
|
||||
if (options?.length) {
|
||||
selectOptions.push(
|
||||
<optgroup label={optGroupTitle} key={i + 1}>
|
||||
{options.map((option, j) => (
|
||||
<option
|
||||
value={addToPrefix(prefix, option)}
|
||||
key={j}
|
||||
disabled={disabledValues.includes(option)}
|
||||
>
|
||||
{option || '--'}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
);
|
||||
}
|
||||
});
|
||||
const selectedValue = addToPrefix(prefix, value || '--');
|
||||
|
||||
return (
|
||||
<select
|
||||
value={selectedValue}
|
||||
name={prefix}
|
||||
onChange={dispatchSelect}
|
||||
className={styles.qb_select}
|
||||
data-test="qb-select"
|
||||
>
|
||||
{selectOptions}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectGroup;
|
||||
|
||||
export const QuotedSelectGroup: React.FC<SelectGroupProps> = props => {
|
||||
return (
|
||||
<span>
|
||||
"
|
||||
<SelectGroup {...props} />
|
||||
"
|
||||
</span>
|
||||
);
|
||||
};
|
@ -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<PermissionColumnCategories> | 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<typeof operatorMap> = {};
|
||||
|
||||
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<PermissionColumnCategories> | 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;
|
||||
}
|
@ -16,75 +16,21 @@ export default {
|
||||
|
||||
const roleName = 'user';
|
||||
|
||||
const dataLeaf = {
|
||||
type: 'schema',
|
||||
name: 'users',
|
||||
leaf: {
|
||||
type: 'table',
|
||||
name: 'users',
|
||||
},
|
||||
};
|
||||
|
||||
export const Showcase: Story<PermissionsFormProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<p className="font-bold py-4">Query Type: Insert</p>
|
||||
|
||||
<PermissionsForm
|
||||
dataLeaf={dataLeaf}
|
||||
roleName={roleName}
|
||||
accessType="partialAccess"
|
||||
queryType="insert"
|
||||
handleClose={() => {}}
|
||||
/>
|
||||
|
||||
<p className="font-bold py-4">Query Type: Select</p>
|
||||
|
||||
<PermissionsForm
|
||||
dataLeaf={dataLeaf}
|
||||
roleName={roleName}
|
||||
accessType="partialAccess"
|
||||
queryType="select"
|
||||
handleClose={() => {}}
|
||||
/>
|
||||
|
||||
<p className="font-bold py-4">Query Type: Update</p>
|
||||
|
||||
<PermissionsForm
|
||||
dataLeaf={dataLeaf}
|
||||
roleName={roleName}
|
||||
accessType="noAccess"
|
||||
queryType="update"
|
||||
handleClose={() => {}}
|
||||
/>
|
||||
|
||||
<p className="font-bold py-4">Query Type: Delete</p>
|
||||
|
||||
<PermissionsForm
|
||||
dataLeaf={dataLeaf}
|
||||
roleName={roleName}
|
||||
accessType="noAccess"
|
||||
queryType="delete"
|
||||
handleClose={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Insert: Story<PermissionsFormProps> = args => (
|
||||
<PermissionsForm {...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<PermissionsFormProps> = args => (
|
||||
<PermissionsForm {...args} />
|
||||
@ -95,13 +41,25 @@ Select.args = {
|
||||
};
|
||||
Select.parameters = Insert.parameters;
|
||||
|
||||
export const GDCSelect: Story<PermissionsFormProps> = args => (
|
||||
<PermissionsForm {...args} />
|
||||
);
|
||||
GDCSelect.args = {
|
||||
currentSource: 'sqlite',
|
||||
dataSourceName: 'sqlite',
|
||||
queryType: 'select',
|
||||
table: ['Artist'],
|
||||
roleName,
|
||||
handleClose: () => {},
|
||||
};
|
||||
GDCSelect.parameters = Insert.parameters;
|
||||
|
||||
export const Update: Story<PermissionsFormProps> = args => (
|
||||
<PermissionsForm {...args} />
|
||||
);
|
||||
Update.args = {
|
||||
...Insert.args,
|
||||
queryType: 'update',
|
||||
accessType: 'noAccess',
|
||||
};
|
||||
Update.parameters = Insert.parameters;
|
||||
|
||||
@ -111,6 +69,5 @@ export const Delete: Story<PermissionsFormProps> = args => (
|
||||
Delete.args = {
|
||||
...Insert.args,
|
||||
queryType: 'delete',
|
||||
accessType: 'noAccess',
|
||||
};
|
||||
Delete.parameters = Insert.parameters;
|
||||
|
@ -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<ResetterProps> = ({ 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<PermissionsFormProps> = ({
|
||||
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<PermissionsFormProps> = ({
|
||||
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<string, unknown>) => {
|
||||
updatePermissions.submit(formData as FormOutput);
|
||||
await updatePermissions.submit(formData as FormOutput);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
@ -116,28 +90,14 @@ export const PermissionsForm: React.FC<PermissionsFormProps> = ({
|
||||
|
||||
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 <div>Error submitting form data</div>;
|
||||
return <div>Error submitting form</div>;
|
||||
}
|
||||
|
||||
// these will be replaced by components once spec is decided
|
||||
@ -152,110 +112,113 @@ export const PermissionsForm: React.FC<PermissionsFormProps> = ({
|
||||
|
||||
return (
|
||||
<Form
|
||||
key={`${queryType}-${roleName}-${accessType}`}
|
||||
onSubmit={handleSubmit}
|
||||
schema={schema}
|
||||
options={{ defaultValues } as UseFormProps<FieldValues>}
|
||||
className="p-4"
|
||||
options={{ defaultValues }}
|
||||
>
|
||||
{() => (
|
||||
<div className="bg-white rounded p-md border border-gray-300">
|
||||
<div className="pb-4 flex items-center gap-4">
|
||||
<Button type="button" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
<h3 data-testid="form-title">
|
||||
<strong>Role:</strong> {roleName} <strong>Action:</strong>{' '}
|
||||
{queryType}
|
||||
</h3>
|
||||
</div>
|
||||
<Resetter defaultValues={defaultValues} />
|
||||
{options => {
|
||||
console.log('form values---->', options.getValues());
|
||||
console.log('form errors---->', options.formState.errors);
|
||||
return (
|
||||
<div className="bg-white rounded p-md border border-gray-300">
|
||||
<div className="pb-4 flex items-center gap-4">
|
||||
<Button type="button" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
<h3 data-testid="form-title">
|
||||
<strong>Role:</strong> {roleName} <strong>Action:</strong>{' '}
|
||||
{queryType}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<RowPermissionsSectionWrapper
|
||||
roleName={roleName}
|
||||
queryType={queryType}
|
||||
defaultOpen
|
||||
>
|
||||
{rowPermissions.map(permissionName => (
|
||||
<React.Fragment key={permissionName}>
|
||||
{/* if queryType is update 2 row permissions sections are rendered (pre and post) */}
|
||||
{/* therefore they need titles */}
|
||||
{queryType === 'update' && (
|
||||
<p className="my-2">
|
||||
<strong>
|
||||
{permissionName === 'pre' ? 'Pre-update' : 'Post-update'}
|
||||
check
|
||||
</strong>
|
||||
|
||||
{permissionName === 'Post-update' && '(optional)'}
|
||||
</p>
|
||||
)}
|
||||
<RowPermissionsSection
|
||||
dataLeaf={dataLeaf}
|
||||
queryType={queryType}
|
||||
subQueryType={
|
||||
queryType === 'update' ? permissionName : undefined
|
||||
}
|
||||
allRowChecks={allRowChecks}
|
||||
allSchemas={tables}
|
||||
allFunctions={allFunctions}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</RowPermissionsSectionWrapper>
|
||||
|
||||
{queryType !== 'delete' && (
|
||||
<ColumnPermissionsSection
|
||||
<RowPermissionsSectionWrapper
|
||||
roleName={roleName}
|
||||
queryType={queryType}
|
||||
columns={columns}
|
||||
/>
|
||||
)}
|
||||
defaultOpen
|
||||
>
|
||||
{rowPermissions.map(permissionName => (
|
||||
<React.Fragment key={permissionName}>
|
||||
{queryType === 'update' && (
|
||||
<p className="my-2">
|
||||
<strong>
|
||||
{permissionName === 'pre'
|
||||
? 'Pre-update'
|
||||
: 'Post-update'}
|
||||
check
|
||||
</strong>
|
||||
|
||||
{permissionName === 'Post-update' && '(optional)'}
|
||||
</p>
|
||||
)}
|
||||
<RowPermissionsSection
|
||||
table={table}
|
||||
queryType={queryType}
|
||||
subQueryType={
|
||||
queryType === 'update' ? permissionName : undefined
|
||||
}
|
||||
allRowChecks={allRowChecks || []}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</RowPermissionsSectionWrapper>
|
||||
|
||||
{['insert', 'update'].includes(queryType) && (
|
||||
<ColumnPresetsSection queryType={queryType} columns={columns} />
|
||||
)}
|
||||
{queryType !== 'delete' && (
|
||||
<ColumnPermissionsSection
|
||||
roleName={roleName}
|
||||
queryType={queryType}
|
||||
columns={data?.columns}
|
||||
/>
|
||||
)}
|
||||
|
||||
{queryType === 'select' && (
|
||||
<AggregationSection queryType={queryType} roleName={roleName} />
|
||||
)}
|
||||
{['insert', 'update'].includes(queryType) && (
|
||||
<ColumnPresetsSection
|
||||
queryType={queryType}
|
||||
columns={data?.columns}
|
||||
/>
|
||||
)}
|
||||
|
||||
{['insert', 'update', 'delete'].includes(queryType) && (
|
||||
<BackendOnlySection queryType={queryType} />
|
||||
)}
|
||||
{queryType === 'select' && (
|
||||
<AggregationSection queryType={queryType} roleName={roleName} />
|
||||
)}
|
||||
|
||||
<hr className="my-4" />
|
||||
{['insert', 'update', 'delete'].includes(queryType) && (
|
||||
<BackendOnlySection queryType={queryType} />
|
||||
)}
|
||||
|
||||
{!!tableNames?.length && (
|
||||
<hr className="my-4" />
|
||||
|
||||
{/* {!!tableNames?.length && (
|
||||
<ClonePermissionsSection
|
||||
queryType={queryType}
|
||||
tables={tableNames}
|
||||
supportedQueryTypes={supportedQueries}
|
||||
roles={allRoles}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<hr className="my-4" />
|
||||
<div className="pt-2 flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={updatePermissions.isLoading}
|
||||
>
|
||||
Save Permissions
|
||||
</Button>
|
||||
<div className="pt-2 flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={updatePermissions.isLoading}
|
||||
>
|
||||
Save Permissions
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
disabled={accessType === 'noAccess'}
|
||||
mode="destructive"
|
||||
isLoading={deletePermissions.isLoading}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete Permissions
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={accessType === 'noAccess'}
|
||||
mode="destructive"
|
||||
isLoading={deletePermissions.isLoading}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete Permissions
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
}
|
||||
`;
|
@ -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();
|
||||
});
|
@ -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<string, string | allowedMetadataTypes>;
|
||||
args: Record<string, string | allowedMetadataTypes | unknown>;
|
||||
}
|
||||
|
||||
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<BulkArgs[]>((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,
|
||||
});
|
||||
|
||||
|
@ -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<typeof api.createInsertBody>;
|
||||
}
|
||||
|
||||
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<typeof api.createInsertBody>;
|
||||
};
|
||||
}
|
||||
|
||||
const useUpdateTablePermissionCache = () => {
|
||||
const client = useQueryClient();
|
||||
|
||||
const handleUpdate = ({ args, response }: HandleUpdateArgs) => {
|
||||
const metadata = client.getQueryData<MetadataResponse>(['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,
|
||||
};
|
@ -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: {
|
@ -1,2 +1 @@
|
||||
export * from './api';
|
||||
export * from './cache';
|
||||
|
@ -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<string, string | number>;
|
||||
computed_fields: string[];
|
||||
backend_only: boolean;
|
||||
allow_aggregations: boolean;
|
||||
check: Record<string, unknown>;
|
||||
filter: Record<string, unknown>;
|
||||
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]);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export const AggregationSection: React.FC<AggregationProps> = ({
|
||||
status={enabled ? 'Enabled' : 'Disabled'}
|
||||
data-test="toggle-agg-permission"
|
||||
disabled={disabled}
|
||||
defaultOpen={defaultOpen}
|
||||
defaultOpen={defaultOpen || enabled}
|
||||
>
|
||||
<Collapse.Content>
|
||||
<div title={disabled ? 'Set row permissions first' : ''}>
|
||||
|
@ -18,7 +18,7 @@ export const BackendOnlySection: React.FC<BackEndOnlySectionProps> = ({
|
||||
const enabled = watch('backendOnly');
|
||||
|
||||
return (
|
||||
<Collapse defaultOpen={defaultOpen}>
|
||||
<Collapse defaultOpen={defaultOpen || enabled}>
|
||||
<Collapse.Header
|
||||
title="Backend only"
|
||||
tooltip={`When enabled, this ${queryType} mutation is accessible only via
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
RowPermissionsWrapperProps,
|
||||
} from './RowPermissions';
|
||||
|
||||
import { allSchemas, allFunctions } from '../mocks/mockData';
|
||||
// import { allSchemas, allFunctions } from '../mocks/mockData';
|
||||
import { QueryType } from '../types';
|
||||
|
||||
export default {
|
||||
@ -18,7 +18,7 @@ export default {
|
||||
component: RowPermissionsSection,
|
||||
decorators: [
|
||||
(StoryComponent: React.FC) => (
|
||||
<Form schema={z.any()} onSubmit={() => {}} className="p-4">
|
||||
<Form schema={z.any()} onSubmit={() => {}}>
|
||||
{() => <StoryComponent />}
|
||||
</Form>
|
||||
),
|
||||
@ -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<Props> = args => (
|
||||
Insert.args = {
|
||||
wrapper: { roleName, queryType: 'insert', defaultOpen: true },
|
||||
section: {
|
||||
dataLeaf,
|
||||
table: {
|
||||
schema: 'public',
|
||||
name: 'user',
|
||||
},
|
||||
queryType: 'delete',
|
||||
allRowChecks,
|
||||
allSchemas,
|
||||
allFunctions,
|
||||
// allSchemas,
|
||||
// allFunctions,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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 = () => (
|
||||
<span data-test="without-checks">
|
||||
@ -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<RowPermissionsProps> = ({
|
||||
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<RowPermissionsProps> = ({
|
||||
const disabled =
|
||||
queryType === 'update' && subQueryType === 'post' && !watch('check');
|
||||
|
||||
const schemaList = React.useMemo(
|
||||
() => allSchemas?.map(({ table_schema }) => table_schema),
|
||||
[allSchemas]
|
||||
);
|
||||
|
||||
const selectedSection = watch(rowPermissionsCheckType);
|
||||
|
||||
return (
|
||||
<fieldset className="grid gap-2">
|
||||
<fieldset key={queryType} className="grid gap-2">
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
@ -126,7 +135,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setValue(rowPermissionsCheckType, SelectedSection.NoChecks);
|
||||
setValue(rowPermissions, '{}');
|
||||
setValue(rowPermissions, {});
|
||||
}}
|
||||
{...register(rowPermissionsCheckType)}
|
||||
/>
|
||||
@ -194,39 +203,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
|
||||
{selectedSection === SelectedSection.Custom && (
|
||||
<div className="pt-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name={rowPermissions}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<>
|
||||
<JSONEditor
|
||||
data={value || '{}'}
|
||||
onChange={output => {
|
||||
onChange(output);
|
||||
}}
|
||||
initData="{}"
|
||||
/>
|
||||
|
||||
{allSchemas && allFunctions && schemaList && (
|
||||
<PermissionBuilder
|
||||
dispatchFuncSetFilter={output => {
|
||||
onChange(output);
|
||||
}}
|
||||
loadSchemasFunc={() => {}}
|
||||
tableDef={generateTableDef(
|
||||
dataLeaf.leaf?.name || '',
|
||||
dataLeaf.name
|
||||
)}
|
||||
allTableSchemas={allSchemas as Table[]}
|
||||
allFunctions={allFunctions}
|
||||
schemaList={schemaList}
|
||||
filter={value || '{}'}
|
||||
dispatch={() => console.log('output')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<RowPermissionBuilder tableName={tableName} nesting={['filter']} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
@ -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);
|
||||
|
@ -95,6 +95,7 @@ export const RenderFormElement = (props: Props) => {
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-blue-800 font-bold text-sm"
|
||||
onClick={() => setValue(columnKey, 'X-Hasura-User-Id')}
|
||||
>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { buildClientSchema, GraphQLSchema, IntrospectionQuery } from 'graphql';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { runIntrospectionQuery } from '../api';
|
||||
import { runIntrospectionQuery } from '@/features/DataSource';
|
||||
|
||||
import { getAllColumnsAndOperators } from '../utils';
|
||||
import { createDefaultValues, getAllColumnsAndOperators } from '../utils';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -50,3 +50,18 @@ export const useData = ({ tableName, schema }: Args) => {
|
||||
const data = getAllColumnsAndOperators({ tableName, schema });
|
||||
return { data };
|
||||
};
|
||||
|
||||
interface A {
|
||||
tableName: string;
|
||||
existingPermission: Record<string, any>;
|
||||
}
|
||||
|
||||
export const useCreateRowPermissionsDefaults = () => {
|
||||
const { data: schema } = useIntrospectSchema();
|
||||
|
||||
const fetchDefaults = async ({ tableName, existingPermission }: A) => {
|
||||
createDefaultValues({ tableName, schema, existingPermission });
|
||||
};
|
||||
|
||||
return fetchDefaults;
|
||||
};
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './RowPermissionBuilder';
|
||||
export * from './utils/createDefaultValues';
|
||||
|
@ -81,11 +81,8 @@ export interface CreateDefaultsArgs {
|
||||
existingPermission?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const createDefaultValues = ({
|
||||
tableName,
|
||||
schema,
|
||||
existingPermission,
|
||||
}: CreateDefaultsArgs) => {
|
||||
export const createDefaultValues = (props: CreateDefaultsArgs) => {
|
||||
const { tableName, schema, existingPermission } = props;
|
||||
if (!existingPermission) {
|
||||
return {};
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { wrapper } from '../../../../hooks/__tests__/common/decorator';
|
||||
import { handlers } from '../../mocks/handlers.mock';
|
||||
|
||||
import { useBulkDeletePermissions } from '../submitHooks';
|
||||
|
||||
const mocks = handlers('http://localhost');
|
||||
|
||||
const server = setupServer(...mocks);
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterAll(() => server.close());
|
||||
|
||||
const dataLeaf = {
|
||||
type: 'schema',
|
||||
name: 'users',
|
||||
leaf: {
|
||||
type: 'table',
|
||||
name: 'users',
|
||||
},
|
||||
};
|
||||
|
||||
const dataTarget = {
|
||||
dataSource: {
|
||||
driver: 'postgres' as const,
|
||||
database: 'default',
|
||||
},
|
||||
dataLeaf,
|
||||
};
|
||||
|
||||
describe("useBulkDeletePermissions hooks' postgres test", () => {
|
||||
test('bulk delete permissions submits correctly', async () => {
|
||||
const { result, waitFor } = renderHook(
|
||||
() => useBulkDeletePermissions(dataTarget),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const roles = ['user'];
|
||||
|
||||
await waitFor(() => result.current.isLoading === false);
|
||||
|
||||
await result.current.submit(roles);
|
||||
expect(result.current.data).toEqual([{ message: 'success' }]);
|
||||
});
|
||||
});
|
@ -1,79 +0,0 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { wrapper } from '../../../../hooks/__tests__/common/decorator';
|
||||
import { handlers } from '../../mocks/handlers.mock';
|
||||
|
||||
import { useDefaultValues } from '../dataFetchingHooks';
|
||||
|
||||
const [query, metadata] = handlers('http://localhost');
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
server.use(metadata);
|
||||
server.use(query);
|
||||
|
||||
const dataLeaf = {
|
||||
type: 'schema',
|
||||
name: 'public',
|
||||
leaf: {
|
||||
type: 'table',
|
||||
name: 'users',
|
||||
},
|
||||
};
|
||||
|
||||
const dataTarget = {
|
||||
dataSource: {
|
||||
driver: 'postgres' as const,
|
||||
database: 'default',
|
||||
},
|
||||
dataLeaf,
|
||||
};
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe("useDefaultValues hooks' postgres test", () => {
|
||||
test('useDefaultValues fetches data correctly', async () => {
|
||||
const roleName = 'user';
|
||||
|
||||
const { result, waitForValueToChange, waitFor } = renderHook(
|
||||
() =>
|
||||
useDefaultValues({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitForValueToChange(() => result.current.data);
|
||||
await waitFor(() => result.current.isLoading === false);
|
||||
|
||||
expect(result.current.data!).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"aggregationEnabled": false,
|
||||
"allRowChecks": Array [
|
||||
Object {
|
||||
"queryType": "select",
|
||||
"value": "{\\"id\\":{\\"_eq\\":1}}",
|
||||
},
|
||||
],
|
||||
"backendOnly": false,
|
||||
"check": "{\\"id\\":{\\"_eq\\":1}}",
|
||||
"checkType": "custom",
|
||||
"clonePermissions": Array [],
|
||||
"columns": Object {
|
||||
"email": true,
|
||||
"id": false,
|
||||
"name": false,
|
||||
"type": true,
|
||||
"username": false,
|
||||
},
|
||||
"filter": "",
|
||||
"filterType": "none",
|
||||
"presets": Array [],
|
||||
"rowCount": "0",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
@ -1,67 +0,0 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { wrapper } from '../../../../hooks/__tests__/common/decorator';
|
||||
import { handlers } from '../../mocks/handlers.mock';
|
||||
|
||||
import { useFormData } from '../dataFetchingHooks';
|
||||
|
||||
const [query, metadata] = handlers('http://localhost');
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
server.use(metadata);
|
||||
server.use(query);
|
||||
|
||||
const dataLeaf = {
|
||||
type: 'schema',
|
||||
name: 'public',
|
||||
leaf: {
|
||||
type: 'table',
|
||||
name: 'users',
|
||||
},
|
||||
};
|
||||
|
||||
const dataTarget = {
|
||||
dataSource: {
|
||||
driver: 'postgres' as const,
|
||||
database: 'default',
|
||||
},
|
||||
dataLeaf,
|
||||
};
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe("useFormData hooks' postgres test", () => {
|
||||
test('useFormData fetches data correctly', async () => {
|
||||
const roleName = 'user';
|
||||
|
||||
const { result, waitForValueToChange } = renderHook(
|
||||
() =>
|
||||
useFormData({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitForValueToChange(() => result.current.data);
|
||||
|
||||
expect(result.current.data?.table?.table_name).toBe('users');
|
||||
expect(result.current.data?.columns).toEqual([
|
||||
'id',
|
||||
'email',
|
||||
'name',
|
||||
'type',
|
||||
'username',
|
||||
]);
|
||||
expect(result.current.data?.supportedQueries).toEqual([
|
||||
'insert',
|
||||
'select',
|
||||
'update',
|
||||
'delete',
|
||||
]);
|
||||
expect(result.current.data?.roles).toEqual(['user']);
|
||||
});
|
||||
});
|
@ -1,103 +0,0 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { wrapper } from '../../../../hooks/__tests__/common/decorator';
|
||||
import { handlers } from '../../mocks/handlers.mock';
|
||||
|
||||
import { useUpdatePermissions } from '../submitHooks';
|
||||
|
||||
const mocks = handlers('http://localhost');
|
||||
|
||||
const server = setupServer(...mocks);
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterAll(() => server.close());
|
||||
|
||||
const dataLeaf = {
|
||||
type: 'schema',
|
||||
name: 'users',
|
||||
leaf: {
|
||||
type: 'table',
|
||||
name: 'users',
|
||||
},
|
||||
};
|
||||
|
||||
const dataTarget = {
|
||||
dataSource: {
|
||||
driver: 'postgres' as const,
|
||||
database: 'default',
|
||||
},
|
||||
dataLeaf,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(console, 'error').mockRestore();
|
||||
});
|
||||
|
||||
describe("useUpdatePermissions hooks' postgres test", () => {
|
||||
test('update permissions submits correctly', async () => {
|
||||
const roleName = 'user';
|
||||
|
||||
const { result, waitFor } = renderHook(
|
||||
() =>
|
||||
useUpdatePermissions({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
accessType: 'partialAccess',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => result.current.updatePermissions.isLoading === false);
|
||||
|
||||
const formData = {
|
||||
checkType: 'custom',
|
||||
filterType: 'none',
|
||||
check: '{"id":{"_eq":1}}',
|
||||
filter: '',
|
||||
rowCount: '0',
|
||||
columns: {
|
||||
id: false,
|
||||
email: true,
|
||||
name: false,
|
||||
type: true,
|
||||
username: false,
|
||||
},
|
||||
presets: [],
|
||||
backendOnly: false,
|
||||
aggregationEnabled: false,
|
||||
clonePermissions: [],
|
||||
allRowChecks: [{ queryType: 'select', value: '{"id":{"_eq":1}}' }],
|
||||
};
|
||||
|
||||
await result.current.updatePermissions.submit(formData);
|
||||
expect(result.current.updatePermissions.data).toEqual([
|
||||
{ message: 'success' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('delete permissions submits correctly', async () => {
|
||||
const roleName = 'user';
|
||||
|
||||
const { result, waitFor } = renderHook(
|
||||
() =>
|
||||
useUpdatePermissions({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
accessType: 'partialAccess',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
await waitFor(() => result.current.updatePermissions.isLoading === false);
|
||||
|
||||
await result.current.deletePermissions.submit(['insert']);
|
||||
expect(result.current.deletePermissions.data).toEqual([
|
||||
{ message: 'success' },
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,2 +1,2 @@
|
||||
export * from './useDefaultValues';
|
||||
export * from './useFormData';
|
||||
export * from './useFormData/useFormData';
|
||||
|
@ -1,80 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactJson from 'react-json-view';
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||
|
||||
import { useDefaultValues, UseDefaultValuesArgs } from './useDefaultValues';
|
||||
import { handlers } from '../../mocks/handlers.mock';
|
||||
|
||||
const dataLeaf = {
|
||||
type: 'schema',
|
||||
name: 'users',
|
||||
leaf: {
|
||||
type: 'table',
|
||||
name: 'users',
|
||||
},
|
||||
};
|
||||
|
||||
const UseDefaultValuesComponent = ({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType,
|
||||
}: UseDefaultValuesArgs) => {
|
||||
const results = useDefaultValues({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType,
|
||||
});
|
||||
return <ReactJson src={results} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Features/Permissions Form/hooks/useDefaultValues',
|
||||
component: UseDefaultValuesComponent,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const roleName = 'user';
|
||||
|
||||
export const Insert: Story<UseDefaultValuesArgs> = args => (
|
||||
<UseDefaultValuesComponent {...args} />
|
||||
);
|
||||
Insert.args = {
|
||||
dataTarget: {
|
||||
dataSource: {
|
||||
driver: 'postgres',
|
||||
database: 'default',
|
||||
},
|
||||
dataLeaf,
|
||||
},
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
};
|
||||
|
||||
export const Select: Story<UseDefaultValuesArgs> = args => (
|
||||
<UseDefaultValuesComponent {...args} />
|
||||
);
|
||||
Select.args = {
|
||||
...Insert.args,
|
||||
queryType: 'select',
|
||||
};
|
||||
|
||||
export const Update: Story<UseDefaultValuesArgs> = args => (
|
||||
<UseDefaultValuesComponent {...args} />
|
||||
);
|
||||
Update.args = {
|
||||
...Insert.args,
|
||||
queryType: 'update',
|
||||
};
|
||||
|
||||
export const Delete: Story<UseDefaultValuesArgs> = args => (
|
||||
<UseDefaultValuesComponent {...args} />
|
||||
);
|
||||
Delete.args = {
|
||||
...Insert.args,
|
||||
queryType: 'delete',
|
||||
};
|
@ -1,198 +0,0 @@
|
||||
import { NormalizedTable } from '@/dataSources/types';
|
||||
import { useMetadataTablePermissions } from '@/features/MetadataAPI';
|
||||
import { useSingleTable } from '@/hooks';
|
||||
|
||||
import { currentDriver } from '@/dataSources';
|
||||
|
||||
import { getCurrentRole } from '../../utils';
|
||||
|
||||
import { QueryType } from '../../types';
|
||||
import { NewDataTarget } from '../../../PermissionsTab/types/types';
|
||||
|
||||
namespace Format {
|
||||
export const getCheckType = (check?: string | null) => {
|
||||
if (!check) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (check === '{}') {
|
||||
return 'no_checks';
|
||||
}
|
||||
|
||||
return 'custom';
|
||||
};
|
||||
|
||||
interface GetRowCountArgs {
|
||||
currentQueryPermissions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const getRowCount = ({ currentQueryPermissions }: GetRowCountArgs) => {
|
||||
return `${currentQueryPermissions?.limit ?? 0}`;
|
||||
};
|
||||
|
||||
interface GetCheckArgs {
|
||||
currentQueryPermissions?: Record<string, any>;
|
||||
type: 'check' | 'filter';
|
||||
}
|
||||
|
||||
export const getCheck = ({ currentQueryPermissions, type }: GetCheckArgs) => {
|
||||
const check = currentQueryPermissions?.[type];
|
||||
return check ? JSON.stringify(check) : '';
|
||||
};
|
||||
|
||||
interface FormatColumnsArgs {
|
||||
table?: NormalizedTable | null;
|
||||
currentQueryPermissions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const formatColumns = ({
|
||||
table,
|
||||
currentQueryPermissions,
|
||||
}: FormatColumnsArgs) => {
|
||||
const allColumns = table?.columns?.map(({ column_name }) => column_name);
|
||||
const selectedColumns = currentQueryPermissions?.columns;
|
||||
|
||||
if (!allColumns || !selectedColumns) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return allColumns?.reduce((acc, column) => {
|
||||
const selected = selectedColumns?.includes(column);
|
||||
acc[column] = selected;
|
||||
return acc;
|
||||
}, {} as Record<typeof selectedColumns, boolean>);
|
||||
};
|
||||
|
||||
interface GetPresetArgs {
|
||||
currentQueryPermissions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const getPresets = ({ currentQueryPermissions }: GetPresetArgs) => {
|
||||
const set = Object.entries(currentQueryPermissions?.set || {}) as Array<
|
||||
[string, string]
|
||||
>;
|
||||
|
||||
return set.map(([columnName, value]) => {
|
||||
return {
|
||||
columnName,
|
||||
presetType: value.startsWith('x-hasura')
|
||||
? 'from session variable'
|
||||
: 'static',
|
||||
value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllRowChecks = (
|
||||
currentQuery: QueryType,
|
||||
permissions?: Record<string, any>
|
||||
) => {
|
||||
const allChecks = Object.entries(permissions || {}) as [QueryType, any];
|
||||
|
||||
return allChecks
|
||||
.filter(([queryType]) => queryType !== currentQuery)
|
||||
.map(([queryType, permission]) => {
|
||||
if (['insert', 'update'].includes(queryType)) {
|
||||
return { queryType, value: JSON.stringify(permission.check || {}) };
|
||||
}
|
||||
|
||||
return {
|
||||
queryType,
|
||||
value: JSON.stringify(permission.filter || {}),
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseDefaultValuesArgs {
|
||||
dataTarget: NewDataTarget;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
|
||||
const useLoadPermissions = ({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType,
|
||||
}: UseDefaultValuesArgs) => {
|
||||
const table = {
|
||||
name: dataTarget.dataLeaf.leaf?.name || '',
|
||||
schema: dataTarget.dataLeaf.name,
|
||||
};
|
||||
|
||||
const {
|
||||
data: tableData,
|
||||
isLoading: tableLoading,
|
||||
isError: tableError,
|
||||
} = useSingleTable({
|
||||
source: dataTarget.dataSource.database,
|
||||
driver: currentDriver,
|
||||
table,
|
||||
});
|
||||
|
||||
const {
|
||||
data: permissions,
|
||||
isLoading: permissionsLoading,
|
||||
isError: permissionsError,
|
||||
} = useMetadataTablePermissions(table, dataTarget.dataSource.database);
|
||||
|
||||
const currentRolePermissions = getCurrentRole({ permissions, roleName });
|
||||
|
||||
const currentQueryPermissions =
|
||||
currentRolePermissions?.permissions?.[queryType];
|
||||
|
||||
const allRowChecks = Format.getAllRowChecks(
|
||||
queryType,
|
||||
currentRolePermissions?.permissions
|
||||
);
|
||||
|
||||
const isLoading = tableLoading || permissionsLoading;
|
||||
const isError = tableError || permissionsError;
|
||||
|
||||
return {
|
||||
data: {
|
||||
currentQueryPermissions,
|
||||
allRowChecks,
|
||||
table: tableData,
|
||||
},
|
||||
isLoading,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDefaultValues = (args: UseDefaultValuesArgs) => {
|
||||
const {
|
||||
data: { currentQueryPermissions, table, allRowChecks },
|
||||
isLoading,
|
||||
isError,
|
||||
} = useLoadPermissions(args);
|
||||
|
||||
const check = Format.getCheck({ currentQueryPermissions, type: 'check' });
|
||||
const filter = Format.getCheck({
|
||||
currentQueryPermissions,
|
||||
type: 'filter',
|
||||
});
|
||||
const checkType = Format.getCheckType(check);
|
||||
const filterType = Format.getCheckType(filter);
|
||||
const rowCount = Format.getRowCount({ currentQueryPermissions });
|
||||
const columns = Format.formatColumns({ table, currentQueryPermissions });
|
||||
const presets = Format.getPresets({ currentQueryPermissions });
|
||||
|
||||
return {
|
||||
data: {
|
||||
checkType,
|
||||
filterType,
|
||||
check,
|
||||
filter,
|
||||
rowCount,
|
||||
columns,
|
||||
presets,
|
||||
backendOnly: currentQueryPermissions?.backend_only || false,
|
||||
aggregationEnabled: currentQueryPermissions?.allow_aggregations || false,
|
||||
clonePermissions: [],
|
||||
allRowChecks,
|
||||
},
|
||||
isLoading,
|
||||
isError,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './useDefaultValues';
|
@ -0,0 +1,48 @@
|
||||
import { createDefaultValues } from '../..';
|
||||
import { schema } from '../../../../components/RowPermissionsBuilder/mocks';
|
||||
|
||||
export const input: Parameters<typeof createDefaultValues>[0] = {
|
||||
queryType: 'select' as const,
|
||||
roleName: 'user',
|
||||
tableColumns: [
|
||||
{
|
||||
name: 'ArtistId',
|
||||
dataType: 'number',
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
graphQLProperties: {
|
||||
name: 'ArtistId',
|
||||
scalarType: 'decimal',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
dataType: 'string',
|
||||
nullable: true,
|
||||
isPrimaryKey: false,
|
||||
graphQLProperties: {
|
||||
name: 'Name',
|
||||
scalarType: 'String',
|
||||
},
|
||||
},
|
||||
],
|
||||
selectedTable: {
|
||||
table: ['Artist'],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['Name'],
|
||||
filter: {
|
||||
ArtistId: {
|
||||
_gt: 5,
|
||||
},
|
||||
},
|
||||
limit: 3,
|
||||
allow_aggregations: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
schema,
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
import { createDefaultValues } from './useDefaultValues';
|
||||
import { input } from './mock';
|
||||
|
||||
const mockResult: ReturnType<typeof createDefaultValues> = {
|
||||
aggregationEnabled: true,
|
||||
allRowChecks: [],
|
||||
backendOnly: false,
|
||||
check: {},
|
||||
checkType: 'none',
|
||||
clonePermissions: [],
|
||||
columns: {
|
||||
ArtistId: false,
|
||||
Name: true,
|
||||
},
|
||||
filter: {
|
||||
ArtistId: {
|
||||
_gt: 5,
|
||||
},
|
||||
},
|
||||
filterType: 'custom',
|
||||
operators: {
|
||||
filter: {
|
||||
columnOperator: '_gt',
|
||||
name: 'ArtistId',
|
||||
type: 'column',
|
||||
typeName: 'ArtistId',
|
||||
},
|
||||
},
|
||||
presets: [],
|
||||
rowCount: '3',
|
||||
};
|
||||
|
||||
test('use default values returns values correctly', () => {
|
||||
const result = createDefaultValues(input);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
@ -0,0 +1,141 @@
|
||||
import { buildClientSchema, GraphQLSchema } from 'graphql';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
import {
|
||||
DataSource,
|
||||
exportMetadata,
|
||||
runIntrospectionQuery,
|
||||
TableColumn,
|
||||
} from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { Metadata, MetadataTable } from '@/features/MetadataAPI';
|
||||
|
||||
import { PermissionsSchema } from '../../../utils';
|
||||
|
||||
import type { QueryType } from '../../../types';
|
||||
import {
|
||||
createPermissionsObject,
|
||||
getRowPermissionsForAllOtherQueriesMatchingSelectedRole,
|
||||
} from './utils';
|
||||
|
||||
interface GetMetadataTableArgs {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
const getMetadataTable = ({
|
||||
dataSourceName,
|
||||
table,
|
||||
metadata,
|
||||
}: GetMetadataTableArgs) => {
|
||||
const trackedTables = metadata.metadata?.sources?.find(
|
||||
source => source.name === dataSourceName
|
||||
)?.tables;
|
||||
|
||||
// find selected table
|
||||
const currentTable = trackedTables?.find(trackedTable =>
|
||||
isEqual(trackedTable.table, table)
|
||||
);
|
||||
|
||||
return currentTable;
|
||||
};
|
||||
|
||||
interface CreateDefaultValuesArgs {
|
||||
queryType: QueryType;
|
||||
roleName: string;
|
||||
selectedTable?: MetadataTable;
|
||||
tableColumns: TableColumn[];
|
||||
schema: GraphQLSchema;
|
||||
}
|
||||
|
||||
export const createDefaultValues = ({
|
||||
queryType,
|
||||
roleName,
|
||||
selectedTable,
|
||||
tableColumns,
|
||||
schema,
|
||||
}: CreateDefaultValuesArgs) => {
|
||||
const allRowChecks = getRowPermissionsForAllOtherQueriesMatchingSelectedRole(
|
||||
queryType,
|
||||
roleName,
|
||||
selectedTable
|
||||
);
|
||||
|
||||
const baseDefaultValues: DefaultValues = {
|
||||
checkType: 'none',
|
||||
filterType: 'none',
|
||||
check: {},
|
||||
filter: {},
|
||||
columns: {},
|
||||
presets: [],
|
||||
backendOnly: false,
|
||||
aggregationEnabled: false,
|
||||
clonePermissions: [],
|
||||
allRowChecks,
|
||||
};
|
||||
if (selectedTable) {
|
||||
const permissionsObject = createPermissionsObject({
|
||||
queryType,
|
||||
selectedTable,
|
||||
roleName,
|
||||
tableColumns,
|
||||
schema,
|
||||
});
|
||||
|
||||
return { ...baseDefaultValues, ...permissionsObject };
|
||||
}
|
||||
|
||||
return baseDefaultValues;
|
||||
};
|
||||
|
||||
type DefaultValues = PermissionsSchema & {
|
||||
allRowChecks: { queryType: QueryType; value: string }[];
|
||||
};
|
||||
|
||||
export interface Args {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
|
||||
export const useDefaultValues = ({
|
||||
dataSourceName,
|
||||
table,
|
||||
roleName,
|
||||
queryType,
|
||||
}: Args): UseQueryResult<DefaultValues> => {
|
||||
const httpClient = useHttpClient();
|
||||
return useQuery<any, Error>({
|
||||
queryKey: [dataSourceName, 'permissionDefaultValues', roleName, queryType],
|
||||
queryFn: async () => {
|
||||
const introspectionResult = await runIntrospectionQuery({ httpClient });
|
||||
const schema = buildClientSchema(introspectionResult.data);
|
||||
|
||||
const metadata = await exportMetadata({ httpClient });
|
||||
|
||||
// get table columns for metadata table from db introspection
|
||||
const tableColumns = await DataSource(httpClient).getTableColumns({
|
||||
dataSourceName,
|
||||
table,
|
||||
});
|
||||
|
||||
const selectedTable = getMetadataTable({
|
||||
dataSourceName,
|
||||
table,
|
||||
metadata,
|
||||
});
|
||||
|
||||
return createDefaultValues({
|
||||
queryType,
|
||||
roleName,
|
||||
selectedTable,
|
||||
tableColumns,
|
||||
schema,
|
||||
});
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
@ -0,0 +1,294 @@
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
|
||||
import type {
|
||||
DeletePermissionDefinition,
|
||||
InsertPermissionDefinition,
|
||||
MetadataTable,
|
||||
Permission,
|
||||
SelectPermissionDefinition,
|
||||
UpdatePermissionDefinition,
|
||||
} from '@/features/MetadataAPI';
|
||||
|
||||
import { isPermission, keyToPermission, permissionToKey } from '../utils';
|
||||
import { createDefaultValues } from '../../../components/RowPermissionsBuilder';
|
||||
|
||||
import type { QueryType } from '../../../types';
|
||||
|
||||
export const getCheckType = (
|
||||
check?: Record<string, unknown> | null
|
||||
): 'none' | 'no_checks' | 'custom' => {
|
||||
if (!check) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (isEqual(check, {})) {
|
||||
return 'no_checks';
|
||||
}
|
||||
|
||||
return 'custom';
|
||||
};
|
||||
|
||||
interface GetRowCountArgs {
|
||||
currentQueryPermissions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const getRowCount = ({ currentQueryPermissions }: GetRowCountArgs) => {
|
||||
return `${currentQueryPermissions?.limit ?? 0}`;
|
||||
};
|
||||
|
||||
interface GetCheckArgs {
|
||||
currentQueryPermissions?: Record<string, any>;
|
||||
type: 'check' | 'filter';
|
||||
}
|
||||
|
||||
export const getCheck = ({ currentQueryPermissions, type }: GetCheckArgs) => {
|
||||
const check = currentQueryPermissions?.[type];
|
||||
return check ? JSON.stringify(check) : '';
|
||||
};
|
||||
|
||||
interface GetPresetArgs {
|
||||
currentQueryPermissions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const getPresets = ({ currentQueryPermissions }: GetPresetArgs) => {
|
||||
const set = Object.entries(currentQueryPermissions?.set || {}) as Array<
|
||||
[string, string]
|
||||
>;
|
||||
|
||||
return set.map(([columnName, value]) => {
|
||||
return {
|
||||
columnName,
|
||||
presetType: value.startsWith('x-hasura')
|
||||
? 'from session variable'
|
||||
: 'static',
|
||||
value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getColumns = (
|
||||
permissionColumns: string[],
|
||||
tableColumns: TableColumn[]
|
||||
) => {
|
||||
return tableColumns.reduce<Record<string, boolean>>((acc, each) => {
|
||||
const columnIncluded = permissionColumns?.includes(each.name);
|
||||
acc[each.name] = !!columnIncluded;
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const getAllRowChecks = (
|
||||
currentQuery: QueryType,
|
||||
allChecks: Array<{ queryType: QueryType; value: any }> = []
|
||||
) => {
|
||||
return allChecks
|
||||
.filter(({ queryType }) => queryType !== currentQuery)
|
||||
.map(({ queryType, value }) => {
|
||||
if (['insert', 'update'].includes(queryType)) {
|
||||
return { queryType, value: JSON.stringify(value.check || {}) };
|
||||
}
|
||||
|
||||
return {
|
||||
queryType,
|
||||
value: JSON.stringify(value.filter || {}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export interface UseDefaultValuesArgs {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
|
||||
export const getRowPermissionsForAllOtherQueriesMatchingSelectedRole = (
|
||||
selectedQuery: QueryType,
|
||||
selectedRole: string,
|
||||
table?: MetadataTable
|
||||
) => {
|
||||
const res = Object.entries(table || {}).reduce<
|
||||
Array<{ queryType: QueryType; value: any }>
|
||||
>((acc, [key, value]) => {
|
||||
const props = { key, value };
|
||||
|
||||
// check object key of metadata is a permission
|
||||
if (isPermission(props)) {
|
||||
// add each role from each permission to the set
|
||||
props.value.forEach(permission => {
|
||||
if (permission.role === selectedRole) {
|
||||
acc.push({
|
||||
queryType: keyToPermission[props.key],
|
||||
value: permission.permission,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return getAllRowChecks(selectedQuery, res);
|
||||
};
|
||||
|
||||
export const createPermission = {
|
||||
insert: (
|
||||
permission: InsertPermissionDefinition,
|
||||
tableColumns: TableColumn[]
|
||||
) => {
|
||||
const check = JSON.stringify(permission.check) || '';
|
||||
const checkType = getCheckType(permission.check);
|
||||
const presets = getPresets({
|
||||
currentQueryPermissions: permission,
|
||||
});
|
||||
const columns = getColumns(permission?.columns || [], tableColumns);
|
||||
const backendOnly: boolean = permission?.backend_only || false;
|
||||
|
||||
return { check, checkType, presets, columns, backendOnly };
|
||||
},
|
||||
select: (
|
||||
permission: SelectPermissionDefinition,
|
||||
tableColumns: TableColumn[],
|
||||
schema: any
|
||||
) => {
|
||||
const { filter, operators } = createDefaultValues({
|
||||
tableName: 'Artist',
|
||||
existingPermission: permission.filter,
|
||||
schema,
|
||||
});
|
||||
|
||||
const filterType = getCheckType(permission?.filter);
|
||||
const presets = getPresets({
|
||||
currentQueryPermissions: permission,
|
||||
});
|
||||
const columns = getColumns(permission?.columns || [], tableColumns);
|
||||
const rowCount = getRowCount({
|
||||
currentQueryPermissions: permission,
|
||||
});
|
||||
const aggregationEnabled: boolean = permission?.allow_aggregations || false;
|
||||
|
||||
const selectPermissions = {
|
||||
filter,
|
||||
filterType,
|
||||
presets,
|
||||
columns,
|
||||
rowCount,
|
||||
aggregationEnabled,
|
||||
operators,
|
||||
};
|
||||
|
||||
if (rowCount) {
|
||||
selectPermissions.rowCount = rowCount;
|
||||
}
|
||||
|
||||
return selectPermissions;
|
||||
},
|
||||
update: (
|
||||
permission: UpdatePermissionDefinition,
|
||||
tableColumns: TableColumn[]
|
||||
) => {
|
||||
const check = JSON.stringify(permission?.check) || '';
|
||||
const filter = JSON.stringify(permission?.filter) || '';
|
||||
const checkType = getCheckType(permission?.check);
|
||||
const filterType = getCheckType(permission?.filter);
|
||||
const presets = getPresets({
|
||||
currentQueryPermissions: permission,
|
||||
});
|
||||
const columns = getColumns(permission?.columns || [], tableColumns);
|
||||
const rowCount = getRowCount({
|
||||
currentQueryPermissions: permission,
|
||||
});
|
||||
|
||||
return { check, checkType, filter, filterType, presets, columns, rowCount };
|
||||
},
|
||||
delete: (permission: DeletePermissionDefinition) => {
|
||||
const filter = JSON.stringify(permission?.filter) || '';
|
||||
const filterType = getCheckType(permission?.filter);
|
||||
const presets = getPresets({
|
||||
currentQueryPermissions: permission,
|
||||
});
|
||||
|
||||
const rowCount = getRowCount({
|
||||
currentQueryPermissions: permission,
|
||||
});
|
||||
|
||||
return { filter, filterType, presets, rowCount };
|
||||
},
|
||||
};
|
||||
|
||||
interface GetCurrentPermissionArgs {
|
||||
table?: MetadataTable;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
|
||||
export const getCurrentPermission = ({
|
||||
table,
|
||||
roleName,
|
||||
queryType,
|
||||
}: GetCurrentPermissionArgs) => {
|
||||
const key = permissionToKey[queryType];
|
||||
const currentPermission = table?.[key] as Permission[];
|
||||
|
||||
const currentPermissionsForSelectedRole = currentPermission?.find(
|
||||
permission => permission.role === roleName
|
||||
);
|
||||
|
||||
if (currentPermissionsForSelectedRole) {
|
||||
return {
|
||||
queryType,
|
||||
permission: currentPermissionsForSelectedRole?.permission,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queryType,
|
||||
permission: {},
|
||||
};
|
||||
};
|
||||
|
||||
interface ObjArgs {
|
||||
queryType: QueryType;
|
||||
selectedTable: MetadataTable;
|
||||
tableColumns: TableColumn[];
|
||||
roleName: string;
|
||||
schema: any;
|
||||
}
|
||||
|
||||
export const createPermissionsObject = ({
|
||||
queryType,
|
||||
selectedTable,
|
||||
tableColumns,
|
||||
roleName,
|
||||
schema,
|
||||
}: ObjArgs) => {
|
||||
const selectedPermission = getCurrentPermission({
|
||||
table: selectedTable,
|
||||
queryType,
|
||||
roleName,
|
||||
});
|
||||
|
||||
switch (selectedPermission.queryType) {
|
||||
case 'insert':
|
||||
return createPermission.insert(
|
||||
selectedPermission.permission,
|
||||
tableColumns
|
||||
);
|
||||
case 'select':
|
||||
return createPermission.select(
|
||||
selectedPermission.permission as SelectPermissionDefinition,
|
||||
tableColumns,
|
||||
schema
|
||||
);
|
||||
case 'update':
|
||||
return createPermission.update(
|
||||
selectedPermission.permission,
|
||||
tableColumns
|
||||
);
|
||||
case 'delete':
|
||||
return createPermission.delete(selectedPermission.permission);
|
||||
default:
|
||||
throw new Error('Case not handled');
|
||||
}
|
||||
};
|
@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactJson from 'react-json-view';
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||
|
||||
import { useFormData, UseFormDataArgs } from './useFormData';
|
||||
import { handlers } from '../../mocks/handlers.mock';
|
||||
|
||||
const dataLeaf = {
|
||||
type: 'schema',
|
||||
name: 'users',
|
||||
leaf: {
|
||||
type: 'table',
|
||||
name: 'users',
|
||||
},
|
||||
};
|
||||
|
||||
const UseFormDataComponent = ({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType,
|
||||
}: UseFormDataArgs) => {
|
||||
const results = useFormData({ dataTarget, roleName, queryType });
|
||||
return <ReactJson src={results} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Features/Permissions Form/hooks/useFormData',
|
||||
component: UseFormDataComponent,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const roleName = 'user';
|
||||
|
||||
export const Primary: Story<UseFormDataArgs> = args => (
|
||||
<UseFormDataComponent {...args} />
|
||||
);
|
||||
Primary.args = {
|
||||
dataTarget: {
|
||||
dataSource: {
|
||||
driver: 'postgres',
|
||||
database: 'default',
|
||||
},
|
||||
dataLeaf,
|
||||
},
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
};
|
@ -1,124 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
useTrackableFunctions,
|
||||
useDataSourceTables,
|
||||
useSingleTable,
|
||||
} from '@/hooks';
|
||||
import { currentDriver, dataSource } from '@/dataSources';
|
||||
|
||||
import { useRoles } from '../../../MetadataAPI';
|
||||
import { QueryType } from '../../types';
|
||||
import { NewDataTarget } from '../../../PermissionsTab/types/types';
|
||||
|
||||
export interface UseFormDataArgs {
|
||||
dataTarget: NewDataTarget;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
|
||||
const useLoadSchemas = ({ dataTarget }: UseFormDataArgs) => {
|
||||
const schemaName = dataTarget.dataLeaf.name;
|
||||
const tableName = dataTarget.dataLeaf.leaf?.name || '';
|
||||
|
||||
const source = dataTarget.dataSource.database;
|
||||
|
||||
const {
|
||||
data: tables,
|
||||
isLoading: tablesLoading,
|
||||
isError: tablesError,
|
||||
} = useDataSourceTables(
|
||||
{ schemas: [schemaName], source, driver: currentDriver },
|
||||
{
|
||||
enabled: !!schemaName,
|
||||
retry: 0,
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: table,
|
||||
isLoading: tableLoading,
|
||||
isError: tableError,
|
||||
} = useSingleTable({
|
||||
source,
|
||||
driver: currentDriver,
|
||||
table: { name: tableName, schema: schemaName },
|
||||
});
|
||||
|
||||
const { data: roles } = useRoles();
|
||||
|
||||
const { data: allFunctions } = useTrackableFunctions(
|
||||
{ schemas: [schemaName], source, driver: currentDriver },
|
||||
{
|
||||
enabled: !!schemaName,
|
||||
retry: 0,
|
||||
}
|
||||
);
|
||||
|
||||
const isError = tablesError || tableError;
|
||||
const isLoading = tablesLoading || tableLoading;
|
||||
|
||||
if (isError) {
|
||||
return {
|
||||
data: {},
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return {
|
||||
data: {},
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: { tables, table, roles, allFunctions },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const useFormData = (props: UseFormDataArgs) => {
|
||||
const {
|
||||
data: { table, tables: allTables, roles, allFunctions },
|
||||
isLoading,
|
||||
isError,
|
||||
} = useLoadSchemas(props);
|
||||
|
||||
const tables = React.useMemo(
|
||||
() => allTables?.filter(({ is_table_tracked }) => is_table_tracked),
|
||||
[allTables]
|
||||
);
|
||||
|
||||
const tableNames = React.useMemo(
|
||||
() => tables?.map(({ table_name }) => table_name),
|
||||
[tables]
|
||||
);
|
||||
|
||||
const columns = React.useMemo(
|
||||
() => table?.columns.map(({ column_name }) => column_name),
|
||||
[table]
|
||||
);
|
||||
|
||||
let supportedQueries: string[] = [];
|
||||
if (table) {
|
||||
supportedQueries = dataSource.getTableSupportedQueries(table);
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
table,
|
||||
tables,
|
||||
tableNames,
|
||||
columns,
|
||||
allFunctions,
|
||||
roles,
|
||||
supportedQueries,
|
||||
},
|
||||
isLoading,
|
||||
isError,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './useFormData';
|
@ -0,0 +1,83 @@
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
import { Metadata } from '@/features/MetadataAPI';
|
||||
|
||||
interface Input {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
metadata: Metadata;
|
||||
tableColumns: TableColumn[];
|
||||
}
|
||||
|
||||
const metadata: Metadata = {
|
||||
resource_version: 1,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
backend_configs: {
|
||||
dataconnector: {
|
||||
sqlite: {
|
||||
uri: 'http://host.docker.internal:8100',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const input: Input = {
|
||||
dataSourceName: 'sqlite',
|
||||
table: ['Artist'],
|
||||
metadata,
|
||||
tableColumns: [
|
||||
{
|
||||
name: 'ArtistId',
|
||||
dataType: 'number',
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
graphQLProperties: {
|
||||
name: 'ArtistId',
|
||||
scalarType: 'decimal',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
dataType: 'string',
|
||||
nullable: true,
|
||||
isPrimaryKey: false,
|
||||
graphQLProperties: {
|
||||
name: 'Name',
|
||||
scalarType: 'String',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import { createFormData } from './useFormData';
|
||||
import { input } from './mock';
|
||||
|
||||
const mockResult: ReturnType<typeof createFormData> = {
|
||||
columns: ['ArtistId', 'Name'],
|
||||
roles: ['user'],
|
||||
supportedQueries: ['insert', 'select', 'update', 'delete'],
|
||||
tableNames: [['Album'], ['Artist']],
|
||||
};
|
||||
|
||||
test('returns correctly formatted formData', () => {
|
||||
const result = createFormData(input);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
@ -0,0 +1,139 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { DataSource, exportMetadata, TableColumn } from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
|
||||
import { Metadata, MetadataTable } from '@/features/MetadataAPI';
|
||||
import { isPermission } from '../utils';
|
||||
|
||||
type Operation = 'insert' | 'select' | 'update' | 'delete';
|
||||
|
||||
const supportedQueries: Operation[] = ['insert', 'select', 'update', 'delete'];
|
||||
|
||||
export const getAllowedFilterKeys = (
|
||||
query: 'insert' | 'select' | 'update' | 'delete'
|
||||
): ('check' | 'filter')[] => {
|
||||
switch (query) {
|
||||
case 'insert':
|
||||
return ['check'];
|
||||
case 'update':
|
||||
return ['filter', 'check'];
|
||||
default:
|
||||
return ['filter'];
|
||||
}
|
||||
};
|
||||
|
||||
type GetMetadataTableArgs = {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
metadata: Metadata;
|
||||
};
|
||||
|
||||
const getMetadataTable = (args: GetMetadataTableArgs) => {
|
||||
const { dataSourceName, table, metadata } = args;
|
||||
|
||||
const trackedTables = metadata.metadata?.sources?.find(
|
||||
source => source.name === dataSourceName
|
||||
)?.tables;
|
||||
|
||||
const selectedTable = trackedTables?.find(
|
||||
trackedTable => JSON.stringify(trackedTable.table) === JSON.stringify(table)
|
||||
);
|
||||
|
||||
// find selected table
|
||||
return {
|
||||
table: selectedTable,
|
||||
tables: trackedTables,
|
||||
// for gdc tables will be an array of strings so this needs updating
|
||||
tableNames: trackedTables?.map(each => each.table),
|
||||
};
|
||||
};
|
||||
|
||||
const getRoles = (metadataTables?: MetadataTable[]) => {
|
||||
// go through all tracked tables
|
||||
const res = metadataTables?.reduce<Set<string>>((acc, each) => {
|
||||
// go through all permissions
|
||||
Object.entries(each).forEach(([key, value]) => {
|
||||
const props = { key, value };
|
||||
// check object key of metadata is a permission
|
||||
if (isPermission(props)) {
|
||||
// add each role from each permission to the set
|
||||
props.value.forEach(permission => {
|
||||
acc.add(permission.role);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, new Set());
|
||||
|
||||
return Array.from(res || []);
|
||||
};
|
||||
|
||||
interface CreateFormDataArgs {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
metadata: Metadata;
|
||||
tableColumns: TableColumn[];
|
||||
}
|
||||
|
||||
export const createFormData = (props: CreateFormDataArgs) => {
|
||||
const { dataSourceName, table, metadata, tableColumns } = props;
|
||||
// find the specific metadata table
|
||||
const metadataTable = getMetadataTable({
|
||||
dataSourceName,
|
||||
table,
|
||||
metadata,
|
||||
});
|
||||
|
||||
const roles = getRoles(metadataTable.tables);
|
||||
|
||||
return {
|
||||
roles,
|
||||
supportedQueries,
|
||||
tableNames: metadataTable.tableNames,
|
||||
columns: tableColumns?.map(({ name }) => name),
|
||||
};
|
||||
};
|
||||
|
||||
export type UseFormDataArgs = {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
roleName: string;
|
||||
queryType: 'select' | 'insert' | 'update' | 'delete';
|
||||
};
|
||||
|
||||
type ReturnValue = {
|
||||
roles: string[];
|
||||
supportedQueries: Operation[];
|
||||
tableNames: unknown;
|
||||
columns: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* creates data for displaying in the form e.g. column names, roles etc.
|
||||
*/
|
||||
export const useFormData = ({ dataSourceName, table }: UseFormDataArgs) => {
|
||||
const httpClient = useHttpClient();
|
||||
return useQuery<ReturnValue, Error>({
|
||||
queryKey: [dataSourceName, 'permissionFormData'],
|
||||
queryFn: async () => {
|
||||
const metadata = await exportMetadata({ httpClient });
|
||||
|
||||
// get table columns for metadata table from db introspection
|
||||
const tableColumns = await DataSource(httpClient).getTableColumns({
|
||||
dataSourceName,
|
||||
table,
|
||||
});
|
||||
|
||||
return createFormData({
|
||||
dataSourceName,
|
||||
table,
|
||||
metadata,
|
||||
tableColumns,
|
||||
});
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
export const permissionToKey = {
|
||||
insert: 'insert_permissions',
|
||||
select: 'select_permissions',
|
||||
update: 'update_permissions',
|
||||
delete: 'delete_permissions',
|
||||
} as const;
|
||||
|
||||
export 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;
|
||||
|
||||
export const isPermission = (props: {
|
||||
key: string;
|
||||
value: any;
|
||||
}): props is {
|
||||
key: typeof metadataPermissionKeys[number];
|
||||
// value: Permission[];
|
||||
value: any[];
|
||||
} => props.key in keyToPermission;
|
@ -1,61 +1,84 @@
|
||||
import {
|
||||
useMetadataTablePermissions,
|
||||
useMetadataMigration,
|
||||
useMetadataVersion,
|
||||
} from '@/features/MetadataAPI';
|
||||
|
||||
import { NewDataTarget } from '../../../PermissionsTab/types/types';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { exportMetadata } from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { Permission, useMetadataMigration } from '@/features/MetadataAPI';
|
||||
|
||||
import { api } from '../../api';
|
||||
import { QueryType } from '../../types';
|
||||
|
||||
const useDataTarget = (dataTarget: NewDataTarget) => {
|
||||
const table = {
|
||||
name: dataTarget.dataLeaf.leaf?.name || '',
|
||||
schema: dataTarget.dataLeaf.name,
|
||||
};
|
||||
interface GetMetadataTableArgs {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
httpClient: AxiosInstance;
|
||||
}
|
||||
|
||||
const {
|
||||
data: resourceVersion,
|
||||
isLoading: metadataLoading,
|
||||
isError: metadataError,
|
||||
} = useMetadataVersion();
|
||||
const getMetadataTable = async ({
|
||||
dataSourceName,
|
||||
table,
|
||||
httpClient,
|
||||
}: GetMetadataTableArgs) => {
|
||||
// get all metadata
|
||||
const { metadata, resource_version } = await exportMetadata({ httpClient });
|
||||
|
||||
if (!resourceVersion && !metadataLoading) {
|
||||
throw new Error('No resource version');
|
||||
}
|
||||
// find current source
|
||||
const currentMetadataSource = metadata?.sources?.find(
|
||||
source => source.name === dataSourceName
|
||||
);
|
||||
|
||||
const {
|
||||
data: permissions,
|
||||
isLoading: permissionsLoading,
|
||||
isError: permissionsError,
|
||||
} = useMetadataTablePermissions(table, dataTarget.dataSource.database);
|
||||
if (!currentMetadataSource)
|
||||
throw Error(`useRolePermissions.metadataSource not found`);
|
||||
|
||||
const isLoading = permissionsLoading || metadataLoading;
|
||||
const isError = permissionsError || metadataError;
|
||||
const trackedTables = currentMetadataSource.tables;
|
||||
|
||||
// find selected table
|
||||
return {
|
||||
dataTarget,
|
||||
resourceVersion,
|
||||
permissions,
|
||||
isLoading,
|
||||
isError,
|
||||
metadataTable: trackedTables.find(
|
||||
trackedTable =>
|
||||
JSON.stringify(trackedTable.table) === JSON.stringify(table)
|
||||
),
|
||||
resourceVersion: resource_version,
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
interface RoleList {
|
||||
roleName: string;
|
||||
queries: QueryType[];
|
||||
}
|
||||
|
||||
export const useBulkDeletePermissions = (dataTarget: NewDataTarget) => {
|
||||
const {
|
||||
resourceVersion,
|
||||
permissions,
|
||||
isLoading: dataTargetLoading,
|
||||
isError: dataTargetError,
|
||||
} = useDataTarget(dataTarget);
|
||||
const isPermission = (props: {
|
||||
key: string;
|
||||
value: any;
|
||||
}): props is {
|
||||
key: typeof metadataPermissionKeys[number];
|
||||
value: Permission[];
|
||||
} => props.key in keyToPermission;
|
||||
|
||||
interface Args {
|
||||
currentSource: string;
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
}
|
||||
|
||||
export const useBulkDeletePermissions = ({
|
||||
currentSource,
|
||||
dataSourceName,
|
||||
table,
|
||||
}: Args) => {
|
||||
const {
|
||||
mutateAsync,
|
||||
isLoading: mutationLoading,
|
||||
@ -63,26 +86,52 @@ export const useBulkDeletePermissions = (dataTarget: NewDataTarget) => {
|
||||
...rest
|
||||
} = useMetadataMigration();
|
||||
|
||||
const submit = async (roles: string[]) => {
|
||||
if (!resourceVersion) {
|
||||
console.error('No resource version');
|
||||
return;
|
||||
}
|
||||
const httpClient = useHttpClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const roleList = permissions?.reduce<RoleList[]>((acc, each) => {
|
||||
if (roles.includes(each.role_name)) {
|
||||
acc.push({
|
||||
roleName: each.role_name,
|
||||
queries: Object.keys(each.permissions) as QueryType[],
|
||||
const submit = async (roles: string[]) => {
|
||||
const { metadataTable, resourceVersion } = await getMetadataTable({
|
||||
dataSourceName,
|
||||
table,
|
||||
httpClient,
|
||||
});
|
||||
|
||||
const permissions = Object.entries(metadataTable || {}).reduce<
|
||||
Record<string, QueryType[]>
|
||||
>((acc, [key, value]) => {
|
||||
const props = { key, value };
|
||||
// check if metadata key is related to permissions
|
||||
if (isPermission(props)) {
|
||||
props.value.forEach(permissionObject => {
|
||||
// only add role if it is one of the selected roles for deletion
|
||||
if (roles.includes(permissionObject.role)) {
|
||||
if (!acc[permissionObject.role]) {
|
||||
acc[permissionObject.role] = [];
|
||||
}
|
||||
|
||||
acc[permissionObject.role].push(keyToPermission[props.key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}, {});
|
||||
|
||||
const roleList = Object.entries(permissions).reduce<RoleList[]>(
|
||||
(acc, [key, value]) => {
|
||||
acc.push({
|
||||
roleName: key,
|
||||
queries: value,
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const body = api.createBulkDeleteBody({
|
||||
dataTarget,
|
||||
roleName: '',
|
||||
source: currentSource,
|
||||
dataSourceName,
|
||||
table,
|
||||
resourceVersion,
|
||||
roleList,
|
||||
});
|
||||
@ -90,10 +139,12 @@ export const useBulkDeletePermissions = (dataTarget: NewDataTarget) => {
|
||||
await mutateAsync({
|
||||
query: body,
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries([dataSourceName, 'permissionsTable']);
|
||||
};
|
||||
|
||||
const isLoading = dataTargetLoading || mutationLoading;
|
||||
const isError = dataTargetError || mutationError;
|
||||
const isLoading = mutationLoading;
|
||||
const isError = mutationError;
|
||||
|
||||
return {
|
||||
submit,
|
||||
|
@ -1,60 +1,43 @@
|
||||
import {
|
||||
useMetadataVersion,
|
||||
useMetadataMigration,
|
||||
} from '@/features/MetadataAPI';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { currentDriver } from '@/dataSources';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useMetadataMigration } from '@/features/MetadataAPI';
|
||||
import { exportMetadata } from '@/features/DataSource';
|
||||
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
|
||||
import { QueryType } from '../../types';
|
||||
import { api } from '../../api';
|
||||
import { NewDataTarget } from '../../../PermissionsTab/types/types';
|
||||
|
||||
export interface UseDeletePermissionArgs {
|
||||
dataTarget: NewDataTarget;
|
||||
currentSource: string;
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
const useDataTarget = () => {
|
||||
const dataSource: string = useAppSelector(
|
||||
state => state.tables.currentDataSource || 'default'
|
||||
);
|
||||
|
||||
const driver = currentDriver;
|
||||
|
||||
const { data: resourceVersion, isLoading, isError } = useMetadataVersion();
|
||||
|
||||
if (!resourceVersion && !isLoading) {
|
||||
throw new Error('No resource version');
|
||||
}
|
||||
|
||||
return {
|
||||
driver,
|
||||
dataSource,
|
||||
resourceVersion,
|
||||
isLoading,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDeletePermission = ({
|
||||
dataTarget,
|
||||
currentSource,
|
||||
dataSourceName,
|
||||
table,
|
||||
roleName,
|
||||
}: UseDeletePermissionArgs) => {
|
||||
const {
|
||||
resourceVersion,
|
||||
isLoading: dataTargetLoading,
|
||||
isError: dataTargetError,
|
||||
} = useDataTarget();
|
||||
const mutate = useMetadataMigration();
|
||||
const httpClient = useHttpClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const submit = async (queries: QueryType[]) => {
|
||||
const { resource_version: resourceVersion } = await exportMetadata({
|
||||
httpClient,
|
||||
});
|
||||
|
||||
if (!resourceVersion) {
|
||||
console.error('No resource version');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = api.createDeleteBody({
|
||||
dataTarget,
|
||||
currentSource,
|
||||
dataSourceName,
|
||||
table,
|
||||
roleName,
|
||||
resourceVersion,
|
||||
queries,
|
||||
@ -63,10 +46,12 @@ export const useDeletePermission = ({
|
||||
await mutate.mutateAsync({
|
||||
query: body,
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries([dataSourceName, 'permissionsTable']);
|
||||
};
|
||||
|
||||
const isLoading = mutate.isLoading || dataTargetLoading;
|
||||
const isError = mutate.isError || dataTargetError;
|
||||
const isLoading = mutate.isLoading;
|
||||
const isError = mutate.isError;
|
||||
|
||||
return {
|
||||
submit,
|
||||
|
@ -1,35 +1,110 @@
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { AxiosInstance } from 'axios';
|
||||
|
||||
import {
|
||||
useMetadataMigration,
|
||||
useMetadataPermissions,
|
||||
useMetadataVersion,
|
||||
} from '@/features/MetadataAPI';
|
||||
import { exportMetadata } from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
|
||||
import { AccessType, FormOutput, QueryType } from '../../types';
|
||||
import { api } from '../../api';
|
||||
import { NewDataTarget } from '../../../PermissionsTab/types/types';
|
||||
|
||||
export interface UseSubmitFormArgs {
|
||||
dataTarget: NewDataTarget;
|
||||
currentSource: string;
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
accessType: AccessType;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const isPermission = (props: {
|
||||
key: string;
|
||||
value: any;
|
||||
}): props is {
|
||||
key: typeof metadataPermissionKeys[number];
|
||||
// value: Permission[];
|
||||
value: any[];
|
||||
} => props.key in keyToPermission;
|
||||
|
||||
interface ExistingPermissions {
|
||||
role: string;
|
||||
queryType: QueryType;
|
||||
table: unknown;
|
||||
}
|
||||
|
||||
interface GetAllPermissionsArgs {
|
||||
dataSourceName: string;
|
||||
httpClient: AxiosInstance;
|
||||
}
|
||||
|
||||
const getAllPermissions = async ({
|
||||
dataSourceName,
|
||||
httpClient,
|
||||
}: GetAllPermissionsArgs) => {
|
||||
const { metadata } = await exportMetadata({ httpClient });
|
||||
|
||||
// find current source
|
||||
const currentMetadataSource = metadata?.sources?.find(
|
||||
source => source.name === dataSourceName
|
||||
);
|
||||
|
||||
return currentMetadataSource?.tables.reduce<ExistingPermissions[]>(
|
||||
(acc, metadataTable) => {
|
||||
Object.entries(metadataTable).forEach(([key, value]) => {
|
||||
const props = { key, value };
|
||||
if (isPermission(props)) {
|
||||
props.value.forEach(permission => {
|
||||
acc.push({
|
||||
role: permission.role,
|
||||
queryType: keyToPermission[props.key],
|
||||
table: metadataTable.table,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
export const useSubmitForm = (args: UseSubmitFormArgs) => {
|
||||
const { dataTarget, roleName, queryType, accessType } = args;
|
||||
const {
|
||||
currentSource,
|
||||
dataSourceName,
|
||||
table,
|
||||
roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
} = args;
|
||||
const {
|
||||
data: resourceVersion,
|
||||
isLoading: resourceVersionLoading,
|
||||
isError: resourceVersionError,
|
||||
} = useMetadataVersion();
|
||||
|
||||
const mutate = useMetadataMigration();
|
||||
const queryClient = useQueryClient();
|
||||
const httpClient = useHttpClient();
|
||||
|
||||
const {
|
||||
data: existingPermissions,
|
||||
isLoading: existingPermissionsLoading,
|
||||
isError: existingPermissionsError,
|
||||
} = useMetadataPermissions(args.dataTarget.dataSource.database);
|
||||
const mutate = useMetadataMigration();
|
||||
|
||||
const submit = async (formData: FormOutput) => {
|
||||
if (!resourceVersion) {
|
||||
@ -37,8 +112,15 @@ export const useSubmitForm = (args: UseSubmitFormArgs) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingPermissions = await getAllPermissions({
|
||||
dataSourceName,
|
||||
httpClient,
|
||||
});
|
||||
|
||||
const body = api.createInsertBody({
|
||||
dataTarget,
|
||||
currentSource,
|
||||
dataSourceName,
|
||||
table,
|
||||
roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
@ -50,12 +132,18 @@ export const useSubmitForm = (args: UseSubmitFormArgs) => {
|
||||
await mutate.mutateAsync({
|
||||
query: body,
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries([
|
||||
dataSourceName,
|
||||
'permissionDefaultValues',
|
||||
roleName,
|
||||
queryType,
|
||||
]);
|
||||
await queryClient.invalidateQueries([dataSourceName, 'permissionsTable']);
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
mutate.isLoading || resourceVersionLoading || existingPermissionsLoading;
|
||||
const isError =
|
||||
mutate.isError || resourceVersionError || existingPermissionsError;
|
||||
const isLoading = mutate.isLoading || resourceVersionLoading;
|
||||
const isError = mutate.isError || resourceVersionError;
|
||||
|
||||
return {
|
||||
submit,
|
||||
|
@ -1,101 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactJson from 'react-json-view';
|
||||
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||
import { Button } from '@/new-components/Button';
|
||||
|
||||
import { handlers } from '../../mocks/handlers.mock';
|
||||
import {
|
||||
useUpdatePermissions,
|
||||
UseUpdatePermissionsArgs,
|
||||
} from './useUpdatePermissions';
|
||||
import { useDefaultValues } from '..';
|
||||
|
||||
const dataLeaf = {
|
||||
type: 'schema',
|
||||
name: 'users',
|
||||
leaf: {
|
||||
type: 'table',
|
||||
name: 'users',
|
||||
},
|
||||
};
|
||||
|
||||
const UseUpdatePermissionsComponent = ({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
}: UseUpdatePermissionsArgs) => {
|
||||
const { data } = useDefaultValues({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType,
|
||||
});
|
||||
|
||||
const { updatePermissions, deletePermissions } = useUpdatePermissions({
|
||||
dataTarget,
|
||||
roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<strong>Data to submit</strong>
|
||||
</p>
|
||||
{!!data && <ReactJson src={data} />}
|
||||
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
mode="primary"
|
||||
isLoading={updatePermissions.isLoading}
|
||||
onClick={() => updatePermissions.submit(data!)}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
mode="destructive"
|
||||
isLoading={deletePermissions.isLoading}
|
||||
onClick={() => deletePermissions.submit(['insert'])}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{!!updatePermissions.data && <ReactJson src={updatePermissions.data} />}
|
||||
|
||||
{!!deletePermissions.data && <ReactJson src={deletePermissions.data} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<UseUpdatePermissionsArgs> = args => (
|
||||
<UseUpdatePermissionsComponent {...args} />
|
||||
);
|
||||
Primary.args = {
|
||||
dataTarget: {
|
||||
dataSource: {
|
||||
driver: 'postgres',
|
||||
database: 'default',
|
||||
},
|
||||
dataLeaf,
|
||||
},
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
accessType: 'fullAccess',
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -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<string, any>;
|
||||
rest.post(`${url}/v2/query`, async (req, res, ctx) => {
|
||||
const body = (await req.json()) as Record<string, any>;
|
||||
|
||||
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<string, any>;
|
||||
|
||||
rest.post(`${url}/v1/metadata`, (req, res, ctx) => {
|
||||
const body = req.body as Record<string, any>;
|
||||
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));
|
||||
}),
|
||||
];
|
||||
|
@ -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<typeof schema>;
|
||||
|
@ -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<PermissionsTabProps> = args => (
|
||||
<PermissionsTab {...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<PermissionsTabProps> = args => (
|
||||
<PermissionsTab {...args} />
|
||||
);
|
||||
GDC.args = {
|
||||
currentSource: 'sqlite',
|
||||
dataSourceName: 'sqlite',
|
||||
table: ['Artist'],
|
||||
};
|
||||
|
||||
GDC.parameters = {
|
||||
msw: handlers(),
|
||||
};
|
||||
|
||||
export const BasicInteraction: Story<PermissionsTabProps> = args => (
|
||||
export const GDCNoMocks: Story<PermissionsTabProps> = args => (
|
||||
<PermissionsTab {...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<PermissionsTabProps> = args => (
|
||||
<PermissionsTab {...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'],
|
||||
};
|
||||
|
@ -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<PermissionsTabProps> = ({ dataLeaf }) => {
|
||||
export const PermissionsTab: React.FC<PermissionsTabProps> = ({
|
||||
currentSource,
|
||||
dataSourceName,
|
||||
table,
|
||||
}) => {
|
||||
const machine = useTableMachine();
|
||||
const [state, send] = machine;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="grid gap-4">
|
||||
<PermissionsTable dataLeaf={dataLeaf} machine={machine} />
|
||||
<PermissionsTable
|
||||
dataSourceName={dataSourceName}
|
||||
table={table}
|
||||
machine={machine}
|
||||
/>
|
||||
|
||||
{state.value === 'bulkOpen' &&
|
||||
!!state.context.bulkSelections.length && (
|
||||
<BulkDelete
|
||||
roles={state.context.bulkSelections}
|
||||
dataLeaf={dataLeaf}
|
||||
currentSource={currentSource}
|
||||
dataSourceName={dataSourceName}
|
||||
table={table}
|
||||
handleClose={() => send('CLOSE')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state.value === 'formOpen' && (
|
||||
<PermissionsForm
|
||||
dataLeaf={dataLeaf}
|
||||
currentSource={currentSource}
|
||||
dataSourceName={dataSourceName}
|
||||
table={table}
|
||||
roleName={state.context.selectedForm.roleName || ''}
|
||||
accessType={state.context.selectedForm.accessType as AccessType}
|
||||
queryType={state.context.selectedForm.queryType || 'insert'}
|
||||
|
@ -10,6 +10,9 @@ export default {
|
||||
title: 'Features/Permissions Table/Table',
|
||||
component: PermissionsTable,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
export const Default: Story<PermissionsTableProps> = args => {
|
||||
@ -19,16 +22,20 @@ export const Default: Story<PermissionsTableProps> = 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<PermissionsTableProps> = args => {
|
||||
const machine = useTableMachine();
|
||||
|
||||
return <PermissionsTable {...args} machine={machine} />;
|
||||
};
|
||||
|
||||
GDCTable.args = {
|
||||
dataSourceName: 'sqlite',
|
||||
table: ['Artist'],
|
||||
};
|
||||
|
@ -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<ViewPermissionsNoteProps> = ({
|
||||
@ -27,7 +22,9 @@ export const ViewPermissionsNote: React.FC<ViewPermissionsNoteProps> = ({
|
||||
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<ViewPermissionsNoteProps> = ({
|
||||
};
|
||||
|
||||
export interface PermissionsTableProps {
|
||||
dataLeaf: DataLeaf;
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
machine: ReturnType<TableMachine>;
|
||||
}
|
||||
|
||||
@ -54,16 +52,22 @@ export interface Selection {
|
||||
}
|
||||
|
||||
export const PermissionsTable: React.FC<PermissionsTableProps> = ({
|
||||
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<PermissionsTableProps> = ({
|
||||
/>
|
||||
|
||||
{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 (
|
||||
|
@ -16,7 +16,7 @@ export default {
|
||||
component: InputCell,
|
||||
decorators: [
|
||||
(StoryComponent: React.FC) => (
|
||||
<Form schema={z.any()} onSubmit={() => {}} className="p-4">
|
||||
<Form schema={z.any()} onSubmit={() => {}}>
|
||||
{() => <StoryComponent />}
|
||||
</Form>
|
||||
),
|
||||
|
@ -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<EditableCellProps> = ({
|
||||
}) => {
|
||||
if (!isEditable) {
|
||||
return (
|
||||
<td className="p-md whitespace-nowrap text-center">
|
||||
<td className="p-md whitespace-nowrap text-center cursor-not-allowed">
|
||||
<PermissionsIcon type={access} selected={isCurrentEdit} />
|
||||
</td>
|
||||
);
|
||||
@ -90,15 +89,15 @@ export const EditableCell: React.FC<EditableCellProps> = ({
|
||||
|
||||
return (
|
||||
<td>
|
||||
<Button
|
||||
<button
|
||||
type="submit"
|
||||
className={`cursor-pointer h-20 border-none w-full whitespace-nowrap text-center ${
|
||||
isCurrentEdit ? 'bg-secondary' : 'hover:bg-indigo-50'
|
||||
isCurrentEdit ? 'bg-amber-300' : 'hover:bg-indigo-50'
|
||||
}`}
|
||||
{...rest}
|
||||
>
|
||||
<PermissionsIcon type={access} selected={isCurrentEdit} />
|
||||
</Button>
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
@ -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<string, any>;
|
||||
};
|
||||
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<Record<QueryType, Access>>;
|
||||
|
||||
const createSupportedQueryObject = (access: Access) =>
|
||||
supportedQueries.reduce<SupportedQueriesObject>((acc, supportedQuery) => {
|
||||
acc[supportedQuery] = access;
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
}, {});
|
||||
|
||||
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<string, Partial<Record<QueryType, Access>>>;
|
||||
|
||||
const createRoleTableData = async ({
|
||||
metadataTable,
|
||||
tableColumns,
|
||||
}: CreateRoleTableDataArgs): Promise<RolePermission[]> => {
|
||||
if (!metadataTable) return [];
|
||||
// create object with key of role
|
||||
// and value describing permissions attached to that role
|
||||
const roleToPermissionsMap = Object.entries(
|
||||
metadataTable
|
||||
).reduce<RoleToPermissionsMap>((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,
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user