console: add permissions for GDC to console

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6592
Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com>
GitOrigin-RevId: 67a72f9b76604c50f647113d170e6e1778c9f7b9
This commit is contained in:
Matt Hardman 2022-11-02 14:53:55 -05:00 committed by hasura-bot
parent 5278e4ed9f
commit 89f639b40b
55 changed files with 1004 additions and 799 deletions

View File

@ -1,6 +1,7 @@
import { BrowseRowsContainer } from '@/features/BrowseRows'; import { BrowseRowsContainer } from '@/features/BrowseRows';
import { DatabaseRelationshipsContainer } from '@/features/DataRelationships'; import { DatabaseRelationshipsContainer } from '@/features/DataRelationships';
import { getTableName } from '@/features/DataSource'; import { getTableName } from '@/features/DataSource';
import { PermissionsTab } from '@/features/PermissionsTab';
import { Table } from '@/features/MetadataAPI'; import { Table } from '@/features/MetadataAPI';
import { IndicatorCard } from '@/new-components/IndicatorCard'; import { IndicatorCard } from '@/new-components/IndicatorCard';
import { Tabs } from '@/new-components/Tabs'; import { Tabs } from '@/new-components/Tabs';
@ -19,16 +20,6 @@ export interface ManageTableProps {
}; };
} }
const FeatureNotImplemented = () => {
return (
<div className="my-4">
<IndicatorCard headline="Feature is currently unavailable">
Feature not implemented
</IndicatorCard>
</div>
);
};
const availableTabs = ( const availableTabs = (
dataSourceName: string, dataSourceName: string,
table: Table, table: Table,
@ -65,7 +56,7 @@ const availableTabs = (
{ {
value: 'permissions', value: 'permissions',
label: 'Permissions', label: 'Permissions',
content: <FeatureNotImplemented />, content: <PermissionsTab dataSourceName={dataSourceName} table={table} />,
}, },
]; ];

View File

@ -17,7 +17,6 @@ export const Primary: Story<BulkDeleteProps> = args => {
return <BulkDelete {...args} />; return <BulkDelete {...args} />;
}; };
Primary.args = { Primary.args = {
currentSource: 'postgres',
dataSourceName: 'default', dataSourceName: 'default',
roles: ['user'], roles: ['user'],
handleClose: () => {}, handleClose: () => {},

View File

@ -4,7 +4,6 @@ import { Button } from '@/new-components/Button';
import { useBulkDeletePermissions } from './hooks'; import { useBulkDeletePermissions } from './hooks';
export interface BulkDeleteProps { export interface BulkDeleteProps {
currentSource: string;
dataSourceName: string; dataSourceName: string;
roles: string[]; roles: string[];
table: unknown; table: unknown;
@ -12,14 +11,12 @@ export interface BulkDeleteProps {
} }
export const BulkDelete: React.FC<BulkDeleteProps> = ({ export const BulkDelete: React.FC<BulkDeleteProps> = ({
currentSource,
dataSourceName, dataSourceName,
roles, roles,
table, table,
handleClose, handleClose,
}) => { }) => {
const { submit, isLoading, isError } = useBulkDeletePermissions({ const { submit, isLoading, isError } = useBulkDeletePermissions({
currentSource,
dataSourceName, dataSourceName,
table, table,
}); });

View File

@ -6,7 +6,7 @@ import { PermissionsForm, PermissionsFormProps } from './PermissionsForm';
import { handlers } from './mocks/handlers.mock'; import { handlers } from './mocks/handlers.mock';
export default { export default {
title: 'Features/Permissions Form/Form', title: 'Features/Permissions Tab/Permissions Form/Form',
component: PermissionsForm, component: PermissionsForm,
decorators: [ReactQueryDecorator()], decorators: [ReactQueryDecorator()],
parameters: { parameters: {
@ -20,7 +20,6 @@ export const Insert: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} /> <PermissionsForm {...args} />
); );
Insert.args = { Insert.args = {
currentSource: 'postgres',
dataSourceName: 'default', dataSourceName: 'default',
queryType: 'insert', queryType: 'insert',
@ -39,20 +38,17 @@ Select.args = {
...Insert.args, ...Insert.args,
queryType: 'select', queryType: 'select',
}; };
Select.parameters = Insert.parameters;
export const GDCSelect: Story<PermissionsFormProps> = args => ( export const GDCSelect: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} /> <PermissionsForm {...args} />
); );
GDCSelect.args = { GDCSelect.args = {
currentSource: 'sqlite',
dataSourceName: 'sqlite', dataSourceName: 'sqlite',
queryType: 'select', queryType: 'select',
table: ['Artist'], table: ['Artist'],
roleName, roleName,
handleClose: () => {}, handleClose: () => {},
}; };
GDCSelect.parameters = Insert.parameters;
export const Update: Story<PermissionsFormProps> = args => ( export const Update: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} /> <PermissionsForm {...args} />
@ -61,7 +57,6 @@ Update.args = {
...Insert.args, ...Insert.args,
queryType: 'update', queryType: 'update',
}; };
Update.parameters = Insert.parameters;
export const Delete: Story<PermissionsFormProps> = args => ( export const Delete: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} /> <PermissionsForm {...args} />
@ -70,4 +65,3 @@ Delete.args = {
...Insert.args, ...Insert.args,
queryType: 'delete', queryType: 'delete',
}; };
Delete.parameters = Insert.parameters;

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { Form } from '@/new-components/Form'; import { UpdatedForm as Form } from '@/new-components/Form';
import { Button } from '@/new-components/Button'; import { Button } from '@/new-components/Button';
import { IndicatorCard } from '@/new-components/IndicatorCard';
import { schema } from './utils/formSchema'; import { PermissionsSchema, schema } from './utils/formSchema';
import { AccessType, FormOutput, QueryType } from './types'; import { AccessType, QueryType } from './types';
import { import {
AggregationSection, AggregationSection,
BackendOnlySection, BackendOnlySection,
@ -15,10 +16,9 @@ import {
RowPermissionsSectionWrapper, RowPermissionsSectionWrapper,
} from './components'; } from './components';
import { useFormData, useDefaultValues, useUpdatePermissions } from './hooks'; import { useFormData, useUpdatePermissions } from './hooks';
export interface PermissionsFormProps { export interface PermissionsFormProps {
currentSource: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
queryType: QueryType; queryType: QueryType;
@ -29,7 +29,6 @@ export interface PermissionsFormProps {
export const PermissionsForm = (props: PermissionsFormProps) => { export const PermissionsForm = (props: PermissionsFormProps) => {
const { const {
currentSource,
dataSourceName, dataSourceName,
table, table,
queryType, queryType,
@ -38,34 +37,15 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
handleClose, handleClose,
} = props; } = props;
// loads all information about selected table const { data, isError, isLoading } = useFormData({
// e.g. column names, supported queries etc.
const {
data,
isLoading: loadingFormData,
isError: formDataError,
} = useFormData({
dataSourceName, dataSourceName,
table, table,
queryType, queryType,
roleName, roleName,
}); });
// loads any existing permissions from the metadata
const {
data: defaultValues,
isLoading: defaultValuesLoading,
isError: defaultValuesError,
} = useDefaultValues({
dataSourceName,
table,
roleName,
queryType,
});
// functions fired when the form is submitted // functions fired when the form is submitted
const { updatePermissions, deletePermissions } = useUpdatePermissions({ const { updatePermissions, deletePermissions } = useUpdatePermissions({
currentSource,
dataSourceName, dataSourceName,
table, table,
queryType, queryType,
@ -73,8 +53,8 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
accessType, accessType,
}); });
const handleSubmit = async (formData: Record<string, unknown>) => { const handleSubmit = async (formData: PermissionsSchema) => {
await updatePermissions.submit(formData as FormOutput); await updatePermissions.submit(formData);
handleClose(); handleClose();
}; };
@ -83,43 +63,49 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
handleClose(); handleClose();
}; };
const isError = formDataError || defaultValuesError;
const isSubmittingError = const isSubmittingError =
updatePermissions.isError || deletePermissions.isError; updatePermissions.isError || deletePermissions.isError;
const isLoading = loadingFormData || defaultValuesLoading;
// allRowChecks relates to other queries and is for duplicating from others
const allRowChecks = defaultValues?.allRowChecks;
// for update it is possible to set pre update and post update row checks // for update it is possible to set pre update and post update row checks
const rowPermissions = queryType === 'update' ? ['pre', 'post'] : [queryType]; const rowPermissions = queryType === 'update' ? ['pre', 'post'] : [queryType];
if (isSubmittingError) { if (isSubmittingError) {
return <div>Error submitting form</div>; return (
<IndicatorCard status="negative">Error submitting form</IndicatorCard>
);
} }
// these will be replaced by components once spec is decided // these will be replaced by components once spec is decided
if (isError) { if (isError) {
return <div>Error loading form data</div>; return (
<IndicatorCard status="negative">Error fetching form data</IndicatorCard>
);
} }
// these will be replaced by components once spec is decided // these will be replaced by components once spec is decided
if (isLoading) { if (isLoading || !data) {
return <div>Loading...</div>; return <IndicatorCard status="info">Loading...</IndicatorCard>;
} }
const { formData, defaultValues } = data;
// allRowChecks relates to other queries and is for duplicating from others
const allRowChecks = defaultValues?.allRowChecks;
const key = `${JSON.stringify(table)}-${queryType}-${roleName}`;
return ( return (
<Form <Form
key={`${queryType}-${roleName}-${accessType}`} key={key}
onSubmit={handleSubmit} onSubmit={handleSubmit}
schema={schema} schema={schema}
options={{ defaultValues }} options={{ defaultValues }}
> >
{options => { {options => {
console.log('form values---->', options.getValues()); const filterType = options.getValues('filterType');
console.log('form errors---->', options.formState.errors); if (Object.keys(options.formState.errors)?.length) {
console.error('form errors:', options.formState.errors);
}
return ( return (
<div className="bg-white rounded p-md border border-gray-300"> <div className="bg-white rounded p-md border border-gray-300">
<div className="pb-4 flex items-center gap-4"> <div className="pb-4 flex items-center gap-4">
@ -158,6 +144,7 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
queryType === 'update' ? permissionName : undefined queryType === 'update' ? permissionName : undefined
} }
allRowChecks={allRowChecks || []} allRowChecks={allRowChecks || []}
dataSourceName={dataSourceName}
/> />
</React.Fragment> </React.Fragment>
))} ))}
@ -167,14 +154,14 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
<ColumnPermissionsSection <ColumnPermissionsSection
roleName={roleName} roleName={roleName}
queryType={queryType} queryType={queryType}
columns={data?.columns} columns={formData?.columns}
/> />
)} )}
{['insert', 'update'].includes(queryType) && ( {['insert', 'update'].includes(queryType) && (
<ColumnPresetsSection <ColumnPresetsSection
queryType={queryType} queryType={queryType}
columns={data?.columns} columns={formData?.columns}
/> />
)} )}
@ -201,6 +188,12 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
<Button <Button
type="submit" type="submit"
mode="primary" mode="primary"
title={
filterType === 'none'
? 'You must select an option for row permissions'
: 'Submit'
}
disabled={filterType === 'none'}
isLoading={updatePermissions.isLoading} isLoading={updatePermissions.isLoading}
> >
Save Permissions Save Permissions

View File

@ -1,10 +1,10 @@
import { allowedMetadataTypes } from '@/features/MetadataAPI'; import { allowedMetadataTypes } from '@/features/MetadataAPI';
import { AccessType, FormOutput, QueryType } from '../types'; import { AccessType, QueryType } from '../types';
import { PermissionsSchema } from '../utils';
import { createInsertArgs } from './utils'; import { createInsertArgs } from './utils';
interface CreateBodyArgs { interface CreateBodyArgs {
currentSource: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
roleName: string; roleName: string;
@ -13,10 +13,11 @@ interface CreateBodyArgs {
interface CreateDeleteBodyArgs extends CreateBodyArgs { interface CreateDeleteBodyArgs extends CreateBodyArgs {
queries: QueryType[]; queries: QueryType[];
driver: string;
} }
const createDeleteBody = ({ const createDeleteBody = ({
currentSource, driver,
dataSourceName, dataSourceName,
table, table,
roleName, roleName,
@ -28,12 +29,8 @@ const createDeleteBody = ({
resource_version: number; resource_version: number;
args: BulkArgs[]; args: BulkArgs[];
} => { } => {
// if (!['postgres', 'mssql'].includes(currentSource)) {
// throw new Error(`${currentSource} not supported`);
// }
const args = queries.map(queryType => ({ const args = queries.map(queryType => ({
type: `${currentSource}_drop_${queryType}_permission` as allowedMetadataTypes, type: `${driver}_drop_${queryType}_permission` as allowedMetadataTypes,
args: { args: {
table, table,
role: roleName, role: roleName,
@ -52,11 +49,11 @@ const createDeleteBody = ({
}; };
interface CreateBulkDeleteBodyArgs { interface CreateBulkDeleteBodyArgs {
source: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
resourceVersion: number; resourceVersion: number;
roleList?: Array<{ roleName: string; queries: string[] }>; roleList?: Array<{ roleName: string; queries: string[] }>;
driver: string;
} }
interface BulkArgs { interface BulkArgs {
@ -65,8 +62,8 @@ interface BulkArgs {
} }
const createBulkDeleteBody = ({ const createBulkDeleteBody = ({
source,
dataSourceName, dataSourceName,
driver,
table, table,
resourceVersion, resourceVersion,
roleList, roleList,
@ -76,15 +73,11 @@ const createBulkDeleteBody = ({
resource_version: number; resource_version: number;
args: BulkArgs[]; args: BulkArgs[];
} => { } => {
// if (!['postgres', 'mssql'].includes(source)) {
// throw new Error(`${dataSourceName} not supported`);
// }
const args = const args =
roleList?.reduce<BulkArgs[]>((acc, role) => { roleList?.reduce<BulkArgs[]>((acc, role) => {
role.queries.forEach(queryType => { role.queries.forEach(queryType => {
acc.push({ acc.push({
type: `${source}_drop_${queryType}_permission` as allowedMetadataTypes, type: `${driver}_drop_${queryType}_permission` as allowedMetadataTypes,
args: { args: {
table, table,
role: role.roleName, role: role.roleName,
@ -108,9 +101,10 @@ const createBulkDeleteBody = ({
interface CreateInsertBodyArgs extends CreateBodyArgs { interface CreateInsertBodyArgs extends CreateBodyArgs {
queryType: QueryType; queryType: QueryType;
formData: FormOutput; formData: PermissionsSchema;
accessType: AccessType; accessType: AccessType;
existingPermissions: any; existingPermissions: any;
driver: string;
} }
export interface InsertBodyResult { export interface InsertBodyResult {
@ -120,7 +114,6 @@ export interface InsertBodyResult {
} }
const createInsertBody = ({ const createInsertBody = ({
currentSource,
dataSourceName, dataSourceName,
table, table,
queryType, queryType,
@ -129,13 +122,10 @@ const createInsertBody = ({
accessType, accessType,
resourceVersion, resourceVersion,
existingPermissions, existingPermissions,
driver,
}: CreateInsertBodyArgs): InsertBodyResult => { }: CreateInsertBodyArgs): InsertBodyResult => {
// if (!['postgres', 'mssql'].includes(currentSource)) {
// throw new Error(`${currentSource} not supported`);
// }
const args = createInsertArgs({ const args = createInsertArgs({
currentSource, driver,
dataSourceName, dataSourceName,
table, table,
queryType, queryType,

View File

@ -1,7 +1,7 @@
import { CreateInsertArgs, createInsertArgs } from './utils'; import { CreateInsertArgs, createInsertArgs } from './utils';
const insertArgs: CreateInsertArgs = { const insertArgs: CreateInsertArgs = {
currentSource: 'postgres', driver: 'postgres',
dataSourceName: 'default', dataSourceName: 'default',
accessType: 'fullAccess', accessType: 'fullAccess',
table: 'users', table: 'users',
@ -88,7 +88,7 @@ test('create insert args object from form data', () => {
}); });
const insertArgsWithClonePermissions: CreateInsertArgs = { const insertArgsWithClonePermissions: CreateInsertArgs = {
currentSource: 'postgres', driver: 'postgres',
dataSourceName: 'default', dataSourceName: 'default',
accessType: 'fullAccess', accessType: 'fullAccess',
table: 'users', table: 'users',

View File

@ -2,23 +2,24 @@ import produce from 'immer';
import { allowedMetadataTypes } from '@/features/MetadataAPI'; import { allowedMetadataTypes } from '@/features/MetadataAPI';
import { AccessType, FormOutput } from '../types'; import { AccessType } from '../types';
import { PermissionsSchema } from '../utils';
interface PermissionArgs { interface PermissionArgs {
columns: string[]; columns: string[];
presets?: Record<string, string | number>; presets?: Record<string, string | number>;
computed_fields: string[]; computed_fields: string[];
backend_only: boolean; backend_only?: boolean;
allow_aggregations: boolean; allow_aggregations?: boolean;
check: Record<string, unknown>; check: Record<string, any>;
filter: Record<string, unknown>; filter: Record<string, any>;
limit?: number; limit?: number;
} }
/** /**
* creates the permissions object for the server * creates the permissions object for the server
*/ */
const createPermission = (formData: FormOutput) => { const createPermission = (formData: PermissionsSchema) => {
// presets need reformatting for server // presets need reformatting for server
const presets = formData.presets?.reduce((acc, preset) => { const presets = formData.presets?.reduce((acc, preset) => {
if (preset.columnValue) { if (preset.columnValue) {
@ -33,6 +34,21 @@ const createPermission = (formData: FormOutput) => {
.filter(({ 1: value }) => value) .filter(({ 1: value }) => value)
.map(([key]) => key); .map(([key]) => key);
// in row permissions builder an extra input is rendered automatically
// this will always be empty and needs to be removed
const filter = Object.entries(formData.filter).reduce<Record<string, any>>(
(acc, [operator, value]) => {
if (operator === '_and' || operator === '_or') {
const newValue = (value as any[])?.slice(0, -1);
acc[operator] = newValue;
return acc;
}
acc[operator] = value;
return acc;
},
{}
);
// return permissions object for args // return permissions object for args
const permissionObject: PermissionArgs = { const permissionObject: PermissionArgs = {
columns, columns,
@ -41,7 +57,7 @@ const createPermission = (formData: FormOutput) => {
backend_only: formData.backendOnly, backend_only: formData.backendOnly,
allow_aggregations: formData.aggregationEnabled, allow_aggregations: formData.aggregationEnabled,
check: formData.check, check: formData.check,
filter: formData.filter, filter,
}; };
if (formData.rowCount && formData.rowCount !== '0') { if (formData.rowCount && formData.rowCount !== '0') {
@ -52,14 +68,14 @@ const createPermission = (formData: FormOutput) => {
}; };
export interface CreateInsertArgs { export interface CreateInsertArgs {
currentSource: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
queryType: string; queryType: string;
role: string; role: string;
accessType: AccessType; accessType: AccessType;
formData: FormOutput; formData: PermissionsSchema;
existingPermissions: ExistingPermission[]; existingPermissions: ExistingPermission[];
driver: string;
} }
interface ExistingPermission { interface ExistingPermission {
@ -73,20 +89,20 @@ interface ExistingPermission {
* and creates drop arguments where permissions already exist * and creates drop arguments where permissions already exist
*/ */
export const createInsertArgs = ({ export const createInsertArgs = ({
currentSource,
dataSourceName, dataSourceName,
table, table,
queryType, queryType,
role, role,
formData, formData,
existingPermissions, existingPermissions,
driver,
}: CreateInsertArgs) => { }: CreateInsertArgs) => {
const permission = createPermission(formData); const permission = createPermission(formData);
// create args object with args from form // create args object with args from form
const initialArgs = [ const initialArgs = [
{ {
type: `${currentSource}_create_${queryType}_permission` as allowedMetadataTypes, type: `${driver}_create_${queryType}_permission` as allowedMetadataTypes,
args: { args: {
table, table,
role, role,
@ -108,7 +124,7 @@ export const createInsertArgs = ({
// if the permission already exists it needs to be dropped // if the permission already exists it needs to be dropped
if (permissionExists) { if (permissionExists) {
draft.unshift({ draft.unshift({
type: `${currentSource}_drop_${queryType}_permission` as allowedMetadataTypes, type: `${driver}_drop_${queryType}_permission` as allowedMetadataTypes,
args: { args: {
table, table,
role, role,
@ -137,7 +153,7 @@ export const createInsertArgs = ({
); );
// add each closed permission to args // add each closed permission to args
draft.push({ draft.push({
type: `${currentSource}_create_${clonedPermission.queryType}_permission` as allowedMetadataTypes, type: `${driver}_create_${clonedPermission.queryType}_permission` as allowedMetadataTypes,
args: { args: {
table: clonedPermission.tableName || '', table: clonedPermission.tableName || '',
role: clonedPermission.roleName || '', role: clonedPermission.roleName || '',
@ -158,7 +174,7 @@ export const createInsertArgs = ({
// if it already exists drop it // if it already exists drop it
if (clonedPermissionExists) { if (clonedPermissionExists) {
draft.unshift({ draft.unshift({
type: `${currentSource}_drop_${clonedPermission.queryType}_permission` as allowedMetadataTypes, type: `${driver}_drop_${clonedPermission.queryType}_permission` as allowedMetadataTypes,
args: { args: {
table: clonedPermission.tableName, table: clonedPermission.tableName,
role: clonedPermission.roleName, role: clonedPermission.roleName,

View File

@ -6,7 +6,8 @@ import { Form } from '@/new-components/Form';
import { AggregationSection, AggregationProps } from './Aggregation'; import { AggregationSection, AggregationProps } from './Aggregation';
export default { export default {
title: 'Features/Permissions Form/Components/Aggregation Section', title:
'Features/Permissions Tab/Permissions Form/Components/Aggregation Section',
component: AggregationSection, component: AggregationSection,
parameters: { parameters: {
// Disable storybook for playground stories // Disable storybook for playground stories

View File

@ -46,11 +46,12 @@ export const AggregationSection: React.FC<AggregationProps> = ({
type="checkbox" type="checkbox"
title={disabled ? 'Set row permissions first' : ''} title={disabled ? 'Set row permissions first' : ''}
disabled={disabled} disabled={disabled}
className="m-0 mt-0 rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400"
{...register('aggregationEnabled')} {...register('aggregationEnabled')}
/> />
<p> <span>
Allow role <strong>{roleName}</strong> to make aggregation queries Allow role <strong>{roleName}</strong> to make aggregation queries
</p> </span>
</label> </label>
</div> </div>
</Collapse.Content> </Collapse.Content>

View File

@ -6,7 +6,8 @@ import { Form } from '@/new-components/Form';
import { BackendOnlySection, BackEndOnlySectionProps } from './BackendOnly'; import { BackendOnlySection, BackEndOnlySectionProps } from './BackendOnly';
export default { export default {
title: 'Features/Permissions Form/Components/Backend Only Section', title:
'Features/Permissions Tab/Permissions Form/Components/Backend Only Section',
component: BackendOnlySection, component: BackendOnlySection,
parameters: { parameters: {
// Disable storybook for playground stories // Disable storybook for playground stories

View File

@ -10,7 +10,8 @@ import {
} from './ClonePermissions'; } from './ClonePermissions';
export default { export default {
title: 'Features/Permissions Form/Components/Clone Permissions', title:
'Features/Permissions Tab/Permissions Form/Components/Clone Permissions',
component: ClonePermissionsSection, component: ClonePermissionsSection,
decorators: [ decorators: [
(StoryComponent: React.FC) => ( (StoryComponent: React.FC) => (

View File

@ -11,7 +11,7 @@ import {
const schema = z.object({ columns: z.record(z.optional(z.boolean())) }); const schema = z.object({ columns: z.record(z.optional(z.boolean())) });
export default { export default {
title: 'Features/Permissions Form/Components/Column Section', title: 'Features/Permissions Tab/Permissions Form/Components/Column Section',
component: ColumnPermissionsSection, component: ColumnPermissionsSection,
decorators: [ decorators: [
(StoryComponent: React.FC) => ( (StoryComponent: React.FC) => (

View File

@ -99,14 +99,15 @@ export const ColumnPermissionsSection: React.FC<ColumnPermissionsSectionProps> =
</p> </p>
</div> </div>
<fieldset className="flex gap-4"> <fieldset className="flex gap-4 flex-wrap">
{columns?.map(fieldName => ( {columns?.map(fieldName => (
<label key={fieldName} className="flex gap-2 items-center"> <label key={fieldName} className="flex gap-2 items-center">
<input <input
type="checkbox" type="checkbox"
title={disabled ? 'Set a row permission first' : ''} title={disabled ? 'Set a row permission first' : ''}
disabled={disabled} disabled={disabled}
className="mt-0 rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400" style={{ marginTop: '0px !important' }}
className="rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400"
{...register(`columns.${fieldName}`)} {...register(`columns.${fieldName}`)}
/> />
<i>{fieldName}</i> <i>{fieldName}</i>

View File

@ -9,7 +9,7 @@ import {
} from './ColumnPresets'; } from './ColumnPresets';
export default { export default {
title: 'Features/Permissions Form/Components/Presets Section', title: 'Features/Permissions Tab/Permissions Form/Components/Presets Section',
component: ColumnPresetsSection, component: ColumnPresetsSection,
decorators: [ decorators: [
(StoryComponent: React.FC) => ( (StoryComponent: React.FC) => (

View File

@ -1,14 +0,0 @@
import React from 'react';
import { Story, Meta } from '@storybook/react';
import { JSONEditor, JSONEditorProps } from './JSONEditor';
export default {
title: 'Features/Permissions Form/Components/JSON Editor',
component: JSONEditor,
} as Meta;
export const Default: Story<JSONEditorProps> = args => <JSONEditor {...args} />;
Default.args = {
initData: '{"id":{"_eq":1}}',
};

View File

@ -1,73 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import AceEditor, { IAnnotation } from 'react-ace';
import { isJsonString } from '@/components/Common/utils/jsUtils';
import { usePrevious } from '@/hooks/usePrevious';
export interface JSONEditorProps {
initData: string;
onChange: (v: string) => void;
data: string;
minLines?: number;
}
export const JSONEditor: React.FC<JSONEditorProps> = ({
initData,
onChange,
data,
minLines,
}) => {
const [value, setValue] = useState(initData || data || '');
const [annotations, setAnnotations] = useState<IAnnotation[]>([]);
const prevData = usePrevious<string>(data);
useEffect(() => {
// if the data prop is changed do nothing
if (prevData !== data) return;
// when state gets new data, trigger parent callback
if (value !== data) onChange(value);
}, [value, data, prevData]);
// check and set error message
useEffect(() => {
if (isJsonString(value)) {
setAnnotations([]);
} else {
setAnnotations([
{ row: 0, column: 0, text: 'Invalid JSON', type: 'error' },
]);
}
return () => {
setAnnotations([]);
};
}, [value]);
useEffect(() => {
// set data to editor only if the prop has a valid json string
// setting value from query editor will always have a valid json
// any invalid json means, the value is set from this component so no need to set that again
if (isJsonString(data)) setValue(data);
}, [data]);
const onEditorValueChange = useCallback(
newVal => setValue(newVal),
[setValue]
);
return (
<AceEditor
mode="json"
onChange={onEditorValueChange}
theme="github"
height="5em"
minLines={minLines || 1}
maxLines={15}
width="100%"
showPrintMargin={false}
value={value}
annotations={annotations}
/>
);
};
export default JSONEditor;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Story, Meta } from '@storybook/react'; import { Story, Meta } from '@storybook/react';
import { Form } from '@/new-components/Form'; import { Form } from '@/new-components/Form';
import { z } from 'zod'; import { z } from 'zod';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { import {
RowPermissionsSection, RowPermissionsSection,
RowPermissionsProps, RowPermissionsProps,
@ -10,11 +10,10 @@ import {
RowPermissionsWrapperProps, RowPermissionsWrapperProps,
} from './RowPermissions'; } from './RowPermissions';
// import { allSchemas, allFunctions } from '../mocks/mockData';
import { QueryType } from '../types'; import { QueryType } from '../types';
export default { export default {
title: 'Features/Permissions Form/Components/Row Section', title: 'Features/Permissions Tab/Permissions Form/Components/Row Section',
component: RowPermissionsSection, component: RowPermissionsSection,
decorators: [ decorators: [
(StoryComponent: React.FC) => ( (StoryComponent: React.FC) => (
@ -22,6 +21,7 @@ export default {
{() => <StoryComponent />} {() => <StoryComponent />}
</Form> </Form>
), ),
ReactQueryDecorator(),
], ],
parameters: { chromatic: { disableSnapshot: true } }, parameters: { chromatic: { disableSnapshot: true } },
} as Meta; } as Meta;
@ -57,6 +57,7 @@ Insert.args = {
schema: 'public', schema: 'public',
name: 'user', name: 'user',
}, },
dataSourceName: 'chinook',
queryType: 'delete', queryType: 'delete',
allRowChecks, allRowChecks,
// allSchemas, // allSchemas,

View File

@ -1,15 +1,17 @@
import React from 'react'; import React from 'react';
import AceEditor from 'react-ace';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Table } from '@/features/MetadataAPI';
import 'brace/mode/json'; import { useHttpClient } from '@/features/Network';
import 'brace/theme/github'; import { useQuery } from 'react-query';
import { exportMetadata } from '@/features/DataSource';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { getTypeName } from '@/features/GraphQLUtils';
import { InputField } from '@/new-components/Form'; import { InputField } from '@/new-components/Form';
import { IconTooltip } from '@/new-components/Tooltip'; import { IconTooltip } from '@/new-components/Tooltip';
import { Collapse } from '@/new-components/deprecated'; import { Collapse } from '@/new-components/deprecated';
import { getIngForm } from '../../../components/Services/Data/utils'; import { getIngForm } from '../../../components/Services/Data/utils';
import JSONEditor from './JSONEditor';
import { RowPermissionBuilder } from './RowPermissionsBuilder'; import { RowPermissionBuilder } from './RowPermissionsBuilder';
import { QueryType } from '../types'; import { QueryType } from '../types';
@ -37,6 +39,7 @@ export interface RowPermissionsProps {
queryType: QueryType; queryType: QueryType;
subQueryType?: string; subQueryType?: string;
allRowChecks: Array<{ queryType: QueryType; value: string }>; allRowChecks: Array<{ queryType: QueryType; value: string }>;
dataSourceName: string;
} }
enum SelectedSection { enum SelectedSection {
@ -80,26 +83,40 @@ const getRowPermissionCheckType = (
return 'filterType'; return 'filterType';
}; };
const isGDCTable = (table: unknown): table is string[] => { const useTypeName = ({
return Array.isArray(table); table,
}; dataSourceName,
}: {
table: Table;
dataSourceName: string;
}) => {
const httpClient = useHttpClient();
const hasTableName = (table: unknown): table is { name: string } => { return useQuery({
return typeof table === 'object' && 'name' in (table || {}); queryKey: ['gql_introspection', 'type_name', table, dataSourceName],
}; queryFn: async () => {
const { metadata } = await exportMetadata({ httpClient });
const metadataSource = metadata.sources.find(
s => s.name === dataSourceName
);
const metadataTable = metadataSource?.tables.find(t =>
areTablesEqual(t.table, table)
);
const getTableName = (table: unknown) => { if (!metadataSource || !metadataTable)
const gdcTable = isGDCTable(table); throw Error('unable to generate type name');
if (gdcTable) {
return table[table.length - 1];
}
const tableName = hasTableName(table); // This is very GDC specific. We have to move this to DAL later
if (tableName) { const typeName = getTypeName({
return table.name; defaultQueryRoot: (table as string[]).join('_'),
} operation: 'select',
sourceCustomization: metadataSource?.customization,
configuration: metadataTable.configuration,
});
throw new Error('cannot read table'); return typeName;
},
});
}; };
export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
@ -107,8 +124,9 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
queryType, queryType,
subQueryType, subQueryType,
allRowChecks, allRowChecks,
dataSourceName,
}) => { }) => {
const tableName = getTableName(table); const { data: tableName, isLoading } = useTypeName({ table, dataSourceName });
const { register, watch, setValue } = useFormContext(); const { register, watch, setValue } = useFormContext();
// determines whether the inputs should be pointed at `check` or `filter` // determines whether the inputs should be pointed at `check` or `filter`
const rowPermissions = getRowPermission(queryType, subQueryType); const rowPermissions = getRowPermission(queryType, subQueryType);
@ -143,13 +161,20 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
</label> </label>
{selectedSection === SelectedSection.NoChecks && ( {selectedSection === SelectedSection.NoChecks && (
<div className="pt-4"> <div className="mt-4 p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full">
<JSONEditor <AceEditor
data="{}" mode="json"
minLines={1}
fontSize={14}
height="18px"
width="100%"
theme="github"
name={`${tableName}-json-editor`}
value="{}"
onChange={() => onChange={() =>
setValue(rowPermissionsCheckType, SelectedSection.Custom) setValue(rowPermissionsCheckType, SelectedSection.Custom)
} }
initData="{}" editorProps={{ $blockScrolling: true }}
/> />
</div> </div>
)} )}
@ -175,14 +200,20 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
</label> </label>
{selectedSection === query && ( {selectedSection === query && (
<div className="pt-4"> <div className="mt-4 p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full">
<JSONEditor <AceEditor
data={value} mode="json"
onChange={output => { minLines={1}
setValue(rowPermissionsCheckType, SelectedSection.Custom); fontSize={14}
setValue(rowPermissions, output); height="18px"
}} width="100%"
initData="" theme="github"
name={`${tableName}-json-editor`}
value="{}"
onChange={() =>
setValue(rowPermissionsCheckType, SelectedSection.Custom)
}
editorProps={{ $blockScrolling: true }}
/> />
</div> </div>
)} )}
@ -203,7 +234,16 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
{selectedSection === SelectedSection.Custom && ( {selectedSection === SelectedSection.Custom && (
<div className="pt-4"> <div className="pt-4">
<RowPermissionBuilder tableName={tableName} nesting={['filter']} /> {!isLoading && tableName ? (
<RowPermissionBuilder
tableName={tableName}
nesting={['filter']}
table={table}
dataSourceName={dataSourceName}
/>
) : (
<>Loading...</>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { z } from 'zod'; import { z } from 'zod';
import { ComponentStory, Meta } from '@storybook/react'; import { ComponentStory, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { UpdatedForm } from '@/new-components/Form'; import { UpdatedForm } from '@/new-components/Form';
import { RowPermissionBuilder } from './RowPermissionBuilder'; import { RowPermissionBuilder } from './RowPermissionBuilder';
@ -15,8 +16,10 @@ import {
} from './mocks'; } from './mocks';
export default { export default {
title: 'Features/Permissions Form/Components/New Builder', title:
'Features/Permissions Tab/Permissions Form/Components/Row Permissions Builder',
component: RowPermissionBuilder, component: RowPermissionBuilder,
decorators: [ReactQueryDecorator()],
parameters: { parameters: {
msw: handlers(), msw: handlers(),
}, },
@ -60,6 +63,7 @@ WithDefaults.decorators = [
tableName: 'Album', tableName: 'Album',
schema, schema,
existingPermission: simpleExample, existingPermission: simpleExample,
tableConfig: {},
}), }),
}} }}
onSubmit={console.log} onSubmit={console.log}
@ -92,6 +96,7 @@ WithDefaultsBool.decorators = [
tableName: 'user', tableName: 'user',
schema, schema,
existingPermission: exampleWithBoolOperator, existingPermission: exampleWithBoolOperator,
tableConfig: {},
}), }),
}} }}
onSubmit={console.log} onSubmit={console.log}
@ -124,6 +129,7 @@ WithDefaultsRelationship.decorators = [
tableName: 'user', tableName: 'user',
schema, schema,
existingPermission: exampleWithRelationship, existingPermission: exampleWithRelationship,
tableConfig: {},
}), }),
}} }}
onSubmit={console.log} onSubmit={console.log}
@ -157,6 +163,7 @@ WithPointlesslyComplicatedRelationship.decorators = [
tableName: 'user', tableName: 'user',
schema, schema,
existingPermission: complicatedExample, existingPermission: complicatedExample,
tableConfig: {},
}), }),
}} }}
onSubmit={console.log} onSubmit={console.log}

View File

@ -1,3 +1,4 @@
import { Table } from '@/features/MetadataAPI';
import React from 'react'; import React from 'react';
import AceEditor from 'react-ace'; import AceEditor from 'react-ace';
@ -16,9 +17,16 @@ interface Props {
* e.g. ['filter', 'Title', '_eq'] would be registered as 'filter.Title._eq' * e.g. ['filter', 'Title', '_eq'] would be registered as 'filter.Title._eq'
*/ */
nesting: string[]; nesting: string[];
table: Table;
dataSourceName: string;
} }
export const RowPermissionBuilder = ({ tableName, nesting }: Props) => { export const RowPermissionBuilder = ({
tableName,
nesting,
table,
dataSourceName,
}: Props) => {
const { watch } = useFormContext(); const { watch } = useFormContext();
const { data: schema } = useIntrospectSchema(); const { data: schema } = useIntrospectSchema();
@ -26,13 +34,17 @@ export const RowPermissionBuilder = ({ tableName, nesting }: Props) => {
// this value will always be 'filter' or 'check' depending on the query type // this value will always be 'filter' or 'check' depending on the query type
const value = watch(nesting[0]); const value = watch(nesting[0]);
const json = createDisplayJson(value || {}); const json = createDisplayJson(value || {});
// const { data: tableConfig } = useTableConfiguration({
// table,
// dataSourceName,
// });
if (!schema) { if (!schema) {
return null; return null;
} }
return ( return (
<div className="flex flex-col space-y-4 w-full"> <div key={tableName} className="flex flex-col space-y-4 w-full">
<div className="p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full"> <div className="p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full">
<AceEditor <AceEditor
mode="json" mode="json"
@ -49,7 +61,13 @@ export const RowPermissionBuilder = ({ tableName, nesting }: Props) => {
<div className="p-6 rounded-lg bg-white border border-gray-200w-full"> <div className="p-6 rounded-lg bg-white border border-gray-200w-full">
<JsonItem text="{" /> <JsonItem text="{" />
<div className="py-2"> <div className="py-2">
<Builder tableName={tableName} nesting={nesting} schema={schema} /> <Builder
tableName={tableName}
nesting={nesting}
schema={schema}
dataSourceName={dataSourceName}
table={table}
/>
</div> </div>
<JsonItem text="}" /> <JsonItem text="}" />
</div> </div>

View File

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { GraphQLSchema } from 'graphql'; import { GraphQLSchema } from 'graphql';
import { Table } from '@/features/MetadataAPI';
import { RenderFormElement } from './RenderFormElement'; import { RenderFormElement } from './RenderFormElement';
import { CustomField } from './Fields'; import { CustomField } from './Fields';
import { JsonItem } from './Elements'; import { JsonItem } from './Elements';
import { getColumnOperators } from '../utils'; import { getColumnOperators } from '../utils';
import { useData } from '../hooks'; import { useData } from '../hooks';
@ -110,15 +109,22 @@ interface Props {
*/ */
nesting: string[]; nesting: string[];
schema: GraphQLSchema; schema: GraphQLSchema;
dataSourceName: string;
table: Table;
} }
export const Builder = (props: Props) => { export const Builder = (props: Props) => {
const { tableName, nesting, schema } = props; const { tableName, nesting, schema, dataSourceName, table } = props;
const { data } = useData({ tableName, schema });
const { data, tableConfig } = useData({
tableName,
schema,
// we have to pass in table like this because if it is a relationship if will
// fetch the wrong table config otherwise
table,
dataSourceName,
});
const { unregister, setValue, getValues } = useFormContext(); const { unregister, setValue, getValues } = useFormContext();
// the selections from the dropdowns are stored on the form state under the key "operators" // the selections from the dropdowns are stored on the form state under the key "operators"
// this will be removed for submitting the form // this will be removed for submitting the form
// and is generated from the permissions object when rendering the form from existing data // and is generated from the permissions object when rendering the form from existing data
@ -138,11 +144,12 @@ export const Builder = (props: Props) => {
tableName, tableName,
columnName: dropDownState.name, columnName: dropDownState.name,
schema, schema,
tableConfig,
}); });
} }
return []; return [];
}, [tableName, dropDownState, schema]); }, [tableName, dropDownState, schema, tableConfig]);
const handleDropdownChange: React.ChangeEventHandler<HTMLSelectElement> = const handleDropdownChange: React.ChangeEventHandler<HTMLSelectElement> =
e => { e => {
@ -216,6 +223,8 @@ export const Builder = (props: Props) => {
handleColumnChange={handleColumnChange} handleColumnChange={handleColumnChange}
nesting={nesting} nesting={nesting}
schema={schema} schema={schema}
table={table}
dataSourceName={dataSourceName}
/> />
</div> </div>
); );

View File

@ -2,9 +2,10 @@ import React from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form'; import { useFieldArray, useFormContext } from 'react-hook-form';
import { GraphQLSchema } from 'graphql'; import { GraphQLSchema } from 'graphql';
import { Table } from '@/features/MetadataAPI';
import { Builder } from './Builder'; import { Builder } from './Builder';
import { JsonItem } from './Elements'; import { JsonItem } from './Elements';
// import { useTableConfiguration } from '../hooks';
interface FieldArrayElementProps { interface FieldArrayElementProps {
index: number; index: number;
@ -15,13 +16,27 @@ interface FieldArrayElementProps {
fields: Field[]; fields: Field[];
append: ReturnType<typeof useFieldArray>['append']; append: ReturnType<typeof useFieldArray>['append'];
schema: GraphQLSchema; schema: GraphQLSchema;
dataSourceName: string;
table: Table;
// tableConfig: ReturnType<typeof useTableConfiguration>['data'];
} }
type Field = Record<'id', string>; type Field = Record<'id', string>;
export const FieldArrayElement = (props: FieldArrayElementProps) => { export const FieldArrayElement = (props: FieldArrayElementProps) => {
const { index, arrayKey, tableName, field, nesting, fields, append, schema } = const {
props; index,
arrayKey,
tableName,
field,
nesting,
fields,
append,
schema,
table,
dataSourceName,
// tableConfig,
} = props;
const { watch } = useFormContext(); const { watch } = useFormContext();
// from this we can determine if the dropdown has been selected // from this we can determine if the dropdown has been selected
@ -44,6 +59,9 @@ export const FieldArrayElement = (props: FieldArrayElementProps) => {
tableName={tableName} tableName={tableName}
nesting={[...nesting, index.toString()]} nesting={[...nesting, index.toString()]}
schema={schema} schema={schema}
dataSourceName={dataSourceName}
table={table}
// tableConfig={tableConfig}
/> />
<JsonItem text="}" /> <JsonItem text="}" />
</div> </div>
@ -60,6 +78,9 @@ export const FieldArrayElement = (props: FieldArrayElementProps) => {
tableName={tableName} tableName={tableName}
nesting={[...nesting, index.toString()]} nesting={[...nesting, index.toString()]}
schema={schema} schema={schema}
dataSourceName={dataSourceName}
table={table}
// tableConfig={tableConfig}
/> />
<JsonItem text="}," /> <JsonItem text="}," />
</div> </div>
@ -70,10 +91,20 @@ interface Props {
tableName: string; tableName: string;
nesting: string[]; nesting: string[];
schema: GraphQLSchema; schema: GraphQLSchema;
dataSourceName: string;
table: Table;
// tableConfig: ReturnType<typeof useTableConfiguration>['data'];
} }
export const FieldArray = (props: Props) => { export const FieldArray = (props: Props) => {
const { tableName, nesting, schema } = props; const {
tableName,
nesting,
schema,
dataSourceName,
table,
// tableConfig
} = props;
const arrayKey = nesting.join('.'); const arrayKey = nesting.join('.');
const { fields, append } = useFieldArray({ const { fields, append } = useFieldArray({
@ -103,6 +134,9 @@ export const FieldArray = (props: Props) => {
nesting={nesting} nesting={nesting}
append={append} append={append}
schema={schema} schema={schema}
dataSourceName={dataSourceName}
table={table}
// tableConfig={tableConfig}
/> />
))} ))}
</div> </div>

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { GraphQLSchema } from 'graphql'; import { GraphQLSchema } from 'graphql';
import { Table } from '@/features/MetadataAPI';
import { CustomField } from './Fields'; import { CustomField } from './Fields';
import { FieldArray } from './FieldArray'; import { FieldArray } from './FieldArray';
import { Builder } from './Builder'; import { Builder } from './Builder';
@ -23,6 +22,8 @@ interface Props {
*/ */
nesting: string[]; nesting: string[];
schema: GraphQLSchema; schema: GraphQLSchema;
table: Table;
dataSourceName: string;
} }
export const RenderFormElement = (props: Props) => { export const RenderFormElement = (props: Props) => {
@ -34,6 +35,9 @@ export const RenderFormElement = (props: Props) => {
handleColumnChange, handleColumnChange,
nesting, nesting,
schema, schema,
table,
dataSourceName,
// tableConfig,
} = props; } = props;
const { register, setValue, watch } = useFormContext(); const { register, setValue, watch } = useFormContext();
@ -117,6 +121,8 @@ export const RenderFormElement = (props: Props) => {
tableName={dropDownState.typeName} tableName={dropDownState.typeName}
nesting={[...nesting, dropDownState.name]} nesting={[...nesting, dropDownState.name]}
schema={schema} schema={schema}
table={table}
dataSourceName={dataSourceName}
/> />
</div> </div>
<JsonItem text="}" /> <JsonItem text="}" />
@ -134,6 +140,8 @@ export const RenderFormElement = (props: Props) => {
tableName={tableName} tableName={tableName}
nesting={[...nesting, dropDownState.name]} nesting={[...nesting, dropDownState.name]}
schema={schema} schema={schema}
table={table}
dataSourceName={dataSourceName}
/> />
</div> </div>
<JsonItem text="}" /> <JsonItem text="}" />
@ -146,6 +154,8 @@ export const RenderFormElement = (props: Props) => {
tableName={tableName} tableName={tableName}
nesting={[...nesting, dropDownState.name]} nesting={[...nesting, dropDownState.name]}
schema={schema} schema={schema}
table={table}
dataSourceName={dataSourceName}
/> />
)} )}
</> </>

View File

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import { buildClientSchema, GraphQLSchema, IntrospectionQuery } from 'graphql'; import { buildClientSchema, GraphQLSchema, IntrospectionQuery } from 'graphql';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { runIntrospectionQuery } from '@/features/DataSource'; import { exportMetadata, runIntrospectionQuery } from '@/features/DataSource';
import { Table } from '@/features/MetadataAPI';
import { createDefaultValues, getAllColumnsAndOperators } from '../utils'; import { useQuery } from 'react-query';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { getAllColumnsAndOperators } from '../utils';
/** /**
* *
@ -28,9 +30,33 @@ export const useIntrospectSchema = () => {
return { data: schema }; return { data: schema };
}; };
export const useTableConfiguration = ({
dataSourceName,
table,
}: {
dataSourceName: string;
table: Table;
}) => {
const httpClient = useHttpClient();
return useQuery({
queryKey: ['export_metadata', dataSourceName, table, 'configuration'],
queryFn: async () => {
const { metadata } = await exportMetadata({ httpClient });
const metadataTable = metadata.sources
.find(s => s.name === dataSourceName)
?.tables.find(t => areTablesEqual(t.table, table));
if (!metadata) throw Error('Unable to find table in metadata');
return metadataTable?.configuration ?? {};
},
});
};
interface Args { interface Args {
tableName: string; tableName: string;
schema?: GraphQLSchema; schema?: GraphQLSchema;
table: Table;
dataSourceName: string;
} }
/** /**
@ -38,7 +64,11 @@ interface Args {
* get all boolOperators, columns and relationships * get all boolOperators, columns and relationships
* and information about types for each * and information about types for each
*/ */
export const useData = ({ tableName, schema }: Args) => { export const useData = ({ tableName, schema, table, dataSourceName }: Args) => {
const { data: tableConfig } = useTableConfiguration({
table,
dataSourceName,
});
if (!schema) if (!schema)
return { return {
data: { data: {
@ -47,21 +77,7 @@ export const useData = ({ tableName, schema }: Args) => {
relationships: [], relationships: [],
}, },
}; };
const data = getAllColumnsAndOperators({ tableName, schema });
return { data }; const data = getAllColumnsAndOperators({ tableName, schema, tableConfig });
}; return { data, tableConfig };
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;
}; };

View File

@ -17,6 +17,7 @@ test('renders basic permission', () => {
tableName: 'Album', tableName: 'Album',
schema, schema,
existingPermission: simpleExample, existingPermission: simpleExample,
tableConfig: {},
}); });
const expected: Expected = { const expected: Expected = {
@ -43,6 +44,7 @@ test('renders bool operator permission', () => {
tableName: 'Album', tableName: 'Album',
schema, schema,
existingPermission: exampleWithBoolOperator, existingPermission: exampleWithBoolOperator,
tableConfig: {},
}); });
const expected = { const expected = {
@ -78,6 +80,7 @@ test('renders permission with relationship', () => {
tableName: 'Album', tableName: 'Album',
schema, schema,
existingPermission: exampleWithRelationship, existingPermission: exampleWithRelationship,
tableConfig: {},
}); });
const expected: Expected = { const expected: Expected = {
@ -101,6 +104,7 @@ test('renders complex permission', () => {
tableName: 'user', tableName: 'user',
schema, schema,
existingPermission: complicatedExample, existingPermission: complicatedExample,
tableConfig: {},
}); });
const expected: Expected = { const expected: Expected = {

View File

@ -1,3 +1,4 @@
import { MetadataTable } from '@/features/MetadataAPI';
import { GraphQLSchema } from 'graphql'; import { GraphQLSchema } from 'graphql';
import { getAllColumnsAndOperators } from '.'; import { getAllColumnsAndOperators } from '.';
@ -5,17 +6,19 @@ export interface CreateOperatorsArgs {
tableName: string; tableName: string;
schema?: GraphQLSchema; schema?: GraphQLSchema;
existingPermission?: Record<string, any>; existingPermission?: Record<string, any>;
tableConfig: MetadataTable['configuration'];
} }
export const createOperatorsObject = ({ export const createOperatorsObject = ({
tableName, tableName,
schema, schema,
existingPermission, existingPermission,
tableConfig,
}: CreateOperatorsArgs): Record<string, any> => { }: CreateOperatorsArgs): Record<string, any> => {
if (!existingPermission || !schema) { if (!existingPermission || !schema) {
return {}; return {};
} }
const data = getAllColumnsAndOperators({ tableName, schema }); const data = getAllColumnsAndOperators({ tableName, schema, tableConfig });
const colNames = data.columns.map(col => col.name); const colNames = data.columns.map(col => col.name);
const boolOperators = data.boolOperators.map(bo => bo.name); const boolOperators = data.boolOperators.map(bo => bo.name);
@ -33,6 +36,7 @@ export const createOperatorsObject = ({
tableName, tableName,
schema, schema,
existingPermission: each, existingPermission: each,
tableConfig,
}) })
), ),
}; };
@ -50,6 +54,7 @@ export const createOperatorsObject = ({
tableName: typeName || '', tableName: typeName || '',
schema, schema,
existingPermission: value, existingPermission: value,
tableConfig,
}), }),
}; };
} }
@ -63,6 +68,7 @@ export const createOperatorsObject = ({
tableName, tableName,
schema, schema,
existingPermission: value, existingPermission: value,
tableConfig,
}), }),
}; };
} }
@ -79,10 +85,11 @@ export interface CreateDefaultsArgs {
tableName: string; tableName: string;
schema?: GraphQLSchema; schema?: GraphQLSchema;
existingPermission?: Record<string, any>; existingPermission?: Record<string, any>;
tableConfig: MetadataTable['configuration'];
} }
export const createDefaultValues = (props: CreateDefaultsArgs) => { export const createDefaultValues = (props: CreateDefaultsArgs) => {
const { tableName, schema, existingPermission } = props; const { tableName, schema, existingPermission, tableConfig } = props;
if (!existingPermission) { if (!existingPermission) {
return {}; return {};
} }
@ -91,6 +98,7 @@ export const createDefaultValues = (props: CreateDefaultsArgs) => {
tableName, tableName,
schema, schema,
existingPermission, existingPermission,
tableConfig,
}); });
return { return {

View File

@ -6,7 +6,11 @@ import {
import { schema } from '../mocks'; import { schema } from '../mocks';
test('correctly fetches items for dropdown from schema', () => { test('correctly fetches items for dropdown from schema', () => {
const result = getAllColumnsAndOperators({ tableName: 'user', schema }); const result = getAllColumnsAndOperators({
tableName: 'user',
schema,
tableConfig: {},
});
expect(result.boolOperators.length).toBe(3); expect(result.boolOperators.length).toBe(3);
expect(result.columns.length).toBe(4); expect(result.columns.length).toBe(4);
@ -25,6 +29,7 @@ test('correctly fetches operators for a given column', () => {
tableName: 'user', tableName: 'user',
schema, schema,
columnName: 'age', columnName: 'age',
tableConfig: {},
}); });
const expected: ReturnType<typeof getColumnOperators> = [ const expected: ReturnType<typeof getColumnOperators> = [
@ -94,6 +99,7 @@ test('correctly fetches information about a column operator', () => {
tableName: 'user', tableName: 'user',
schema, schema,
columnName: 'age', columnName: 'age',
tableConfig: {},
}); });
const result = findColumnOperator({ columnKey: '_eq', columnOperators }); const result = findColumnOperator({ columnKey: '_eq', columnOperators });
const expected: ReturnType<typeof findColumnOperator> = { const expected: ReturnType<typeof findColumnOperator> = {

View File

@ -1,3 +1,4 @@
import { MetadataTable } from '@/features/MetadataAPI';
import { import {
GraphQLFieldMap, GraphQLFieldMap,
GraphQLInputFieldMap, GraphQLInputFieldMap,
@ -71,16 +72,20 @@ interface GetColumnOperatorsArgs {
tableName: string; tableName: string;
columnName: string; columnName: string;
schema: GraphQLSchema; schema: GraphQLSchema;
tableConfig: MetadataTable['configuration'];
} }
export const getColumnOperators = ({ export const getColumnOperators = ({
tableName, tableName,
columnName, columnName,
schema, schema,
tableConfig,
}: GetColumnOperatorsArgs) => { }: GetColumnOperatorsArgs) => {
const fields = getFields(`${tableName}_bool_exp`, schema); const fields = getFields(`${tableName}_bool_exp`, schema);
const col = fields?.[columnName]; const customName =
tableConfig?.column_config?.[columnName]?.custom_name ?? columnName;
const col = fields?.[customName];
if (col?.type && !isListType(col?.type)) { if (col?.type && !isListType(col?.type)) {
const colType = schema.getType(col.type.name); const colType = schema.getType(col.type.name);
@ -156,14 +161,32 @@ export const findColumnOperator = ({
interface Args { interface Args {
tableName: string; tableName: string;
schema: GraphQLSchema; schema: GraphQLSchema;
tableConfig: MetadataTable['configuration'];
} }
const getOriginalTableNameFromCustomName = (
tableConfig: MetadataTable['configuration'],
columnName: string
) => {
const columnConfig = tableConfig?.column_config;
const matchingEntry = Object.entries(columnConfig ?? {}).find(value => {
return value?.[1]?.custom_name === columnName;
});
return matchingEntry?.[0] ?? columnName;
};
/** /**
* *
* Returns a list of all the boolOperators, columns and relationships for the selected table * Returns a list of all the boolOperators, columns and relationships for the selected table
*/ */
export const getAllColumnsAndOperators = ({ tableName, schema }: Args) => { export const getAllColumnsAndOperators = ({
const fields = getFields(tableName, schema); tableName,
schema,
tableConfig,
}: Args) => {
const metadataTableName = tableConfig?.custom_name ?? tableName;
const fields = getFields(metadataTableName, schema);
const boolOperators = getBoolOperators(); const boolOperators = getBoolOperators();
const columns = getColumns(fields); const columns = getColumns(fields);
const relationships = getRelationships(fields); const relationships = getRelationships(fields);
@ -174,7 +197,7 @@ export const getAllColumnsAndOperators = ({ tableName, schema }: Args) => {
meta: null, meta: null,
})); }));
const colMap = columns.map(column => ({ const colMap = columns.map(column => ({
name: column.name, name: getOriginalTableNameFromCustomName(tableConfig, column.name),
kind: 'column', kind: 'column',
meta: column, meta: column,
})); }));
@ -183,6 +206,5 @@ export const getAllColumnsAndOperators = ({ tableName, schema }: Args) => {
kind: 'relationship', kind: 'relationship',
meta: relationship, meta: relationship,
})); }));
return { boolOperators: boolMap, columns: colMap, relationships: relMap }; return { boolOperators: boolMap, columns: colMap, relationships: relMap };
}; };

View File

@ -1,2 +1 @@
export * from './useDefaultValues'; export * from './useFormData';
export * from './useFormData/useFormData';

View File

@ -1 +0,0 @@
export * from './useDefaultValues';

View File

@ -1,48 +0,0 @@
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,
};

View File

@ -1,36 +0,0 @@
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);
});

View File

@ -1,141 +0,0 @@
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,
});
};

View File

@ -0,0 +1,111 @@
import { GraphQLSchema } from 'graphql';
import isEqual from 'lodash.isequal';
import { TableColumn } from '@/features/DataSource';
import { getTypeName } from '@/features/GraphQLUtils';
import { Metadata } 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 Args {
queryType: QueryType;
roleName: string;
table: unknown;
dataSourceName: string;
metadata: Metadata;
tableColumns: TableColumn[];
schema: GraphQLSchema;
}
export const createDefaultValues = ({
queryType,
roleName,
table,
dataSourceName,
metadata,
tableColumns,
schema,
}: Args) => {
const selectedTable = getMetadataTable({
dataSourceName,
table,
metadata,
});
const metadataSource = metadata.metadata.sources.find(
s => s.name === dataSourceName
);
/**
* This is GDC specific, we have to move this to DAL later
*/
const tableName = getTypeName({
defaultQueryRoot: (table as string[]).join('_'),
operation: 'select',
sourceCustomization: metadataSource?.customization,
configuration: selectedTable?.configuration,
});
const allRowChecks = getRowPermissionsForAllOtherQueriesMatchingSelectedRole(
queryType,
roleName,
selectedTable
);
const baseDefaultValues: DefaultValues = {
checkType: 'none',
filterType: 'none',
columns: {},
allRowChecks,
};
if (selectedTable) {
const permissionsObject = createPermissionsObject({
queryType,
selectedTable,
roleName,
tableColumns,
schema,
tableName,
});
return { ...baseDefaultValues, ...permissionsObject };
}
return baseDefaultValues;
};
type DefaultValues = PermissionsSchema & {
allRowChecks: { queryType: QueryType; value: string }[];
operators?: Record<string, unknown>;
};

View File

@ -1,4 +1,5 @@
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { GraphQLSchema } from 'graphql';
import { TableColumn } from '@/features/DataSource'; import { TableColumn } from '@/features/DataSource';
import type { import type {
@ -10,10 +11,14 @@ import type {
UpdatePermissionDefinition, UpdatePermissionDefinition,
} from '@/features/MetadataAPI'; } from '@/features/MetadataAPI';
import { isPermission, keyToPermission, permissionToKey } from '../utils'; import {
import { createDefaultValues } from '../../../components/RowPermissionsBuilder'; isPermission,
keyToPermission,
permissionToKey,
} from '../../../../utils';
import { createDefaultValues } from '../../../../components/RowPermissionsBuilder';
import type { QueryType } from '../../../types'; import type { QueryType } from '../../../../types';
export const getCheckType = ( export const getCheckType = (
check?: Record<string, unknown> | null check?: Record<string, unknown> | null
@ -150,12 +155,15 @@ export const createPermission = {
select: ( select: (
permission: SelectPermissionDefinition, permission: SelectPermissionDefinition,
tableColumns: TableColumn[], tableColumns: TableColumn[],
schema: any schema: GraphQLSchema,
tableName: string,
tableConfig: MetadataTable['configuration']
) => { ) => {
const { filter, operators } = createDefaultValues({ const { filter, operators } = createDefaultValues({
tableName: 'Artist', tableName,
existingPermission: permission.filter, existingPermission: permission.filter,
schema, schema,
tableConfig,
}); });
const filterType = getCheckType(permission?.filter); const filterType = getCheckType(permission?.filter);
@ -254,6 +262,7 @@ interface ObjArgs {
tableColumns: TableColumn[]; tableColumns: TableColumn[];
roleName: string; roleName: string;
schema: any; schema: any;
tableName: string;
} }
export const createPermissionsObject = ({ export const createPermissionsObject = ({
@ -262,6 +271,7 @@ export const createPermissionsObject = ({
tableColumns, tableColumns,
roleName, roleName,
schema, schema,
tableName,
}: ObjArgs) => { }: ObjArgs) => {
const selectedPermission = getCurrentPermission({ const selectedPermission = getCurrentPermission({
table: selectedTable, table: selectedTable,
@ -279,7 +289,9 @@ export const createPermissionsObject = ({
return createPermission.select( return createPermission.select(
selectedPermission.permission as SelectPermissionDefinition, selectedPermission.permission as SelectPermissionDefinition,
tableColumns, tableColumns,
schema schema,
tableName,
selectedTable.configuration
); );
case 'update': case 'update':
return createPermission.update( return createPermission.update(

View File

@ -0,0 +1,93 @@
import { TableColumn } from '@/features/DataSource';
import { Metadata, MetadataTable } from '@/features/MetadataAPI';
import { isPermission } from '../../../../utils';
type Operation = 'insert' | 'select' | 'update' | 'delete';
const supportedQueries: Operation[] = ['select'];
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 Args {
dataSourceName: string;
table: unknown;
metadata: Metadata;
tableColumns: TableColumn[];
}
export const createFormData = (props: Args) => {
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),
};
};

View File

@ -1 +1,2 @@
export * from './useFormData'; export * from './useFormData';
export * from './createDefaultValues';

View File

@ -1,5 +1,7 @@
import { TableColumn } from '@/features/DataSource'; import { TableColumn } from '@/features/DataSource';
import { Metadata } from '@/features/MetadataAPI'; import { Metadata } from '@/features/MetadataAPI';
import { createDefaultValues } from '../createDefaultValues';
import { schema } from '../../../../components/RowPermissionsBuilder/mocks';
interface Input { interface Input {
dataSourceName: string; dataSourceName: string;
@ -26,9 +28,14 @@ const metadata: Metadata = {
{ {
role: 'user', role: 'user',
permission: { permission: {
columns: ['ArtistId', 'Name'], columns: ['Name'],
filter: {}, filter: {
ArtistId: {
_gt: 5,
},
},
allow_aggregations: true, allow_aggregations: true,
limit: 3,
}, },
}, },
], ],
@ -54,7 +61,7 @@ const metadata: Metadata = {
}, },
}; };
export const input: Input = { export const formDataInput: Input = {
dataSourceName: 'sqlite', dataSourceName: 'sqlite',
table: ['Artist'], table: ['Artist'],
metadata, metadata,
@ -81,3 +88,34 @@ export const input: Input = {
}, },
], ],
}; };
export const defaultValuesInput: Parameters<typeof createDefaultValues>[0] = {
dataSourceName: 'sqlite',
table: ['Artist'],
metadata,
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',
},
},
],
schema,
};

View File

@ -1,14 +1,46 @@
import { createFormData } from './useFormData'; import { createFormData } from './createFormData';
import { input } from './mock'; import { createDefaultValues } from './createDefaultValues';
import { defaultValuesInput, formDataInput } from './mock';
const mockResult: ReturnType<typeof createFormData> = { const formDataMockResult: ReturnType<typeof createFormData> = {
columns: ['ArtistId', 'Name'], columns: ['ArtistId', 'Name'],
roles: ['user'], roles: ['user'],
supportedQueries: ['insert', 'select', 'update', 'delete'], supportedQueries: ['select'],
tableNames: [['Album'], ['Artist']], tableNames: [['Album'], ['Artist']],
}; };
test('returns correctly formatted formData', () => { test('returns correctly formatted formData', () => {
const result = createFormData(input); const result = createFormData(formDataInput);
expect(result).toEqual(mockResult); expect(result).toEqual(formDataMockResult);
});
const defaultValuesMockResult: ReturnType<typeof createDefaultValues> = {
aggregationEnabled: true,
checkType: 'none',
allRowChecks: [],
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(defaultValuesInput);
expect(result).toEqual(defaultValuesMockResult);
}); });

View File

@ -1,102 +1,17 @@
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { buildClientSchema } from 'graphql';
import { DataSource, exportMetadata, TableColumn } from '@/features/DataSource'; import {
DataSource,
exportMetadata,
runIntrospectionQuery,
} from '@/features/DataSource';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { Metadata, MetadataTable } from '@/features/MetadataAPI'; import { createDefaultValues } from './createDefaultValues';
import { isPermission } from '../utils'; import { createFormData } from './createFormData';
type Operation = 'insert' | 'select' | 'update' | 'delete'; export type Args = {
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; dataSourceName: string;
table: unknown; table: unknown;
roleName: string; roleName: string;
@ -104,21 +19,32 @@ export type UseFormDataArgs = {
}; };
type ReturnValue = { type ReturnValue = {
roles: string[]; formData: ReturnType<typeof createFormData>;
supportedQueries: Operation[]; defaultValues: ReturnType<typeof createDefaultValues>;
tableNames: unknown;
columns: string[];
}; };
/** /**
* *
* creates data for displaying in the form e.g. column names, roles etc. * creates data for displaying in the form e.g. column names, roles etc.
* creates default values for form i.e. existing permissions from metadata
*/ */
export const useFormData = ({ dataSourceName, table }: UseFormDataArgs) => { export const useFormData = ({
dataSourceName,
table,
roleName,
queryType,
}: Args) => {
const httpClient = useHttpClient(); const httpClient = useHttpClient();
return useQuery<ReturnValue, Error>({ return useQuery<ReturnValue, Error>({
queryKey: [dataSourceName, 'permissionFormData'], queryKey: [
dataSourceName,
'permissionFormData',
JSON.stringify(table),
roleName,
],
queryFn: async () => { queryFn: async () => {
const introspectionResult = await runIntrospectionQuery({ httpClient });
const schema = buildClientSchema(introspectionResult.data);
const metadata = await exportMetadata({ httpClient }); const metadata = await exportMetadata({ httpClient });
// get table columns for metadata table from db introspection // get table columns for metadata table from db introspection
@ -127,12 +53,24 @@ export const useFormData = ({ dataSourceName, table }: UseFormDataArgs) => {
table, table,
}); });
return createFormData({ const defaultValues = createDefaultValues({
queryType,
roleName,
dataSourceName,
metadata,
table,
tableColumns,
schema,
});
const formData = createFormData({
dataSourceName, dataSourceName,
table, table,
metadata, metadata,
tableColumns, tableColumns,
}); });
return { formData, defaultValues };
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });

View File

@ -1,29 +0,0 @@
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;

View File

@ -3,6 +3,7 @@ import { AxiosInstance } from 'axios';
import { exportMetadata } from '@/features/DataSource'; import { exportMetadata } from '@/features/DataSource';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { Permission, useMetadataMigration } from '@/features/MetadataAPI'; import { Permission, useMetadataMigration } from '@/features/MetadataAPI';
import { useFireNotification } from '@/new-components/Notifications';
import { api } from '../../api'; import { api } from '../../api';
import { QueryType } from '../../types'; import { QueryType } from '../../types';
@ -38,6 +39,7 @@ const getMetadataTable = async ({
JSON.stringify(trackedTable.table) === JSON.stringify(table) JSON.stringify(trackedTable.table) === JSON.stringify(table)
), ),
resourceVersion: resource_version, resourceVersion: resource_version,
driver: currentMetadataSource.kind,
}; };
}; };
@ -69,16 +71,11 @@ const isPermission = (props: {
} => props.key in keyToPermission; } => props.key in keyToPermission;
interface Args { interface Args {
currentSource: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
} }
export const useBulkDeletePermissions = ({ export const useBulkDeletePermissions = ({ dataSourceName, table }: Args) => {
currentSource,
dataSourceName,
table,
}: Args) => {
const { const {
mutateAsync, mutateAsync,
isLoading: mutationLoading, isLoading: mutationLoading,
@ -88,9 +85,10 @@ export const useBulkDeletePermissions = ({
const httpClient = useHttpClient(); const httpClient = useHttpClient();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { fireNotification } = useFireNotification();
const submit = async (roles: string[]) => { const submit = async (roles: string[]) => {
const { metadataTable, resourceVersion } = await getMetadataTable({ const { metadataTable, resourceVersion, driver } = await getMetadataTable({
dataSourceName, dataSourceName,
table, table,
httpClient, httpClient,
@ -129,18 +127,48 @@ export const useBulkDeletePermissions = ({
); );
const body = api.createBulkDeleteBody({ const body = api.createBulkDeleteBody({
source: currentSource, driver,
dataSourceName, dataSourceName,
table, table,
resourceVersion, resourceVersion,
roleList, roleList,
}); });
await mutateAsync({ await mutateAsync(
query: body, {
}); query: body,
},
{
onSuccess: () => {
fireNotification({
type: 'success',
title: 'Success!',
message: 'Permissions successfully deleted',
});
},
onError: err => {
fireNotification({
type: 'error',
title: 'Error!',
message:
err?.message ?? 'Something went wrong while deleting permissions',
});
},
onSettled: () => {
queryClient.invalidateQueries([
dataSourceName,
'permissionFormData',
JSON.stringify(table),
]);
queryClient.invalidateQueries([dataSourceName, 'permissionsTable']); queryClient.invalidateQueries([
dataSourceName,
'permissionsTable',
JSON.stringify(table),
]);
},
}
);
}; };
const isLoading = mutationLoading; const isLoading = mutationLoading;

View File

@ -1,6 +1,7 @@
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import { useMetadataMigration } from '@/features/MetadataAPI'; import { useMetadataMigration } from '@/features/MetadataAPI';
import { exportMetadata } from '@/features/DataSource'; import { exportMetadata } from '@/features/DataSource';
import { useFireNotification } from '@/new-components/Notifications';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
@ -8,14 +9,12 @@ import { QueryType } from '../../types';
import { api } from '../../api'; import { api } from '../../api';
export interface UseDeletePermissionArgs { export interface UseDeletePermissionArgs {
currentSource: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
roleName: string; roleName: string;
} }
export const useDeletePermission = ({ export const useDeletePermission = ({
currentSource,
dataSourceName, dataSourceName,
table, table,
roleName, roleName,
@ -23,19 +22,25 @@ export const useDeletePermission = ({
const mutate = useMetadataMigration(); const mutate = useMetadataMigration();
const httpClient = useHttpClient(); const httpClient = useHttpClient();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { fireNotification } = useFireNotification();
const submit = async (queries: QueryType[]) => { const submit = async (queries: QueryType[]) => {
const { resource_version: resourceVersion } = await exportMetadata({ const { resource_version: resourceVersion, metadata } =
httpClient, await exportMetadata({
}); httpClient,
});
if (!resourceVersion) { if (!resourceVersion) {
console.error('No resource version'); console.error('No resource version');
return; return;
} }
const driver = metadata.sources.find(s => s.name === dataSourceName)?.kind;
if (!driver) throw Error('Unable to find driver in metadata');
const body = api.createDeleteBody({ const body = api.createDeleteBody({
currentSource, driver,
dataSourceName, dataSourceName,
table, table,
roleName, roleName,
@ -43,11 +48,41 @@ export const useDeletePermission = ({
queries, queries,
}); });
await mutate.mutateAsync({ await mutate.mutateAsync(
query: body, {
}); query: body,
},
{
onSuccess: () => {
fireNotification({
type: 'success',
title: 'Success!',
message: 'Permissions successfully deleted',
});
},
onError: err => {
fireNotification({
type: 'error',
title: 'Error!',
message:
err?.message ?? 'Something went wrong while deleting permissions',
});
},
onSettled: async () => {
await queryClient.invalidateQueries([
dataSourceName,
'permissionFormData',
JSON.stringify(table),
]);
queryClient.invalidateQueries([dataSourceName, 'permissionsTable']); await queryClient.invalidateQueries([
dataSourceName,
'permissionsTable',
JSON.stringify(table),
]);
},
}
);
}; };
const isLoading = mutate.isLoading; const isLoading = mutate.isLoading;

View File

@ -1,18 +1,15 @@
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import { AxiosInstance } from 'axios'; import { AxiosInstance } from 'axios';
import { import { useMetadataMigration } from '@/features/MetadataAPI';
useMetadataMigration,
useMetadataVersion,
} from '@/features/MetadataAPI';
import { exportMetadata } from '@/features/DataSource'; import { exportMetadata } from '@/features/DataSource';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { useFireNotification } from '@/new-components/Notifications';
import { AccessType, FormOutput, QueryType } from '../../types'; import { AccessType, QueryType } from '../../types';
import { api } from '../../api'; import { api } from '../../api';
import { isPermission, keyToPermission, PermissionsSchema } from '../../utils';
export interface UseSubmitFormArgs { export interface UseSubmitFormArgs {
currentSource: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
roleName: string; roleName: string;
@ -20,29 +17,6 @@ export interface UseSubmitFormArgs {
accessType: AccessType; 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 { interface ExistingPermissions {
role: string; role: string;
queryType: QueryType; queryType: QueryType;
@ -87,28 +61,24 @@ const getAllPermissions = async ({
}; };
export const useSubmitForm = (args: UseSubmitFormArgs) => { export const useSubmitForm = (args: UseSubmitFormArgs) => {
const { const { dataSourceName, table, roleName, queryType, accessType } = args;
currentSource,
dataSourceName,
table,
roleName,
queryType,
accessType,
} = args;
const {
data: resourceVersion,
isLoading: resourceVersionLoading,
isError: resourceVersionError,
} = useMetadataVersion();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const httpClient = useHttpClient(); const httpClient = useHttpClient();
const { fireNotification } = useFireNotification();
const mutate = useMetadataMigration(); const mutate = useMetadataMigration();
const submit = async (formData: FormOutput) => { const submit = async (formData: PermissionsSchema) => {
if (!resourceVersion) { const { metadata, resource_version } = await exportMetadata({ httpClient });
console.error('No resource version');
const metadataSource = metadata?.sources.find(
s => s.name === dataSourceName
);
if (!resource_version || !metadataSource) {
console.error('Something went wrong!');
return; return;
} }
@ -118,32 +88,57 @@ export const useSubmitForm = (args: UseSubmitFormArgs) => {
}); });
const body = api.createInsertBody({ const body = api.createInsertBody({
currentSource,
dataSourceName, dataSourceName,
driver: metadataSource.kind,
table, table,
roleName, roleName,
queryType, queryType,
accessType, accessType,
resourceVersion, resourceVersion: resource_version,
formData, formData,
existingPermissions, existingPermissions,
}); });
await mutate.mutateAsync({ await mutate.mutateAsync(
query: body, {
}); query: body,
},
await queryClient.invalidateQueries([ {
dataSourceName, onSuccess: () => {
'permissionDefaultValues', fireNotification({
roleName, type: 'success',
queryType, title: 'Success!',
]); message: 'Permissions saved successfully!',
await queryClient.invalidateQueries([dataSourceName, 'permissionsTable']); });
},
onError: err => {
fireNotification({
type: 'error',
title: 'Error!',
message:
err?.message ?? 'Something went wrong while saving permissions',
});
},
onSettled: () => {
queryClient.invalidateQueries(['export_metadata', 'roles']);
queryClient.invalidateQueries([
dataSourceName,
'permissionFormData',
JSON.stringify(table),
roleName,
]);
queryClient.invalidateQueries([
dataSourceName,
'permissionsTable',
JSON.stringify(table),
]);
},
}
);
}; };
const isLoading = mutate.isLoading || resourceVersionLoading; const isLoading = mutate.isLoading;
const isError = mutate.isError || resourceVersionError; const isError = mutate.isError;
return { return {
submit, submit,

View File

@ -4,7 +4,6 @@ import { useDeletePermission } from './useDeletePermission';
import { AccessType, QueryType } from '../../types'; import { AccessType, QueryType } from '../../types';
export interface UseUpdatePermissionsArgs { export interface UseUpdatePermissionsArgs {
currentSource: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
roleName: string; roleName: string;
@ -13,7 +12,6 @@ export interface UseUpdatePermissionsArgs {
} }
export const useUpdatePermissions = ({ export const useUpdatePermissions = ({
currentSource,
dataSourceName, dataSourceName,
table, table,
roleName, roleName,
@ -21,7 +19,6 @@ export const useUpdatePermissions = ({
accessType, accessType,
}: UseUpdatePermissionsArgs) => { }: UseUpdatePermissionsArgs) => {
const updatePermissions = useSubmitForm({ const updatePermissions = useSubmitForm({
currentSource,
dataSourceName, dataSourceName,
table, table,
roleName, roleName,
@ -30,7 +27,6 @@ export const useUpdatePermissions = ({
}); });
const deletePermissions = useDeletePermission({ const deletePermissions = useDeletePermission({
currentSource,
dataSourceName, dataSourceName,
table, table,
roleName, roleName,

View File

@ -1,11 +1,6 @@
import * as z from 'zod';
import { schema } from '../utils/formSchema';
export type QueryType = 'insert' | 'select' | 'update' | 'delete'; export type QueryType = 'insert' | 'select' | 'update' | 'delete';
export type AccessType = export type AccessType =
| 'fullAccess' | 'fullAccess'
| 'noAccess' | 'noAccess'
| 'partialAccess' | 'partialAccess'
| 'partialAccessWarning'; | 'partialAccessWarning';
export type FormOutput = z.infer<typeof schema>;

View File

@ -3,8 +3,8 @@ import * as z from 'zod';
export const schema = z.object({ export const schema = z.object({
checkType: z.string(), checkType: z.string(),
filterType: z.string(), filterType: z.string(),
check: z.any(), check: z.any().optional(),
filter: z.any(), filter: z.any().optional(),
rowCount: z.string().optional(), rowCount: z.string().optional(),
columns: z.record(z.optional(z.boolean())), columns: z.record(z.optional(z.boolean())),
presets: z.optional( presets: z.optional(
@ -16,8 +16,8 @@ export const schema = z.object({
}) })
) )
), ),
aggregationEnabled: z.boolean(), aggregationEnabled: z.boolean().optional(),
backendOnly: z.boolean(), backendOnly: z.boolean().optional(),
clonePermissions: z.optional( clonePermissions: z.optional(
z.array( z.array(
z.object({ z.object({

View File

@ -1,14 +1,30 @@
import { Permission } from '@/dataSources/types'; import { Permission } from '@/features/MetadataAPI';
interface Args { export const permissionToKey = {
permissions?: Permission[]; insert: 'insert_permissions',
roleName: string; select: 'select_permissions',
} update: 'update_permissions',
delete: 'delete_permissions',
} as const;
export const getCurrentRole = ({ permissions, roleName }: Args) => { export const metadataPermissionKeys = [
const rolePermissions = permissions?.find( 'insert_permissions',
({ role_name }) => role_name === roleName 'select_permissions',
); 'update_permissions',
'delete_permissions',
] as const;
return rolePermissions; 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[];
} => props.key in keyToPermission;

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Story, Meta } from '@storybook/react'; import { Story, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query'; import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
@ -16,7 +17,6 @@ export const Primary: Story<PermissionsTabProps> = args => (
<PermissionsTab {...args} /> <PermissionsTab {...args} />
); );
Primary.args = { Primary.args = {
currentSource: 'postgres',
dataSourceName: 'default', dataSourceName: 'default',
table: { table: {
name: 'user', name: 'user',
@ -28,7 +28,6 @@ export const GDC: Story<PermissionsTabProps> = args => (
<PermissionsTab {...args} /> <PermissionsTab {...args} />
); );
GDC.args = { GDC.args = {
currentSource: 'sqlite',
dataSourceName: 'sqlite', dataSourceName: 'sqlite',
table: ['Artist'], table: ['Artist'],
}; };
@ -41,7 +40,6 @@ export const GDCNoMocks: Story<PermissionsTabProps> = args => (
<PermissionsTab {...args} /> <PermissionsTab {...args} />
); );
GDCNoMocks.args = { GDCNoMocks.args = {
currentSource: 'sqlite',
dataSourceName: 'sqlite', dataSourceName: 'sqlite',
table: ['Artist'], table: ['Artist'],
}; };

View File

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import { useTableMachine, PermissionsTable } from '../PermissionsTable'; import { useTableMachine, PermissionsTable } from '../PermissionsTable';
import { BulkDelete } from '../PermissionsForm'; import { BulkDelete } from '../PermissionsForm';
import { PermissionsForm } from '../PermissionsForm/PermissionsForm'; import { PermissionsForm } from '../PermissionsForm/PermissionsForm';
import { AccessType } from '../PermissionsForm/types'; import { AccessType } from '../PermissionsForm/types';
export interface PermissionsTabProps { export interface PermissionsTabProps {
currentSource: string;
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
} }
export const PermissionsTab: React.FC<PermissionsTabProps> = ({ export const PermissionsTab: React.FC<PermissionsTabProps> = ({
currentSource,
dataSourceName, dataSourceName,
table, table,
}) => { }) => {
@ -31,7 +30,6 @@ export const PermissionsTab: React.FC<PermissionsTabProps> = ({
!!state.context.bulkSelections.length && ( !!state.context.bulkSelections.length && (
<BulkDelete <BulkDelete
roles={state.context.bulkSelections} roles={state.context.bulkSelections}
currentSource={currentSource}
dataSourceName={dataSourceName} dataSourceName={dataSourceName}
table={table} table={table}
handleClose={() => send('CLOSE')} handleClose={() => send('CLOSE')}
@ -40,7 +38,6 @@ export const PermissionsTab: React.FC<PermissionsTabProps> = ({
{state.value === 'formOpen' && ( {state.value === 'formOpen' && (
<PermissionsForm <PermissionsForm
currentSource={currentSource}
dataSourceName={dataSourceName} dataSourceName={dataSourceName}
table={table} table={table}
roleName={state.context.selectedForm.roleName || ''} roleName={state.context.selectedForm.roleName || ''}

View File

@ -7,7 +7,7 @@ import { handlers } from '../PermissionsForm/mocks/handlers.mock';
import { useTableMachine } from './hooks'; import { useTableMachine } from './hooks';
export default { export default {
title: 'Features/Permissions Table/Table', title: 'Features/Permissions Tab/Permissions Table/Table',
component: PermissionsTable, component: PermissionsTable,
decorators: [ReactQueryDecorator()], decorators: [ReactQueryDecorator()],
parameters: { parameters: {

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { FaInfo } from 'react-icons/fa'; import { FaInfo } from 'react-icons/fa';
import Skeleton from 'react-loading-skeleton';
import { IndicatorCard } from '@/new-components/IndicatorCard';
import { useRolePermissions } from './hooks/usePermissions'; import { useRolePermissions } from './hooks/usePermissions';
import { PermissionsLegend } from './components/PermissionsLegend'; import { PermissionsLegend } from './components/PermissionsLegend';
import { EditableCell, InputCell } from './components/Cells'; import { EditableCell, InputCell } from './components/Cells';
@ -56,15 +57,28 @@ export const PermissionsTable: React.FC<PermissionsTableProps> = ({
table, table,
machine, machine,
}) => { }) => {
const { data } = useRolePermissions({ const { data, isLoading } = useRolePermissions({
dataSourceName, dataSourceName,
table, table,
}); });
const [state, send] = machine; const [state, send] = machine;
if (isLoading)
return (
<div>
<Skeleton count={5} height={30} className="my-1.5" />
</div>
);
if (!data) { if (!data) {
return null; return (
<div>
<IndicatorCard status="negative" headline="Error">
Something went wrong while fetching permissions
</IndicatorCard>
</div>
);
} }
const { supportedQueries, rolePermissions } = data; const { supportedQueries, rolePermissions } = data;

View File

@ -4,9 +4,6 @@ import { PermissionsIcon } from './PermissionsIcons';
export const PermissionsLegend: React.FC = () => ( export const PermissionsLegend: React.FC = () => (
<div className="grid gap-2"> <div className="grid gap-2">
<p>
<strong>Permissions</strong>
</p>
<div className="flex gap-4"> <div className="flex gap-4">
<span> <span>
<PermissionsIcon type="fullAccess" /> <PermissionsIcon type="fullAccess" />

View File

@ -1,10 +1,10 @@
import { AxiosInstance } from 'axios';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { DataSource, exportMetadata } from '@/features/DataSource'; import { DataSource, exportMetadata } from '@/features/DataSource';
import type { TableColumn } from '@/features/DataSource'; import type { TableColumn } from '@/features/DataSource';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { MetadataTable, Metadata } from '@/features/MetadataAPI';
interface RolePermission { interface RolePermission {
roleName: string; roleName: string;
@ -95,19 +95,16 @@ const getAccessType = ({
type GetMetadataTableArgs = { type GetMetadataTableArgs = {
dataSourceName: string; dataSourceName: string;
table: unknown; table: unknown;
httpClient: AxiosInstance; metadata?: Metadata;
}; };
const getMetadataTable = async ({ const getMetadataTable = ({
httpClient, metadata,
dataSourceName, dataSourceName,
table, table,
}: GetMetadataTableArgs) => { }: GetMetadataTableArgs) => {
// get all metadata
const { metadata } = await exportMetadata({ httpClient });
// find current source // find current source
const currentMetadataSource = metadata?.sources?.find( const currentMetadataSource = metadata?.metadata?.sources?.find(
source => source.name === dataSourceName source => source.name === dataSourceName
); );
@ -140,14 +137,16 @@ const isPermission = (props: {
type CreateRoleTableDataArgs = { type CreateRoleTableDataArgs = {
metadataTable: any; metadataTable: any;
tableColumns?: TableColumn[]; tableColumns?: TableColumn[];
allRoles: string[];
}; };
type RoleToPermissionsMap = Record<string, Partial<Record<QueryType, Access>>>; type RoleToPermissionsMap = Record<string, Partial<Record<QueryType, Access>>>;
const createRoleTableData = async ({ const createRoleTableData = ({
metadataTable, metadataTable,
tableColumns, tableColumns,
}: CreateRoleTableDataArgs): Promise<RolePermission[]> => { allRoles,
}: CreateRoleTableDataArgs): RolePermission[] => {
if (!metadataTable) return []; if (!metadataTable) return [];
// create object with key of role // create object with key of role
// and value describing permissions attached to that role // and value describing permissions attached to that role
@ -178,8 +177,22 @@ const createRoleTableData = async ({
return acc; return acc;
}, {}); }, {});
const allRolesToPermissionsMap = allRoles.reduce<RoleToPermissionsMap>(
(acc, role) => {
return {
...acc,
[role]: roleToPermissionsMap[role] ?? {
insert: 'noAccess',
select: 'noAccess',
update: 'noAccess',
delete: 'noAccess',
},
};
},
{}
);
// create the array that has the relevant information for each row of the table // create the array that has the relevant information for each row of the table
const permissions = Object.entries(roleToPermissionsMap).map( const permissions = Object.entries(allRolesToPermissionsMap).map(
([roleName, permission]) => { ([roleName, permission]) => {
const permissionEntries = Object.entries(permission) as [ const permissionEntries = Object.entries(permission) as [
QueryType, QueryType,
@ -246,34 +259,113 @@ type UseRolePermissionsArgs = {
table: unknown; table: unknown;
}; };
type PermKeys = Pick<
MetadataTable,
| 'update_permissions'
| 'select_permissions'
| 'delete_permissions'
| 'insert_permissions'
>;
const permKeys: Array<keyof PermKeys> = [
'insert_permissions',
'update_permissions',
'select_permissions',
'delete_permissions',
];
const getRoles = (m: Metadata) => {
if (!m) return null;
const { metadata } = m;
const actions = metadata.actions;
const tableEntries: MetadataTable[] = metadata.sources.reduce<
MetadataTable[]
>((acc, source) => {
return [...acc, ...source.tables];
}, []);
const inheritedRoles = metadata.inherited_roles;
const remoteSchemas = metadata.remote_schemas;
const allowlists = metadata.allowlist;
const securitySettings = {
api_limits: metadata.api_limits,
graphql_schema_introspection: metadata.graphql_schema_introspection,
};
const roleNames: string[] = [];
tableEntries?.forEach(table =>
permKeys.forEach(key =>
table[key]?.forEach(({ role }: { role: string }) => roleNames.push(role))
)
);
actions?.forEach(action =>
action.permissions?.forEach(p => roleNames.push(p.role))
);
remoteSchemas?.forEach(remoteSchema => {
remoteSchema?.permissions?.forEach(p => roleNames.push(p.role));
});
allowlists?.forEach(allowlist => {
if (allowlist?.scope?.global === false) {
allowlist?.scope?.roles?.forEach(role => roleNames.push(role));
}
});
inheritedRoles?.forEach(role => roleNames.push(role.role_name));
Object.entries(securitySettings?.api_limits ?? {}).forEach(
([limit, value]) => {
if (limit !== 'disabled' && typeof value !== 'boolean') {
Object.keys(value?.per_role ?? {}).forEach(role =>
roleNames.push(role)
);
}
}
);
securitySettings?.graphql_schema_introspection?.disabled_for_roles.forEach(
role => roleNames.push(role)
);
return Array.from(new Set(roleNames));
};
export const useRolePermissions = ({ export const useRolePermissions = ({
dataSourceName, dataSourceName,
table, table,
}: UseRolePermissionsArgs) => { }: UseRolePermissionsArgs) => {
const httpClient = useHttpClient(); const httpClient = useHttpClient();
return useQuery< return useQuery<
{ supportedQueries: QueryType[]; rolePermissions: RolePermission[] }, { supportedQueries: QueryType[]; rolePermissions: RolePermission[] },
Error Error
>({ >({
queryKey: [dataSourceName, 'permissionsTable'], queryKey: [dataSourceName, 'permissionsTable', JSON.stringify(table)],
queryFn: async () => { queryFn: async () => {
// find the specific metadata table const metadata = await exportMetadata({ httpClient });
const metadataTable = await getMetadataTable({
httpClient,
dataSourceName,
table,
});
// get table columns for metadata table from db introspection // get table columns for metadata table from db introspection
const tableColumns = await DataSource(httpClient).getTableColumns({ const tableColumns = await DataSource(httpClient).getTableColumns({
dataSourceName, dataSourceName,
table, table,
}); });
// find the specific metadata table
const metadataTable = getMetadataTable({
metadata,
dataSourceName,
table,
});
// get all roles
const roles = getRoles(metadata);
// // extract the permissions data in the format required for the table // // extract the permissions data in the format required for the table
const rolePermissions = await createRoleTableData({ const rolePermissions = createRoleTableData({
metadataTable, metadataTable,
tableColumns, tableColumns,
allRoles: roles ?? [],
}); });
return { rolePermissions, supportedQueries }; return { rolePermissions, supportedQueries };