mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +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 { LandingPageRoute as NativeQueries } from '../../../features/Data/LogicalModels/LandingPage/LandingPage';
|
||||||
import { AddNativeQueryRoute } from '../../../features/Data/LogicalModels/AddNativeQuery/AddNativeQueryRoute';
|
import { AddNativeQueryRoute } from '../../../features/Data/LogicalModels/AddNativeQuery/AddNativeQueryRoute';
|
||||||
import { TrackStoredProcedureRoute } from '../../../features/Data/LogicalModels/StoredProcedures/StoredProcedureWidget.route';
|
import { TrackStoredProcedureRoute } from '../../../features/Data/LogicalModels/StoredProcedures/StoredProcedureWidget.route';
|
||||||
|
import { ManageFunction } from '../../../features/Data/ManageFunction/ManageFunction';
|
||||||
|
|
||||||
const makeDataRouter = (
|
const makeDataRouter = (
|
||||||
connect,
|
connect,
|
||||||
@ -66,6 +67,10 @@ const makeDataRouter = (
|
|||||||
<IndexRedirect to="modify" />
|
<IndexRedirect to="modify" />
|
||||||
<Route path=":operation" component={ManageTable} />
|
<Route path=":operation" component={ManageTable} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="function" component={ManageFunction}>
|
||||||
|
<IndexRedirect to="modify" />
|
||||||
|
<Route path=":operation" component={ManageFunction} />
|
||||||
|
</Route>
|
||||||
<Route path="database" component={ManageDatabaseRoute} />
|
<Route path="database" component={ManageDatabaseRoute} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="edit" component={Connect.EditConnection} />
|
<Route path="edit" component={Connect.EditConnection} />
|
||||||
|
@ -30,6 +30,7 @@ export const useGDCTreeItemClick = (dispatch: Dispatch) => {
|
|||||||
* Handling click for GDC DBs
|
* Handling click for GDC DBs
|
||||||
*/
|
*/
|
||||||
const isTableClicked = Object.keys(rest?.table || {}).length !== 0;
|
const isTableClicked = Object.keys(rest?.table || {}).length !== 0;
|
||||||
|
const isFunctionClicked = Object.keys(rest?.function || {}).length !== 0;
|
||||||
if (isTableClicked) {
|
if (isTableClicked) {
|
||||||
dispatch(
|
dispatch(
|
||||||
_push(
|
_push(
|
||||||
@ -40,6 +41,8 @@ export const useGDCTreeItemClick = (dispatch: Dispatch) => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else if (isFunctionClicked) {
|
||||||
|
dispatch(_push(getRoute().function(database, rest.function)));
|
||||||
} else {
|
} else {
|
||||||
dispatch(_push(getRoute().database(database)));
|
dispatch(_push(getRoute().database(database)));
|
||||||
}
|
}
|
||||||
|
@ -14,18 +14,18 @@ export interface ManageDatabaseProps {
|
|||||||
export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
|
export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
areFunctionsSupported = false,
|
|
||||||
areForeignKeysSupported = false,
|
areForeignKeysSupported = false,
|
||||||
|
areUserDefinedFunctionsSupported = false,
|
||||||
} = {},
|
} = {},
|
||||||
} = useDriverCapabilities({
|
} = useDriverCapabilities({
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
select: data => {
|
select: data => {
|
||||||
return {
|
return {
|
||||||
areFunctionsSupported: !!get(data, 'functions'),
|
|
||||||
areForeignKeysSupported: !!get(
|
areForeignKeysSupported: !!get(
|
||||||
data,
|
data,
|
||||||
'data_schema.supports_foreign_keys'
|
'data_schema.supports_foreign_keys'
|
||||||
),
|
),
|
||||||
|
areUserDefinedFunctionsSupported: !!get(data, 'user_defined_functions'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -60,7 +60,7 @@ export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
|
|||||||
</CollapsibleResource>
|
</CollapsibleResource>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{areFunctionsSupported && (
|
{areUserDefinedFunctionsSupported && (
|
||||||
<CollapsibleResource
|
<CollapsibleResource
|
||||||
title="Untracked Custom Functions"
|
title="Untracked Custom Functions"
|
||||||
tooltip="Expose the functions available in your database via the GraphQL API"
|
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;
|
dataSourceName?: string;
|
||||||
table: Table;
|
table: Table;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!table) return null;
|
||||||
|
|
||||||
const tableName = getQualifiedTable(table);
|
const tableName = getQualifiedTable(table);
|
||||||
const content = () => (
|
const content = () => (
|
||||||
<>
|
<>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FaAngleRight, FaDatabase } from 'react-icons/fa';
|
import { FaAngleRight, FaDatabase, FaTable } from 'react-icons/fa';
|
||||||
|
|
||||||
export const Breadcrumbs: React.VFC<{
|
export const Breadcrumbs: React.VFC<{
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
@ -12,7 +12,7 @@ export const Breadcrumbs: React.VFC<{
|
|||||||
</div>
|
</div>
|
||||||
<FaAngleRight className="text-muted" />
|
<FaAngleRight className="text-muted" />
|
||||||
<div className="cursor-pointer flex items-center text-muted hover:text-gray-900">
|
<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>
|
<span className="text-sm">{tableName}</span>
|
||||||
</div>
|
</div>
|
||||||
<FaAngleRight className="text-muted" />
|
<FaAngleRight className="text-muted" />
|
||||||
|
@ -33,7 +33,7 @@ export const ManageTrackedFunctions = ({
|
|||||||
return (
|
return (
|
||||||
<TrackableResourceTabs
|
<TrackableResourceTabs
|
||||||
introText={
|
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}
|
value={tab}
|
||||||
onValueChange={value => {
|
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 { hasuraToast } from '../../../../../new-components/Toasts';
|
||||||
import { DisplayToastErrorMessage } from '../../../components/DisplayErrorMessage';
|
import { DisplayToastErrorMessage } from '../../../components/DisplayErrorMessage';
|
||||||
import { useTrackFunction } from '../../../hooks/useTrackFunction';
|
import { useTrackFunction } from '../../../hooks/useTrackFunction';
|
||||||
|
import { ModifyFunctionConfiguration } from '../../../ManageFunction/components/ModifyFunctionConfiguration';
|
||||||
|
import { FaEdit } from 'react-icons/fa';
|
||||||
|
|
||||||
export type TrackedFunctionsProps = {
|
export type TrackedFunctionsProps = {
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
@ -27,6 +29,9 @@ export const TrackedFunctions = (props: TrackedFunctionsProps) => {
|
|||||||
const { data: trackedFunctions = [], isLoading } =
|
const { data: trackedFunctions = [], isLoading } =
|
||||||
useTrackedFunctions(dataSourceName);
|
useTrackedFunctions(dataSourceName);
|
||||||
|
|
||||||
|
const [isConfigurationModalOpen, setIsConfigurationModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const [activeRow, setActiveRow] = useState<number | undefined>();
|
const [activeRow, setActiveRow] = useState<number | undefined>();
|
||||||
|
|
||||||
const functionsWithId = React.useMemo(() => {
|
const functionsWithId = React.useMemo(() => {
|
||||||
@ -151,6 +156,22 @@ export const TrackedFunctions = (props: TrackedFunctionsProps) => {
|
|||||||
</CardedTable.TableBodyCell>
|
</CardedTable.TableBodyCell>
|
||||||
<CardedTable.TableBodyCell>
|
<CardedTable.TableBodyCell>
|
||||||
<div className="flex gap-2 justify-end">
|
<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
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveRow(index);
|
setActiveRow(index);
|
||||||
|
@ -5,8 +5,12 @@ import { CardedTable } from '../../../../../new-components/CardedTable';
|
|||||||
import { DropdownMenu } from '../../../../../new-components/DropdownMenu';
|
import { DropdownMenu } from '../../../../../new-components/DropdownMenu';
|
||||||
import { IndicatorCard } from '../../../../../new-components/IndicatorCard';
|
import { IndicatorCard } from '../../../../../new-components/IndicatorCard';
|
||||||
import { LearnMoreLink } from '../../../../../new-components/LearnMoreLink';
|
import { LearnMoreLink } from '../../../../../new-components/LearnMoreLink';
|
||||||
import { Feature } from '../../../../DataSource';
|
import { Feature, nativeDrivers } from '../../../../DataSource';
|
||||||
import { useInvalidateMetadata } from '../../../../hasura-metadata-api';
|
import {
|
||||||
|
MetadataSelectors,
|
||||||
|
useInvalidateMetadata,
|
||||||
|
useMetadata,
|
||||||
|
} from '../../../../hasura-metadata-api';
|
||||||
import { FunctionDisplayName } from './FunctionDisplayName';
|
import { FunctionDisplayName } from './FunctionDisplayName';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
@ -18,12 +22,16 @@ import { useTrackFunction } from '../../../hooks/useTrackFunction';
|
|||||||
import { QualifiedFunction } from '../../../../hasura-metadata-types';
|
import { QualifiedFunction } from '../../../../hasura-metadata-types';
|
||||||
import { DisplayToastErrorMessage } from '../../../components/DisplayErrorMessage';
|
import { DisplayToastErrorMessage } from '../../../components/DisplayErrorMessage';
|
||||||
import { useHasuraAlert } from '../../../../../new-components/Alert';
|
import { useHasuraAlert } from '../../../../../new-components/Alert';
|
||||||
|
import {
|
||||||
|
TrackFunctionForm,
|
||||||
|
TrackFunctionFormSchema,
|
||||||
|
} from './TrackFunctionForm';
|
||||||
|
|
||||||
export type UntrackedFunctionsProps = {
|
export type UntrackedFunctionsProps = {
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AllowedFunctionTypes = 'mutation' | 'query' | 'root_field';
|
export type AllowedFunctionTypes = 'mutation' | 'query' | 'root_field';
|
||||||
|
|
||||||
export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
||||||
const { dataSourceName } = props;
|
const { dataSourceName } = props;
|
||||||
@ -31,22 +39,33 @@ export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
|||||||
const { data: untrackedFunctions = [], isLoading } =
|
const { data: untrackedFunctions = [], isLoading } =
|
||||||
useUntrackedFunctions(dataSourceName);
|
useUntrackedFunctions(dataSourceName);
|
||||||
|
|
||||||
const functionsWithId = React.useMemo(() => {
|
const functionsWithId = React.useMemo(
|
||||||
if (Array.isArray(untrackedFunctions)) {
|
() =>
|
||||||
return untrackedFunctions.map(f => ({ ...f, id: f.name }));
|
Array.isArray(untrackedFunctions)
|
||||||
} else {
|
? untrackedFunctions.map(f => ({ ...f, id: f.name }))
|
||||||
return [];
|
: [],
|
||||||
}
|
[untrackedFunctions]
|
||||||
}, [untrackedFunctions]);
|
);
|
||||||
|
|
||||||
const invalidateMetadata = useInvalidateMetadata();
|
const invalidateMetadata = useInvalidateMetadata();
|
||||||
|
|
||||||
const [activeRow, setActiveRow] = useState<number | undefined>();
|
const [activeRow, setActiveRow] = useState<number | undefined>();
|
||||||
|
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [modalFormDefaultValues, setModalFormDefaultValues] =
|
||||||
|
useState<TrackFunctionFormSchema>();
|
||||||
|
|
||||||
const [activeOperation, setActiveOperation] =
|
const [activeOperation, setActiveOperation] =
|
||||||
useState<AllowedFunctionTypes>();
|
useState<AllowedFunctionTypes>();
|
||||||
|
|
||||||
const { hasuraConfirm } = useHasuraAlert();
|
const { hasuraConfirm } = useHasuraAlert();
|
||||||
|
|
||||||
|
const { data: driver = '' } = useMetadata(
|
||||||
|
m => MetadataSelectors.findSource(dataSourceName)(m)?.kind
|
||||||
|
);
|
||||||
|
|
||||||
|
const isNativeDriver = nativeDrivers.includes(driver);
|
||||||
|
|
||||||
const { trackFunction, isLoading: isTrackingInProgress } = useTrackFunction({
|
const { trackFunction, isLoading: isTrackingInProgress } = useTrackFunction({
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -92,6 +111,17 @@ export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
|||||||
fn: QualifiedFunction,
|
fn: QualifiedFunction,
|
||||||
type: AllowedFunctionTypes
|
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);
|
setActiveRow(index);
|
||||||
setActiveOperation(type);
|
setActiveOperation(type);
|
||||||
trackFunction({
|
trackFunction({
|
||||||
@ -115,125 +145,137 @@ export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div>
|
||||||
<TrackableListMenu
|
<div className="space-y-4">
|
||||||
checkActionText={`Track Selected (${checkedItems.length})`}
|
<TrackableListMenu
|
||||||
isLoading={isLoading}
|
checkActionText={`Track Selected (${checkedItems.length})`}
|
||||||
{...listProps}
|
isLoading={isLoading}
|
||||||
/>
|
{...listProps}
|
||||||
<CardedTable.Table>
|
/>
|
||||||
<CardedTable.TableHead>
|
<CardedTable.Table>
|
||||||
<CardedTable.TableHeadRow>
|
<CardedTable.TableHead>
|
||||||
<CardedTable.TableHeadCell>Function</CardedTable.TableHeadCell>
|
<CardedTable.TableHeadRow>
|
||||||
<CardedTable.TableHeadCell>
|
<CardedTable.TableHeadCell>Function</CardedTable.TableHeadCell>
|
||||||
<div className="float-right">
|
<CardedTable.TableHeadCell>
|
||||||
<DropdownMenu
|
<div className="float-right">
|
||||||
items={[
|
<DropdownMenu
|
||||||
[
|
items={[
|
||||||
<span
|
[
|
||||||
className="py-2"
|
<span
|
||||||
onClick={() => invalidateMetadata()}
|
className="py-2"
|
||||||
>
|
onClick={() => invalidateMetadata()}
|
||||||
Refresh
|
>
|
||||||
</span>,
|
Refresh
|
||||||
],
|
</span>,
|
||||||
]}
|
],
|
||||||
options={{
|
]}
|
||||||
content: {
|
options={{
|
||||||
alignOffset: -50,
|
content: {
|
||||||
avoidCollisions: false,
|
alignOffset: -50,
|
||||||
},
|
avoidCollisions: false,
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
<SlOptionsVertical />
|
>
|
||||||
</DropdownMenu>
|
<SlOptionsVertical />
|
||||||
</div>
|
</DropdownMenu>
|
||||||
</CardedTable.TableHeadCell>
|
</div>
|
||||||
</CardedTable.TableHeadRow>
|
</CardedTable.TableHeadCell>
|
||||||
</CardedTable.TableHead>
|
</CardedTable.TableHeadRow>
|
||||||
<CardedTable.TableBody>
|
</CardedTable.TableHead>
|
||||||
{paginatedData.map((untrackedFunction, index) => (
|
<CardedTable.TableBody>
|
||||||
<CardedTable.TableBodyRow>
|
{paginatedData.map((untrackedFunction, index) => (
|
||||||
<CardedTable.TableBodyCell>
|
<CardedTable.TableBodyRow key={untrackedFunction.id}>
|
||||||
<FunctionDisplayName
|
<CardedTable.TableBodyCell>
|
||||||
qualifiedFunction={untrackedFunction.qualifiedFunction}
|
<FunctionDisplayName
|
||||||
/>
|
qualifiedFunction={untrackedFunction.qualifiedFunction}
|
||||||
</CardedTable.TableBodyCell>
|
/>
|
||||||
<CardedTable.TableBodyCell>
|
</CardedTable.TableBodyCell>
|
||||||
<div className="flex gap-2 justify-end">
|
<CardedTable.TableBodyCell>
|
||||||
{untrackedFunction.isVolatile ? (
|
<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
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleTrack(
|
handleTrack(
|
||||||
index,
|
index,
|
||||||
untrackedFunction.qualifiedFunction,
|
untrackedFunction.qualifiedFunction,
|
||||||
'mutation'
|
'root_field'
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
isLoading={
|
isLoading={
|
||||||
activeRow === index &&
|
activeRow === index &&
|
||||||
isTrackingInProgress &&
|
isTrackingInProgress &&
|
||||||
activeOperation === 'mutation'
|
activeOperation === 'root_field'
|
||||||
}
|
}
|
||||||
disabled={activeRow === index && isTrackingInProgress}
|
disabled={activeRow === index && isTrackingInProgress}
|
||||||
loadingText="Please wait..."
|
loadingText="Please wait..."
|
||||||
>
|
>
|
||||||
Track as Mutation
|
Track as Root Field
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
)}
|
||||||
onClick={() =>
|
</div>
|
||||||
hasuraConfirm({
|
</CardedTable.TableBodyCell>
|
||||||
message:
|
</CardedTable.TableBodyRow>
|
||||||
'Queries are supposed to be read only and as such recommended to be STABLE or IMMUTABLE',
|
))}
|
||||||
title: `Confirm tracking ${untrackedFunction.name} as a query`,
|
</CardedTable.TableBody>
|
||||||
onClose: ({ confirmed }) => {
|
</CardedTable.Table>
|
||||||
if (confirmed)
|
</div>
|
||||||
handleTrack(
|
<div>
|
||||||
index,
|
{isModalOpen ? (
|
||||||
untrackedFunction.qualifiedFunction,
|
<TrackFunctionForm
|
||||||
'query'
|
dataSourceName={dataSourceName}
|
||||||
);
|
onSuccess={() => setIsModalOpen(false)}
|
||||||
},
|
onClose={() => setIsModalOpen(false)}
|
||||||
})
|
defaultValues={modalFormDefaultValues}
|
||||||
}
|
/>
|
||||||
isLoading={
|
) : null}
|
||||||
activeRow === index &&
|
</div>
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -55,8 +55,6 @@ export const useCheckRows = <T,>(
|
|||||||
setCheckedIds([]);
|
setCheckedIds([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('state of checked', checkedIds);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!checkboxRef.current) return;
|
if (!checkboxRef.current) return;
|
||||||
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
|
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
import { UseQueryOptions, useQuery } from 'react-query';
|
import { UseQueryOptions, useQuery } from 'react-query';
|
||||||
import { DataSource, DriverCapability, Feature } from '../../DataSource';
|
import { DataSource, Feature } from '../../DataSource';
|
||||||
import { useMetadata } from '../../hasura-metadata-api';
|
import { useMetadata } from '../../hasura-metadata-api';
|
||||||
import { useHttpClient } from '../../Network';
|
import { useHttpClient } from '../../Network';
|
||||||
import { APIError } from '../../../hooks/error';
|
import { APIError } from '../../../hooks/error';
|
||||||
|
import { Capabilities } from '@hasura/dc-api-types';
|
||||||
|
|
||||||
type UseDatabaseCapabilitiesArgs = {
|
type UseDatabaseCapabilitiesArgs = {
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDriverCapabilities = <
|
export const useDriverCapabilities = <FinalResult = Feature | Capabilities>({
|
||||||
FinalResult = Feature | DriverCapability
|
|
||||||
>({
|
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
select,
|
select,
|
||||||
options = {},
|
options = {},
|
||||||
}: UseDatabaseCapabilitiesArgs & {
|
}: UseDatabaseCapabilitiesArgs & {
|
||||||
select?: (data: Feature | DriverCapability) => FinalResult;
|
select?: (data: Feature | Capabilities) => FinalResult;
|
||||||
options?: UseQueryOptions<Feature | DriverCapability, APIError, FinalResult>;
|
options?: UseQueryOptions<Feature | Capabilities, APIError, FinalResult>;
|
||||||
}) => {
|
}) => {
|
||||||
const httpClient = useHttpClient();
|
const httpClient = useHttpClient();
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ export const useDriverCapabilities = <
|
|||||||
m => m.metadata.sources.find(source => source.name === dataSourceName)?.kind
|
m => m.metadata.sources.find(source => source.name === dataSourceName)?.kind
|
||||||
);
|
);
|
||||||
|
|
||||||
return useQuery<Feature | DriverCapability, APIError, FinalResult>(
|
return useQuery<Feature | Capabilities, APIError, FinalResult>(
|
||||||
[dataSourceName, 'capabilities'],
|
[dataSourceName, 'capabilities'],
|
||||||
async () => {
|
async () => {
|
||||||
const result =
|
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: {
|
mutations: {
|
||||||
insert: {},
|
insert: {},
|
||||||
update: {},
|
update: {},
|
||||||
delete: {},
|
delete: {},
|
||||||
},
|
},
|
||||||
queries: {},
|
queries: {},
|
||||||
functions: {},
|
user_defined_functions: {},
|
||||||
data_schema: {
|
data_schema: {
|
||||||
supports_foreign_keys: true,
|
supports_foreign_keys: true,
|
||||||
},
|
},
|
||||||
|
@ -16,6 +16,7 @@ export const defaultIntrospectionProps = {
|
|||||||
getIsTableView: async () => Feature.NotImplemented,
|
getIsTableView: async () => Feature.NotImplemented,
|
||||||
getSupportedDataTypes: async () => Feature.NotImplemented,
|
getSupportedDataTypes: async () => Feature.NotImplemented,
|
||||||
getStoredProcedures: async () => Feature.NotImplemented,
|
getStoredProcedures: async () => Feature.NotImplemented,
|
||||||
|
getTrackableObjects: async () => Feature.NotImplemented,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultDatabaseProps: Database = {
|
export const defaultDatabaseProps: Database = {
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
getSupportedOperators,
|
getSupportedOperators,
|
||||||
getFKRelationships,
|
getFKRelationships,
|
||||||
getDriverCapabilities,
|
getDriverCapabilities,
|
||||||
|
getTrackableObjects,
|
||||||
} from './introspection';
|
} from './introspection';
|
||||||
import { getTableRows } from './query';
|
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"].
|
* you'd have just the table name -> ["Album"] but in a db with schemas -> ["Public", "Album"].
|
||||||
*/
|
*/
|
||||||
export type GDCTable = string[];
|
export type GDCTable = string[];
|
||||||
|
export type GDCFunction = string[];
|
||||||
|
|
||||||
export const gdc: Database = {
|
export const gdc: Database = {
|
||||||
...defaultDatabaseProps,
|
...defaultDatabaseProps,
|
||||||
@ -30,6 +32,7 @@ export const gdc: Database = {
|
|||||||
getDatabaseConfiguration,
|
getDatabaseConfiguration,
|
||||||
getDriverCapabilities,
|
getDriverCapabilities,
|
||||||
getTrackableTables,
|
getTrackableTables,
|
||||||
|
getTrackableObjects,
|
||||||
getDatabaseHierarchy: async () => Feature.NotImplemented,
|
getDatabaseHierarchy: async () => Feature.NotImplemented,
|
||||||
getTableColumns,
|
getTableColumns,
|
||||||
getFKRelationships,
|
getFKRelationships,
|
||||||
|
@ -5,6 +5,7 @@ import { GDCTable } from '..';
|
|||||||
import { exportMetadata } from '../../api';
|
import { exportMetadata } from '../../api';
|
||||||
import { GetTablesListAsTreeProps } from '../../types';
|
import { GetTablesListAsTreeProps } from '../../types';
|
||||||
import { convertToTreeData } from './utils';
|
import { convertToTreeData } from './utils';
|
||||||
|
// import { QualifiedFunction } from '../../../hasura-metadata-types';
|
||||||
|
|
||||||
export const getTablesListAsTree = async ({
|
export const getTablesListAsTree = async ({
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
@ -24,6 +25,11 @@ export const getTablesListAsTree = async ({
|
|||||||
return table.table as GDCTable;
|
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 {
|
return {
|
||||||
title: (
|
title: (
|
||||||
<div className="inline-block">
|
<div className="inline-block">
|
||||||
@ -37,6 +43,11 @@ export const getTablesListAsTree = async ({
|
|||||||
),
|
),
|
||||||
key: JSON.stringify({ database: source.name }),
|
key: JSON.stringify({ database: source.name }),
|
||||||
icon: <FaDatabase />,
|
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 { getFKRelationships } from './getFKRelationships';
|
||||||
export { getTablesListAsTree } from './getTablesListAsTree';
|
export { getTablesListAsTree } from './getTablesListAsTree';
|
||||||
export { getTrackableTables } from './getTrackableTables';
|
export { getTrackableTables } from './getTrackableTables';
|
||||||
|
export { getTrackableObjects } from './getTrackableObjects';
|
||||||
export { convertToTreeData } from './utils';
|
export { convertToTreeData } from './utils';
|
||||||
export type { GetTableInfoResponse } from './types';
|
export type { GetTableInfoResponse } from './types';
|
||||||
|
@ -3,22 +3,35 @@ import { DataNode } from 'antd/lib/tree';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FaTable, FaFolder } from 'react-icons/fa';
|
import { FaTable, FaFolder } from 'react-icons/fa';
|
||||||
import { TableColumn } from '../../types';
|
import { TableColumn } from '../../types';
|
||||||
|
import { TbMathFunction } from 'react-icons/tb';
|
||||||
|
|
||||||
export function convertToTreeData(
|
export function convertToTreeData(
|
||||||
tables: string[][],
|
tables: string[][],
|
||||||
key: string[],
|
key: string[],
|
||||||
dataSourceName: string
|
dataSourceName: string,
|
||||||
|
mode?: 'function'
|
||||||
): DataNode[] {
|
): DataNode[] {
|
||||||
if (tables.length === 0) return [];
|
if (tables.length === 0) return [];
|
||||||
|
|
||||||
if (tables[0].length === 1) {
|
if (tables[0].length === 1) {
|
||||||
const leafNodes: DataNode[] = tables.map(table => {
|
const leafNodes: DataNode[] = tables.map(table => {
|
||||||
return {
|
return {
|
||||||
icon: <FaTable />,
|
icon:
|
||||||
key: JSON.stringify({
|
mode === 'function' ? (
|
||||||
database: dataSourceName,
|
<TbMathFunction className="text-muted mr-xs" />
|
||||||
table: [...key, table[0]],
|
) : (
|
||||||
}),
|
<FaTable />
|
||||||
|
),
|
||||||
|
key:
|
||||||
|
mode === 'function'
|
||||||
|
? JSON.stringify({
|
||||||
|
database: dataSourceName,
|
||||||
|
function: [...key, table[0]],
|
||||||
|
})
|
||||||
|
: JSON.stringify({
|
||||||
|
database: dataSourceName,
|
||||||
|
table: [...key, table[0]],
|
||||||
|
}),
|
||||||
title: 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 { DataNode } from 'antd/lib/tree';
|
||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
import pickBy from 'lodash/pickBy';
|
import pickBy from 'lodash/pickBy';
|
||||||
@ -35,7 +35,6 @@ import type {
|
|||||||
TableRow,
|
TableRow,
|
||||||
Version,
|
Version,
|
||||||
WhereClause,
|
WhereClause,
|
||||||
DriverCapability,
|
|
||||||
StoredProcedure,
|
StoredProcedure,
|
||||||
GetStoredProceduresProps,
|
GetStoredProceduresProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
@ -57,6 +56,11 @@ import {
|
|||||||
import { getAllSourceKinds } from './common/getAllSourceKinds';
|
import { getAllSourceKinds } from './common/getAllSourceKinds';
|
||||||
import { getTableName } from './common/getTableName';
|
import { getTableName } from './common/getTableName';
|
||||||
import { ReleaseType } from './types';
|
import { ReleaseType } from './types';
|
||||||
|
import {
|
||||||
|
GetTrackableObjectsProps,
|
||||||
|
GetTrackableObjectsResponse,
|
||||||
|
} from './gdc/introspection/getTrackableObjects';
|
||||||
|
import { isObject } from '../../components/Common/utils/jsUtils';
|
||||||
|
|
||||||
export * from './common/utils';
|
export * from './common/utils';
|
||||||
export type { GDCTable } from './gdc';
|
export type { GDCTable } from './gdc';
|
||||||
@ -121,7 +125,7 @@ export type Database = {
|
|||||||
getDriverCapabilities: (
|
getDriverCapabilities: (
|
||||||
httpClient: AxiosInstance,
|
httpClient: AxiosInstance,
|
||||||
driver?: string
|
driver?: string
|
||||||
) => Promise<DriverCapability | Feature>;
|
) => Promise<Capabilities | Feature>;
|
||||||
getTrackableTables: (
|
getTrackableTables: (
|
||||||
props: GetTrackableTablesProps
|
props: GetTrackableTablesProps
|
||||||
) => Promise<IntrospectedTable[] | Feature.NotImplemented>;
|
) => Promise<IntrospectedTable[] | Feature.NotImplemented>;
|
||||||
@ -141,6 +145,9 @@ export type Database = {
|
|||||||
getTrackableFunctions: (
|
getTrackableFunctions: (
|
||||||
props: GetTrackableFunctionProps
|
props: GetTrackableFunctionProps
|
||||||
) => Promise<IntrospectedFunction[] | Feature.NotImplemented>;
|
) => Promise<IntrospectedFunction[] | Feature.NotImplemented>;
|
||||||
|
getTrackableObjects: (
|
||||||
|
props: GetTrackableObjectsProps
|
||||||
|
) => Promise<GetTrackableObjectsResponse | Feature.NotImplemented>;
|
||||||
getDatabaseSchemas: (
|
getDatabaseSchemas: (
|
||||||
props: GetDatabaseSchemaProps
|
props: GetDatabaseSchemaProps
|
||||||
) => Promise<string[] | Feature.NotImplemented>;
|
) => Promise<string[] | Feature.NotImplemented>;
|
||||||
@ -547,13 +554,33 @@ export const DataSource = (httpClient: AxiosInstance) => ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
getTrackableFunctions: async (dataSourceName: string) => {
|
getTrackableFunctions: async (dataSourceName: string) => {
|
||||||
|
const functions: IntrospectedFunction[] = [];
|
||||||
const database = await getDatabaseMethods({ dataSourceName, httpClient });
|
const database = await getDatabaseMethods({ dataSourceName, httpClient });
|
||||||
return (
|
|
||||||
database.introspection?.getTrackableFunctions({
|
const trackableFunctions =
|
||||||
|
(await database.introspection?.getTrackableFunctions({
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
httpClient,
|
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 ({
|
getDatabaseSchemas: async ({
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Capabilities } from '@hasura/dc-api-types';
|
|
||||||
import {
|
import {
|
||||||
Legacy_SourceToRemoteSchemaRelationship,
|
Legacy_SourceToRemoteSchemaRelationship,
|
||||||
LocalTableArrayRelationship,
|
LocalTableArrayRelationship,
|
||||||
@ -195,10 +194,6 @@ export type GetIsTableViewProps = {
|
|||||||
httpClient: NetworkArgs['httpClient'];
|
httpClient: NetworkArgs['httpClient'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DriverCapability = Capabilities & {
|
|
||||||
functions?: Record<string, any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StoredProcedure = unknown;
|
export type StoredProcedure = unknown;
|
||||||
export type GetStoredProceduresProps = {
|
export type GetStoredProceduresProps = {
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from './configuration';
|
} from './configuration';
|
||||||
import { LogicalModel } from './logicalModel';
|
import { LogicalModel } from './logicalModel';
|
||||||
import { NativeQuery } from './nativeQuery';
|
import { NativeQuery } from './nativeQuery';
|
||||||
import { MetadataTable } from './table';
|
import { MetadataTable, Table } from './table';
|
||||||
import { StoredProcedure } from './storedProcedure';
|
import { StoredProcedure } from './storedProcedure';
|
||||||
|
|
||||||
export type NativeDrivers =
|
export type NativeDrivers =
|
||||||
@ -48,6 +48,10 @@ export type MetadataFunction = {
|
|||||||
};
|
};
|
||||||
session_argument?: string;
|
session_argument?: string;
|
||||||
exposed_as?: 'mutation' | 'query';
|
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 = () => ({
|
export const getRoute = () => ({
|
||||||
connectDatabase: (driver?: string) =>
|
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",
|
"@babel/plugin-transform-runtime": "^7.14.5",
|
||||||
"@graphql-codegen/core": "^1.17.8",
|
"@graphql-codegen/core": "^1.17.8",
|
||||||
"@graphql-codegen/typescript": "^1.17.10",
|
"@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",
|
"@hookform/resolvers": "2.8.10",
|
||||||
"@radix-ui/colors": "^0.1.8",
|
"@radix-ui/colors": "^0.1.8",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.3",
|
"@radix-ui/react-alert-dialog": "^1.0.3",
|
||||||
|
@ -3375,10 +3375,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@hasura/dc-api-types@npm:^0.30.0":
|
"@hasura/dc-api-types@npm:^0.32.0":
|
||||||
version: 0.30.0
|
version: 0.32.0
|
||||||
resolution: "@hasura/dc-api-types@npm:0.30.0"
|
resolution: "@hasura/dc-api-types@npm:0.32.0"
|
||||||
checksum: fbf0376dae3252cac85dd343d77faaf82d7e9100ce5c3a18822e87e7d35550bca0c110f901da766e4099698228eb3b7e25d10f4b305b454e1f64d9ad4c87ae2b
|
checksum: c95adc03c894cf737a0e1d366e5726f17156f13ec5d30be205dc705e2e0b55a0796d33603d8eee89926b1153503c171b461c4bace8475598bae7db33f94e0910
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -18392,7 +18392,7 @@ __metadata:
|
|||||||
"@graphql-codegen/core": ^1.17.8
|
"@graphql-codegen/core": ^1.17.8
|
||||||
"@graphql-codegen/typescript": ^1.17.10
|
"@graphql-codegen/typescript": ^1.17.10
|
||||||
"@graphql-codegen/typescript-operations": ^2.5.5
|
"@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/devtools": 4.0.1
|
||||||
"@hookform/resolvers": 2.8.10
|
"@hookform/resolvers": 2.8.10
|
||||||
"@nrwl/cli": 15.8.1
|
"@nrwl/cli": 15.8.1
|
||||||
|
Loading…
Reference in New Issue
Block a user