mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
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
This commit is contained in:
parent
39396c50b8
commit
5a2821bb01
@ -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 = (
|
||||
<IndexRedirect to="modify" />
|
||||
<Route path=":operation" component={ManageTable} />
|
||||
</Route>
|
||||
<Route path="function" component={ManageFunction}>
|
||||
<IndexRedirect to="modify" />
|
||||
<Route path=":operation" component={ManageFunction} />
|
||||
</Route>
|
||||
<Route path="database" component={ManageDatabaseRoute} />
|
||||
</Route>
|
||||
<Route path="edit" component={Connect.EditConnection} />
|
||||
|
@ -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)));
|
||||
}
|
||||
|
@ -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) => {
|
||||
</CollapsibleResource>
|
||||
)}
|
||||
|
||||
{areFunctionsSupported && (
|
||||
{areUserDefinedFunctionsSupported && (
|
||||
<CollapsibleResource
|
||||
title="Untracked Custom Functions"
|
||||
tooltip="Expose the functions available in your database via the GraphQL API"
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { QualifiedFunction } from '../../hasura-metadata-types';
|
||||
import { Tabs } from '../../../new-components/Tabs';
|
||||
import { getRoute } from '..';
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import _push from '../../../components/Services/Data/push';
|
||||
import { useURLParameters } from './hooks/useUrlParameters';
|
||||
import { Breadcrumbs } from './components/Breadcrumbs';
|
||||
import { Heading } from './components/Heading';
|
||||
import { Modify } from './components/Modify';
|
||||
|
||||
type AllowedTabs = 'modify';
|
||||
export interface ManageFunctionProps {
|
||||
params: {
|
||||
operation: AllowedTabs;
|
||||
};
|
||||
}
|
||||
|
||||
type Tab = {
|
||||
value: string;
|
||||
label: string;
|
||||
content: JSX.Element;
|
||||
};
|
||||
|
||||
const availableTabs = (
|
||||
dataSourceName: string,
|
||||
qualifiedFunction: QualifiedFunction
|
||||
): Tab[] => [
|
||||
{
|
||||
value: 'modify',
|
||||
label: 'Modify',
|
||||
content: (
|
||||
<Modify
|
||||
qualifiedFunction={qualifiedFunction}
|
||||
dataSourceName={dataSourceName}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const ManageFunction: React.VFC<ManageFunctionProps> = (
|
||||
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 (
|
||||
<div className="w-full bg-gray-50">
|
||||
<div className="px-md pt-md mb-xs">
|
||||
<Breadcrumbs
|
||||
dataSourceName={dataSourceName}
|
||||
qualifiedFunction={qualifiedFunction}
|
||||
/>
|
||||
<Heading
|
||||
dataSourceName={dataSourceName}
|
||||
qualifiedFunction={qualifiedFunction}
|
||||
/>
|
||||
<Tabs
|
||||
value={operation}
|
||||
onValueChange={_operation => {
|
||||
dispatch(
|
||||
_push(getRoute().function(dataSourceName, qualifiedFunction))
|
||||
);
|
||||
}}
|
||||
items={availableTabs(dataSourceName, qualifiedFunction)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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 }) => (
|
||||
<div className="flex items-center space-x-xs mb-4">
|
||||
<div className="cursor-pointer flex items-center text-muted hover:text-gray-900">
|
||||
<FaDatabase className="mr-1.5" />
|
||||
<span className="text-sm">{dataSourceName}</span>
|
||||
</div>
|
||||
<FaAngleRight className="text-muted" />
|
||||
<div className="cursor-pointer flex items-center text-muted hover:text-gray-900">
|
||||
<FunctionDisplayName qualifiedFunction={qualifiedFunction} />
|
||||
</div>
|
||||
<FaAngleRight className="text-muted" />
|
||||
<div className="cursor-pointer flex items-center">
|
||||
<span className="text-sm font-semibold text-yellow-500">Manage</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -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 (
|
||||
<div className="mx-sm">
|
||||
<Skeleton count={8} height={25} className="mb-2" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!metadataFunction)
|
||||
return (
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Could not find function in metadata"
|
||||
>
|
||||
{JSON.stringify(error)}
|
||||
</IndicatorCard>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 mb-sm">
|
||||
<b>Custom Name: </b>
|
||||
{metadataFunction.configuration?.custom_name ?? <Badge>Not Set</Badge>}
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<b>Return Type: </b>
|
||||
<TableDisplayName
|
||||
table={metadataFunction.configuration?.response?.table}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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: <DisplayToastErrorMessage message={err.message} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="group relative">
|
||||
<div>
|
||||
<DropdownMenu
|
||||
items={[
|
||||
[
|
||||
<span
|
||||
className="py-xs text-red-600"
|
||||
onClick={() => {
|
||||
untrackFunction({
|
||||
functionsToBeUntracked: [qualifiedFunction],
|
||||
});
|
||||
}}
|
||||
>
|
||||
Untrack
|
||||
</span>,
|
||||
],
|
||||
]}
|
||||
>
|
||||
<div className="flex gap-0.5 items-center">
|
||||
<Button
|
||||
iconPosition="end"
|
||||
icon={
|
||||
<FaChevronDown
|
||||
size={12}
|
||||
className="text-gray-400 text-sm transition-transform group-radix-state-open:rotate-180"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center ">
|
||||
<FunctionDisplayName qualifiedFunction={qualifiedFunction} />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Badge color="green">Tracked</Badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<div className="py-4">
|
||||
<div className="w-full bg-white p-4 rounded-sm border my-2">
|
||||
<div className="flex gap-2 mb-sm items-center">
|
||||
<div className="font-semibold text-2xl">Configuration</div>
|
||||
<IconTooltip
|
||||
message="allows you to customize any given function with a custom name and custom root fields of an already tracked function. This will replace the already present customization."
|
||||
icon={<FaQuestionCircle />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DisplayConfigurationDetails {...props} />
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={() => setIsEditConfigurationModalOpen(true)}
|
||||
icon={<FaEdit />}
|
||||
>
|
||||
Edit Configuration
|
||||
</Button>
|
||||
</div>
|
||||
{isEditConfigurationModalOpen && (
|
||||
<ModifyFunctionConfiguration
|
||||
{...props}
|
||||
onSuccess={() => setIsEditConfigurationModalOpen(false)}
|
||||
onClose={() => setIsEditConfigurationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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<typeof validationSchema>;
|
||||
|
||||
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: <DisplayToastErrorMessage message={err.message} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const customName = watch('custom_name');
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
hasBackdrop
|
||||
title="Edit Function Configuration"
|
||||
onClose={props.onClose}
|
||||
footer={
|
||||
<Dialog.Footer
|
||||
onSubmit={() => {
|
||||
handleSubmit(onHandleSubmit)();
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
onClose={props.onClose}
|
||||
callToDeny="Cancel"
|
||||
callToAction="Edit Configuration"
|
||||
onSubmitAnalyticsName="actions-tab-generate-types-submit"
|
||||
onCancelAnalyticsName="actions-tab-generate-types-cancel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="p-4">
|
||||
<Form
|
||||
onSubmit={data => {
|
||||
console.log('>>>', data);
|
||||
}}
|
||||
>
|
||||
<InputField
|
||||
name="custom_name"
|
||||
label="Custom Name"
|
||||
placeholder={adaptFunctionName(props.qualifiedFunction).join('_')}
|
||||
clearButton
|
||||
tooltip="The GraphQL nodes for the function will be generated according to the custom name"
|
||||
/>
|
||||
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">Custom Root Fields</div>
|
||||
}
|
||||
>
|
||||
<InputField
|
||||
name="custom_root_fields.function"
|
||||
label="Function"
|
||||
placeholder={
|
||||
customName?.length
|
||||
? customName
|
||||
: adaptFunctionName(props.qualifiedFunction).join('_')
|
||||
}
|
||||
tooltip="Customize the <function-name> root field"
|
||||
clearButton
|
||||
/>
|
||||
<InputField
|
||||
name="custom_root_fields.function_aggregate"
|
||||
label="Function Aggregate"
|
||||
placeholder={`${
|
||||
customName?.length
|
||||
? customName
|
||||
: adaptFunctionName(props.qualifiedFunction).join('_')
|
||||
}_aggregate`}
|
||||
tooltip="Customize the <function-name>_aggregate root field"
|
||||
clearButton
|
||||
/>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">Response Settings</div>
|
||||
}
|
||||
defaultOpen
|
||||
>
|
||||
<div className="hidden">
|
||||
<InputField name="response.type" label="type" />
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Select a return type"
|
||||
placeholder="Return type must be one of the tables tracked"
|
||||
name="response.table"
|
||||
options={tableOptions}
|
||||
/>
|
||||
</Collapsible>
|
||||
</Form>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
import { QualifiedFunction } from '../../../hasura-metadata-types';
|
||||
|
||||
// TYPES
|
||||
export type QueryStringParseResult =
|
||||
| {
|
||||
querystringParseResult: 'success';
|
||||
data: FunctionDefinition;
|
||||
}
|
||||
| {
|
||||
querystringParseResult: 'error';
|
||||
errorType: 'invalidTableDefinition' | 'invalidDatabaseDefinition';
|
||||
};
|
||||
|
||||
// TODO better types once GDC kicks in
|
||||
type FunctionDefinition = { database: string; function?: QualifiedFunction };
|
||||
|
||||
// CONSTANTS
|
||||
const FUNCTION_DEFINITION_SEARCH_KEY = 'function';
|
||||
const DATASOURCE_DEFINITION_SEARCH_KEY = 'database';
|
||||
|
||||
const invalidTableDefinitionResult: QueryStringParseResult = {
|
||||
querystringParseResult: 'error',
|
||||
errorType: 'invalidTableDefinition',
|
||||
};
|
||||
const invalidDatabaseDefinitionResult: QueryStringParseResult = {
|
||||
querystringParseResult: 'error',
|
||||
errorType: 'invalidDatabaseDefinition',
|
||||
};
|
||||
|
||||
// FUNCTION
|
||||
const getFunctionDefinitionFromUrl = (
|
||||
location: Location
|
||||
): QueryStringParseResult => {
|
||||
if (!location.search) return invalidTableDefinitionResult;
|
||||
// if tableDefinition is present in query params;
|
||||
// Idea is to use query params for GDC tables
|
||||
const params = new URLSearchParams(location.search);
|
||||
const qualifiedFunction = params.get(FUNCTION_DEFINITION_SEARCH_KEY);
|
||||
const database = params.get(DATASOURCE_DEFINITION_SEARCH_KEY);
|
||||
|
||||
if (!database) {
|
||||
return invalidDatabaseDefinitionResult;
|
||||
}
|
||||
|
||||
if (!qualifiedFunction) {
|
||||
return { querystringParseResult: 'success', data: { database } };
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
querystringParseResult: 'success',
|
||||
data: { database, function: JSON.parse(qualifiedFunction) },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Unable to parse the function definition', error);
|
||||
}
|
||||
|
||||
return invalidTableDefinitionResult;
|
||||
};
|
||||
|
||||
export const useURLParameters = (location = window.location) => {
|
||||
return getFunctionDefinitionFromUrl(location);
|
||||
};
|
@ -12,6 +12,8 @@ export const TableDisplayName = ({
|
||||
dataSourceName?: string;
|
||||
table: Table;
|
||||
}) => {
|
||||
if (!table) return null;
|
||||
|
||||
const tableName = getQualifiedTable(table);
|
||||
const content = () => (
|
||||
<>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { FaAngleRight, FaDatabase } from 'react-icons/fa';
|
||||
import { FaAngleRight, FaDatabase, FaTable } from 'react-icons/fa';
|
||||
|
||||
export const Breadcrumbs: React.VFC<{
|
||||
dataSourceName: string;
|
||||
@ -12,7 +12,7 @@ export const Breadcrumbs: React.VFC<{
|
||||
</div>
|
||||
<FaAngleRight className="text-muted" />
|
||||
<div className="cursor-pointer flex items-center text-muted hover:text-gray-900">
|
||||
<FaDatabase className="mr-1.5" />
|
||||
<FaTable className="mr-1.5" />
|
||||
<span className="text-sm">{tableName}</span>
|
||||
</div>
|
||||
<FaAngleRight className="text-muted" />
|
||||
|
@ -33,7 +33,7 @@ export const ManageTrackedFunctions = ({
|
||||
return (
|
||||
<TrackableResourceTabs
|
||||
introText={
|
||||
'Tracking tables adds them to your GraphQL API. All objects will be admin-only until permissions have been set.'
|
||||
'Tracking functions adds them to your GraphQL API. All objects will be admin-only until permissions have been set.'
|
||||
}
|
||||
value={tab}
|
||||
onValueChange={value => {
|
||||
|
@ -0,0 +1,179 @@
|
||||
import { z } from 'zod';
|
||||
import { Dialog } from '../../../../../new-components/Dialog';
|
||||
import { Select, useConsoleForm } from '../../../../../new-components/Form';
|
||||
import { AllowedFunctionTypes } from './UntrackedFunctions';
|
||||
import { useUntrackedFunctions } from '../hooks/useUntrackedFunctions';
|
||||
import { Feature } from '../../../../DataSource';
|
||||
import { IndicatorCard } from '../../../../../new-components/IndicatorCard';
|
||||
import { adaptFunctionName } from '../utils';
|
||||
import {
|
||||
MetadataSelectors,
|
||||
useMetadata,
|
||||
} from '../../../../hasura-metadata-api';
|
||||
import { getQualifiedTable } from '../../utils';
|
||||
import { DisplayToastErrorMessage } from '../../../components/DisplayErrorMessage';
|
||||
import { hasuraToast } from '../../../../../new-components/Toasts';
|
||||
import { useTrackFunction } from '../../../hooks/useTrackFunction';
|
||||
|
||||
const validationSchema = z.object({
|
||||
qualifiedFunction: z.any(),
|
||||
table: z.string().min(1, { message: 'The return type is mandatory' }),
|
||||
type: z.union([
|
||||
z.literal('mutation'),
|
||||
z.literal('query'),
|
||||
z.literal('root_field'),
|
||||
]),
|
||||
});
|
||||
|
||||
export type TrackFunctionFormSchema = z.infer<typeof validationSchema>;
|
||||
|
||||
const allowedFunctionTypes: AllowedFunctionTypes[] = [
|
||||
'mutation',
|
||||
'query',
|
||||
'root_field',
|
||||
];
|
||||
|
||||
export type TrackFunctionFormProps = {
|
||||
dataSourceName: string;
|
||||
onSuccess: () => void;
|
||||
onClose: () => void;
|
||||
defaultValues?: TrackFunctionFormSchema;
|
||||
};
|
||||
|
||||
export const TrackFunctionForm = (props: TrackFunctionFormProps) => {
|
||||
const { data: untrackedFunctions = [] } = useUntrackedFunctions(
|
||||
props.dataSourceName
|
||||
);
|
||||
|
||||
const { trackFunction, isLoading: isTrackingInProgress } = useTrackFunction({
|
||||
dataSourceName: props.dataSourceName,
|
||||
onSuccess: () => {
|
||||
hasuraToast({
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
message: `Tracked object successfully`,
|
||||
});
|
||||
},
|
||||
onError: err => {
|
||||
hasuraToast({
|
||||
type: 'error',
|
||||
title: err.name,
|
||||
children: <DisplayToastErrorMessage message={err.message} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
Form,
|
||||
methods: {
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
},
|
||||
} = useConsoleForm({
|
||||
schema: validationSchema,
|
||||
options: {
|
||||
defaultValues: props.defaultValues,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: tableOptions = [] } = useMetadata(m =>
|
||||
MetadataSelectors.findSource(props.dataSourceName)(m)?.tables.map(t => ({
|
||||
label: getQualifiedTable(t.table).join(' / '),
|
||||
value: JSON.stringify(t.table),
|
||||
}))
|
||||
);
|
||||
|
||||
console.log(tableOptions);
|
||||
|
||||
if (untrackedFunctions === Feature.NotImplemented)
|
||||
return (
|
||||
<IndicatorCard headline="Feature is not implemented">
|
||||
This feature is not available for {props.dataSourceName}
|
||||
</IndicatorCard>
|
||||
);
|
||||
|
||||
const onHandleSubmit = (data: TrackFunctionFormSchema) => {
|
||||
console.log(data);
|
||||
trackFunction({
|
||||
functionsToBeTracked: [
|
||||
{
|
||||
function: JSON.parse(data.qualifiedFunction),
|
||||
configuration: {
|
||||
...(data.type !== 'root_field' ? { exposed_as: data.type } : {}),
|
||||
response: {
|
||||
type: 'table',
|
||||
table: JSON.parse(data.table),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
console.log(errors);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
hasBackdrop
|
||||
title="Track Function"
|
||||
onClose={props.onClose}
|
||||
footer={
|
||||
<Dialog.Footer
|
||||
onSubmit={() => {
|
||||
handleSubmit(onHandleSubmit)();
|
||||
}}
|
||||
isLoading={isTrackingInProgress}
|
||||
onClose={props.onClose}
|
||||
callToDeny="Cancel"
|
||||
callToAction="Track Function"
|
||||
onSubmitAnalyticsName="actions-tab-generate-types-submit"
|
||||
onCancelAnalyticsName="actions-tab-generate-types-cancel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="p-4">
|
||||
<Form
|
||||
onSubmit={data => {
|
||||
console.log('>>>', data);
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
label="Track Function"
|
||||
name="qualifiedFunction"
|
||||
placeholder="Select a function"
|
||||
options={untrackedFunctions.map(f => ({
|
||||
value: JSON.stringify(f.qualifiedFunction),
|
||||
label: adaptFunctionName(f.qualifiedFunction).join(' / '),
|
||||
}))}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Tracked as"
|
||||
name="type"
|
||||
placeholder="Select type"
|
||||
options={allowedFunctionTypes.map(type => ({
|
||||
value: type,
|
||||
label: type,
|
||||
}))}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Select a return type"
|
||||
placeholder="Return type must be one of the tables tracked"
|
||||
name="table"
|
||||
options={tableOptions}
|
||||
disabled={!tableOptions.length}
|
||||
/>
|
||||
{!tableOptions.length && (
|
||||
<IndicatorCard headline="You do not have any tables tracked for this database">
|
||||
Tables that are tracked in Hasura can be used as the return type
|
||||
for your function.
|
||||
</IndicatorCard>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@ -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<number | undefined>();
|
||||
|
||||
const functionsWithId = React.useMemo(() => {
|
||||
@ -151,6 +156,22 @@ export const TrackedFunctions = (props: TrackedFunctionsProps) => {
|
||||
</CardedTable.TableBodyCell>
|
||||
<CardedTable.TableBodyCell>
|
||||
<div className="flex gap-2 justify-end">
|
||||
{isConfigurationModalOpen ? (
|
||||
<ModifyFunctionConfiguration
|
||||
dataSourceName={dataSourceName}
|
||||
qualifiedFunction={trackedFunction.qualifiedFunction}
|
||||
onSuccess={() => setIsConfigurationModalOpen(false)}
|
||||
onClose={() => setIsConfigurationModalOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsConfigurationModalOpen(true);
|
||||
}}
|
||||
icon={<FaEdit />}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveRow(index);
|
||||
|
@ -5,8 +5,12 @@ import { CardedTable } from '../../../../../new-components/CardedTable';
|
||||
import { DropdownMenu } from '../../../../../new-components/DropdownMenu';
|
||||
import { IndicatorCard } from '../../../../../new-components/IndicatorCard';
|
||||
import { LearnMoreLink } from '../../../../../new-components/LearnMoreLink';
|
||||
import { Feature } from '../../../../DataSource';
|
||||
import { useInvalidateMetadata } from '../../../../hasura-metadata-api';
|
||||
import { Feature, nativeDrivers } from '../../../../DataSource';
|
||||
import {
|
||||
MetadataSelectors,
|
||||
useInvalidateMetadata,
|
||||
useMetadata,
|
||||
} from '../../../../hasura-metadata-api';
|
||||
import { FunctionDisplayName } from './FunctionDisplayName';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
@ -18,12 +22,16 @@ import { useTrackFunction } from '../../../hooks/useTrackFunction';
|
||||
import { QualifiedFunction } from '../../../../hasura-metadata-types';
|
||||
import { DisplayToastErrorMessage } from '../../../components/DisplayErrorMessage';
|
||||
import { useHasuraAlert } from '../../../../../new-components/Alert';
|
||||
import {
|
||||
TrackFunctionForm,
|
||||
TrackFunctionFormSchema,
|
||||
} from './TrackFunctionForm';
|
||||
|
||||
export type UntrackedFunctionsProps = {
|
||||
dataSourceName: string;
|
||||
};
|
||||
|
||||
type AllowedFunctionTypes = 'mutation' | 'query' | 'root_field';
|
||||
export type AllowedFunctionTypes = 'mutation' | 'query' | 'root_field';
|
||||
|
||||
export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
||||
const { dataSourceName } = props;
|
||||
@ -31,22 +39,33 @@ export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
||||
const { data: untrackedFunctions = [], isLoading } =
|
||||
useUntrackedFunctions(dataSourceName);
|
||||
|
||||
const functionsWithId = React.useMemo(() => {
|
||||
if (Array.isArray(untrackedFunctions)) {
|
||||
return untrackedFunctions.map(f => ({ ...f, id: f.name }));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [untrackedFunctions]);
|
||||
const functionsWithId = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(untrackedFunctions)
|
||||
? untrackedFunctions.map(f => ({ ...f, id: f.name }))
|
||||
: [],
|
||||
[untrackedFunctions]
|
||||
);
|
||||
|
||||
const invalidateMetadata = useInvalidateMetadata();
|
||||
|
||||
const [activeRow, setActiveRow] = useState<number | undefined>();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [modalFormDefaultValues, setModalFormDefaultValues] =
|
||||
useState<TrackFunctionFormSchema>();
|
||||
|
||||
const [activeOperation, setActiveOperation] =
|
||||
useState<AllowedFunctionTypes>();
|
||||
|
||||
const { hasuraConfirm } = useHasuraAlert();
|
||||
|
||||
const { data: driver = '' } = useMetadata(
|
||||
m => MetadataSelectors.findSource(dataSourceName)(m)?.kind
|
||||
);
|
||||
|
||||
const isNativeDriver = nativeDrivers.includes(driver);
|
||||
|
||||
const { trackFunction, isLoading: isTrackingInProgress } = useTrackFunction({
|
||||
dataSourceName,
|
||||
onSuccess: () => {
|
||||
@ -92,6 +111,17 @@ export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
||||
fn: QualifiedFunction,
|
||||
type: AllowedFunctionTypes
|
||||
) => {
|
||||
// hack until capabilities or function schema can tell us if the function supports return types
|
||||
if (!isNativeDriver) {
|
||||
setModalFormDefaultValues({
|
||||
qualifiedFunction: JSON.stringify(fn),
|
||||
type,
|
||||
table: '',
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveRow(index);
|
||||
setActiveOperation(type);
|
||||
trackFunction({
|
||||
@ -115,125 +145,137 @@ export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<TrackableListMenu
|
||||
checkActionText={`Track Selected (${checkedItems.length})`}
|
||||
isLoading={isLoading}
|
||||
{...listProps}
|
||||
/>
|
||||
<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>
|
||||
{paginatedData.map((untrackedFunction, index) => (
|
||||
<CardedTable.TableBodyRow>
|
||||
<CardedTable.TableBodyCell>
|
||||
<FunctionDisplayName
|
||||
qualifiedFunction={untrackedFunction.qualifiedFunction}
|
||||
/>
|
||||
</CardedTable.TableBodyCell>
|
||||
<CardedTable.TableBodyCell>
|
||||
<div className="flex gap-2 justify-end">
|
||||
{untrackedFunction.isVolatile ? (
|
||||
<>
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
<TrackableListMenu
|
||||
checkActionText={`Track Selected (${checkedItems.length})`}
|
||||
isLoading={isLoading}
|
||||
{...listProps}
|
||||
/>
|
||||
<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>
|
||||
{paginatedData.map((untrackedFunction, index) => (
|
||||
<CardedTable.TableBodyRow key={untrackedFunction.id}>
|
||||
<CardedTable.TableBodyCell>
|
||||
<FunctionDisplayName
|
||||
qualifiedFunction={untrackedFunction.qualifiedFunction}
|
||||
/>
|
||||
</CardedTable.TableBodyCell>
|
||||
<CardedTable.TableBodyCell>
|
||||
<div className="flex gap-2 justify-end">
|
||||
{untrackedFunction.isVolatile ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleTrack(
|
||||
index,
|
||||
untrackedFunction.qualifiedFunction,
|
||||
'mutation'
|
||||
);
|
||||
}}
|
||||
isLoading={
|
||||
activeRow === index &&
|
||||
isTrackingInProgress &&
|
||||
activeOperation === 'mutation'
|
||||
}
|
||||
disabled={activeRow === index && isTrackingInProgress}
|
||||
loadingText="Please wait..."
|
||||
>
|
||||
Track as Mutation
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
hasuraConfirm({
|
||||
message:
|
||||
'Queries are supposed to be read only and as such recommended to be STABLE or IMMUTABLE',
|
||||
title: `Confirm tracking ${untrackedFunction.name} as a query`,
|
||||
onClose: ({ confirmed }) => {
|
||||
if (confirmed)
|
||||
handleTrack(
|
||||
index,
|
||||
untrackedFunction.qualifiedFunction,
|
||||
'query'
|
||||
);
|
||||
},
|
||||
})
|
||||
}
|
||||
isLoading={
|
||||
activeRow === index &&
|
||||
isTrackingInProgress &&
|
||||
activeOperation === 'query'
|
||||
}
|
||||
disabled={activeRow === index && isTrackingInProgress}
|
||||
loadingText="Please wait..."
|
||||
>
|
||||
Track as Query
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleTrack(
|
||||
index,
|
||||
untrackedFunction.qualifiedFunction,
|
||||
'mutation'
|
||||
'root_field'
|
||||
);
|
||||
}}
|
||||
isLoading={
|
||||
activeRow === index &&
|
||||
isTrackingInProgress &&
|
||||
activeOperation === 'mutation'
|
||||
activeOperation === 'root_field'
|
||||
}
|
||||
disabled={activeRow === index && isTrackingInProgress}
|
||||
loadingText="Please wait..."
|
||||
>
|
||||
Track as Mutation
|
||||
Track as Root Field
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
hasuraConfirm({
|
||||
message:
|
||||
'Queries are supposed to be read only and as such recommended to be STABLE or IMMUTABLE',
|
||||
title: `Confirm tracking ${untrackedFunction.name} as a query`,
|
||||
onClose: ({ confirmed }) => {
|
||||
if (confirmed)
|
||||
handleTrack(
|
||||
index,
|
||||
untrackedFunction.qualifiedFunction,
|
||||
'query'
|
||||
);
|
||||
},
|
||||
})
|
||||
}
|
||||
isLoading={
|
||||
activeRow === index &&
|
||||
isTrackingInProgress &&
|
||||
activeOperation === 'query'
|
||||
}
|
||||
disabled={activeRow === index && isTrackingInProgress}
|
||||
loadingText="Please wait..."
|
||||
>
|
||||
Track as Query
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleTrack(
|
||||
index,
|
||||
untrackedFunction.qualifiedFunction,
|
||||
'root_field'
|
||||
);
|
||||
}}
|
||||
isLoading={
|
||||
activeRow === index &&
|
||||
isTrackingInProgress &&
|
||||
activeOperation === 'root_field'
|
||||
}
|
||||
disabled={activeRow === index && isTrackingInProgress}
|
||||
loadingText="Please wait..."
|
||||
>
|
||||
Track as Root Field
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardedTable.TableBodyCell>
|
||||
</CardedTable.TableBodyRow>
|
||||
))}
|
||||
</CardedTable.TableBody>
|
||||
</CardedTable.Table>
|
||||
)}
|
||||
</div>
|
||||
</CardedTable.TableBodyCell>
|
||||
</CardedTable.TableBodyRow>
|
||||
))}
|
||||
</CardedTable.TableBody>
|
||||
</CardedTable.Table>
|
||||
</div>
|
||||
<div>
|
||||
{isModalOpen ? (
|
||||
<TrackFunctionForm
|
||||
dataSourceName={dataSourceName}
|
||||
onSuccess={() => setIsModalOpen(false)}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
defaultValues={modalFormDefaultValues}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -55,8 +55,6 @@ export const useCheckRows = <T,>(
|
||||
setCheckedIds([]);
|
||||
};
|
||||
|
||||
console.log('state of checked', checkedIds);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!checkboxRef.current) return;
|
||||
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
|
||||
|
@ -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 = <FinalResult = Feature | Capabilities>({
|
||||
dataSourceName,
|
||||
select,
|
||||
options = {},
|
||||
}: UseDatabaseCapabilitiesArgs & {
|
||||
select?: (data: Feature | DriverCapability) => FinalResult;
|
||||
options?: UseQueryOptions<Feature | DriverCapability, APIError, FinalResult>;
|
||||
select?: (data: Feature | Capabilities) => FinalResult;
|
||||
options?: UseQueryOptions<Feature | Capabilities, APIError, FinalResult>;
|
||||
}) => {
|
||||
const httpClient = useHttpClient();
|
||||
|
||||
@ -24,7 +23,7 @@ export const useDriverCapabilities = <
|
||||
m => m.metadata.sources.find(source => source.name === dataSourceName)?.kind
|
||||
);
|
||||
|
||||
return useQuery<Feature | DriverCapability, APIError, FinalResult>(
|
||||
return useQuery<Feature | Capabilities, APIError, FinalResult>(
|
||||
[dataSourceName, 'capabilities'],
|
||||
async () => {
|
||||
const result =
|
||||
|
@ -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 };
|
||||
};
|
@ -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,
|
||||
},
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
|
@ -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: (
|
||||
<div className="inline-block">
|
||||
@ -37,6 +43,11 @@ export const getTablesListAsTree = async ({
|
||||
),
|
||||
key: JSON.stringify({ database: source.name }),
|
||||
icon: <FaDatabase />,
|
||||
children: tables.length ? convertToTreeData(tables, [], source.name) : [],
|
||||
children: tables.length
|
||||
? [
|
||||
...convertToTreeData(tables, [], source.name),
|
||||
...convertToTreeData(functions, [], source.name, 'function'),
|
||||
]
|
||||
: [],
|
||||
};
|
||||
};
|
||||
|
@ -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<TrackableObjects>({
|
||||
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');
|
||||
}
|
||||
};
|
@ -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';
|
||||
|
@ -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: <FaTable />,
|
||||
key: JSON.stringify({
|
||||
database: dataSourceName,
|
||||
table: [...key, table[0]],
|
||||
}),
|
||||
icon:
|
||||
mode === 'function' ? (
|
||||
<TbMathFunction className="text-muted mr-xs" />
|
||||
) : (
|
||||
<FaTable />
|
||||
),
|
||||
key:
|
||||
mode === 'function'
|
||||
? JSON.stringify({
|
||||
database: dataSourceName,
|
||||
function: [...key, table[0]],
|
||||
})
|
||||
: JSON.stringify({
|
||||
database: dataSourceName,
|
||||
table: [...key, table[0]],
|
||||
}),
|
||||
title: table[0],
|
||||
};
|
||||
});
|
||||
|
@ -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<DriverCapability | Feature>;
|
||||
) => Promise<Capabilities | Feature>;
|
||||
getTrackableTables: (
|
||||
props: GetTrackableTablesProps
|
||||
) => Promise<IntrospectedTable[] | Feature.NotImplemented>;
|
||||
@ -141,6 +145,9 @@ export type Database = {
|
||||
getTrackableFunctions: (
|
||||
props: GetTrackableFunctionProps
|
||||
) => Promise<IntrospectedFunction[] | Feature.NotImplemented>;
|
||||
getTrackableObjects: (
|
||||
props: GetTrackableObjectsProps
|
||||
) => Promise<GetTrackableObjectsResponse | Feature.NotImplemented>;
|
||||
getDatabaseSchemas: (
|
||||
props: GetDatabaseSchemaProps
|
||||
) => Promise<string[] | Feature.NotImplemented>;
|
||||
@ -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,
|
||||
|
@ -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<string, any>;
|
||||
};
|
||||
|
||||
export type StoredProcedure = unknown;
|
||||
export type GetStoredProceduresProps = {
|
||||
dataSourceName: string;
|
||||
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
)}`
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user