mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-17 13:37:26 +03:00
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
This commit is contained in:
parent
fad183d854
commit
2f7e89a9c2
@ -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 (
|
||||
<div className="flex gap-1 items-center">
|
||||
<TbMathFunction className="text-2xl text-gray-600" />
|
||||
{functionName.join(' / ')}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 items-center">
|
||||
<TbMathFunction className="text-2xl text-gray-600" />
|
||||
{dataSourceName} / {functionName.join(' / ')}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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<typeof UntrackedFunctions>;
|
||||
|
||||
export const Primary: ComponentStory<typeof UntrackedFunctions> = () => (
|
||||
<UntrackedFunctions dataSourceName="chinook" />
|
||||
);
|
@ -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<Feature | IntrospectedFunction[]>(),
|
||||
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 <Skeleton count={5} height={20} className="mb-1" />;
|
||||
|
||||
if (untrackedFunctions === Feature.NotImplemented) return null;
|
||||
|
||||
if (!untrackedFunctions.length)
|
||||
return (
|
||||
<IndicatorCard status="info" headline="No untracked functions found">
|
||||
We couldn't find any compatible functions in your database that can be
|
||||
tracked in Hasura.{' '}
|
||||
<LearnMoreLink href="https://hasura.io/docs/latest/schema/postgres/postgres-guides/functions/" />
|
||||
</IndicatorCard>
|
||||
);
|
||||
|
||||
const filteredResult = search(untrackedFunctions, searchText);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between space-x-4 mb-sm">
|
||||
<div className="flex gap-5">
|
||||
<div className="flex gap-2">
|
||||
<SearchBar
|
||||
onSearch={data => {
|
||||
setSearchText(data);
|
||||
setPageNumber(DEFAULT_PAGE_NUMBER);
|
||||
}}
|
||||
/>
|
||||
{searchText.length ? (
|
||||
<Badge>{filteredResult.length} results found</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
icon={<FaAngleLeft />}
|
||||
onClick={() => setPageNumber(pageNumber - 1)}
|
||||
disabled={pageNumber === 1}
|
||||
/>
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
className="block w-full max-w-xl h-8 min-h-full shadow-sm rounded pl-3 pr-6 py-0.5 border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400"
|
||||
>
|
||||
{DEFAULT_PAGE_SIZES.map(_pageSize => (
|
||||
<option key={_pageSize} value={_pageSize}>
|
||||
Show {_pageSize} tables
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
icon={<FaAngleRight />}
|
||||
onClick={() => setPageNumber(pageNumber + 1)}
|
||||
disabled={pageNumber >= filteredResult.length / pageSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CardedTable.Table>
|
||||
<CardedTable.TableHead>
|
||||
<CardedTable.TableHeadRow>
|
||||
<CardedTable.TableHeadCell>Function</CardedTable.TableHeadCell>
|
||||
<CardedTable.TableHeadCell>
|
||||
<div className="float-right">
|
||||
<DropdownMenu
|
||||
items={[
|
||||
[
|
||||
<span
|
||||
className="py-2"
|
||||
onClick={() => invalidateMetadata()}
|
||||
>
|
||||
Refresh
|
||||
</span>,
|
||||
],
|
||||
]}
|
||||
options={{
|
||||
content: {
|
||||
alignOffset: -50,
|
||||
avoidCollisions: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SlOptionsVertical />
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardedTable.TableHeadCell>
|
||||
</CardedTable.TableHeadRow>
|
||||
</CardedTable.TableHead>
|
||||
<CardedTable.TableBody>
|
||||
{paginate(filteredResult, pageSize, pageNumber).map(
|
||||
untrackedFunction => (
|
||||
<CardedTable.TableBodyRow>
|
||||
<CardedTable.TableBodyCell>
|
||||
<FunctionDisplayName
|
||||
qualifiedFunction={untrackedFunction.qualifiedFunction}
|
||||
/>
|
||||
</CardedTable.TableBodyCell>
|
||||
<CardedTable.TableBodyCell>
|
||||
<div className="flex gap-2 justify-end">
|
||||
{untrackedFunction.isVolatile ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() =>
|
||||
trackFunction({
|
||||
function: untrackedFunction.qualifiedFunction,
|
||||
configuration: {
|
||||
exposed_as: 'mutation',
|
||||
},
|
||||
source: dataSourceName,
|
||||
})
|
||||
}
|
||||
>
|
||||
Track as Mutation
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
trackFunction({
|
||||
function: untrackedFunction.qualifiedFunction,
|
||||
configuration: {
|
||||
exposed_as: 'query',
|
||||
},
|
||||
source: dataSourceName,
|
||||
})
|
||||
}
|
||||
>
|
||||
Track as Query
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
trackFunction({
|
||||
function: untrackedFunction.qualifiedFunction,
|
||||
source: dataSourceName,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Track as Root Field
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardedTable.TableBodyCell>
|
||||
</CardedTable.TableBodyRow>
|
||||
)
|
||||
)}
|
||||
</CardedTable.TableBody>
|
||||
</CardedTable.Table>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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 };
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
export type TrackableFunction = {
|
||||
id: string;
|
||||
name: string;
|
||||
isVolatile: boolean;
|
||||
};
|
@ -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())
|
||||
);
|
||||
};
|
@ -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<ReturnType, ErrorType, FinalResult> => ({
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: DEFAULT_STALE_TIME,
|
||||
});
|
@ -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: {
|
||||
|
@ -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',
|
||||
|
@ -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: {
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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<Operator[] | Feature.NotImplemented>;
|
||||
getTrackableFunctions: (
|
||||
props: GetTrackableFunctionProps
|
||||
) => Promise<IntrospectedFunction[] | Feature.NotImplemented>;
|
||||
getDatabaseSchemas: (
|
||||
props: GetDatabaseSchemaProps
|
||||
) => Promise<string[] | Feature.NotImplemented>;
|
||||
@ -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,
|
||||
}: {
|
||||
|
@ -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: {
|
||||
|
@ -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',
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
};
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import produce from 'immer';
|
||||
|
||||
export const useCheckRows = <T>(data: (T & { id: string })[]) => {
|
||||
export const useCheckRows = <T extends { id: string }>(data: T[]) => {
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// Derived statuses
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user