From 89f639b40be58bac5286f538b35f2a28fd97d74f Mon Sep 17 00:00:00 2001 From: Matt Hardman Date: Wed, 2 Nov 2022 14:53:55 -0500 Subject: [PATCH] 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 --- .../features/Data/ManageTable/ManageTable.tsx | 13 +- .../PermissionsForm/BulkDelete.stories.tsx | 1 - .../features/PermissionsForm/BulkDelete.tsx | 3 - .../PermissionsForm.stories.tsx | 8 +- .../PermissionsForm/PermissionsForm.tsx | 81 +++++----- .../src/features/PermissionsForm/api/api.ts | 34 ++--- .../api/createInsertArgs.test.ts | 4 +- .../src/features/PermissionsForm/api/utils.ts | 44 ++++-- .../components/Aggregation.stories.tsx | 3 +- .../components/Aggregation.tsx | 5 +- .../components/BackendOnly.stories.tsx | 3 +- .../components/ClonePermissions.stories.tsx | 3 +- .../components/ColumnPermissions.stories.tsx | 2 +- .../components/ColumnPermissions.tsx | 5 +- .../components/ColumnPresets.stories.tsx | 2 +- .../components/JSONEditor.stories.tsx | 14 -- .../PermissionsForm/components/JSONEditor.tsx | 73 --------- .../components/RowPermissions.stories.tsx | 7 +- .../components/RowPermissions.tsx | 110 +++++++++----- .../RowPermissionBuilder.stories.tsx | 9 +- .../RowPermissionBuilder.tsx | 24 ++- .../components/Builder.tsx | 23 ++- .../components/FieldArray.tsx | 42 +++++- .../components/RenderFormElement.tsx | 14 +- .../RowPermissionsBuilder/hooks/index.ts | 58 ++++--- .../utils/createDefaultValues.test.ts | 4 + .../utils/createDefaultValues.ts | 12 +- .../utils/graphQlParsers.test.ts | 8 +- .../utils/graphqlParsers.ts | 34 ++++- .../hooks/dataFetchingHooks/index.ts | 3 +- .../useDefaultValues/index.ts | 1 - .../useDefaultValues/mock/index.ts | 48 ------ .../useDefaultValues/useDefaultValues.test.ts | 36 ----- .../useDefaultValues/useDefaultValues.tsx | 141 ------------------ .../useFormData/createDefaultValues/index.ts | 111 ++++++++++++++ .../createDefaultValues}/utils.ts | 24 ++- .../useFormData/createFormData/index.ts | 93 ++++++++++++ .../dataFetchingHooks/useFormData/index.ts | 1 + .../useFormData/mock/index.ts | 44 +++++- .../useFormData/useFormData.test.ts | 44 +++++- .../useFormData/useFormData.tsx | 140 +++++------------ .../hooks/dataFetchingHooks/utils.ts | 29 ---- .../hooks/submitHooks/useBulkDelete.tsx | 52 +++++-- .../hooks/submitHooks/useDeletePermission.tsx | 55 +++++-- .../hooks/submitHooks/useSubmitForm.tsx | 117 +++++++-------- .../submitHooks/useUpdatePermissions.tsx | 4 - .../features/PermissionsForm/types/index.ts | 5 - .../PermissionsForm/utils/formSchema.ts | 8 +- .../PermissionsForm/utils/functions.ts | 38 +++-- .../PermissionsTab/PermissionsTab.stories.tsx | 4 +- .../PermissionsTab/PermissionsTab.tsx | 5 +- .../PermissionsTable.stories.tsx | 2 +- .../PermissionsTable/PermissionsTable.tsx | 20 ++- .../components/PermissionsLegend.tsx | 3 - .../PermissionsTable/hooks/usePermissions.tsx | 132 +++++++++++++--- 55 files changed, 1004 insertions(+), 799 deletions(-) delete mode 100644 console/src/features/PermissionsForm/components/JSONEditor.stories.tsx delete mode 100644 console/src/features/PermissionsForm/components/JSONEditor.tsx delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/index.ts delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/mock/index.ts delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.test.ts delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.tsx create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createDefaultValues/index.ts rename console/src/features/PermissionsForm/hooks/dataFetchingHooks/{useDefaultValues => useFormData/createDefaultValues}/utils.ts (93%) create mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createFormData/index.ts delete mode 100644 console/src/features/PermissionsForm/hooks/dataFetchingHooks/utils.ts diff --git a/console/src/features/Data/ManageTable/ManageTable.tsx b/console/src/features/Data/ManageTable/ManageTable.tsx index 3066688830e..7182c7eb4b9 100644 --- a/console/src/features/Data/ManageTable/ManageTable.tsx +++ b/console/src/features/Data/ManageTable/ManageTable.tsx @@ -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 ( -
- - Feature not implemented - -
- ); -}; - const availableTabs = ( dataSourceName: string, table: Table, @@ -65,7 +56,7 @@ const availableTabs = ( { value: 'permissions', label: 'Permissions', - content: , + content: , }, ]; diff --git a/console/src/features/PermissionsForm/BulkDelete.stories.tsx b/console/src/features/PermissionsForm/BulkDelete.stories.tsx index ebd35a86b12..8203788bc1f 100644 --- a/console/src/features/PermissionsForm/BulkDelete.stories.tsx +++ b/console/src/features/PermissionsForm/BulkDelete.stories.tsx @@ -17,7 +17,6 @@ export const Primary: Story = args => { return ; }; Primary.args = { - currentSource: 'postgres', dataSourceName: 'default', roles: ['user'], handleClose: () => {}, diff --git a/console/src/features/PermissionsForm/BulkDelete.tsx b/console/src/features/PermissionsForm/BulkDelete.tsx index f45276e0056..144af9e758e 100644 --- a/console/src/features/PermissionsForm/BulkDelete.tsx +++ b/console/src/features/PermissionsForm/BulkDelete.tsx @@ -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 = ({ - currentSource, dataSourceName, roles, table, handleClose, }) => { const { submit, isLoading, isError } = useBulkDeletePermissions({ - currentSource, dataSourceName, table, }); diff --git a/console/src/features/PermissionsForm/PermissionsForm.stories.tsx b/console/src/features/PermissionsForm/PermissionsForm.stories.tsx index 231a9c2c9aa..c9e883eeadd 100644 --- a/console/src/features/PermissionsForm/PermissionsForm.stories.tsx +++ b/console/src/features/PermissionsForm/PermissionsForm.stories.tsx @@ -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 = 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 = args => ( ); GDCSelect.args = { - currentSource: 'sqlite', dataSourceName: 'sqlite', queryType: 'select', table: ['Artist'], roleName, handleClose: () => {}, }; -GDCSelect.parameters = Insert.parameters; export const Update: Story = args => ( @@ -61,7 +57,6 @@ Update.args = { ...Insert.args, queryType: 'update', }; -Update.parameters = Insert.parameters; export const Delete: Story = args => ( @@ -70,4 +65,3 @@ Delete.args = { ...Insert.args, queryType: 'delete', }; -Delete.parameters = Insert.parameters; diff --git a/console/src/features/PermissionsForm/PermissionsForm.tsx b/console/src/features/PermissionsForm/PermissionsForm.tsx index cdcc53bdc39..5d21a2876a5 100644 --- a/console/src/features/PermissionsForm/PermissionsForm.tsx +++ b/console/src/features/PermissionsForm/PermissionsForm.tsx @@ -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) => { - 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
Error submitting form
; + return ( + Error submitting form + ); } // these will be replaced by components once spec is decided if (isError) { - return
Error loading form data
; + return ( + Error fetching form data + ); } // these will be replaced by components once spec is decided - if (isLoading) { - return
Loading...
; + if (isLoading || !data) { + return Loading...; } + 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 (
{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 (
@@ -158,6 +144,7 @@ export const PermissionsForm = (props: PermissionsFormProps) => { queryType === 'update' ? permissionName : undefined } allRowChecks={allRowChecks || []} + dataSourceName={dataSourceName} /> ))} @@ -167,14 +154,14 @@ export const PermissionsForm = (props: PermissionsFormProps) => { )} {['insert', 'update'].includes(queryType) && ( )} @@ -201,6 +188,12 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
diff --git a/console/src/features/PermissionsForm/components/BackendOnly.stories.tsx b/console/src/features/PermissionsForm/components/BackendOnly.stories.tsx index 68f8ad54ed3..686423aa666 100644 --- a/console/src/features/PermissionsForm/components/BackendOnly.stories.tsx +++ b/console/src/features/PermissionsForm/components/BackendOnly.stories.tsx @@ -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 diff --git a/console/src/features/PermissionsForm/components/ClonePermissions.stories.tsx b/console/src/features/PermissionsForm/components/ClonePermissions.stories.tsx index 3b9de038610..c34e210a447 100644 --- a/console/src/features/PermissionsForm/components/ClonePermissions.stories.tsx +++ b/console/src/features/PermissionsForm/components/ClonePermissions.stories.tsx @@ -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) => ( diff --git a/console/src/features/PermissionsForm/components/ColumnPermissions.stories.tsx b/console/src/features/PermissionsForm/components/ColumnPermissions.stories.tsx index 13c5df00367..46836e68a0d 100644 --- a/console/src/features/PermissionsForm/components/ColumnPermissions.stories.tsx +++ b/console/src/features/PermissionsForm/components/ColumnPermissions.stories.tsx @@ -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) => ( diff --git a/console/src/features/PermissionsForm/components/ColumnPermissions.tsx b/console/src/features/PermissionsForm/components/ColumnPermissions.tsx index 4f71c809cb8..c6d7b94c730 100644 --- a/console/src/features/PermissionsForm/components/ColumnPermissions.tsx +++ b/console/src/features/PermissionsForm/components/ColumnPermissions.tsx @@ -99,14 +99,15 @@ export const ColumnPermissionsSection: React.FC =

-
+
{columns?.map(fieldName => ( {selectedSection === SelectedSection.NoChecks && ( -
- + setValue(rowPermissionsCheckType, SelectedSection.Custom) } - initData="{}" + editorProps={{ $blockScrolling: true }} />
)} @@ -175,14 +200,20 @@ export const RowPermissionsSection: React.FC = ({ {selectedSection === query && ( -
- { - setValue(rowPermissionsCheckType, SelectedSection.Custom); - setValue(rowPermissions, output); - }} - initData="" +
+ + setValue(rowPermissionsCheckType, SelectedSection.Custom) + } + editorProps={{ $blockScrolling: true }} />
)} @@ -203,7 +234,16 @@ export const RowPermissionsSection: React.FC = ({ {selectedSection === SelectedSection.Custom && (
- + {!isLoading && tableName ? ( + + ) : ( + <>Loading... + )}
)}
diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/RowPermissionBuilder.stories.tsx b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/RowPermissionBuilder.stories.tsx index 4f2aebb52f8..4a1b02583f2 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/RowPermissionBuilder.stories.tsx +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/RowPermissionBuilder.stories.tsx @@ -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} diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/RowPermissionBuilder.tsx b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/RowPermissionBuilder.tsx index abf037ad603..afbf8dbf0ff 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/RowPermissionBuilder.tsx +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/RowPermissionBuilder.tsx @@ -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 ( -
+
{
- +
diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/Builder.tsx b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/Builder.tsx index 44506d48f94..0e302c0f43b 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/Builder.tsx +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/Builder.tsx @@ -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 = e => { @@ -216,6 +223,8 @@ export const Builder = (props: Props) => { handleColumnChange={handleColumnChange} nesting={nesting} schema={schema} + table={table} + dataSourceName={dataSourceName} />
); diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/FieldArray.tsx b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/FieldArray.tsx index d6d19127764..3aa39a151d3 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/FieldArray.tsx +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/FieldArray.tsx @@ -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['append']; schema: GraphQLSchema; + dataSourceName: string; + table: Table; + // tableConfig: ReturnType['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} />
@@ -60,6 +78,9 @@ export const FieldArrayElement = (props: FieldArrayElementProps) => { tableName={tableName} nesting={[...nesting, index.toString()]} schema={schema} + dataSourceName={dataSourceName} + table={table} + // tableConfig={tableConfig} />
@@ -70,10 +91,20 @@ interface Props { tableName: string; nesting: string[]; schema: GraphQLSchema; + dataSourceName: string; + table: Table; + // tableConfig: ReturnType['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} /> ))} diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/RenderFormElement.tsx b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/RenderFormElement.tsx index b391127780f..b4ed55ab3ef 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/RenderFormElement.tsx +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/components/RenderFormElement.tsx @@ -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} /> @@ -134,6 +140,8 @@ export const RenderFormElement = (props: Props) => { tableName={tableName} nesting={[...nesting, dropDownState.name]} schema={schema} + table={table} + dataSourceName={dataSourceName} /> @@ -146,6 +154,8 @@ export const RenderFormElement = (props: Props) => { tableName={tableName} nesting={[...nesting, dropDownState.name]} schema={schema} + table={table} + dataSourceName={dataSourceName} /> )} diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/hooks/index.ts b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/hooks/index.ts index baef8d58213..6aa2986ea59 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/hooks/index.ts +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/hooks/index.ts @@ -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; -} - -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 }; }; diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/createDefaultValues.test.ts b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/createDefaultValues.test.ts index 99844616f49..32366b69b8c 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/createDefaultValues.test.ts +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/createDefaultValues.test.ts @@ -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 = { diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/createDefaultValues.ts b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/createDefaultValues.ts index 9e0ca6da432..7e447c33441 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/createDefaultValues.ts +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/createDefaultValues.ts @@ -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; + tableConfig: MetadataTable['configuration']; } export const createOperatorsObject = ({ tableName, schema, existingPermission, + tableConfig, }: CreateOperatorsArgs): Record => { 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; + 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 { diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/graphQlParsers.test.ts b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/graphQlParsers.test.ts index 53e0d82ea08..46fc4c55acf 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/graphQlParsers.test.ts +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/graphQlParsers.test.ts @@ -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 = [ @@ -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 = { diff --git a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/graphqlParsers.ts b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/graphqlParsers.ts index 87d5e622a9c..45bc12dc860 100644 --- a/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/graphqlParsers.ts +++ b/console/src/features/PermissionsForm/components/RowPermissionsBuilder/utils/graphqlParsers.ts @@ -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 }; }; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/index.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/index.ts index 988066267e0..1c112501ef3 100644 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/index.ts +++ b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/index.ts @@ -1,2 +1 @@ -export * from './useDefaultValues'; -export * from './useFormData/useFormData'; +export * from './useFormData'; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/index.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/index.ts deleted file mode 100644 index 36686cc09c2..00000000000 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useDefaultValues'; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/mock/index.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/mock/index.ts deleted file mode 100644 index 7468d012c18..00000000000 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/mock/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { createDefaultValues } from '../..'; -import { schema } from '../../../../components/RowPermissionsBuilder/mocks'; - -export const input: Parameters[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, -}; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.test.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.test.ts deleted file mode 100644 index 992588ee08c..00000000000 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createDefaultValues } from './useDefaultValues'; -import { input } from './mock'; - -const mockResult: ReturnType = { - 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); -}); diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.tsx b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.tsx deleted file mode 100644 index 2fb51d13dcf..00000000000 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/useDefaultValues.tsx +++ /dev/null @@ -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 => { - const httpClient = useHttpClient(); - return useQuery({ - 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, - }); -}; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createDefaultValues/index.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createDefaultValues/index.ts new file mode 100644 index 00000000000..cebc8ebb5ee --- /dev/null +++ b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createDefaultValues/index.ts @@ -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; +}; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/utils.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createDefaultValues/utils.ts similarity index 93% rename from console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/utils.ts rename to console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createDefaultValues/utils.ts index d1a4076924f..8d383fd5f9e 100644 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useDefaultValues/utils.ts +++ b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createDefaultValues/utils.ts @@ -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 | 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( diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createFormData/index.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createFormData/index.ts new file mode 100644 index 00000000000..524f7f939c4 --- /dev/null +++ b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/createFormData/index.ts @@ -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>((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), + }; +}; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/index.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/index.ts index 1c112501ef3..b99545bbca5 100644 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/index.ts +++ b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/index.ts @@ -1 +1,2 @@ export * from './useFormData'; +export * from './createDefaultValues'; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/mock/index.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/mock/index.ts index 8cfe809361b..f9e48621218 100644 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/mock/index.ts +++ b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/mock/index.ts @@ -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[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, +}; diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.test.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.test.ts index 6a5c5f8901d..fab114483cd 100644 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.test.ts +++ b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.test.ts @@ -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 = { +const formDataMockResult: ReturnType = { 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 = { + 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); }); diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.tsx b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.tsx index 24977775396..fdd6cedc774 100644 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.tsx +++ b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/useFormData/useFormData.tsx @@ -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>((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; + defaultValues: ReturnType; }; /** * * 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({ - 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, }); diff --git a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/utils.ts b/console/src/features/PermissionsForm/hooks/dataFetchingHooks/utils.ts deleted file mode 100644 index 283a75d8daa..00000000000 --- a/console/src/features/PermissionsForm/hooks/dataFetchingHooks/utils.ts +++ /dev/null @@ -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; diff --git a/console/src/features/PermissionsForm/hooks/submitHooks/useBulkDelete.tsx b/console/src/features/PermissionsForm/hooks/submitHooks/useBulkDelete.tsx index 1c8bcfe8b04..160effbb37f 100644 --- a/console/src/features/PermissionsForm/hooks/submitHooks/useBulkDelete.tsx +++ b/console/src/features/PermissionsForm/hooks/submitHooks/useBulkDelete.tsx @@ -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({ - query: body, - }); + 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; diff --git a/console/src/features/PermissionsForm/hooks/submitHooks/useDeletePermission.tsx b/console/src/features/PermissionsForm/hooks/submitHooks/useDeletePermission.tsx index 0f82b53326a..55a0798f43f 100644 --- a/console/src/features/PermissionsForm/hooks/submitHooks/useDeletePermission.tsx +++ b/console/src/features/PermissionsForm/hooks/submitHooks/useDeletePermission.tsx @@ -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,19 +22,25 @@ 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({ - httpClient, - }); + const { resource_version: resourceVersion, metadata } = + await exportMetadata({ + httpClient, + }); if (!resourceVersion) { console.error('No resource version'); 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({ - query: body, - }); + 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; diff --git a/console/src/features/PermissionsForm/hooks/submitHooks/useSubmitForm.tsx b/console/src/features/PermissionsForm/hooks/submitHooks/useSubmitForm.tsx index 59f0ddf12c2..24a61d320f1 100644 --- a/console/src/features/PermissionsForm/hooks/submitHooks/useSubmitForm.tsx +++ b/console/src/features/PermissionsForm/hooks/submitHooks/useSubmitForm.tsx @@ -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({ - query: body, - }); - - await queryClient.invalidateQueries([ - dataSourceName, - 'permissionDefaultValues', - roleName, - queryType, - ]); - await queryClient.invalidateQueries([dataSourceName, 'permissionsTable']); + await mutate.mutateAsync( + { + query: body, + }, + { + onSuccess: () => { + fireNotification({ + type: 'success', + title: 'Success!', + message: 'Permissions saved successfully!', + }); + }, + onError: err => { + fireNotification({ + type: 'error', + title: 'Error!', + message: + err?.message ?? 'Something went wrong while saving permissions', + }); + }, + onSettled: () => { + queryClient.invalidateQueries(['export_metadata', 'roles']); + queryClient.invalidateQueries([ + dataSourceName, + 'permissionFormData', + JSON.stringify(table), + roleName, + ]); + queryClient.invalidateQueries([ + dataSourceName, + 'permissionsTable', + JSON.stringify(table), + ]); + }, + } + ); }; - const isLoading = mutate.isLoading || resourceVersionLoading; - const isError = mutate.isError || resourceVersionError; + const isLoading = mutate.isLoading; + const isError = mutate.isError; return { submit, diff --git a/console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.tsx b/console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.tsx index e397fb43444..50b73e5d90c 100644 --- a/console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.tsx +++ b/console/src/features/PermissionsForm/hooks/submitHooks/useUpdatePermissions.tsx @@ -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, diff --git a/console/src/features/PermissionsForm/types/index.ts b/console/src/features/PermissionsForm/types/index.ts index a44f97c7ecf..9ed1c84dc8f 100644 --- a/console/src/features/PermissionsForm/types/index.ts +++ b/console/src/features/PermissionsForm/types/index.ts @@ -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; diff --git a/console/src/features/PermissionsForm/utils/formSchema.ts b/console/src/features/PermissionsForm/utils/formSchema.ts index 55040f045f2..46d7cf77f8c 100644 --- a/console/src/features/PermissionsForm/utils/formSchema.ts +++ b/console/src/features/PermissionsForm/utils/formSchema.ts @@ -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({ diff --git a/console/src/features/PermissionsForm/utils/functions.ts b/console/src/features/PermissionsForm/utils/functions.ts index 67a97e00bf4..f84e8ab8cf8 100644 --- a/console/src/features/PermissionsForm/utils/functions.ts +++ b/console/src/features/PermissionsForm/utils/functions.ts @@ -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; diff --git a/console/src/features/PermissionsTab/PermissionsTab.stories.tsx b/console/src/features/PermissionsTab/PermissionsTab.stories.tsx index 8fa762b60ce..0f2964bf01b 100644 --- a/console/src/features/PermissionsTab/PermissionsTab.stories.tsx +++ b/console/src/features/PermissionsTab/PermissionsTab.stories.tsx @@ -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 = args => ( ); Primary.args = { - currentSource: 'postgres', dataSourceName: 'default', table: { name: 'user', @@ -28,7 +28,6 @@ export const GDC: Story = args => ( ); GDC.args = { - currentSource: 'sqlite', dataSourceName: 'sqlite', table: ['Artist'], }; @@ -41,7 +40,6 @@ export const GDCNoMocks: Story = args => ( ); GDCNoMocks.args = { - currentSource: 'sqlite', dataSourceName: 'sqlite', table: ['Artist'], }; diff --git a/console/src/features/PermissionsTab/PermissionsTab.tsx b/console/src/features/PermissionsTab/PermissionsTab.tsx index b8d39d2ac21..dd345bdf8f0 100644 --- a/console/src/features/PermissionsTab/PermissionsTab.tsx +++ b/console/src/features/PermissionsTab/PermissionsTab.tsx @@ -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 = ({ - currentSource, dataSourceName, table, }) => { @@ -31,7 +30,6 @@ export const PermissionsTab: React.FC = ({ !!state.context.bulkSelections.length && ( send('CLOSE')} @@ -40,7 +38,6 @@ export const PermissionsTab: React.FC = ({ {state.value === 'formOpen' && ( = ({ table, machine, }) => { - const { data } = useRolePermissions({ + const { data, isLoading } = useRolePermissions({ dataSourceName, table, }); const [state, send] = machine; + if (isLoading) + return ( +
+ +
+ ); + if (!data) { - return null; + return ( +
+ + Something went wrong while fetching permissions + +
+ ); } const { supportedQueries, rolePermissions } = data; diff --git a/console/src/features/PermissionsTable/components/PermissionsLegend.tsx b/console/src/features/PermissionsTable/components/PermissionsLegend.tsx index 6cd06550b38..35c907fa214 100644 --- a/console/src/features/PermissionsTable/components/PermissionsLegend.tsx +++ b/console/src/features/PermissionsTable/components/PermissionsLegend.tsx @@ -4,9 +4,6 @@ import { PermissionsIcon } from './PermissionsIcons'; export const PermissionsLegend: React.FC = () => (
-

- Permissions -

diff --git a/console/src/features/PermissionsTable/hooks/usePermissions.tsx b/console/src/features/PermissionsTable/hooks/usePermissions.tsx index c8573be4cdc..db97fefb3ad 100644 --- a/console/src/features/PermissionsTable/hooks/usePermissions.tsx +++ b/console/src/features/PermissionsTable/hooks/usePermissions.tsx @@ -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>>; -const createRoleTableData = async ({ +const createRoleTableData = ({ metadataTable, tableColumns, -}: CreateRoleTableDataArgs): Promise => { + 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( + (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 = [ + '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 };