mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
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:
parent
5278e4ed9f
commit
89f639b40b
@ -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} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -17,7 +17,6 @@ export const Primary: Story<BulkDeleteProps> = args => {
|
||||
return <BulkDelete {...args} />;
|
||||
};
|
||||
Primary.args = {
|
||||
currentSource: 'postgres',
|
||||
dataSourceName: 'default',
|
||||
roles: ['user'],
|
||||
handleClose: () => {},
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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) => (
|
||||
|
@ -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) => (
|
||||
|
@ -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>
|
||||
|
@ -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) => (
|
||||
|
@ -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}}',
|
||||
};
|
@ -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;
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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 = {
|
||||
|
@ -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 {
|
||||
|
@ -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> = {
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -1,2 +1 @@
|
||||
export * from './useDefaultValues';
|
||||
export * from './useFormData/useFormData';
|
||||
export * from './useFormData';
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './useDefaultValues';
|
@ -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,
|
||||
};
|
@ -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);
|
||||
});
|
@ -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,
|
||||
});
|
||||
};
|
@ -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>;
|
||||
};
|
@ -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(
|
@ -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),
|
||||
};
|
||||
};
|
@ -1 +1,2 @@
|
||||
export * from './useFormData';
|
||||
export * from './createDefaultValues';
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
@ -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'],
|
||||
};
|
||||
|
@ -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 || ''}
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
|
@ -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" />
|
||||
|
@ -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 };
|
||||
|
Loading…
Reference in New Issue
Block a user