From 5a2821bb019f673a9d2b379e11dfdf49274b89a0 Mon Sep 17 00:00:00 2001 From: Luca Restagno <59067245+lucarestagno@users.noreply.github.com> Date: Thu, 25 May 2023 16:03:35 +0200 Subject: [PATCH] Add console support for Snowflake UDFs PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9268 Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com> GitOrigin-RevId: 046afcab867c3a91aea989bed92de894f4b67b16 --- .../components/Services/Data/DataRouter.js | 5 + .../Data/GDCTree/hooks/useGDCTreeItemClick.ts | 3 + .../Data/ManageDatabase/ManageDatabase.tsx | 6 +- .../Data/ManageFunction/ManageFunction.tsx | 80 ++++++ .../ManageFunction/components/Breadcrumbs.tsx | 24 ++ .../DisplayConfigurationDetails.tsx | 60 ++++ .../ManageFunction/components/Heading.tsx | 81 ++++++ .../Data/ManageFunction/components/Modify.tsx | 48 ++++ .../ModifyFunctionConfiguration.tsx | 202 +++++++++++++ .../ManageFunction/hooks/useUrlParameters.ts | 63 ++++ .../components/TableDisplayName.tsx | 2 + .../Data/ManageTable/parts/Breadcrumbs.tsx | 4 +- .../components/ManageTrackedFunctions.tsx | 2 +- .../components/TrackFunctionForm.tsx | 179 ++++++++++++ .../components/TrackedFunctions.tsx | 21 ++ .../components/UntrackedFunctions.tsx | 270 ++++++++++-------- .../TrackResources/hooks/useCheckRows.tsx | 2 - .../Data/hooks/useDriverCapabilities.ts | 13 +- .../hooks/useSetFunctionConfiguration.tsx | 87 ++++++ .../DataSource/common/capabilities.ts | 6 +- .../DataSource/common/defaultDatabaseProps.ts | 1 + .../src/lib/features/DataSource/gdc/index.ts | 3 + .../gdc/introspection/getTablesListAsTree.tsx | 13 +- .../gdc/introspection/getTrackableObjects.ts | 79 +++++ .../DataSource/gdc/introspection/index.ts | 1 + .../DataSource/gdc/introspection/utils.tsx | 25 +- .../src/lib/features/DataSource/index.ts | 41 ++- .../src/lib/features/DataSource/types.ts | 5 - .../hasura-metadata-types/source/source.ts | 6 +- .../legacy-ce/src/lib/utils/getDataRoute.ts | 9 +- frontend/package.json | 2 +- frontend/yarn.lock | 10 +- 32 files changed, 1194 insertions(+), 159 deletions(-) create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/ManageFunction.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Breadcrumbs.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/DisplayConfigurationDetails.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Heading.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Modify.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/ModifyFunctionConfiguration.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/hooks/useUrlParameters.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/TrackFunctions/components/TrackFunctionForm.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useSetFunctionConfiguration.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/getTrackableObjects.ts diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js index 9d312958092..f4cc230cb82 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js @@ -39,6 +39,7 @@ import { ModifyTableContainer } from './TableModify/ModifyTableContainer'; import { LandingPageRoute as NativeQueries } from '../../../features/Data/LogicalModels/LandingPage/LandingPage'; import { AddNativeQueryRoute } from '../../../features/Data/LogicalModels/AddNativeQuery/AddNativeQueryRoute'; import { TrackStoredProcedureRoute } from '../../../features/Data/LogicalModels/StoredProcedures/StoredProcedureWidget.route'; +import { ManageFunction } from '../../../features/Data/ManageFunction/ManageFunction'; const makeDataRouter = ( connect, @@ -66,6 +67,10 @@ const makeDataRouter = ( + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/GDCTree/hooks/useGDCTreeItemClick.ts b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/GDCTree/hooks/useGDCTreeItemClick.ts index a4510a2829c..5e37ddd209c 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/GDCTree/hooks/useGDCTreeItemClick.ts +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/GDCTree/hooks/useGDCTreeItemClick.ts @@ -30,6 +30,7 @@ export const useGDCTreeItemClick = (dispatch: Dispatch) => { * Handling click for GDC DBs */ const isTableClicked = Object.keys(rest?.table || {}).length !== 0; + const isFunctionClicked = Object.keys(rest?.function || {}).length !== 0; if (isTableClicked) { dispatch( _push( @@ -40,6 +41,8 @@ export const useGDCTreeItemClick = (dispatch: Dispatch) => { ) ) ); + } else if (isFunctionClicked) { + dispatch(_push(getRoute().function(database, rest.function))); } else { dispatch(_push(getRoute().database(database))); } diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageDatabase/ManageDatabase.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageDatabase/ManageDatabase.tsx index e31ee9957bb..3ff8ec0e94e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageDatabase/ManageDatabase.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageDatabase/ManageDatabase.tsx @@ -14,18 +14,18 @@ export interface ManageDatabaseProps { export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => { const { data: { - areFunctionsSupported = false, areForeignKeysSupported = false, + areUserDefinedFunctionsSupported = false, } = {}, } = useDriverCapabilities({ dataSourceName, select: data => { return { - areFunctionsSupported: !!get(data, 'functions'), areForeignKeysSupported: !!get( data, 'data_schema.supports_foreign_keys' ), + areUserDefinedFunctionsSupported: !!get(data, 'user_defined_functions'), }; }, }); @@ -60,7 +60,7 @@ export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => { )} - {areFunctionsSupported && ( + {areUserDefinedFunctionsSupported && ( [ + { + value: 'modify', + label: 'Modify', + content: ( + + ), + }, +]; + +export const ManageFunction: React.VFC = ( + props: ManageFunctionProps +) => { + const { + params: { operation }, + } = props; + + const urlData = useURLParameters(window.location); + const dispatch = useDispatch(); + + if (urlData.querystringParseResult === 'error') + throw Error('Unable to render'); + + const { database: dataSourceName, function: qualifiedFunction } = + urlData.data; + + return ( +
+
+ + + { + dispatch( + _push(getRoute().function(dataSourceName, qualifiedFunction)) + ); + }} + items={availableTabs(dataSourceName, qualifiedFunction)} + /> +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Breadcrumbs.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Breadcrumbs.tsx new file mode 100644 index 00000000000..8969fe5bd77 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Breadcrumbs.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { FaAngleRight, FaDatabase } from 'react-icons/fa'; +import { FunctionDisplayName } from '../../TrackResources/TrackFunctions/components/FunctionDisplayName'; +import { QualifiedFunction } from '../../../hasura-metadata-types'; + +export const Breadcrumbs: React.VFC<{ + dataSourceName: string; + qualifiedFunction: QualifiedFunction; +}> = ({ dataSourceName, qualifiedFunction }) => ( +
+
+ + {dataSourceName} +
+ +
+ +
+ +
+ Manage +
+
+); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/DisplayConfigurationDetails.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/DisplayConfigurationDetails.tsx new file mode 100644 index 00000000000..2176c489a6b --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/DisplayConfigurationDetails.tsx @@ -0,0 +1,60 @@ +import Skeleton from 'react-loading-skeleton'; +import { + MetadataSelectors, + areTablesEqual, + useMetadata, +} from '../../../hasura-metadata-api'; +import { IndicatorCard } from '../../../../new-components/IndicatorCard'; +import { Badge } from '../../../../new-components/Badge'; +import { TableDisplayName } from '../../ManageTable/components/TableDisplayName'; +import { QualifiedFunction } from '../../../hasura-metadata-types'; + +export type DisplayConfigurationDetailsProps = { + dataSourceName: string; + qualifiedFunction: QualifiedFunction; +}; +export const DisplayConfigurationDetails = ( + props: DisplayConfigurationDetailsProps +) => { + const { + data: metadataFunction, + isLoading, + error, + } = useMetadata(m => + MetadataSelectors.findSource(props.dataSourceName)(m)?.functions?.find(f => + areTablesEqual(f.function, props.qualifiedFunction) + ) + ); + + if (isLoading) + return ( +
+ +
+ ); + + if (!metadataFunction) + return ( + + {JSON.stringify(error)} + + ); + + return ( +
+
+ Custom Name: + {metadataFunction.configuration?.custom_name ?? Not Set} +
+
+ Return Type: + +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Heading.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Heading.tsx new file mode 100644 index 00000000000..1fe5948b515 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Heading.tsx @@ -0,0 +1,81 @@ +import { Badge } from '../../../../new-components/Badge'; +import { Button } from '../../../../new-components/Button'; +import { DropdownMenu } from '../../../../new-components/DropdownMenu'; +import React from 'react'; +import { FaChevronDown } from 'react-icons/fa'; +import { QualifiedFunction } from '../../../hasura-metadata-types'; +import { hasuraToast } from '../../../../new-components/Toasts'; +import { useAppDispatch } from '../../../../storeHooks'; +import { getRoute } from '../../../../utils/getDataRoute'; +import _push from '../../../../components/Services/Data/push'; +import { useTrackFunction } from '../../hooks/useTrackFunction'; +import { DisplayToastErrorMessage } from '../../components/DisplayErrorMessage'; +import { FunctionDisplayName } from '../../TrackResources/TrackFunctions/components/FunctionDisplayName'; + +export const Heading: React.VFC<{ + dataSourceName: string; + qualifiedFunction: QualifiedFunction; +}> = ({ qualifiedFunction, dataSourceName }) => { + const dispatch = useAppDispatch(); + const { untrackFunction } = useTrackFunction({ + dataSourceName, + onSuccess: () => { + hasuraToast({ + type: 'success', + title: 'Successfully untracked function', + }); + dispatch(_push(getRoute().database(dataSourceName))); + }, + onError: err => { + hasuraToast({ + type: 'error', + title: 'Error while untracking table', + children: , + }); + }, + }); + + return ( +
+
+
+ { + untrackFunction({ + functionsToBeUntracked: [qualifiedFunction], + }); + }} + > + Untrack + , + ], + ]} + > +
+ +
+
+
+
+
+ Tracked +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Modify.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Modify.tsx new file mode 100644 index 00000000000..95c0af487c0 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/Modify.tsx @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import { Button } from '../../../../new-components/Button'; +import { QualifiedFunction } from '../../../hasura-metadata-types'; +import { ModifyFunctionConfiguration } from './ModifyFunctionConfiguration'; +import { IconTooltip } from '../../../../new-components/Tooltip'; +import { FaEdit, FaQuestionCircle } from 'react-icons/fa'; +import { DisplayConfigurationDetails } from './DisplayConfigurationDetails'; + +export type ModifyProps = { + dataSourceName: string; + qualifiedFunction: QualifiedFunction; +}; + +export const Modify = (props: ModifyProps) => { + const [isEditConfigurationModalOpen, setIsEditConfigurationModalOpen] = + useState(false); + + return ( +
+
+
+
Configuration
+ } + /> +
+ + +
+ +
+ {isEditConfigurationModalOpen && ( + setIsEditConfigurationModalOpen(false)} + onClose={() => setIsEditConfigurationModalOpen(false)} + /> + )} +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/ModifyFunctionConfiguration.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/ModifyFunctionConfiguration.tsx new file mode 100644 index 00000000000..5f2da17a676 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/ManageFunction/components/ModifyFunctionConfiguration.tsx @@ -0,0 +1,202 @@ +import { z } from 'zod'; +import { Dialog } from '../../../../new-components/Dialog'; +import { + InputField, + Select, + useConsoleForm, +} from '../../../../new-components/Form'; +import { QualifiedFunction } from '../../../hasura-metadata-types'; +import { useEffect } from 'react'; +import { + MetadataSelectors, + areTablesEqual, + useMetadata, +} from '../../../hasura-metadata-api'; +import { getQualifiedTable } from '../../ManageTable/utils'; +import { useSetFunctionConfiguration } from '../../hooks/useSetFunctionConfiguration'; +import { hasuraToast } from '../../../../new-components/Toasts'; +import { DisplayToastErrorMessage } from '../../components/DisplayErrorMessage'; +import { cleanEmpty } from '../../../ConnectDBRedesign/components/ConnectPostgresWidget/utils/helpers'; +import { Collapsible } from '../../../../new-components/Collapsible'; +import { adaptFunctionName } from '../../TrackResources/TrackFunctions'; + +export type ModifyFunctionConfigurationProps = { + qualifiedFunction: QualifiedFunction; + dataSourceName: string; + onClose: () => void; + onSuccess: () => void; +}; + +const validationSchema = z.object({ + custom_name: z.string().optional(), + custom_root_fields: z + .object({ + function: z.string().optional(), + function_aggregate: z.string().optional(), + }) + .optional(), + response: z + .object({ + type: z.literal('table'), + table: z.string().min(1, { message: 'The return type is mandatory' }), + }) + .optional(), +}); + +export type Schema = z.infer; + +export const ModifyFunctionConfiguration = ( + props: ModifyFunctionConfigurationProps +) => { + const { setFunctionConfiguration, isLoading } = useSetFunctionConfiguration({ + dataSourceName: props.dataSourceName, + }); + + const { data: tableOptions = [] } = useMetadata(m => + MetadataSelectors.findSource(props.dataSourceName)(m)?.tables.map(t => ({ + label: getQualifiedTable(t.table).join(' / '), + value: JSON.stringify(t.table), + })) + ); + + const { data: metadataFunction, isFetched } = useMetadata(m => + MetadataSelectors.findSource(props.dataSourceName)(m)?.functions?.find(f => + areTablesEqual(f.function, props.qualifiedFunction) + ) + ); + + const { + Form, + methods: { handleSubmit, reset, watch }, + } = useConsoleForm({ + schema: validationSchema, + }); + + useEffect(() => { + if (isFetched && metadataFunction) { + reset({ + ...metadataFunction.configuration, + response: { + type: metadataFunction.configuration?.response?.type ?? 'table', + table: JSON.stringify( + metadataFunction.configuration?.response?.table + ), + }, + }); + } + }, [isFetched, metadataFunction, reset]); + + const onHandleSubmit = (data: Schema) => { + setFunctionConfiguration({ + qualifiedFunction: props.qualifiedFunction, + configuration: cleanEmpty({ + ...data, + response: { + ...data.response, + table: data.response?.table ? JSON.parse(data.response?.table) : {}, + }, + }), + onSuccess: () => { + hasuraToast({ + type: 'success', + title: 'Success', + message: `Updated successfully`, + }); + props.onSuccess(); + }, + onError: err => { + hasuraToast({ + type: 'error', + title: err.name, + children: , + }); + }, + }); + }; + + const customName = watch('custom_name'); + + return ( + { + handleSubmit(onHandleSubmit)(); + }} + isLoading={isLoading} + onClose={props.onClose} + callToDeny="Cancel" + callToAction="Edit Configuration" + onSubmitAnalyticsName="actions-tab-generate-types-submit" + onCancelAnalyticsName="actions-tab-generate-types-cancel" + /> + } + > +
+
{ + console.log('>>>', data); + }} + > + + + Custom Root Fields
+ } + > + + + + + Response Settings + } + defaultOpen + > +
+ +
+ + ({ + value: JSON.stringify(f.qualifiedFunction), + label: adaptFunctionName(f.qualifiedFunction).join(' / '), + }))} + disabled + /> + + + {!tableOptions.length && ( + + Tables that are tracked in Hasura can be used as the return type + for your function. + + )} + + +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/TrackFunctions/components/TrackedFunctions.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/TrackFunctions/components/TrackedFunctions.tsx index 23a3728766b..93ec1a28e82 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/TrackFunctions/components/TrackedFunctions.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/TrackFunctions/components/TrackedFunctions.tsx @@ -16,6 +16,8 @@ import { useTrackedFunctions } from '../hooks/useTrackedFunctions'; import { hasuraToast } from '../../../../../new-components/Toasts'; import { DisplayToastErrorMessage } from '../../../components/DisplayErrorMessage'; import { useTrackFunction } from '../../../hooks/useTrackFunction'; +import { ModifyFunctionConfiguration } from '../../../ManageFunction/components/ModifyFunctionConfiguration'; +import { FaEdit } from 'react-icons/fa'; export type TrackedFunctionsProps = { dataSourceName: string; @@ -27,6 +29,9 @@ export const TrackedFunctions = (props: TrackedFunctionsProps) => { const { data: trackedFunctions = [], isLoading } = useTrackedFunctions(dataSourceName); + const [isConfigurationModalOpen, setIsConfigurationModalOpen] = + useState(false); + const [activeRow, setActiveRow] = useState(); const functionsWithId = React.useMemo(() => { @@ -151,6 +156,22 @@ export const TrackedFunctions = (props: TrackedFunctionsProps) => {
+ {isConfigurationModalOpen ? ( + setIsConfigurationModalOpen(false)} + onClose={() => setIsConfigurationModalOpen(false)} + /> + ) : null} + + + + ) : ( - - - ) : ( - - )} -
-
- - ))} - - + )} + + + + ))} + + + +
+ {isModalOpen ? ( + setIsModalOpen(false)} + onClose={() => setIsModalOpen(false)} + defaultValues={modalFormDefaultValues} + /> + ) : null} +
); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/hooks/useCheckRows.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/hooks/useCheckRows.tsx index fe438f11ba0..70a10d33e46 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/hooks/useCheckRows.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackResources/hooks/useCheckRows.tsx @@ -55,8 +55,6 @@ export const useCheckRows = ( setCheckedIds([]); }; - console.log('state of checked', checkedIds); - React.useEffect(() => { if (!checkboxRef.current) return; checkboxRef.current.indeterminate = inputStatus === 'indeterminate'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useDriverCapabilities.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useDriverCapabilities.ts index 83ae5c213c3..ae322502b28 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useDriverCapabilities.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useDriverCapabilities.ts @@ -1,22 +1,21 @@ import { UseQueryOptions, useQuery } from 'react-query'; -import { DataSource, DriverCapability, Feature } from '../../DataSource'; +import { DataSource, Feature } from '../../DataSource'; import { useMetadata } from '../../hasura-metadata-api'; import { useHttpClient } from '../../Network'; import { APIError } from '../../../hooks/error'; +import { Capabilities } from '@hasura/dc-api-types'; type UseDatabaseCapabilitiesArgs = { dataSourceName: string; }; -export const useDriverCapabilities = < - FinalResult = Feature | DriverCapability ->({ +export const useDriverCapabilities = ({ dataSourceName, select, options = {}, }: UseDatabaseCapabilitiesArgs & { - select?: (data: Feature | DriverCapability) => FinalResult; - options?: UseQueryOptions; + select?: (data: Feature | Capabilities) => FinalResult; + options?: UseQueryOptions; }) => { const httpClient = useHttpClient(); @@ -24,7 +23,7 @@ export const useDriverCapabilities = < m => m.metadata.sources.find(source => source.name === dataSourceName)?.kind ); - return useQuery( + return useQuery( [dataSourceName, 'capabilities'], async () => { const result = diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useSetFunctionConfiguration.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useSetFunctionConfiguration.tsx new file mode 100644 index 00000000000..3a13cef86fb --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useSetFunctionConfiguration.tsx @@ -0,0 +1,87 @@ +import { useCallback } from 'react'; +import { + MetadataFunction, + QualifiedFunction, +} from '../../hasura-metadata-types'; +import { + MetadataMigrationOptions, + useMetadataMigration, +} from '../../MetadataAPI/hooks/useMetadataMigration'; +import { + MetadataSelectors, + areTablesEqual, + useInvalidateMetadata, + useMetadata, +} from '../../hasura-metadata-api'; +import { transformErrorResponse } from '../errorUtils'; + +export type MetadataFunctionPayload = { + function: QualifiedFunction; + configuration?: MetadataFunction['configuration']; + source: string; + comment?: string; +}; + +export const useSetFunctionConfiguration = ({ + dataSourceName, + ...globalMutateOptions +}: { dataSourceName: string } & MetadataMigrationOptions) => { + const invalidateMetadata = useInvalidateMetadata(); + + const { mutate, ...rest } = useMetadataMigration({ + ...globalMutateOptions, + onSuccess: (data, variables, ctx) => { + invalidateMetadata(); + globalMutateOptions?.onSuccess?.(data, variables, ctx); + }, + errorTransform: transformErrorResponse, + }); + + const { data: { driver, resource_version, functions = [] } = {} } = + useMetadata(m => ({ + driver: MetadataSelectors.findSource(dataSourceName)(m)?.kind, + resource_version: m.resource_version, + functions: MetadataSelectors.findSource(dataSourceName)(m)?.functions, + })); + + const setFunctionConfiguration = useCallback( + ({ + qualifiedFunction, + configuration, + ...mutationOptions + }: { + qualifiedFunction: QualifiedFunction; + configuration: MetadataFunction['configuration']; + } & MetadataMigrationOptions) => { + const metadataFunction = functions.find(fn => + areTablesEqual(fn.function, qualifiedFunction) + ); + + const payload = { + type: `${driver}_set_function_customization`, + args: { + source: dataSourceName, + function: metadataFunction?.function, + configuration, + }, + }; + + mutate( + { + query: { + type: 'bulk', + source: dataSourceName, + resource_version, + args: [payload], + }, + }, + { + ...mutationOptions, + } + ); + }, + [functions, driver, dataSourceName, mutate, resource_version] + ); + + return { setFunctionConfiguration, ...rest }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/capabilities.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/capabilities.ts index fc2cf40cbd3..3db04fbe48b 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/capabilities.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/capabilities.ts @@ -1,13 +1,13 @@ -import { DriverCapability } from '../types'; +import { Capabilities } from '@hasura/dc-api-types'; -export const postgresCapabilities: DriverCapability = { +export const postgresCapabilities: Capabilities = { mutations: { insert: {}, update: {}, delete: {}, }, queries: {}, - functions: {}, + user_defined_functions: {}, data_schema: { supports_foreign_keys: true, }, diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/defaultDatabaseProps.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/defaultDatabaseProps.ts index db55430a701..2e535db03d3 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/defaultDatabaseProps.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/defaultDatabaseProps.ts @@ -16,6 +16,7 @@ export const defaultIntrospectionProps = { getIsTableView: async () => Feature.NotImplemented, getSupportedDataTypes: async () => Feature.NotImplemented, getStoredProcedures: async () => Feature.NotImplemented, + getTrackableObjects: async () => Feature.NotImplemented, }; export const defaultDatabaseProps: Database = { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/index.ts index a8b3a49d144..cd9050e4b5c 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/index.ts @@ -12,6 +12,7 @@ import { getSupportedOperators, getFKRelationships, getDriverCapabilities, + getTrackableObjects, } from './introspection'; import { getTableRows } from './query'; @@ -21,6 +22,7 @@ import { getTableRows } from './query'; * you'd have just the table name -> ["Album"] but in a db with schemas -> ["Public", "Album"]. */ export type GDCTable = string[]; +export type GDCFunction = string[]; export const gdc: Database = { ...defaultDatabaseProps, @@ -30,6 +32,7 @@ export const gdc: Database = { getDatabaseConfiguration, getDriverCapabilities, getTrackableTables, + getTrackableObjects, getDatabaseHierarchy: async () => Feature.NotImplemented, getTableColumns, getFKRelationships, diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/getTablesListAsTree.tsx b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/getTablesListAsTree.tsx index a8459e80b08..ceb649bd17e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/getTablesListAsTree.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/getTablesListAsTree.tsx @@ -5,6 +5,7 @@ import { GDCTable } from '..'; import { exportMetadata } from '../../api'; import { GetTablesListAsTreeProps } from '../../types'; import { convertToTreeData } from './utils'; +// import { QualifiedFunction } from '../../../hasura-metadata-types'; export const getTablesListAsTree = async ({ dataSourceName, @@ -24,6 +25,11 @@ export const getTablesListAsTree = async ({ return table.table as GDCTable; }); + const functions = (source?.functions ?? []).map(f => { + if (typeof f.function === 'string') return [f.function] as GDCTable; + return f.function as GDCTable; + }); + return { title: (
@@ -37,6 +43,11 @@ export const getTablesListAsTree = async ({ ), key: JSON.stringify({ database: source.name }), icon: , - children: tables.length ? convertToTreeData(tables, [], source.name) : [], + children: tables.length + ? [ + ...convertToTreeData(tables, [], source.name), + ...convertToTreeData(functions, [], source.name, 'function'), + ] + : [], }; }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/getTrackableObjects.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/getTrackableObjects.ts new file mode 100644 index 00000000000..110b493335d --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/getTrackableObjects.ts @@ -0,0 +1,79 @@ +import { Table } from '../../../hasura-metadata-types'; +import { runMetadataQuery } from '../../api'; +import { GetTrackableTablesProps, IntrospectedFunction } from '../../types'; + +type TrackableObjects = { + functions: { + name: unknown; + volatility: 'STABLE' | 'VOLATILE'; + }[]; + tables: { + name: Table; + }[]; +}; + +export type GetTrackableObjectsResponse = { + tables: { + name: string; + table: Table; + type: string; + }[]; + functions: IntrospectedFunction[]; +}; + +const adaptName = (name: unknown): string => { + if (typeof name === 'string') { + return name; + } + if (Array.isArray(name)) { + return name.join('.'); + } + + throw Error('getTrackableObjects: name is not string nor array:' + name); +}; + +export type GetTrackableObjectsProps = Pick< + GetTrackableTablesProps, + 'httpClient' | 'dataSourceName' +>; + +export const getTrackableObjects = async ({ + httpClient, + dataSourceName, +}: GetTrackableObjectsProps) => { + try { + const result = await runMetadataQuery({ + httpClient, + body: { + type: 'reference_get_source_trackables', + args: { + source: dataSourceName, + }, + }, + }); + + const tables = result.tables.map(({ name }) => { + /** + * Ideally each table is supposed to be GDCTable, but the server fix has not yet been merged to main. + * Right now it returns string as a table. + */ + return { + name: adaptName(name), + table: name, + type: 'BASE TABLE', + }; + }); + + const functions: IntrospectedFunction[] = result.functions.map(fn => { + return { + name: adaptName(fn.name), + qualifiedFunction: fn.name, + isVolatile: fn.volatility === 'VOLATILE', + }; + }); + + return { tables, functions }; + } catch (error) { + throw new Error('Error fetching GDC trackable objects'); + } +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/index.ts index 0648ebdad24..cec03b61e51 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/index.ts @@ -5,5 +5,6 @@ export { getTableColumns } from './getTableColumns'; export { getFKRelationships } from './getFKRelationships'; export { getTablesListAsTree } from './getTablesListAsTree'; export { getTrackableTables } from './getTrackableTables'; +export { getTrackableObjects } from './getTrackableObjects'; export { convertToTreeData } from './utils'; export type { GetTableInfoResponse } from './types'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/utils.tsx b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/utils.tsx index 0e23e1de672..3e201f4a040 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/utils.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/gdc/introspection/utils.tsx @@ -3,22 +3,35 @@ import { DataNode } from 'antd/lib/tree'; import React from 'react'; import { FaTable, FaFolder } from 'react-icons/fa'; import { TableColumn } from '../../types'; +import { TbMathFunction } from 'react-icons/tb'; export function convertToTreeData( tables: string[][], key: string[], - dataSourceName: string + dataSourceName: string, + mode?: 'function' ): DataNode[] { if (tables.length === 0) return []; if (tables[0].length === 1) { const leafNodes: DataNode[] = tables.map(table => { return { - icon: , - key: JSON.stringify({ - database: dataSourceName, - table: [...key, table[0]], - }), + icon: + mode === 'function' ? ( + + ) : ( + + ), + key: + mode === 'function' + ? JSON.stringify({ + database: dataSourceName, + function: [...key, table[0]], + }) + : JSON.stringify({ + database: dataSourceName, + table: [...key, table[0]], + }), title: table[0], }; }); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts index 136ca420de7..83316607236 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts @@ -1,4 +1,4 @@ -import { OpenApiSchema } from '@hasura/dc-api-types'; +import { Capabilities, OpenApiSchema } from '@hasura/dc-api-types'; import { DataNode } from 'antd/lib/tree'; import { AxiosInstance } from 'axios'; import pickBy from 'lodash/pickBy'; @@ -35,7 +35,6 @@ import type { TableRow, Version, WhereClause, - DriverCapability, StoredProcedure, GetStoredProceduresProps, } from './types'; @@ -57,6 +56,11 @@ import { import { getAllSourceKinds } from './common/getAllSourceKinds'; import { getTableName } from './common/getTableName'; import { ReleaseType } from './types'; +import { + GetTrackableObjectsProps, + GetTrackableObjectsResponse, +} from './gdc/introspection/getTrackableObjects'; +import { isObject } from '../../components/Common/utils/jsUtils'; export * from './common/utils'; export type { GDCTable } from './gdc'; @@ -121,7 +125,7 @@ export type Database = { getDriverCapabilities: ( httpClient: AxiosInstance, driver?: string - ) => Promise; + ) => Promise; getTrackableTables: ( props: GetTrackableTablesProps ) => Promise; @@ -141,6 +145,9 @@ export type Database = { getTrackableFunctions: ( props: GetTrackableFunctionProps ) => Promise; + getTrackableObjects: ( + props: GetTrackableObjectsProps + ) => Promise; getDatabaseSchemas: ( props: GetDatabaseSchemaProps ) => Promise; @@ -547,13 +554,33 @@ export const DataSource = (httpClient: AxiosInstance) => ({ ); }, getTrackableFunctions: async (dataSourceName: string) => { + const functions: IntrospectedFunction[] = []; const database = await getDatabaseMethods({ dataSourceName, httpClient }); - return ( - database.introspection?.getTrackableFunctions({ + + const trackableFunctions = + (await database.introspection?.getTrackableFunctions({ dataSourceName, httpClient, - }) ?? Feature.NotImplemented - ); + })) ?? []; + + if (Array.isArray(trackableFunctions)) { + functions.push(...trackableFunctions); + } + + const getTrackableObjectsFn = database.introspection?.getTrackableObjects; + + if (getTrackableObjectsFn) { + const trackableObjects = await getTrackableObjectsFn({ + dataSourceName, + httpClient, + }); + + if (isObject(trackableObjects) && 'functions' in trackableObjects) { + functions.push(...trackableObjects.functions); + } + } + + return functions; }, getDatabaseSchemas: async ({ dataSourceName, diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts index 70574991f8d..2f0312e3b0a 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts @@ -1,4 +1,3 @@ -import { Capabilities } from '@hasura/dc-api-types'; import { Legacy_SourceToRemoteSchemaRelationship, LocalTableArrayRelationship, @@ -195,10 +194,6 @@ export type GetIsTableViewProps = { httpClient: NetworkArgs['httpClient']; }; -export type DriverCapability = Capabilities & { - functions?: Record; -}; - export type StoredProcedure = unknown; export type GetStoredProceduresProps = { dataSourceName: string; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/source.ts b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/source.ts index 179a353fd90..6f66bbb5018 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/source.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/source.ts @@ -7,7 +7,7 @@ import { } from './configuration'; import { LogicalModel } from './logicalModel'; import { NativeQuery } from './nativeQuery'; -import { MetadataTable } from './table'; +import { MetadataTable, Table } from './table'; import { StoredProcedure } from './storedProcedure'; export type NativeDrivers = @@ -48,6 +48,10 @@ export type MetadataFunction = { }; session_argument?: string; exposed_as?: 'mutation' | 'query'; + response?: { + type: 'table'; + table: Table; + }; }; }; diff --git a/frontend/libs/console/legacy-ce/src/lib/utils/getDataRoute.ts b/frontend/libs/console/legacy-ce/src/lib/utils/getDataRoute.ts index 276787c1ab7..82d5abd778d 100644 --- a/frontend/libs/console/legacy-ce/src/lib/utils/getDataRoute.ts +++ b/frontend/libs/console/legacy-ce/src/lib/utils/getDataRoute.ts @@ -1,4 +1,4 @@ -import { Table } from '../features/hasura-metadata-types'; +import { QualifiedFunction, Table } from '../features/hasura-metadata-types'; export const getRoute = () => ({ connectDatabase: (driver?: string) => @@ -21,4 +21,11 @@ export const getRoute = () => ({ )}` ); }, + function: (dataSourceName: string, fn: QualifiedFunction) => { + return encodeURI( + `/data/v2/manage/function?database=${dataSourceName}&function=${JSON.stringify( + fn + )}` + ); + }, }); diff --git a/frontend/package.json b/frontend/package.json index 9780159287b..651d3344e4a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "@babel/plugin-transform-runtime": "^7.14.5", "@graphql-codegen/core": "^1.17.8", "@graphql-codegen/typescript": "^1.17.10", - "@hasura/dc-api-types": "^0.30.0", + "@hasura/dc-api-types": "^0.32.0", "@hookform/resolvers": "2.8.10", "@radix-ui/colors": "^0.1.8", "@radix-ui/react-alert-dialog": "^1.0.3", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c1d89235475..3b9a325f42c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3375,10 +3375,10 @@ __metadata: languageName: node linkType: hard -"@hasura/dc-api-types@npm:^0.30.0": - version: 0.30.0 - resolution: "@hasura/dc-api-types@npm:0.30.0" - checksum: fbf0376dae3252cac85dd343d77faaf82d7e9100ce5c3a18822e87e7d35550bca0c110f901da766e4099698228eb3b7e25d10f4b305b454e1f64d9ad4c87ae2b +"@hasura/dc-api-types@npm:^0.32.0": + version: 0.32.0 + resolution: "@hasura/dc-api-types@npm:0.32.0" + checksum: c95adc03c894cf737a0e1d366e5726f17156f13ec5d30be205dc705e2e0b55a0796d33603d8eee89926b1153503c171b461c4bace8475598bae7db33f94e0910 languageName: node linkType: hard @@ -18392,7 +18392,7 @@ __metadata: "@graphql-codegen/core": ^1.17.8 "@graphql-codegen/typescript": ^1.17.10 "@graphql-codegen/typescript-operations": ^2.5.5 - "@hasura/dc-api-types": ^0.30.0 + "@hasura/dc-api-types": ^0.32.0 "@hookform/devtools": 4.0.1 "@hookform/resolvers": 2.8.10 "@nrwl/cli": 15.8.1