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 { DatabaseRelationshipsContainer } from '@/features/DataRelationships';
import { getTableName } from '@/features/DataSource';
import { PermissionsTab } from '@/features/PermissionsTab';
import { Table } from '@/features/MetadataAPI';
import { IndicatorCard } from '@/new-components/IndicatorCard';
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 = (
dataSourceName: string,
table: Table,
@ -65,7 +56,7 @@ const availableTabs = (
{
value: '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} />;
};
Primary.args = {
currentSource: 'postgres',
dataSourceName: 'default',
roles: ['user'],
handleClose: () => {},

View File

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

View File

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

View File

@ -1,11 +1,12 @@
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 { 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 {
AggregationSection,
BackendOnlySection,
@ -15,10 +16,9 @@ import {
RowPermissionsSectionWrapper,
} from './components';
import { useFormData, useDefaultValues, useUpdatePermissions } from './hooks';
import { useFormData, useUpdatePermissions } from './hooks';
export interface PermissionsFormProps {
currentSource: string;
dataSourceName: string;
table: unknown;
queryType: QueryType;
@ -29,7 +29,6 @@ export interface PermissionsFormProps {
export const PermissionsForm = (props: PermissionsFormProps) => {
const {
currentSource,
dataSourceName,
table,
queryType,
@ -38,34 +37,15 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
handleClose,
} = props;
// loads all information about selected table
// e.g. column names, supported queries etc.
const {
data,
isLoading: loadingFormData,
isError: formDataError,
} = useFormData({
const { data, isError, isLoading } = useFormData({
dataSourceName,
table,
queryType,
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
const { updatePermissions, deletePermissions } = useUpdatePermissions({
currentSource,
dataSourceName,
table,
queryType,
@ -73,8 +53,8 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
accessType,
});
const handleSubmit = async (formData: Record<string, unknown>) => {
await updatePermissions.submit(formData as FormOutput);
const handleSubmit = async (formData: PermissionsSchema) => {
await updatePermissions.submit(formData);
handleClose();
};
@ -83,43 +63,49 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
handleClose();
};
const isError = formDataError || defaultValuesError;
const isSubmittingError =
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
const rowPermissions = queryType === 'update' ? ['pre', 'post'] : [queryType];
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
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
if (isLoading) {
return <div>Loading...</div>;
if (isLoading || !data) {
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 (
<Form
key={`${queryType}-${roleName}-${accessType}`}
key={key}
onSubmit={handleSubmit}
schema={schema}
options={{ defaultValues }}
>
{options => {
console.log('form values---->', options.getValues());
console.log('form errors---->', options.formState.errors);
const filterType = options.getValues('filterType');
if (Object.keys(options.formState.errors)?.length) {
console.error('form errors:', options.formState.errors);
}
return (
<div className="bg-white rounded p-md border border-gray-300">
<div className="pb-4 flex items-center gap-4">
@ -158,6 +144,7 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
queryType === 'update' ? permissionName : undefined
}
allRowChecks={allRowChecks || []}
dataSourceName={dataSourceName}
/>
</React.Fragment>
))}
@ -167,14 +154,14 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
<ColumnPermissionsSection
roleName={roleName}
queryType={queryType}
columns={data?.columns}
columns={formData?.columns}
/>
)}
{['insert', 'update'].includes(queryType) && (
<ColumnPresetsSection
queryType={queryType}
columns={data?.columns}
columns={formData?.columns}
/>
)}
@ -201,6 +188,12 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
<Button
type="submit"
mode="primary"
title={
filterType === 'none'
? 'You must select an option for row permissions'
: 'Submit'
}
disabled={filterType === 'none'}
isLoading={updatePermissions.isLoading}
>
Save Permissions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -99,14 +99,15 @@ export const ColumnPermissionsSection: React.FC<ColumnPermissionsSectionProps> =
</p>
</div>
<fieldset className="flex gap-4">
<fieldset className="flex gap-4 flex-wrap">
{columns?.map(fieldName => (
<label key={fieldName} className="flex gap-2 items-center">
<input
type="checkbox"
title={disabled ? 'Set a row permission first' : ''}
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}`)}
/>
<i>{fieldName}</i>

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { Table } from '@/features/MetadataAPI';
import React from 'react';
import AceEditor from 'react-ace';
@ -16,9 +17,16 @@ interface Props {
* e.g. ['filter', 'Title', '_eq'] would be registered as 'filter.Title._eq'
*/
nesting: string[];
table: Table;
dataSourceName: string;
}
export const RowPermissionBuilder = ({ tableName, nesting }: Props) => {
export const RowPermissionBuilder = ({
tableName,
nesting,
table,
dataSourceName,
}: Props) => {
const { watch } = useFormContext();
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
const value = watch(nesting[0]);
const json = createDisplayJson(value || {});
// const { data: tableConfig } = useTableConfiguration({
// table,
// dataSourceName,
// });
if (!schema) {
return null;
}
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">
<AceEditor
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">
<JsonItem text="{" />
<div className="py-2">
<Builder tableName={tableName} nesting={nesting} schema={schema} />
<Builder
tableName={tableName}
nesting={nesting}
schema={schema}
dataSourceName={dataSourceName}
table={table}
/>
</div>
<JsonItem text="}" />
</div>

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import React from 'react';
import { buildClientSchema, GraphQLSchema, IntrospectionQuery } from 'graphql';
import { useHttpClient } from '@/features/Network';
import { runIntrospectionQuery } from '@/features/DataSource';
import { createDefaultValues, getAllColumnsAndOperators } from '../utils';
import { exportMetadata, runIntrospectionQuery } from '@/features/DataSource';
import { Table } from '@/features/MetadataAPI';
import { useQuery } from 'react-query';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { getAllColumnsAndOperators } from '../utils';
/**
*
@ -28,9 +30,33 @@ export const useIntrospectSchema = () => {
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 {
tableName: string;
schema?: GraphQLSchema;
table: Table;
dataSourceName: string;
}
/**
@ -38,7 +64,11 @@ interface Args {
* get all boolOperators, columns and relationships
* 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)
return {
data: {
@ -47,21 +77,7 @@ export const useData = ({ tableName, schema }: Args) => {
relationships: [],
},
};
const data = getAllColumnsAndOperators({ tableName, schema });
return { data };
};
interface A {
tableName: string;
existingPermission: Record<string, any>;
}
export const useCreateRowPermissionsDefaults = () => {
const { data: schema } = useIntrospectSchema();
const fetchDefaults = async ({ tableName, existingPermission }: A) => {
createDefaultValues({ tableName, schema, existingPermission });
};
return fetchDefaults;
const data = getAllColumnsAndOperators({ tableName, schema, tableConfig });
return { data, tableConfig };
};

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { MetadataTable } from '@/features/MetadataAPI';
import {
GraphQLFieldMap,
GraphQLInputFieldMap,
@ -71,16 +72,20 @@ interface GetColumnOperatorsArgs {
tableName: string;
columnName: string;
schema: GraphQLSchema;
tableConfig: MetadataTable['configuration'];
}
export const getColumnOperators = ({
tableName,
columnName,
schema,
tableConfig,
}: GetColumnOperatorsArgs) => {
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)) {
const colType = schema.getType(col.type.name);
@ -156,14 +161,32 @@ export const findColumnOperator = ({
interface Args {
tableName: string;
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
*/
export const getAllColumnsAndOperators = ({ tableName, schema }: Args) => {
const fields = getFields(tableName, schema);
export const getAllColumnsAndOperators = ({
tableName,
schema,
tableConfig,
}: Args) => {
const metadataTableName = tableConfig?.custom_name ?? tableName;
const fields = getFields(metadataTableName, schema);
const boolOperators = getBoolOperators();
const columns = getColumns(fields);
const relationships = getRelationships(fields);
@ -174,7 +197,7 @@ export const getAllColumnsAndOperators = ({ tableName, schema }: Args) => {
meta: null,
}));
const colMap = columns.map(column => ({
name: column.name,
name: getOriginalTableNameFromCustomName(tableConfig, column.name),
kind: 'column',
meta: column,
}));
@ -183,6 +206,5 @@ export const getAllColumnsAndOperators = ({ tableName, schema }: Args) => {
kind: 'relationship',
meta: relationship,
}));
return { boolOperators: boolMap, columns: colMap, relationships: relMap };
};

View File

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

View File

@ -1,5 +1,7 @@
import { TableColumn } from '@/features/DataSource';
import { Metadata } from '@/features/MetadataAPI';
import { createDefaultValues } from '../createDefaultValues';
import { schema } from '../../../../components/RowPermissionsBuilder/mocks';
interface Input {
dataSourceName: string;
@ -26,9 +28,14 @@ const metadata: Metadata = {
{
role: 'user',
permission: {
columns: ['ArtistId', 'Name'],
filter: {},
columns: ['Name'],
filter: {
ArtistId: {
_gt: 5,
},
},
allow_aggregations: true,
limit: 3,
},
},
],
@ -54,7 +61,7 @@ const metadata: Metadata = {
},
};
export const input: Input = {
export const formDataInput: Input = {
dataSourceName: 'sqlite',
table: ['Artist'],
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 { input } from './mock';
import { createFormData } from './createFormData';
import { createDefaultValues } from './createDefaultValues';
import { defaultValuesInput, formDataInput } from './mock';
const mockResult: ReturnType<typeof createFormData> = {
const formDataMockResult: ReturnType<typeof createFormData> = {
columns: ['ArtistId', 'Name'],
roles: ['user'],
supportedQueries: ['insert', 'select', 'update', 'delete'],
supportedQueries: ['select'],
tableNames: [['Album'], ['Artist']],
};
test('returns correctly formatted formData', () => {
const result = createFormData(input);
expect(result).toEqual(mockResult);
const result = createFormData(formDataInput);
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 { buildClientSchema } from 'graphql';
import { DataSource, exportMetadata, TableColumn } from '@/features/DataSource';
import {
DataSource,
exportMetadata,
runIntrospectionQuery,
} from '@/features/DataSource';
import { useHttpClient } from '@/features/Network';
import { Metadata, MetadataTable } from '@/features/MetadataAPI';
import { isPermission } from '../utils';
import { createDefaultValues } from './createDefaultValues';
import { createFormData } from './createFormData';
type Operation = 'insert' | 'select' | 'update' | 'delete';
const supportedQueries: Operation[] = ['insert', 'select', 'update', 'delete'];
export const getAllowedFilterKeys = (
query: 'insert' | 'select' | 'update' | 'delete'
): ('check' | 'filter')[] => {
switch (query) {
case 'insert':
return ['check'];
case 'update':
return ['filter', 'check'];
default:
return ['filter'];
}
};
type GetMetadataTableArgs = {
dataSourceName: string;
table: unknown;
metadata: Metadata;
};
const getMetadataTable = (args: GetMetadataTableArgs) => {
const { dataSourceName, table, metadata } = args;
const trackedTables = metadata.metadata?.sources?.find(
source => source.name === dataSourceName
)?.tables;
const selectedTable = trackedTables?.find(
trackedTable => JSON.stringify(trackedTable.table) === JSON.stringify(table)
);
// find selected table
return {
table: selectedTable,
tables: trackedTables,
// for gdc tables will be an array of strings so this needs updating
tableNames: trackedTables?.map(each => each.table),
};
};
const getRoles = (metadataTables?: MetadataTable[]) => {
// go through all tracked tables
const res = metadataTables?.reduce<Set<string>>((acc, each) => {
// go through all permissions
Object.entries(each).forEach(([key, value]) => {
const props = { key, value };
// check object key of metadata is a permission
if (isPermission(props)) {
// add each role from each permission to the set
props.value.forEach(permission => {
acc.add(permission.role);
});
}
});
return acc;
}, new Set());
return Array.from(res || []);
};
interface CreateFormDataArgs {
dataSourceName: string;
table: unknown;
metadata: Metadata;
tableColumns: TableColumn[];
}
export const createFormData = (props: CreateFormDataArgs) => {
const { dataSourceName, table, metadata, tableColumns } = props;
// find the specific metadata table
const metadataTable = getMetadataTable({
dataSourceName,
table,
metadata,
});
const roles = getRoles(metadataTable.tables);
return {
roles,
supportedQueries,
tableNames: metadataTable.tableNames,
columns: tableColumns?.map(({ name }) => name),
};
};
export type UseFormDataArgs = {
export type Args = {
dataSourceName: string;
table: unknown;
roleName: string;
@ -104,21 +19,32 @@ export type UseFormDataArgs = {
};
type ReturnValue = {
roles: string[];
supportedQueries: Operation[];
tableNames: unknown;
columns: string[];
formData: ReturnType<typeof createFormData>;
defaultValues: ReturnType<typeof createDefaultValues>;
};
/**
*
* 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();
return useQuery<ReturnValue, Error>({
queryKey: [dataSourceName, 'permissionFormData'],
queryKey: [
dataSourceName,
'permissionFormData',
JSON.stringify(table),
roleName,
],
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
@ -127,12 +53,24 @@ export const useFormData = ({ dataSourceName, table }: UseFormDataArgs) => {
table,
});
return createFormData({
const defaultValues = createDefaultValues({
queryType,
roleName,
dataSourceName,
metadata,
table,
tableColumns,
schema,
});
const formData = createFormData({
dataSourceName,
table,
metadata,
tableColumns,
});
return { formData, defaultValues };
},
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 { useHttpClient } from '@/features/Network';
import { Permission, useMetadataMigration } from '@/features/MetadataAPI';
import { useFireNotification } from '@/new-components/Notifications';
import { api } from '../../api';
import { QueryType } from '../../types';
@ -38,6 +39,7 @@ const getMetadataTable = async ({
JSON.stringify(trackedTable.table) === JSON.stringify(table)
),
resourceVersion: resource_version,
driver: currentMetadataSource.kind,
};
};
@ -69,16 +71,11 @@ const isPermission = (props: {
} => props.key in keyToPermission;
interface Args {
currentSource: string;
dataSourceName: string;
table: unknown;
}
export const useBulkDeletePermissions = ({
currentSource,
dataSourceName,
table,
}: Args) => {
export const useBulkDeletePermissions = ({ dataSourceName, table }: Args) => {
const {
mutateAsync,
isLoading: mutationLoading,
@ -88,9 +85,10 @@ export const useBulkDeletePermissions = ({
const httpClient = useHttpClient();
const queryClient = useQueryClient();
const { fireNotification } = useFireNotification();
const submit = async (roles: string[]) => {
const { metadataTable, resourceVersion } = await getMetadataTable({
const { metadataTable, resourceVersion, driver } = await getMetadataTable({
dataSourceName,
table,
httpClient,
@ -129,18 +127,48 @@ export const useBulkDeletePermissions = ({
);
const body = api.createBulkDeleteBody({
source: currentSource,
driver,
dataSourceName,
table,
resourceVersion,
roleList,
});
await mutateAsync({
await mutateAsync(
{
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;

View File

@ -1,6 +1,7 @@
import { useQueryClient } from 'react-query';
import { useMetadataMigration } from '@/features/MetadataAPI';
import { exportMetadata } from '@/features/DataSource';
import { useFireNotification } from '@/new-components/Notifications';
import { useHttpClient } from '@/features/Network';
@ -8,14 +9,12 @@ import { QueryType } from '../../types';
import { api } from '../../api';
export interface UseDeletePermissionArgs {
currentSource: string;
dataSourceName: string;
table: unknown;
roleName: string;
}
export const useDeletePermission = ({
currentSource,
dataSourceName,
table,
roleName,
@ -23,9 +22,11 @@ export const useDeletePermission = ({
const mutate = useMetadataMigration();
const httpClient = useHttpClient();
const queryClient = useQueryClient();
const { fireNotification } = useFireNotification();
const submit = async (queries: QueryType[]) => {
const { resource_version: resourceVersion } = await exportMetadata({
const { resource_version: resourceVersion, metadata } =
await exportMetadata({
httpClient,
});
@ -34,8 +35,12 @@ export const useDeletePermission = ({
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({
currentSource,
driver,
dataSourceName,
table,
roleName,
@ -43,11 +48,41 @@ export const useDeletePermission = ({
queries,
});
await mutate.mutateAsync({
await mutate.mutateAsync(
{
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;

View File

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

View File

@ -4,7 +4,6 @@ import { useDeletePermission } from './useDeletePermission';
import { AccessType, QueryType } from '../../types';
export interface UseUpdatePermissionsArgs {
currentSource: string;
dataSourceName: string;
table: unknown;
roleName: string;
@ -13,7 +12,6 @@ export interface UseUpdatePermissionsArgs {
}
export const useUpdatePermissions = ({
currentSource,
dataSourceName,
table,
roleName,
@ -21,7 +19,6 @@ export const useUpdatePermissions = ({
accessType,
}: UseUpdatePermissionsArgs) => {
const updatePermissions = useSubmitForm({
currentSource,
dataSourceName,
table,
roleName,
@ -30,7 +27,6 @@ export const useUpdatePermissions = ({
});
const deletePermissions = useDeletePermission({
currentSource,
dataSourceName,
table,
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 AccessType =
| 'fullAccess'
| 'noAccess'
| 'partialAccess'
| 'partialAccessWarning';
export type FormOutput = z.infer<typeof schema>;

View File

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

View File

@ -1,14 +1,30 @@
import { Permission } from '@/dataSources/types';
import { Permission } from '@/features/MetadataAPI';
interface Args {
permissions?: Permission[];
roleName: string;
}
export const permissionToKey = {
insert: 'insert_permissions',
select: 'select_permissions',
update: 'update_permissions',
delete: 'delete_permissions',
} as const;
export const getCurrentRole = ({ permissions, roleName }: Args) => {
const rolePermissions = permissions?.find(
({ role_name }) => role_name === roleName
);
export const metadataPermissionKeys = [
'insert_permissions',
'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 { Story, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
@ -16,7 +17,6 @@ export const Primary: Story<PermissionsTabProps> = args => (
<PermissionsTab {...args} />
);
Primary.args = {
currentSource: 'postgres',
dataSourceName: 'default',
table: {
name: 'user',
@ -28,7 +28,6 @@ export const GDC: Story<PermissionsTabProps> = args => (
<PermissionsTab {...args} />
);
GDC.args = {
currentSource: 'sqlite',
dataSourceName: 'sqlite',
table: ['Artist'],
};
@ -41,7 +40,6 @@ export const GDCNoMocks: Story<PermissionsTabProps> = args => (
<PermissionsTab {...args} />
);
GDCNoMocks.args = {
currentSource: 'sqlite',
dataSourceName: 'sqlite',
table: ['Artist'],
};

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { AxiosInstance } from 'axios';
import isEqual from 'lodash.isequal';
import { DataSource, exportMetadata } from '@/features/DataSource';
import type { TableColumn } from '@/features/DataSource';
import { useQuery } from 'react-query';
import { useHttpClient } from '@/features/Network';
import { MetadataTable, Metadata } from '@/features/MetadataAPI';
interface RolePermission {
roleName: string;
@ -95,19 +95,16 @@ const getAccessType = ({
type GetMetadataTableArgs = {
dataSourceName: string;
table: unknown;
httpClient: AxiosInstance;
metadata?: Metadata;
};
const getMetadataTable = async ({
httpClient,
const getMetadataTable = ({
metadata,
dataSourceName,
table,
}: GetMetadataTableArgs) => {
// get all metadata
const { metadata } = await exportMetadata({ httpClient });
// find current source
const currentMetadataSource = metadata?.sources?.find(
const currentMetadataSource = metadata?.metadata?.sources?.find(
source => source.name === dataSourceName
);
@ -140,14 +137,16 @@ const isPermission = (props: {
type CreateRoleTableDataArgs = {
metadataTable: any;
tableColumns?: TableColumn[];
allRoles: string[];
};
type RoleToPermissionsMap = Record<string, Partial<Record<QueryType, Access>>>;
const createRoleTableData = async ({
const createRoleTableData = ({
metadataTable,
tableColumns,
}: CreateRoleTableDataArgs): Promise<RolePermission[]> => {
allRoles,
}: CreateRoleTableDataArgs): RolePermission[] => {
if (!metadataTable) return [];
// create object with key of role
// and value describing permissions attached to that role
@ -178,8 +177,22 @@ const createRoleTableData = async ({
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
const permissions = Object.entries(roleToPermissionsMap).map(
const permissions = Object.entries(allRolesToPermissionsMap).map(
([roleName, permission]) => {
const permissionEntries = Object.entries(permission) as [
QueryType,
@ -246,34 +259,113 @@ type UseRolePermissionsArgs = {
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 = ({
dataSourceName,
table,
}: UseRolePermissionsArgs) => {
const httpClient = useHttpClient();
return useQuery<
{ supportedQueries: QueryType[]; rolePermissions: RolePermission[] },
Error
>({
queryKey: [dataSourceName, 'permissionsTable'],
queryKey: [dataSourceName, 'permissionsTable', JSON.stringify(table)],
queryFn: async () => {
// find the specific metadata table
const metadataTable = await getMetadataTable({
httpClient,
dataSourceName,
table,
});
const metadata = await exportMetadata({ httpClient });
// get table columns for metadata table from db introspection
const tableColumns = await DataSource(httpClient).getTableColumns({
dataSourceName,
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
const rolePermissions = await createRoleTableData({
const rolePermissions = createRoleTableData({
metadataTable,
tableColumns,
allRoles: roles ?? [],
});
return { rolePermissions, supportedQueries };