From 2f7e89a9c2768213f6e2e71ff6ddbf8c1e6115b0 Mon Sep 17 00:00:00 2001 From: Vijay Prasanna Date: Wed, 19 Apr 2023 18:21:40 +0530 Subject: [PATCH] console (feature): component to introspect and track functions in the new data tab UI PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8570 GitOrigin-RevId: addf15967efcbcdc86aedd42096d59b95be14a27 --- .../components/FunctionDisplayName.tsx | 28 +++ .../components/UntrackedFunctions.stories.tsx | 13 + .../components/UntrackedFunctions.tsx | 234 ++++++++++++++++++ .../TrackFunctions/hooks/useTrackFunction.ts | 78 ++++++ .../lib/features/Data/TrackFunctions/index.ts | 0 .../lib/features/Data/TrackFunctions/types.ts | 5 + .../lib/features/Data/TrackFunctions/utils.ts | 33 +++ .../src/lib/features/Data/reactQueryUtils.ts | 13 + .../lib/features/DataSource/alloydb/index.ts | 6 +- .../lib/features/DataSource/bigquery/index.ts | 6 +- .../lib/features/DataSource/citus/index.ts | 6 +- .../features/DataSource/cockroach/index.ts | 6 +- .../DataSource/common/defaultDatabaseProps.ts | 31 +-- .../src/lib/features/DataSource/gdc/index.ts | 6 +- .../src/lib/features/DataSource/index.ts | 12 + .../lib/features/DataSource/mssql/index.ts | 6 +- .../lib/features/DataSource/mysql/index.ts | 6 +- .../lib/features/DataSource/postgres/index.ts | 2 + .../introspection/getTrackableFunctions.ts | 44 ++++ .../postgres/introspection/index.ts | 1 + .../src/lib/features/DataSource/types.ts | 10 + .../hooks/useCheckRows.ts | 2 +- .../hasura-metadata-types/source/source.ts | 8 +- 23 files changed, 531 insertions(+), 25 deletions(-) create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/FunctionDisplayName.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/UntrackedFunctions.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/UntrackedFunctions.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/hooks/useTrackFunction.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/types.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/utils.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Data/reactQueryUtils.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/introspection/getTrackableFunctions.ts diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/FunctionDisplayName.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/FunctionDisplayName.tsx new file mode 100644 index 00000000000..50c6d97d43d --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/FunctionDisplayName.tsx @@ -0,0 +1,28 @@ +import { TbMathFunction } from 'react-icons/tb'; +import { QualifiedFunction } from '../../../hasura-metadata-types'; +import { adaptFunctionName } from '../utils'; + +export const FunctionDisplayName = ({ + dataSourceName, + qualifiedFunction, +}: { + dataSourceName?: string; + qualifiedFunction: QualifiedFunction; +}) => { + const functionName = adaptFunctionName(qualifiedFunction); + + if (!dataSourceName) + return ( +
+ + {functionName.join(' / ')} +
+ ); + + return ( +
+ + {dataSourceName} / {functionName.join(' / ')} +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/UntrackedFunctions.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/UntrackedFunctions.stories.tsx new file mode 100644 index 00000000000..ae41b158ca5 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/UntrackedFunctions.stories.tsx @@ -0,0 +1,13 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; + +import { UntrackedFunctions } from './UntrackedFunctions'; + +export default { + component: UntrackedFunctions, + decorators: [ReactQueryDecorator()], +} as ComponentMeta; + +export const Primary: ComponentStory = () => ( + +); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/UntrackedFunctions.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/UntrackedFunctions.tsx new file mode 100644 index 00000000000..f5aac8681a3 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/components/UntrackedFunctions.tsx @@ -0,0 +1,234 @@ +import Skeleton from 'react-loading-skeleton'; +import { useQuery } from 'react-query'; +import { Button } from '../../../../new-components/Button'; +import { CardedTable } from '../../../../new-components/CardedTable'; +import { DropdownMenu } from '../../../../new-components/DropdownMenu'; +import { DataSource, Feature, IntrospectedFunction } from '../../../DataSource'; +import { + areTablesEqual, + MetadataSelectors, + useInvalidateMetadata, + useMetadata, +} from '../../../hasura-metadata-api'; +import { useHttpClient } from '../../../Network'; +import { useTrackFunction } from '../hooks/useTrackFunction'; +import { adaptFunctionName, search } from '../utils'; +import { FunctionDisplayName } from './FunctionDisplayName'; +import { SlOptionsVertical } from 'react-icons/sl'; +import { IndicatorCard } from '../../../../new-components/IndicatorCard'; +import { LearnMoreLink } from '../../../../new-components/LearnMoreLink'; +import { SearchBar } from '../../TrackResources/components/SearchBar'; +import { Badge } from '../../../../new-components/Badge'; +import { useState } from 'react'; +import { + DEFAULT_PAGE_NUMBER, + DEFAULT_PAGE_SIZE, + DEFAULT_PAGE_SIZES, +} from '../../TrackResources/constants'; +import { FaAngleLeft, FaAngleRight } from 'react-icons/fa'; +import { paginate } from '../../TrackResources/utils'; +import { getDefaultQueryOptions } from '../../reactQueryUtils'; + +type UntrackedFunctionsProps = { + dataSourceName: string; +}; + +const useGetUntrackedFunctions = ( + dataSourceName: string, + autoFireOnMount = true +) => { + const httpClient = useHttpClient(); + + const { data: trackedFunctions = [], isFetching } = useMetadata(m => + (MetadataSelectors.findSource(dataSourceName)(m)?.functions ?? []).map(fn => + adaptFunctionName(fn.function) + ) + ); + + return useQuery({ + queryKey: [dataSourceName, 'functions'], + queryFn: async () => { + const result = await DataSource(httpClient).getTrackableFunctions( + dataSourceName + ); + + if (result === Feature.NotImplemented) return result; + + return (result ?? []).filter(fn => { + const isAlreadyTracked = trackedFunctions.find(trackedFn => + areTablesEqual(fn.qualifiedFunction, trackedFn) + ); + return !isAlreadyTracked; + }); + }, + ...getDefaultQueryOptions(), + enabled: autoFireOnMount && !isFetching, + }); +}; + +export const UntrackedFunctions = (props: UntrackedFunctionsProps) => { + const { dataSourceName } = props; + + const [pageNumber, setPageNumber] = useState(DEFAULT_PAGE_NUMBER); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [searchText, setSearchText] = useState(''); + + const { data: untrackedFunctions = [], isLoading } = + useGetUntrackedFunctions(dataSourceName); + + const invalidateMetadata = useInvalidateMetadata(); + + const { trackFunction } = useTrackFunction({ + dataSourceName, + }); + + if (isLoading) return ; + + if (untrackedFunctions === Feature.NotImplemented) return null; + + if (!untrackedFunctions.length) + return ( + + We couldn't find any compatible functions in your database that can be + tracked in Hasura.{' '} + + + ); + + const filteredResult = search(untrackedFunctions, searchText); + + return ( +
+
+
+
+ { + setSearchText(data); + setPageNumber(DEFAULT_PAGE_NUMBER); + }} + /> + {searchText.length ? ( + {filteredResult.length} results found + ) : null} +
+
+ +
+
+
+ + + + Function + +
+ invalidateMetadata()} + > + Refresh + , + ], + ]} + options={{ + content: { + alignOffset: -50, + avoidCollisions: false, + }, + }} + > + + +
+
+
+
+ + {paginate(filteredResult, pageSize, pageNumber).map( + untrackedFunction => ( + + + + + +
+ {untrackedFunction.isVolatile ? ( + <> + + + + ) : ( + + )} +
+
+
+ ) + )} +
+
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/hooks/useTrackFunction.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/hooks/useTrackFunction.ts new file mode 100644 index 00000000000..d04285c5d24 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/hooks/useTrackFunction.ts @@ -0,0 +1,78 @@ +import { useCallback, useMemo } from 'react'; +import { APIError } from '../../../../hooks/error'; +import { hasuraToast } from '../../../../new-components/Toasts'; +import { + MetadataSelectors, + useInvalidateMetadata, + useMetadata, +} from '../../../hasura-metadata-api'; +import { + MetadataFunction, + QualifiedFunction, +} from '../../../hasura-metadata-types'; +import { useMetadataMigration } from '../../../MetadataAPI'; + +export type MetadataFunctionPayload = { + function: QualifiedFunction; + configuration?: MetadataFunction['configuration']; + source: string; + comment?: string; +}; + +export const useTrackFunction = ({ + dataSourceName, + onSuccess, + onError, +}: { + dataSourceName: string; + onSuccess?: () => void; + onError?: (err: unknown) => void; +}) => { + const { mutate, ...rest } = useMetadataMigration(); + const invalidateMetadata = useInvalidateMetadata(); + const { data: driver } = useMetadata( + m => MetadataSelectors.findSource(dataSourceName)(m)?.kind + ); + + const mutationOptions = useMemo( + () => ({ + onSuccess: () => { + hasuraToast({ + type: 'success', + title: 'Tracked Successfully!', + }); + onSuccess?.(); + invalidateMetadata(); + }, + onError: (err: APIError) => { + console.log(err.message); + hasuraToast({ + type: 'error', + title: 'Failed to track!', + message: err.message, + }); + onError?.(err); + }, + }), + [invalidateMetadata, onError, onSuccess] + ); + + const trackFunction = useCallback( + (values: MetadataFunctionPayload) => { + mutate( + { + query: { + type: `${driver}_track_function`, + args: values, + }, + }, + { + ...mutationOptions, + } + ); + }, + [mutate, mutationOptions, driver] + ); + + return { trackFunction, ...rest }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/types.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/types.ts new file mode 100644 index 00000000000..03476dee46c --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/types.ts @@ -0,0 +1,5 @@ +export type TrackableFunction = { + id: string; + name: string; + isVolatile: boolean; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/utils.ts new file mode 100644 index 00000000000..f531c5ef626 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/TrackFunctions/utils.ts @@ -0,0 +1,33 @@ +import { IntrospectedFunction } from '../../DataSource'; +import { QualifiedFunction } from '../../hasura-metadata-types'; + +export const adaptFunctionName = ( + qualifiedFunction: QualifiedFunction +): string[] => { + if (Array.isArray(qualifiedFunction)) return qualifiedFunction; + + // This is a safe assumption to make because the only native database that supports functions is postgres( and variants) + if (typeof qualifiedFunction === 'string') + return ['public', qualifiedFunction]; + + const { schema, name } = qualifiedFunction as { + schema: string; + name: string; + }; + + return [schema, name]; +}; + +export const search = ( + functions: IntrospectedFunction[], + searchText: string +) => { + if (!searchText.length) return functions; + + return functions.filter(fn => + adaptFunctionName(fn.qualifiedFunction) + .join(' / ') + .toLowerCase() + .includes(searchText.toLowerCase()) + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/reactQueryUtils.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/reactQueryUtils.ts new file mode 100644 index 00000000000..321ac9c4fa7 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/reactQueryUtils.ts @@ -0,0 +1,13 @@ +import { UseQueryOptions } from 'react-query'; +import { APIError } from '../../hooks/error'; + +export const DEFAULT_STALE_TIME = 5 * 60000; // 5 minutes as default stale time + +export const getDefaultQueryOptions = < + ReturnType, + FinalResult = ReturnType, + ErrorType = APIError +>(): UseQueryOptions => ({ + refetchOnWindowFocus: false, + staleTime: DEFAULT_STALE_TIME, +}); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/alloydb/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/alloydb/index.ts index 9a7ea5a2e63..8ff616b303e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/alloydb/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/alloydb/index.ts @@ -1,6 +1,9 @@ import { Table } from '../../hasura-metadata-types'; +import { + defaultDatabaseProps, + defaultIntrospectionProps, +} from '../common/defaultDatabaseProps'; import { Database, GetVersionProps } from '..'; -import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { getDatabaseConfiguration, getTrackableTables, @@ -20,6 +23,7 @@ export type AlloyDbTable = { name: string; schema: string }; export const alloy: Database = { ...defaultDatabaseProps, introspection: { + ...defaultIntrospectionProps, getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { const result = await runSQL({ source: { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/bigquery/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/bigquery/index.ts index eb98254983e..2e96239309f 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/bigquery/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/bigquery/index.ts @@ -1,6 +1,9 @@ import { Table } from '../../hasura-metadata-types'; import { Database, Feature } from '..'; -import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; +import { + defaultDatabaseProps, + defaultIntrospectionProps, +} from '../common/defaultDatabaseProps'; import { getTrackableTables, getTableColumns, @@ -15,6 +18,7 @@ export type BigQueryTable = { name: string; dataset: string }; export const bigquery: Database = { ...defaultDatabaseProps, introspection: { + ...defaultIntrospectionProps, getDriverInfo: async () => ({ name: 'bigquery', displayName: 'BigQuery', diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/citus/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/citus/index.ts index 468f99a7d20..cec04c71bc2 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/citus/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/citus/index.ts @@ -1,7 +1,10 @@ import { Table } from '../../hasura-metadata-types'; import { Database, Feature } from '..'; import { runSQL } from '../api'; -import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; +import { + defaultDatabaseProps, + defaultIntrospectionProps, +} from '../common/defaultDatabaseProps'; import { adaptIntrospectedTables } from '../common/utils'; import { GetTrackableTablesProps, GetVersionProps } from '../types'; import { @@ -19,6 +22,7 @@ export type CitusTable = { name: string; schema: string }; export const citus: Database = { ...defaultDatabaseProps, introspection: { + ...defaultIntrospectionProps, getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { const result = await runSQL({ source: { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/index.ts index c383444fc7b..593f519d267 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/index.ts @@ -1,7 +1,10 @@ import { Table } from '../../hasura-metadata-types'; import { Database, Feature } from '..'; import { runSQL } from '../api'; -import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; +import { + defaultDatabaseProps, + defaultIntrospectionProps, +} from '../common/defaultDatabaseProps'; import { adaptIntrospectedTables } from '../common/utils'; import { GetTrackableTablesProps, GetVersionProps } from '../types'; import { @@ -19,6 +22,7 @@ export type CockroachDBTable = { name: string; schema: string }; export const cockroach: Database = { ...defaultDatabaseProps, introspection: { + ...defaultIntrospectionProps, getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { const result = await runSQL({ source: { 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 57def5377db..a1c33c10417 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 @@ -1,24 +1,27 @@ import { Database, Feature } from '..'; +export const defaultIntrospectionProps = { + getVersion: async () => Feature.NotImplemented, + getDriverInfo: async () => Feature.NotImplemented, + getDatabaseConfiguration: async () => Feature.NotImplemented, + getDriverCapabilities: async () => Feature.NotImplemented, + getTrackableTables: async () => Feature.NotImplemented, + getDatabaseHierarchy: async () => Feature.NotImplemented, + getTableColumns: async () => Feature.NotImplemented, + getFKRelationships: async () => Feature.NotImplemented, + getTablesListAsTree: async () => Feature.NotImplemented, + getSupportedOperators: async () => Feature.NotImplemented, + getTrackableFunctions: async () => Feature.NotImplemented, + getDatabaseSchemas: async () => Feature.NotImplemented, + getIsTableView: async () => Feature.NotImplemented, +}; + export const defaultDatabaseProps: Database = { config: { getDefaultQueryRoot: async () => Feature.NotImplemented, getSupportedQueryTypes: async () => Feature.NotImplemented, }, - introspection: { - getVersion: async () => Feature.NotImplemented, - getDriverInfo: async () => Feature.NotImplemented, - getDatabaseConfiguration: async () => Feature.NotImplemented, - getDriverCapabilities: async () => Feature.NotImplemented, - getTrackableTables: async () => Feature.NotImplemented, - getDatabaseHierarchy: async () => Feature.NotImplemented, - getTableColumns: async () => Feature.NotImplemented, - getFKRelationships: async () => Feature.NotImplemented, - getTablesListAsTree: async () => Feature.NotImplemented, - getSupportedOperators: async () => Feature.NotImplemented, - getDatabaseSchemas: async () => Feature.NotImplemented, - getIsTableView: async () => Feature.NotImplemented, - }, + introspection: defaultIntrospectionProps, query: { getTableRows: async () => Feature.NotImplemented, }, 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 f91565b1105..a8b3a49d144 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 @@ -1,5 +1,8 @@ import { Table } from '../../hasura-metadata-types'; -import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; +import { + defaultDatabaseProps, + defaultIntrospectionProps, +} from '../common/defaultDatabaseProps'; import { Database, Feature } from '../index'; import { getTablesListAsTree, @@ -22,6 +25,7 @@ export type GDCTable = string[]; export const gdc: Database = { ...defaultDatabaseProps, introspection: { + ...defaultIntrospectionProps, getDriverInfo: async () => Feature.NotImplemented, getDatabaseConfiguration, getDriverCapabilities, 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 24924d10895..ae55b9acd62 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 @@ -21,7 +21,9 @@ import type { GetTableColumnsProps, GetTableRowsProps, GetTablesListAsTreeProps, + GetTrackableFunctionProps, GetTrackableTablesProps, + IntrospectedFunction, GetVersionProps, GetIsTableViewProps, // Property, @@ -133,6 +135,9 @@ export type Database = { getSupportedOperators: ( props: GetSupportedOperatorsProps ) => Promise; + getTrackableFunctions: ( + props: GetTrackableFunctionProps + ) => Promise; getDatabaseSchemas: ( props: GetDatabaseSchemaProps ) => Promise; @@ -531,6 +536,13 @@ export const DataSource = (httpClient: AxiosInstance) => ({ driver ); }, + getTrackableFunctions: async (dataSourceName: string) => { + const database = await getDatabaseMethods({ dataSourceName, httpClient }); + return database.introspection?.getTrackableFunctions({ + dataSourceName, + httpClient, + }); + }, getDatabaseSchemas: async ({ dataSourceName, }: { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/index.ts index e70f3c5109c..0a8bd8a04d7 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/index.ts @@ -6,8 +6,11 @@ import { } from '..'; import { Table } from '../../hasura-metadata-types'; import { NetworkArgs, runSQL } from '../api'; +import { + defaultDatabaseProps, + defaultIntrospectionProps, +} from '../common/defaultDatabaseProps'; import { postgresCapabilities } from '../common/capabilities'; -import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { adaptIntrospectedTables } from '../common/utils'; import { getDatabaseSchemas, @@ -31,6 +34,7 @@ const getDropSchemaSql = (schema: string) => { export const mssql: Database = { ...defaultDatabaseProps, introspection: { + ...defaultIntrospectionProps, getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { const result = await runSQL({ source: { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mysql/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mysql/index.ts index d719d8f0bd8..e1ab8928c76 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mysql/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mysql/index.ts @@ -1,11 +1,15 @@ import { Database, Feature } from '..'; -import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; +import { + defaultDatabaseProps, + defaultIntrospectionProps, +} from '../common/defaultDatabaseProps'; export type MySQLTable = { name: string }; export const mysql: Database = { ...defaultDatabaseProps, introspection: { + ...defaultIntrospectionProps, getDriverInfo: async () => ({ name: 'mysql', displayName: 'MySQL', diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts index 2b8eeac0fd3..b9cbf7fb382 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts @@ -13,6 +13,7 @@ import { getDatabaseSchemas, getFKRelationships, getSupportedOperators, + getTrackableFunctions, getTableColumns, getTablesListAsTree, getTrackableTables, @@ -31,6 +32,7 @@ const getCreateSchemaSql = (schemaName: string) => export const postgres: Database = { ...defaultDatabaseProps, introspection: { + getTrackableFunctions, getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { const result = await runSQL({ source: { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/introspection/getTrackableFunctions.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/introspection/getTrackableFunctions.ts new file mode 100644 index 00000000000..64315452ce1 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/introspection/getTrackableFunctions.ts @@ -0,0 +1,44 @@ +import { runSQL, RunSQLResponse } from '../../api'; +import { GetTrackableFunctionProps, IntrospectedFunction } from '../../types'; + +const adaptIntrospectedFunctions = ( + sqlResponse: RunSQLResponse +): IntrospectedFunction[] => { + return (sqlResponse.result ?? []).slice(1).map(row => ({ + name: row[0], + qualifiedFunction: { name: row[0], schema: row[1] }, + isVolatile: row[2] === 'VOLATILE', + })); +}; + +export const getTrackableFunctions = async ({ + dataSourceName, + httpClient, +}: GetTrackableFunctionProps) => { + const sql = ` + SELECT + pgp.proname AS function_name, + pn.nspname AS schema, + CASE + WHEN pgp.provolatile::text = 'i'::character(1)::text THEN 'IMMUTABLE'::text + WHEN pgp.provolatile::text = 's'::character(1)::text THEN 'STABLE'::text + WHEN pgp.provolatile::text = 'v'::character(1)::text THEN 'VOLATILE'::text + ELSE NULL::text + END AS function_type + FROM + pg_proc pgp JOIN pg_namespace pn ON pgp.pronamespace = pn.oid + WHERE + pn.nspname NOT IN ('information_schema') AND pn.nspname NOT LIKE 'pg_%'; + `; + + const sqlResult = await runSQL({ + source: { + name: dataSourceName, + kind: 'postgres', + }, + sql, + httpClient, + }); + + return adaptIntrospectedFunctions(sqlResult); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/introspection/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/introspection/index.ts index e568141b44a..99223b65a08 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/introspection/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/introspection/index.ts @@ -4,5 +4,6 @@ export { getTableColumns } from './getTableColumns'; export { getFKRelationships } from './getFKRelationships'; export { getTablesListAsTree } from './getTablesListAsTree'; export { getSupportedOperators } from './getSupportedOperators'; +export { getTrackableFunctions } from './getTrackableFunctions'; export { getDatabaseSchemas } from './getDatabaseSchemas'; export { getIsTableView } from './getIsTableView'; 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 35179a25094..5d4a421d528 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 @@ -10,6 +10,7 @@ import { SourceToSourceRelationship, SupportedDrivers, Table, + QualifiedFunction, } from '../hasura-metadata-types'; import type { NetworkArgs } from './api'; @@ -170,6 +171,15 @@ export type GetDefaultQueryRootProps = { table: Table; }; +export type GetTrackableFunctionProps = { + dataSourceName: string; +} & NetworkArgs; + +export type IntrospectedFunction = { + name: string; + qualifiedFunction: QualifiedFunction; + isVolatile: boolean; +}; export type GetDatabaseSchemaProps = { dataSourceName: string; } & NetworkArgs; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DatabaseRelationships/hooks/useCheckRows.ts b/frontend/libs/console/legacy-ce/src/lib/features/DatabaseRelationships/hooks/useCheckRows.ts index 6e143ac7600..26a56521a89 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DatabaseRelationships/hooks/useCheckRows.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DatabaseRelationships/hooks/useCheckRows.ts @@ -1,7 +1,7 @@ import { useState } from 'react'; import produce from 'immer'; -export const useCheckRows = (data: (T & { id: string })[]) => { +export const useCheckRows = (data: T[]) => { const [checkedIds, setCheckedIds] = useState([]); // Derived statuses 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 e1a5f4a0025..031e9209f56 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 @@ -39,8 +39,8 @@ export type SourceCustomization = { naming_convention?: NamingConvention; }; -export type PGFunction = { - function: string | { name: string; schema: string }; +export type MetadataFunction = { + function: QualifiedFunction; configuration?: { custom_name?: string; custom_root_fields?: { @@ -56,11 +56,11 @@ export type Source = { name: string; tables: MetadataTable[]; customization?: SourceCustomization; + functions?: MetadataFunction[]; } & ( | { kind: 'postgres'; configuration: PostgresConfiguration; - functions?: PGFunction[]; } | { kind: 'mssql'; @@ -83,3 +83,5 @@ export type Source = { configuration: unknown; } ); + +export type QualifiedFunction = unknown;