mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
Matt/console/permissions form
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2893 Co-authored-by: Ikechukwu Eze <22247592+iykekings@users.noreply.github.com> GitOrigin-RevId: e6c8dedf8369df887fdc376c606b9b2e9b3d5ddd
This commit is contained in:
parent
476eb5fe2b
commit
d2724878d3
@ -187,7 +187,8 @@
|
||||
"**/*.spec.tsx",
|
||||
"**/*.stories.tsx",
|
||||
"**/*.stories.mdx",
|
||||
"**/*.mock.tsx"
|
||||
"**/*.mock.tsx",
|
||||
"**/*.mock.ts"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
10945
console/package-lock.json
generated
10945
console/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -78,6 +78,7 @@
|
||||
"graphql-voyager": "1.0.0-rc.29",
|
||||
"highlight.js": "9.15.8",
|
||||
"history": "3.3.0",
|
||||
"immer": "9.0.12",
|
||||
"inflection": "1.12.0",
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
@ -241,7 +242,7 @@
|
||||
"lint-staged": "10.2.2",
|
||||
"mini-css-extract-plugin": "0.4.5",
|
||||
"msw": "0.29.0",
|
||||
"msw-storybook-addon": "1.3.0",
|
||||
"msw-storybook-addon": "1.6.0",
|
||||
"node-sass": "4.14.1",
|
||||
"nyc": "15.0.1",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
@ -275,5 +276,5 @@
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "static"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,11 +108,11 @@ export const getTableColumn = (table: Table, columnName: string) => {
|
||||
};
|
||||
|
||||
export const getTableRelationshipNames = (table: Table) => {
|
||||
return table.relationships.map(r => r.rel_name);
|
||||
return table?.relationships?.map(r => r.rel_name);
|
||||
};
|
||||
|
||||
export function getTableRelationship(table: Table, relationshipName: string) {
|
||||
return table.relationships.find(
|
||||
return table?.relationships?.find(
|
||||
relationship => relationship.rel_name === relationshipName
|
||||
);
|
||||
}
|
||||
|
@ -227,7 +227,7 @@ export const getGroupedTableComputedFields = (
|
||||
table: ComputedField[];
|
||||
} = { scalar: [], table: [] };
|
||||
|
||||
computed_fields.forEach(computedField => {
|
||||
computed_fields?.forEach(computedField => {
|
||||
const computedFieldFnDef = computedField.definition.function;
|
||||
const computedFieldFn = findFunction(
|
||||
allFunctions,
|
||||
|
@ -126,7 +126,7 @@ class PermissionBuilder extends React.Component<PermissionBuilderProps> {
|
||||
|
||||
const allSchemaNames = allTableSchemas.map(t => t.table_schema);
|
||||
|
||||
if (!allSchemaNames.includes(existTableSchema)) {
|
||||
if (!allSchemaNames?.includes(existTableSchema)) {
|
||||
missingSchemas.push(existTableSchema);
|
||||
}
|
||||
}
|
||||
@ -148,7 +148,7 @@ class PermissionBuilder extends React.Component<PermissionBuilderProps> {
|
||||
tableRelationshipNames = getTableRelationshipNames(tableSchema);
|
||||
}
|
||||
|
||||
if (tableRelationshipNames.includes(operator)) {
|
||||
if (tableRelationshipNames?.includes(operator)) {
|
||||
const relationship = getTableRelationship(tableSchema!, operator);
|
||||
const refTable = getRelationshipRefTable(tableSchema!, relationship!);
|
||||
|
||||
@ -884,7 +884,7 @@ class PermissionBuilder extends React.Component<PermissionBuilderProps> {
|
||||
}
|
||||
|
||||
let columnExp = null;
|
||||
if (tableRelationshipNames.includes(columnName)) {
|
||||
if (tableRelationshipNames?.includes(columnName)) {
|
||||
const relationship = getTableRelationship(tableSchema!, columnName);
|
||||
const refTable = getRelationshipRefTable(tableSchema!, relationship!);
|
||||
|
||||
|
114
console/src/features/PermissionsForm/PermissionsForm.stories.tsx
Normal file
114
console/src/features/PermissionsForm/PermissionsForm.stories.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||
|
||||
import { PermissionsForm, PermissionsFormProps } from './PermissionsForm';
|
||||
import { handlers } from './mocks/handlers.mock';
|
||||
|
||||
export default {
|
||||
title: 'Permissions Form/Form',
|
||||
component: PermissionsForm,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
parameters: {
|
||||
msw: handlers,
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const schemaName = 'public';
|
||||
const tableName = 'users';
|
||||
const roleName = 'user';
|
||||
|
||||
export const Showcase: Story<PermissionsFormProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<p className="font-bold py-4">Query Type: Insert</p>
|
||||
|
||||
<PermissionsForm
|
||||
schemaName={schemaName}
|
||||
tableName={tableName}
|
||||
roleName={roleName}
|
||||
accessType="partialAccess"
|
||||
queryType="insert"
|
||||
handleClose={() => {}}
|
||||
/>
|
||||
|
||||
<p className="font-bold py-4">Query Type: Select</p>
|
||||
|
||||
<PermissionsForm
|
||||
schemaName={schemaName}
|
||||
tableName={tableName}
|
||||
roleName={roleName}
|
||||
accessType="partialAccess"
|
||||
queryType="select"
|
||||
handleClose={() => {}}
|
||||
/>
|
||||
|
||||
<p className="font-bold py-4">Query Type: Update</p>
|
||||
|
||||
<PermissionsForm
|
||||
schemaName={schemaName}
|
||||
tableName={tableName}
|
||||
roleName={roleName}
|
||||
accessType="noAccess"
|
||||
queryType="update"
|
||||
handleClose={() => {}}
|
||||
/>
|
||||
|
||||
<p className="font-bold py-4">Query Type: Delete</p>
|
||||
|
||||
<PermissionsForm
|
||||
schemaName={schemaName}
|
||||
tableName={tableName}
|
||||
roleName={roleName}
|
||||
accessType="noAccess"
|
||||
queryType="delete"
|
||||
handleClose={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Insert: Story<PermissionsFormProps> = args => (
|
||||
<PermissionsForm {...args} />
|
||||
);
|
||||
Insert.args = {
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
accessType: 'partialAccess',
|
||||
queryType: 'insert',
|
||||
handleClose: () => {},
|
||||
};
|
||||
Insert.parameters = {
|
||||
// Disable storybook for Insert stories
|
||||
chromatic: { disableSnapshot: true },
|
||||
};
|
||||
|
||||
export const Select: Story<PermissionsFormProps> = args => (
|
||||
<PermissionsForm {...args} />
|
||||
);
|
||||
Select.args = {
|
||||
...Insert.args,
|
||||
queryType: 'select',
|
||||
};
|
||||
Select.parameters = Insert.parameters;
|
||||
|
||||
export const Update: Story<PermissionsFormProps> = args => (
|
||||
<PermissionsForm {...args} />
|
||||
);
|
||||
Update.args = {
|
||||
...Insert.args,
|
||||
queryType: 'update',
|
||||
accessType: 'noAccess',
|
||||
};
|
||||
Update.parameters = Insert.parameters;
|
||||
|
||||
export const Delete: Story<PermissionsFormProps> = args => (
|
||||
<PermissionsForm {...args} />
|
||||
);
|
||||
Delete.args = {
|
||||
...Insert.args,
|
||||
queryType: 'delete',
|
||||
accessType: 'noAccess',
|
||||
};
|
||||
Delete.parameters = Insert.parameters;
|
246
console/src/features/PermissionsForm/PermissionsForm.tsx
Normal file
246
console/src/features/PermissionsForm/PermissionsForm.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import React from 'react';
|
||||
import { useFormContext } 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 { useDefaultValues, useFormData, useUpdatePermissions } from './hooks';
|
||||
import { schema } from './utils/formSchema';
|
||||
|
||||
import { AccessType, FormOutput, QueryType } from './types';
|
||||
|
||||
export interface PermissionsFormProps {
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
queryType: QueryType;
|
||||
roleName: string;
|
||||
accessType: AccessType;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
interface ResetterProps {
|
||||
defaultValues: FormOutput;
|
||||
}
|
||||
|
||||
// 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> = ({
|
||||
schemaName,
|
||||
tableName,
|
||||
queryType,
|
||||
roleName,
|
||||
accessType,
|
||||
handleClose,
|
||||
}) => {
|
||||
const {
|
||||
data,
|
||||
isLoading: loadingFormData,
|
||||
isError: formDataError,
|
||||
} = useFormData({
|
||||
schemaName,
|
||||
tableName,
|
||||
queryType,
|
||||
roleName,
|
||||
});
|
||||
|
||||
const {
|
||||
data: defaults,
|
||||
isLoading: defaultValuesLoading,
|
||||
isError: defaultValuesError,
|
||||
} = useDefaultValues({
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType,
|
||||
});
|
||||
|
||||
const { updatePermissions, deletePermissions } = useUpdatePermissions({
|
||||
schemaName,
|
||||
tableName,
|
||||
queryType,
|
||||
roleName,
|
||||
accessType,
|
||||
});
|
||||
|
||||
const handleSubmit = async (formData: FormOutput) => {
|
||||
updatePermissions.submit(formData);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deletePermissions.submit([queryType]);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const isError = formDataError || defaultValuesError;
|
||||
|
||||
// these will be replaced by components once spec is decided
|
||||
if (isError) {
|
||||
return <div>Error loading form data</div>;
|
||||
}
|
||||
|
||||
const isSubmittingError =
|
||||
updatePermissions.isError || deletePermissions.isError;
|
||||
|
||||
// these will be replaced by components once spec is decided
|
||||
if (isSubmittingError) {
|
||||
return <div>Error submitting form data</div>;
|
||||
}
|
||||
|
||||
const isLoading = loadingFormData || defaultValuesLoading;
|
||||
|
||||
// these will be replaced by components once spec is decided
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const {
|
||||
// i.e. tables that are not the currently open table (this is for cloning permissions)
|
||||
otherTableNames,
|
||||
allFunctions,
|
||||
roles,
|
||||
tables,
|
||||
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;
|
||||
|
||||
// for update it is possible to set pre update and post update row checks
|
||||
const rowPermissions = queryType === 'update' ? ['pre', 'post'] : [queryType];
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} schema={schema} 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} />
|
||||
|
||||
<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
|
||||
queryType={queryType}
|
||||
subQueryType={
|
||||
queryType === 'update' ? permissionName : undefined
|
||||
}
|
||||
schemaName={schemaName}
|
||||
tableName={tableName}
|
||||
allRowChecks={allRowChecks}
|
||||
allSchemas={tables}
|
||||
allFunctions={allFunctions}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</RowPermissionsSectionWrapper>
|
||||
|
||||
{queryType !== 'delete' && (
|
||||
<ColumnPermissionsSection
|
||||
roleName={roleName}
|
||||
queryType={queryType}
|
||||
columns={columns!}
|
||||
/>
|
||||
)}
|
||||
|
||||
{['insert', 'update'].includes(queryType) && (
|
||||
<ColumnPresetsSection queryType={queryType} columns={columns!} />
|
||||
)}
|
||||
|
||||
{queryType === 'select' && (
|
||||
<AggregationSection queryType={queryType} roleName={roleName} />
|
||||
)}
|
||||
|
||||
{queryType === 'insert' && (
|
||||
<BackendOnlySection queryType={queryType} />
|
||||
)}
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
{otherTableNames && roles && (
|
||||
<ClonePermissionsSection
|
||||
queryType={queryType}
|
||||
tables={otherTableNames}
|
||||
supportedQueryTypes={supportedQueries}
|
||||
roles={roles}
|
||||
/>
|
||||
)}
|
||||
|
||||
<hr className="my-4" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionsForm;
|
@ -0,0 +1,99 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`update metadata in cache 1`] = `
|
||||
Object {
|
||||
"metadata": Object {
|
||||
"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,
|
||||
}
|
||||
`;
|
@ -0,0 +1,47 @@
|
||||
import { updateTablePermission } from '../cache';
|
||||
import { metadata } from '../../mocks/dataStubs';
|
||||
|
||||
const data = {
|
||||
type: 'bulk',
|
||||
source: 'default',
|
||||
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();
|
||||
});
|
142
console/src/features/PermissionsForm/api/api.ts
Normal file
142
console/src/features/PermissionsForm/api/api.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { Api } from '@/hooks/apiUtils';
|
||||
import Endpoints from '@/Endpoints';
|
||||
|
||||
import { QualifiedTable } from '@/metadata/types';
|
||||
import { AccessType, FormOutput, QueryType } from '../types';
|
||||
|
||||
interface CreateBodyArgs {
|
||||
dataSource: string;
|
||||
qualifiedTable: QualifiedTable;
|
||||
roleName: string;
|
||||
resourceVersion: number;
|
||||
}
|
||||
|
||||
interface CreateDeleteBodyArgs extends CreateBodyArgs {
|
||||
queries: QueryType[];
|
||||
}
|
||||
|
||||
const createDeleteBody = ({
|
||||
dataSource,
|
||||
qualifiedTable,
|
||||
roleName,
|
||||
resourceVersion,
|
||||
queries,
|
||||
}: CreateDeleteBodyArgs) => {
|
||||
const args = queries.map(queryType => ({
|
||||
type: `${dataSource}_drop_${queryType}_permission`,
|
||||
args: {
|
||||
table: qualifiedTable,
|
||||
role: roleName,
|
||||
source: 'default',
|
||||
},
|
||||
}));
|
||||
|
||||
const body = {
|
||||
type: 'bulk',
|
||||
source: 'default',
|
||||
resource_version: resourceVersion,
|
||||
args,
|
||||
};
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
interface CreateInsertBodyArgs extends CreateBodyArgs {
|
||||
queryType: QueryType;
|
||||
formData: FormOutput;
|
||||
accessType: AccessType;
|
||||
}
|
||||
|
||||
const createInsertBody = ({
|
||||
dataSource,
|
||||
qualifiedTable,
|
||||
queryType,
|
||||
roleName,
|
||||
formData,
|
||||
accessType,
|
||||
resourceVersion,
|
||||
}: CreateInsertBodyArgs) => {
|
||||
const presets = formData.presets?.reduce((acc, preset) => {
|
||||
if (preset.columnValue) {
|
||||
acc[preset.columnName] = preset.columnValue;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, string | number>);
|
||||
|
||||
const columns = Object.entries(formData.columns)
|
||||
.filter(({ 1: value }) => value)
|
||||
.map(([key]) => key);
|
||||
|
||||
const permission = {
|
||||
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 || '{}'),
|
||||
};
|
||||
|
||||
const args = [
|
||||
{
|
||||
type: `${dataSource}_create_${queryType}_permission`,
|
||||
args: {
|
||||
table: qualifiedTable,
|
||||
role: roleName,
|
||||
permission,
|
||||
source: 'default',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (accessType !== 'noAccess') {
|
||||
args.unshift({
|
||||
type: `${dataSource}_drop_${queryType}_permission`,
|
||||
args: {
|
||||
table: qualifiedTable,
|
||||
role: roleName,
|
||||
source: 'default',
|
||||
},
|
||||
} as typeof args[0]);
|
||||
}
|
||||
|
||||
const formBody = {
|
||||
type: 'bulk',
|
||||
source: 'default',
|
||||
resource_version: resourceVersion,
|
||||
args,
|
||||
};
|
||||
|
||||
return formBody;
|
||||
};
|
||||
|
||||
interface CreatePermissionsArgs {
|
||||
headers: any;
|
||||
body: ReturnType<typeof createInsertBody>;
|
||||
}
|
||||
|
||||
const createPermissions = ({ headers, body }: CreatePermissionsArgs) =>
|
||||
Api.post<string[]>(
|
||||
{ headers, body, url: Endpoints.metadata },
|
||||
result => result
|
||||
);
|
||||
|
||||
interface DeletePermissionsArgs {
|
||||
headers: any;
|
||||
body: ReturnType<typeof createDeleteBody>;
|
||||
}
|
||||
|
||||
const deletePermissions = ({ headers, body }: DeletePermissionsArgs) =>
|
||||
Api.post<string[]>(
|
||||
{ headers, body, url: Endpoints.metadata },
|
||||
result => result
|
||||
);
|
||||
|
||||
export const api = {
|
||||
createPermissions,
|
||||
deletePermissions,
|
||||
createInsertBody,
|
||||
createDeleteBody,
|
||||
};
|
118
console/src/features/PermissionsForm/api/cache.ts
Normal file
118
console/src/features/PermissionsForm/api/cache.ts
Normal file
@ -0,0 +1,118 @@
|
||||
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,
|
||||
};
|
2
console/src/features/PermissionsForm/api/index.ts
Normal file
2
console/src/features/PermissionsForm/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './api';
|
||||
export * from './cache';
|
@ -23,7 +23,7 @@ export const AggregationSection: React.FC<AggregationProps> = ({
|
||||
// if no row permissions are selected selection should be disabled
|
||||
const disabled = useIsDisabled(queryType);
|
||||
|
||||
const enabled = watch('enableAggregation');
|
||||
const enabled = watch('aggregationEnabled');
|
||||
|
||||
if (!isFeatureSupported('tables.permissions.aggregation')) {
|
||||
return null;
|
||||
@ -46,7 +46,7 @@ export const AggregationSection: React.FC<AggregationProps> = ({
|
||||
type="checkbox"
|
||||
title={disabled ? 'Set row permissions first' : ''}
|
||||
disabled={disabled}
|
||||
{...register('enableAggregation')}
|
||||
{...register('aggregationEnabled')}
|
||||
/>
|
||||
<p>
|
||||
Allow role <strong>{roleName}</strong> to make aggregation queries
|
||||
|
@ -42,11 +42,11 @@ const useStatus = (disabled: boolean) => {
|
||||
return { data: 'Disabled: Set row permissions first', isError: false };
|
||||
}
|
||||
|
||||
if (selectedColumns.length === 0) {
|
||||
if (selectedColumns?.length === 0) {
|
||||
return { data: 'No columns', isError: false };
|
||||
}
|
||||
|
||||
if (selectedColumns.length === columnValues.length) {
|
||||
if (selectedColumns?.length === columnValues?.length) {
|
||||
return { data: 'All columns', isError: false };
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ export const ColumnPermissionsSection: React.FC<ColumnPermissionsSectionProps> =
|
||||
</div>
|
||||
|
||||
<fieldset className="flex gap-4">
|
||||
{columns.map(fieldName => (
|
||||
{columns?.map(fieldName => (
|
||||
<label key={fieldName} className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
@ -83,7 +83,7 @@ const PresetsRow: React.FC<PresetsRowProps> = ({
|
||||
className={className}
|
||||
placeholder="Column value"
|
||||
disabled={disabled}
|
||||
{...register(`presets.${id}.columnValue`)}
|
||||
{...register(`presets.${id}.value`)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -131,7 +131,7 @@ const useStatus = (disabled: boolean) => {
|
||||
.map(({ columnName }) => columnName)
|
||||
.filter(columnName => columnName !== 'default');
|
||||
|
||||
if (!columnNames.length) {
|
||||
if (!columnNames?.length) {
|
||||
return 'No Presets';
|
||||
}
|
||||
|
||||
@ -164,8 +164,8 @@ export const ColumnPresetsSection: React.FC<ColumnPresetsSectionProps> = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
const finalRowIsNotDefault =
|
||||
controlledFields[controlledFields.length - 1]?.columnName !== 'default';
|
||||
const allColumnsSet = controlledFields.length === columns.length;
|
||||
controlledFields[controlledFields?.length - 1]?.columnName !== 'default';
|
||||
const allColumnsSet = controlledFields?.length === columns?.length;
|
||||
|
||||
if (finalRowIsNotDefault && !allColumnsSet) {
|
||||
append({
|
||||
@ -174,7 +174,7 @@ export const ColumnPresetsSection: React.FC<ColumnPresetsSectionProps> = ({
|
||||
columnValue: '',
|
||||
});
|
||||
}
|
||||
}, [controlledFields, columns.length, append]);
|
||||
}, [controlledFields, columns?.length, append]);
|
||||
|
||||
return (
|
||||
<Collapse defaultOpen={defaultOpen}>
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
RowPermissionsWrapperProps,
|
||||
} from './RowPermissions';
|
||||
|
||||
import { currentSchema, allSchemas, allFunctions } from '../mocks/mockData';
|
||||
import { allSchemas, allFunctions } from '../mocks/mockData';
|
||||
import { QueryType } from '../types';
|
||||
|
||||
export default {
|
||||
@ -29,23 +29,16 @@ export default {
|
||||
const roleName = 'two';
|
||||
|
||||
// this will be moved into a utils folder
|
||||
const allRowChecks = ({ role, query }: { role: string; query: string }) => {
|
||||
const currentRole = currentSchema.permissions.find(
|
||||
({ role_name }) => role === role_name
|
||||
);
|
||||
|
||||
if (currentRole) {
|
||||
const { permissions } = currentRole;
|
||||
return Object.entries(permissions)
|
||||
.filter(([name, info]) => name !== query && info.filter)
|
||||
.map(([name, info]) => ({
|
||||
queryType: name as QueryType,
|
||||
filter: JSON.stringify(info.filter),
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
const allRowChecks = [
|
||||
{
|
||||
queryType: 'insert' as QueryType,
|
||||
value: '{"id":{"_eq":1}}',
|
||||
},
|
||||
{
|
||||
queryType: 'select' as QueryType,
|
||||
value: '{"id":{"_eq":1}}',
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
wrapper: RowPermissionsWrapperProps;
|
||||
@ -63,7 +56,7 @@ Insert.args = {
|
||||
schemaName: 'public',
|
||||
tableName: 'users',
|
||||
queryType: 'delete',
|
||||
allRowChecks: allRowChecks({ role: roleName, query: 'insert' }),
|
||||
allRowChecks,
|
||||
allSchemas,
|
||||
allFunctions,
|
||||
},
|
||||
@ -77,9 +70,9 @@ export const Select: Story<Props> = args => (
|
||||
Select.args = {
|
||||
wrapper: { roleName, queryType: 'select', defaultOpen: true },
|
||||
section: {
|
||||
...Insert.args.section!,
|
||||
...Insert!.args!.section!,
|
||||
queryType: 'select',
|
||||
allRowChecks: allRowChecks({ role: roleName, query: 'select' }),
|
||||
allRowChecks,
|
||||
},
|
||||
};
|
||||
|
||||
@ -93,7 +86,7 @@ Update.args = {
|
||||
section: {
|
||||
...Insert.args.section!,
|
||||
queryType: 'update',
|
||||
allRowChecks: allRowChecks({ role: roleName, query: 'update' }),
|
||||
allRowChecks,
|
||||
},
|
||||
};
|
||||
|
||||
@ -107,7 +100,7 @@ Delete.args = {
|
||||
section: {
|
||||
...Insert.args.section!,
|
||||
queryType: 'delete',
|
||||
allRowChecks: allRowChecks({ role: roleName, query: 'delete' }),
|
||||
allRowChecks,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { useFormContext, Controller } from 'react-hook-form';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/theme/github';
|
||||
|
||||
import { Table } from '@/dataSources/types';
|
||||
import { NormalizedTable, Table } from '@/dataSources/types';
|
||||
import { PGFunction } from '@/dataSources/services/postgresql/types';
|
||||
import { generateTableDef } from '@/dataSources';
|
||||
import { InputField } from '@/new-components/Form';
|
||||
@ -40,9 +40,9 @@ export interface RowPermissionsProps {
|
||||
subQueryType?: string;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
allRowChecks: Array<{ queryType: QueryType; filter: string }>;
|
||||
allSchemas: Table[];
|
||||
allFunctions: PGFunction[];
|
||||
allRowChecks: Array<{ queryType: QueryType; value: string }>;
|
||||
allSchemas?: NormalizedTable[];
|
||||
allFunctions?: PGFunction[];
|
||||
}
|
||||
|
||||
enum SelectedSection {
|
||||
@ -51,7 +51,7 @@ enum SelectedSection {
|
||||
NoneSelected = 'none',
|
||||
}
|
||||
|
||||
const getRowPermission = (queryType: string, subQueryType?: string) => {
|
||||
const getRowPermission = (queryType: QueryType, subQueryType?: string) => {
|
||||
if (queryType === 'insert') {
|
||||
return 'check';
|
||||
}
|
||||
@ -68,7 +68,7 @@ const getRowPermission = (queryType: string, subQueryType?: string) => {
|
||||
};
|
||||
|
||||
const getRowPermissionCheckType = (
|
||||
queryType: string,
|
||||
queryType: QueryType,
|
||||
subQueryType?: string
|
||||
) => {
|
||||
if (queryType === 'insert') {
|
||||
@ -96,7 +96,6 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
allFunctions,
|
||||
}) => {
|
||||
const { control, 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`
|
||||
@ -110,7 +109,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
queryType === 'update' && subQueryType === 'post' && !watch('check');
|
||||
|
||||
const schemaList = React.useMemo(
|
||||
() => allSchemas.map(({ table_schema }) => table_schema),
|
||||
() => allSchemas?.map(({ table_schema }) => table_schema),
|
||||
[allSchemas]
|
||||
);
|
||||
|
||||
@ -147,7 +146,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{allRowChecks.map(({ queryType: query, filter }) => (
|
||||
{allRowChecks?.map(({ queryType: query, value }) => (
|
||||
<div key={query}>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
@ -157,7 +156,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setValue(rowPermissionsCheckType, query);
|
||||
setValue(rowPermissions, filter);
|
||||
setValue(rowPermissions, value);
|
||||
}}
|
||||
{...register(rowPermissionsCheckType)}
|
||||
/>
|
||||
@ -169,7 +168,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
{selectedSection === query && (
|
||||
<div className="pt-4">
|
||||
<JSONEditor
|
||||
data={filter}
|
||||
data={value}
|
||||
onChange={output => {
|
||||
setValue(rowPermissionsCheckType, SelectedSection.Custom);
|
||||
setValue(rowPermissions, output);
|
||||
@ -208,18 +207,20 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
initData="{}"
|
||||
/>
|
||||
|
||||
<PermissionBuilder
|
||||
dispatchFuncSetFilter={output => {
|
||||
onChange(output);
|
||||
}}
|
||||
loadSchemasFunc={() => {}}
|
||||
tableDef={generateTableDef(tableName, schemaName)}
|
||||
allTableSchemas={allSchemas}
|
||||
allFunctions={allFunctions}
|
||||
schemaList={schemaList}
|
||||
filter={value || '{}'}
|
||||
dispatch={() => console.log('output')}
|
||||
/>
|
||||
{allSchemas && allFunctions && schemaList && (
|
||||
<PermissionBuilder
|
||||
dispatchFuncSetFilter={output => {
|
||||
onChange(output);
|
||||
}}
|
||||
loadSchemasFunc={() => {}}
|
||||
tableDef={generateTableDef(tableName, schemaName)}
|
||||
allTableSchemas={allSchemas as Table[]}
|
||||
allFunctions={allFunctions}
|
||||
schemaList={schemaList}
|
||||
filter={value || '{}'}
|
||||
dispatch={() => console.log('output')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
@ -0,0 +1,2 @@
|
||||
export * from './useDefaultValues';
|
||||
export * from './useFormData';
|
@ -0,0 +1,70 @@
|
||||
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 UseDefaultValuesComponent = ({
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType,
|
||||
}: UseDefaultValuesArgs) => {
|
||||
const results = useDefaultValues({
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType,
|
||||
});
|
||||
return <ReactJson src={results} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Permissions Form/hooks/useDefaultValues',
|
||||
component: UseDefaultValuesComponent,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
parameters: {
|
||||
msw: handlers,
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const schemaName = 'public';
|
||||
const tableName = 'users';
|
||||
const roleName = 'user';
|
||||
|
||||
export const Insert: Story<UseDefaultValuesArgs> = args => (
|
||||
<UseDefaultValuesComponent {...args} />
|
||||
);
|
||||
Insert.args = {
|
||||
schemaName,
|
||||
tableName,
|
||||
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',
|
||||
};
|
@ -0,0 +1,203 @@
|
||||
import { NormalizedTable } from '@/dataSources/types';
|
||||
import { useMetadataTablePermissions } from '@/features/MetadataAPI';
|
||||
import { useSingleTable } from '@/hooks';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { currentDriver } from '@/dataSources';
|
||||
|
||||
import { getCurrentRole } from '../../utils';
|
||||
|
||||
import { QueryType } from '../../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 {
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
|
||||
const useLoadPermissions = ({
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType,
|
||||
}: UseDefaultValuesArgs) => {
|
||||
const dataSource: string =
|
||||
useAppSelector(state => state.tables.currentDataSource) || 'default';
|
||||
|
||||
const {
|
||||
data: table,
|
||||
isLoading: tableLoading,
|
||||
isError: tableError,
|
||||
} = useSingleTable({
|
||||
source: dataSource,
|
||||
driver: currentDriver,
|
||||
table: { name: tableName, schema: schemaName },
|
||||
});
|
||||
|
||||
const {
|
||||
data: permissions,
|
||||
isLoading: permissionsLoading,
|
||||
isError: permissionsError,
|
||||
} = useMetadataTablePermissions(
|
||||
{
|
||||
schema: schemaName,
|
||||
name: tableName,
|
||||
},
|
||||
dataSource
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
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,40 @@
|
||||
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 UseFormDataComponent = ({
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType,
|
||||
}: UseFormDataArgs) => {
|
||||
const results = useFormData({ schemaName, tableName, roleName, queryType });
|
||||
return <ReactJson src={results} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Permissions Form/hooks/useFormData',
|
||||
component: UseFormDataComponent,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
parameters: {
|
||||
msw: handlers,
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const schemaName = 'public';
|
||||
const tableName = 'users';
|
||||
const roleName = 'two';
|
||||
|
||||
export const Primary: Story<UseFormDataArgs> = args => (
|
||||
<UseFormDataComponent {...args} />
|
||||
);
|
||||
Primary.args = {
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
};
|
@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
useTrackableFunctions,
|
||||
useDataSourceTables,
|
||||
useSingleTable,
|
||||
} from '@/hooks';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { currentDriver, dataSource } from '@/dataSources';
|
||||
|
||||
import { useRoles } from '../../../MetadataAPI';
|
||||
import { QueryType } from '../../types';
|
||||
|
||||
export interface UseFormDataArgs {
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
|
||||
const useLoadSchemas = ({ schemaName, tableName }: UseFormDataArgs) => {
|
||||
const source: string = useAppSelector(
|
||||
state => state.tables.currentDataSource
|
||||
);
|
||||
|
||||
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, roles, allFunctions },
|
||||
isLoading,
|
||||
isError,
|
||||
} = useLoadSchemas(props);
|
||||
|
||||
const otherTableNames = React.useMemo(
|
||||
() =>
|
||||
tables
|
||||
?.filter(({ table_name }) => table_name !== table?.table_name)
|
||||
.map(({ table_name }) => table_name),
|
||||
[tables, table]
|
||||
);
|
||||
const columns = React.useMemo(
|
||||
() => table?.columns.map(({ column_name }) => column_name),
|
||||
[table]
|
||||
);
|
||||
|
||||
let supportedQueries: string[] = [];
|
||||
if (table) {
|
||||
// supportedQueries = ['insert', 'select', 'update', 'delete'];
|
||||
supportedQueries = dataSource.getTableSupportedQueries(table);
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
table,
|
||||
tables,
|
||||
otherTableNames,
|
||||
columns,
|
||||
allFunctions,
|
||||
roles,
|
||||
supportedQueries,
|
||||
},
|
||||
isLoading,
|
||||
isError,
|
||||
};
|
||||
};
|
3
console/src/features/PermissionsForm/hooks/index.ts
Normal file
3
console/src/features/PermissionsForm/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './dataFetchingHooks';
|
||||
export * from './submitHooks';
|
||||
export * from './useIsDisabled';
|
@ -0,0 +1 @@
|
||||
export * from './useUpdatePermissions';
|
@ -0,0 +1,55 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { useMetadataVersion } from '@/features/MetadataAPI';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
import { QueryType } from '../../types';
|
||||
import { api } from '../../api';
|
||||
|
||||
export interface UseDeletePermissionArgs {
|
||||
tableName: string;
|
||||
schemaName: string;
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
export const useDeletePermission = ({
|
||||
tableName,
|
||||
schemaName,
|
||||
roleName,
|
||||
}: UseDeletePermissionArgs) => {
|
||||
const client = useQueryClient();
|
||||
|
||||
const headers = useAppSelector(state => state.tables.dataHeaders);
|
||||
const { data: resourceVersion } = useMetadataVersion();
|
||||
|
||||
const { mutateAsync, ...rest } = useMutation(api.deletePermissions, {
|
||||
onError: (_err, _, context: any) => {
|
||||
// if there is an error set the metadata query to the original value
|
||||
client.setQueryData('metadata', context.metadata);
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
// once the mutation is complete invalidate the query
|
||||
// ensure the client state is upto date with the server state
|
||||
client.invalidateQueries('metadata');
|
||||
},
|
||||
});
|
||||
|
||||
const submit = async (queries: QueryType[]) => {
|
||||
if (!resourceVersion) {
|
||||
throw new Error('No resource version provided');
|
||||
}
|
||||
|
||||
const body = api.createDeleteBody({
|
||||
dataSource: 'pg',
|
||||
qualifiedTable: { name: tableName, schema: schemaName },
|
||||
roleName,
|
||||
resourceVersion,
|
||||
queries,
|
||||
});
|
||||
|
||||
await mutateAsync({ headers, body });
|
||||
};
|
||||
|
||||
return { submit, ...rest };
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { useAppSelector } from '@/store';
|
||||
import { useMetadataVersion } from '../../../MetadataAPI';
|
||||
|
||||
import { AccessType, FormOutput, QueryType } from '../../types';
|
||||
import { api, cache } from '../../api';
|
||||
|
||||
export interface UseSubmitFormArgs {
|
||||
tableName: string;
|
||||
schemaName: string;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
accessType: AccessType;
|
||||
}
|
||||
|
||||
export const useSubmitForm = (args: UseSubmitFormArgs) => {
|
||||
const client = useQueryClient();
|
||||
const headers = useAppSelector(state => state.tables.dataHeaders);
|
||||
const { data: resourceVersion } = useMetadataVersion();
|
||||
const { handleUpdate } = cache.useUpdateTablePermissionCache();
|
||||
|
||||
const { mutateAsync, ...rest } = useMutation(api.createPermissions, {
|
||||
mutationKey: 'permissionsUpdate',
|
||||
// this performs an optimistic cache update
|
||||
// once the mutation has resolved the cache will be updated if it failed
|
||||
onMutate: response => handleUpdate({ args, response }),
|
||||
onError: (_err, _, context: any) => {
|
||||
// if there is an error set the metadata query to the original value
|
||||
client.setQueryData('metadata', context.metadata);
|
||||
},
|
||||
onSettled: () => {
|
||||
// once the mutation is complete invalidate the query
|
||||
// ensure the client state is upto date with the server state
|
||||
client.invalidateQueries('metadata');
|
||||
},
|
||||
});
|
||||
|
||||
const submit = async (formData: FormOutput) => {
|
||||
if (!resourceVersion) {
|
||||
throw new Error('No resource version provided');
|
||||
}
|
||||
const { tableName, schemaName, roleName, queryType, accessType } = args;
|
||||
|
||||
const body = api.createInsertBody({
|
||||
dataSource: 'pg',
|
||||
qualifiedTable: { name: tableName, schema: schemaName },
|
||||
roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
resourceVersion,
|
||||
formData,
|
||||
});
|
||||
await mutateAsync({ headers, body });
|
||||
};
|
||||
|
||||
return { submit, ...rest };
|
||||
};
|
@ -0,0 +1,92 @@
|
||||
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 UseUpdatePermissionsComponent = ({
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
}: UseUpdatePermissionsArgs) => {
|
||||
const { data } = useDefaultValues({
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType,
|
||||
});
|
||||
|
||||
const { updatePermissions, deletePermissions } = useUpdatePermissions({
|
||||
schemaName,
|
||||
tableName,
|
||||
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: 'Permissions Form/hooks/useUpdatePermissions',
|
||||
component: UseUpdatePermissionsComponent,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
parameters: {
|
||||
msw: handlers,
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const schemaName = 'public';
|
||||
const tableName = 'users';
|
||||
const roleName = 'user';
|
||||
|
||||
export const Primary: Story<UseUpdatePermissionsArgs> = args => (
|
||||
<UseUpdatePermissionsComponent {...args} />
|
||||
);
|
||||
Primary.args = {
|
||||
schemaName,
|
||||
tableName,
|
||||
roleName,
|
||||
queryType: 'insert',
|
||||
accessType: 'fullAccess',
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
import { useSubmitForm } from './useSubmitForm';
|
||||
import { useDeletePermission } from './useDeletePermission';
|
||||
|
||||
import { AccessType, QueryType } from '../../types';
|
||||
|
||||
export interface UseUpdatePermissionsArgs {
|
||||
tableName: string;
|
||||
schemaName: string;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
accessType: AccessType;
|
||||
}
|
||||
|
||||
export const useUpdatePermissions = ({
|
||||
tableName,
|
||||
schemaName,
|
||||
roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
}: UseUpdatePermissionsArgs) => {
|
||||
const updatePermissions = useSubmitForm({
|
||||
tableName,
|
||||
schemaName,
|
||||
roleName,
|
||||
queryType,
|
||||
accessType,
|
||||
});
|
||||
|
||||
const deletePermissions = useDeletePermission({
|
||||
tableName,
|
||||
schemaName,
|
||||
roleName,
|
||||
});
|
||||
|
||||
return { updatePermissions, deletePermissions };
|
||||
};
|
1
console/src/features/PermissionsForm/index.ts
Normal file
1
console/src/features/PermissionsForm/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { PermissionsForm } from './PermissionsForm';
|
71
console/src/features/PermissionsForm/mocks/dataStubs.ts
Normal file
71
console/src/features/PermissionsForm/mocks/dataStubs.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { HasuraMetadataV3 } from '@/metadata/types';
|
||||
|
||||
export const schemaList = {
|
||||
result_type: 'TuplesOk',
|
||||
result: [['schema_name'], ['public'], ['default']],
|
||||
};
|
||||
|
||||
export const query = {
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['tables'],
|
||||
[
|
||||
'[{"table_schema":"public","table_name":"another_table","table_type":"TABLE","comment":"comment","columns":[{"comment": null, "data_type": "integer", "table_name": "another_table", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": "nextval(\'another_table_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": "column comment", "data_type": "text", "table_name": "another_table", "column_name": "name", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}],"triggers":[],"view_info":null}, {"table_schema":"public","table_name":"users","table_type":"TABLE","comment":null,"columns":[{"comment": null, "data_type": "integer", "table_name": "users", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": "nextval(\'users_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "email", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 3}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "name", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "type", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 5}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "username", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 4}],"triggers":[],"view_info":null}]',
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export const metadata = {
|
||||
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 } },
|
||||
set: { id: 'x-hasura-as' },
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
} as HasuraMetadataV3,
|
||||
};
|
27
console/src/features/PermissionsForm/mocks/handlers.mock.ts
Normal file
27
console/src/features/PermissionsForm/mocks/handlers.mock.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { rest } from 'msw';
|
||||
import { metadata, schemaList, query } from './dataStubs';
|
||||
|
||||
const url = 'http://localhost:8080';
|
||||
|
||||
export const handlers = [
|
||||
rest.post(`${url}/v2/query`, (req, res, ctx) => {
|
||||
const body = req.body as Record<string, any>;
|
||||
|
||||
const isUseSchemaList = body?.args?.sql?.includes('SELECT schema_name');
|
||||
|
||||
if (isUseSchemaList) {
|
||||
return res(ctx.json(schemaList));
|
||||
}
|
||||
|
||||
return res(ctx.json(query));
|
||||
}),
|
||||
|
||||
rest.post(`${url}/v1/metadata`, (req, res, ctx) => {
|
||||
const body = req.body as Record<string, any>;
|
||||
if (body.type === 'export_metadata') {
|
||||
return res(ctx.json(metadata));
|
||||
}
|
||||
|
||||
return res(ctx.json([{ message: 'success' }]));
|
||||
}),
|
||||
];
|
@ -1,106 +1,9 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import { ArgType, PGFunction } from '@/dataSources/services/postgresql/types';
|
||||
import { Table } from '@/dataSources/types';
|
||||
import { NormalizedTable } from '@/dataSources/types';
|
||||
|
||||
export const currentSchema: Table = {
|
||||
table_schema: 'public',
|
||||
table_name: 'users',
|
||||
table_type: 'TABLE',
|
||||
is_table_tracked: true,
|
||||
columns: [
|
||||
{
|
||||
comment: null,
|
||||
data_type: 'integer',
|
||||
table_name: 'users',
|
||||
column_name: 'id',
|
||||
is_nullable: 'NO',
|
||||
table_schema: 'public',
|
||||
column_default: null,
|
||||
data_type_name: 'int4',
|
||||
ordinal_position: 1,
|
||||
},
|
||||
{
|
||||
comment: null,
|
||||
data_type: 'text',
|
||||
table_name: 'users',
|
||||
column_name: 'name',
|
||||
is_nullable: 'YES',
|
||||
table_schema: 'public',
|
||||
column_default: null,
|
||||
data_type_name: 'text',
|
||||
ordinal_position: 2,
|
||||
},
|
||||
{
|
||||
comment: null,
|
||||
data_type: 'text',
|
||||
table_name: 'users',
|
||||
column_name: 'location',
|
||||
is_nullable: 'YES',
|
||||
table_schema: 'public',
|
||||
column_default: null,
|
||||
data_type_name: 'text',
|
||||
ordinal_position: 3,
|
||||
},
|
||||
],
|
||||
// comment: null,
|
||||
// triggers: [],
|
||||
primary_key: {
|
||||
table_schema: 'public',
|
||||
table_name: 'users',
|
||||
constraint_name: 'users_pkey',
|
||||
columns: ['id'],
|
||||
},
|
||||
relationships: [],
|
||||
permissions: [
|
||||
{
|
||||
role_name: 'one',
|
||||
permissions: {
|
||||
insert: {
|
||||
check: {},
|
||||
columns: [],
|
||||
backend_only: false,
|
||||
},
|
||||
},
|
||||
table_name: 'users',
|
||||
table_schema: 'public',
|
||||
},
|
||||
{
|
||||
role_name: 'two',
|
||||
permissions: {
|
||||
insert: {
|
||||
check: {},
|
||||
set: {
|
||||
name: 'moo',
|
||||
},
|
||||
columns: [],
|
||||
backend_only: false,
|
||||
},
|
||||
select: {
|
||||
columns: ['name'],
|
||||
filter: {
|
||||
name: {
|
||||
_eq: 'moo',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
table_name: 'users',
|
||||
table_schema: 'public',
|
||||
},
|
||||
],
|
||||
unique_constraints: [],
|
||||
check_constraints: [],
|
||||
foreign_key_constraints: [],
|
||||
opp_foreign_key_constraints: [],
|
||||
view_info: null,
|
||||
remote_relationships: [],
|
||||
is_enum: false,
|
||||
// configuration: {},
|
||||
computed_fields: [],
|
||||
};
|
||||
|
||||
export const allSchemas: Table[] = [
|
||||
export const allSchemas: NormalizedTable[] = [
|
||||
{
|
||||
table_schema: 'public',
|
||||
table_name: 'users',
|
||||
|
@ -1 +1,11 @@
|
||||
import * as z from 'zod';
|
||||
import { schema } from '../utils/formSchema';
|
||||
|
||||
export type QueryType = 'insert' | 'select' | 'update' | 'delete';
|
||||
export type AccessType =
|
||||
| 'fullAccess'
|
||||
| 'noAccess'
|
||||
| 'partialAccess'
|
||||
| 'partialAccessWarning';
|
||||
|
||||
export type FormOutput = z.infer<typeof schema>;
|
||||
|
30
console/src/features/PermissionsForm/utils/formSchema.ts
Normal file
30
console/src/features/PermissionsForm/utils/formSchema.ts
Normal file
@ -0,0 +1,30 @@
|
||||
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(),
|
||||
columns: z.record(z.optional(z.boolean())),
|
||||
presets: z.optional(
|
||||
z.array(
|
||||
z.object({
|
||||
columnName: z.string(),
|
||||
presetType: z.optional(z.string()),
|
||||
columnValue: z.optional(z.union([z.string(), z.number()])),
|
||||
})
|
||||
)
|
||||
),
|
||||
aggregationEnabled: z.boolean(),
|
||||
backendOnly: z.boolean(),
|
||||
clonePermissions: z.optional(
|
||||
z.array(
|
||||
z.object({
|
||||
tableName: z.optional(z.string()),
|
||||
queryType: z.optional(z.string()),
|
||||
roleName: z.optional(z.string()),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
14
console/src/features/PermissionsForm/utils/functions.ts
Normal file
14
console/src/features/PermissionsForm/utils/functions.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Permission } from '@/dataSources/types';
|
||||
|
||||
interface Args {
|
||||
permissions?: Permission[];
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
export const getCurrentRole = ({ permissions, roleName }: Args) => {
|
||||
const rolePermissions = permissions?.find(
|
||||
({ role_name }) => role_name === roleName
|
||||
);
|
||||
|
||||
return rolePermissions;
|
||||
};
|
2
console/src/features/PermissionsForm/utils/index.ts
Normal file
2
console/src/features/PermissionsForm/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './functions';
|
||||
export * from './formSchema';
|
@ -1,6 +1,6 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
const url = 'http://localhost:6006';
|
||||
const url = 'http://localhost:8080';
|
||||
|
||||
export const handlers = [
|
||||
rest.post(`${url}/v2/query`, (req, res, ctx) => {
|
||||
@ -18,29 +18,15 @@ export const handlers = [
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['tables'],
|
||||
[
|
||||
'[{"table_schema":"public","table_name":"users","table_type":"TABLE","comment":"hias","columns":[{"comment": null, "data_type": "integer", "table_name": "users", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "int4", "ordinal_position": 1}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "description", "is_nullable": "YES", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 3}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "name", "is_nullable": "YES", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}],"triggers":[],"view_info":null}, {"table_schema":"default","table_name":"users","table_type":"TABLE","comment":null,"columns":[{"comment": null, "data_type": "integer", "table_name": "users", "column_name": "id", "is_nullable": "NO", "table_schema": "default", "column_default": "nextval(\'\\"default\\".users_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "name", "is_nullable": "NO", "table_schema": "default", "column_default": null, "data_type_name": "text", "ordinal_position": 2}],"triggers":[],"view_info":null}]',
|
||||
],
|
||||
ctx.json({
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['tables'],
|
||||
[
|
||||
'[{"table_schema":"public","table_name":"another_table","table_type":"TABLE","comment":"comment","columns":[{"comment": null, "data_type": "integer", "table_name": "another_table", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": "nextval(\'another_table_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": "column comment", "data_type": "text", "table_name": "another_table", "column_name": "name", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}],"triggers":[],"view_info":null}, {"table_schema":"public","table_name":"users","table_type":"TABLE","comment":null,"columns":[{"comment": null, "data_type": "integer", "table_name": "users", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": "nextval(\'users_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "email", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 3}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "name", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "type", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 5}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "username", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 4}],"triggers":[],"view_info":null}]',
|
||||
],
|
||||
},
|
||||
{ result_type: 'TuplesOk', result: [['coalesce'], ['[]']] },
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['coalesce'],
|
||||
[
|
||||
'[{"table_schema":"default","table_name":"users","constraint_name":"users_pkey","columns":["id"]}, {"table_schema":"public","table_name":"users","constraint_name":"users_pkey","columns":["id"]}]',
|
||||
],
|
||||
],
|
||||
},
|
||||
{ result_type: 'TuplesOk', result: [['coalesce'], ['[]']] },
|
||||
{ result_type: 'TuplesOk', result: [['coalesce'], ['[]']] },
|
||||
])
|
||||
],
|
||||
})
|
||||
);
|
||||
}),
|
||||
|
||||
@ -49,7 +35,7 @@ export const handlers = [
|
||||
if (body.type === 'export_metadata') {
|
||||
return res(
|
||||
ctx.json({
|
||||
resource_version: 380,
|
||||
resource_version: 30,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
@ -57,14 +43,16 @@ export const handlers = [
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{ table: { schema: 'public', name: 'a_table' } },
|
||||
{
|
||||
table: { schema: 'public', name: 'users' },
|
||||
insert_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
check: {},
|
||||
columns: ['id', 'name'],
|
||||
check: { id: { _eq: 1 } },
|
||||
set: { id: 'x-hasura-as' },
|
||||
columns: ['email', 'type'],
|
||||
backend_only: false,
|
||||
},
|
||||
},
|
||||
@ -73,21 +61,29 @@ export const handlers = [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['id', 'description', 'name'],
|
||||
filter: {},
|
||||
limit: 0,
|
||||
columns: ['email', 'id', 'type'],
|
||||
filter: { id: { _eq: 1 } },
|
||||
limit: 5,
|
||||
allow_aggregations: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
functions: [{ function: { schema: 'public', name: 'me' } }],
|
||||
functions: [
|
||||
{ function: { schema: 'public', name: 'search_user2' } },
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
use_prepared_statements: false,
|
||||
database_url:
|
||||
'postgres://postgres:postgrespassword@postgres:5432/postgres',
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user