mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: add Function Custom Field Customization for Postgres
[DSF-449]: https://hasurahq.atlassian.net/browse/DSF-449?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9736 GitOrigin-RevId: 0f39db28f98ef1bc83b4877e4b742a9c614ec667
This commit is contained in:
parent
af51a481c2
commit
319e9855f9
@ -16,7 +16,9 @@ class TextAreaWithCopy extends React.Component {
|
||||
if (copyText.length > 0) {
|
||||
switch (textLanguage) {
|
||||
case 'sql':
|
||||
text = sqlFormatter.format(copyText, { language: textLanguage });
|
||||
text = copyText
|
||||
? sqlFormatter.format(copyText, { language: textLanguage })
|
||||
: '';
|
||||
break;
|
||||
default:
|
||||
text = copyText;
|
||||
|
@ -42,6 +42,7 @@
|
||||
|
||||
.codeBlockCustom .formattedCode {
|
||||
padding: 0px 0px !important;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.schemaPreWrapper {
|
||||
|
@ -0,0 +1,81 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import {
|
||||
CustomFunctionFieldsForm,
|
||||
CustomFunctionFieldsFormProps,
|
||||
} from './CustomFunctionFieldsForm';
|
||||
|
||||
export default {
|
||||
component: CustomFunctionFieldsForm,
|
||||
} as Meta<typeof CustomFunctionFieldsForm>;
|
||||
|
||||
export const Primary: StoryObj<CustomFunctionFieldsFormProps> = {
|
||||
render: args => (
|
||||
<div className="w-[600px] h-auto overflow-auto border pt-4 mb-4 bg-white">
|
||||
<CustomFunctionFieldsForm {...args} />
|
||||
</div>
|
||||
),
|
||||
|
||||
args: {},
|
||||
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const clearAllFieldsButton = canvas.getByText('Clear All Fields');
|
||||
|
||||
await expect(clearAllFieldsButton).toBeVisible();
|
||||
|
||||
await expect(canvas.getByLabelText('Custom Function Name')).toBeVisible();
|
||||
await expect(canvas.getByPlaceholderText('custom_name')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
canvas.getByLabelText('Function name root field')
|
||||
).toBeVisible();
|
||||
await expect(canvas.getByPlaceholderText('function_name')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
canvas.getByLabelText('Function aggregate root field')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
canvas.getByPlaceholderText('function_aggregate')
|
||||
).toBeVisible();
|
||||
|
||||
await expect(canvas.getByText('Cancel')).toBeVisible();
|
||||
await expect(canvas.getByText('Save')).toBeVisible();
|
||||
|
||||
const customNameInput = canvas.getByPlaceholderText('custom_name');
|
||||
await userEvent.type(customNameInput, 'my_custom_name');
|
||||
await expect(customNameInput).toHaveValue('my_custom_name');
|
||||
|
||||
const functionNameInput = canvas.getByPlaceholderText('function_name');
|
||||
await userEvent.type(functionNameInput, 'my_function_name');
|
||||
await expect(functionNameInput).toHaveValue('my_function_name');
|
||||
|
||||
const functionAggregateInput =
|
||||
canvas.getByPlaceholderText('function_aggregate');
|
||||
await userEvent.type(functionAggregateInput, 'my_function_aggregate');
|
||||
await expect(functionAggregateInput).toHaveValue('my_function_aggregate');
|
||||
|
||||
await userEvent.click(clearAllFieldsButton);
|
||||
await expect(customNameInput).toHaveValue('');
|
||||
await expect(functionNameInput).toHaveValue('');
|
||||
await expect(functionAggregateInput).toHaveValue('');
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDefaultValues: StoryObj<CustomFunctionFieldsFormProps> = {
|
||||
render: args => (
|
||||
<div className="w-[600px] h-auto overflow-auto border pt-4 mb-4 bg-white">
|
||||
<CustomFunctionFieldsForm {...args} />
|
||||
</div>
|
||||
),
|
||||
|
||||
args: {
|
||||
defaultValues: {
|
||||
custom_name: 'a_custom_name',
|
||||
function: 'a_function',
|
||||
function_aggregate: 'a_function_aggregate',
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,129 @@
|
||||
import { z } from 'zod';
|
||||
import { Analytics } from '../../../../../../features/Analytics';
|
||||
import { Button } from '../../../../../../new-components/Button';
|
||||
import { Dialog } from '../../../../../../new-components/Dialog';
|
||||
import {
|
||||
GraphQLSanitizedInputField,
|
||||
useConsoleForm,
|
||||
} from '../../../../../../new-components/Form';
|
||||
import { SanitizeTips } from '../../../../../../utils/sanitizeGraphQLFieldNames';
|
||||
import { implement } from '../../../../../../utils/zodUtils';
|
||||
|
||||
export type CustomFunctionFieldsFormValues = {
|
||||
custom_name: string;
|
||||
function: string;
|
||||
function_aggregate: string;
|
||||
};
|
||||
|
||||
export type CustomFunctionFieldsFormProps = {
|
||||
onSubmit: (data: CustomFunctionFieldsFormValues) => void;
|
||||
onClose: () => void;
|
||||
callToAction?: string;
|
||||
callToActionLoadingText?: string;
|
||||
callToDeny?: string;
|
||||
isLoading: boolean;
|
||||
defaultValues: CustomFunctionFieldsFormValues;
|
||||
};
|
||||
|
||||
const schema = implement<CustomFunctionFieldsFormValues>().with({
|
||||
custom_name: z.string(),
|
||||
function: z.string(),
|
||||
function_aggregate: z.string(),
|
||||
});
|
||||
|
||||
export const CustomFunctionFieldsForm = ({
|
||||
isLoading,
|
||||
onClose,
|
||||
callToAction = 'Save',
|
||||
callToActionLoadingText = 'Saving...',
|
||||
callToDeny = 'Cancel',
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
}: CustomFunctionFieldsFormProps) => {
|
||||
const { methods, Form } = useConsoleForm({
|
||||
schema,
|
||||
options: {
|
||||
defaultValues,
|
||||
},
|
||||
});
|
||||
|
||||
const { watch, handleSubmit } = methods;
|
||||
|
||||
const values = watch();
|
||||
|
||||
const onSubmitHandler = (data: CustomFunctionFieldsFormValues) => {
|
||||
handleSubmit(onSubmit)();
|
||||
};
|
||||
|
||||
const hasValues = Object.values(values).some(value => !!value);
|
||||
|
||||
const reset = () => {
|
||||
methods.reset({
|
||||
custom_name: '',
|
||||
function: '',
|
||||
function_aggregate: '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={onSubmitHandler}>
|
||||
<div>
|
||||
<div className="px-4 pb-sm">
|
||||
<SanitizeTips />
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button disabled={!hasValues} size="sm" onClick={reset}>
|
||||
Clear All Fields
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* custom_name */}
|
||||
<div>
|
||||
<Analytics name="custom_name" htmlAttributesToRedact="value">
|
||||
<GraphQLSanitizedInputField
|
||||
hideTips
|
||||
clearButton
|
||||
name="custom_name"
|
||||
label="Custom Function Name"
|
||||
placeholder="custom_name"
|
||||
/>
|
||||
</Analytics>
|
||||
</div>
|
||||
|
||||
{/* function */}
|
||||
<div>
|
||||
<Analytics name="function" htmlAttributesToRedact="value">
|
||||
<GraphQLSanitizedInputField
|
||||
hideTips
|
||||
clearButton
|
||||
name="function"
|
||||
label="Function name root field"
|
||||
placeholder="function_name"
|
||||
/>
|
||||
</Analytics>
|
||||
</div>
|
||||
|
||||
{/* function_aggregate */}
|
||||
<div>
|
||||
<Analytics name="function_aggregate" htmlAttributesToRedact="value">
|
||||
<GraphQLSanitizedInputField
|
||||
hideTips
|
||||
clearButton
|
||||
name="function_aggregate"
|
||||
label="Function aggregate root field"
|
||||
placeholder="function_aggregate"
|
||||
/>
|
||||
</Analytics>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Footer
|
||||
callToAction={callToAction}
|
||||
isLoading={isLoading}
|
||||
callToActionLoadingText={callToActionLoadingText}
|
||||
callToDeny={callToDeny}
|
||||
onClose={onClose}
|
||||
className="sticky w-full bottom-0 left-0"
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
@ -0,0 +1,112 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { handlers } from './mocks/handlers';
|
||||
import {
|
||||
FunctionGraphQLCustomization,
|
||||
FunctionGraphQLCustomizationProps,
|
||||
} from './FunctionGraphQLCustomization';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
component: FunctionGraphQLCustomization,
|
||||
argTypes: {
|
||||
onSubmit: { action: true },
|
||||
onClose: { action: true },
|
||||
},
|
||||
decorators: [
|
||||
(Story: React.FC) => (
|
||||
<div className="max-w-xl">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
},
|
||||
} as Meta<typeof FunctionGraphQLCustomization>;
|
||||
|
||||
export const Primary: StoryObj<FunctionGraphQLCustomizationProps> = {
|
||||
args: {
|
||||
dataSourceName: 'aPostgres',
|
||||
driver: 'postgres',
|
||||
qualifiedFunction: {
|
||||
name: 'search_album',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(canvas.getByText('Custom Field Names')).toBeVisible();
|
||||
await expect(canvas.getByText('(Learn More)')).toBeVisible();
|
||||
await expect(canvas.getByText('Add Custom Field Names')).toBeVisible();
|
||||
|
||||
await userEvent.click(canvas.getByText('Add Custom Field Names'));
|
||||
|
||||
await waitFor(async () => {
|
||||
await expect(await canvas.getByText('search_album')).toBeVisible();
|
||||
});
|
||||
|
||||
await expect(
|
||||
canvas.getByText(
|
||||
'GraphQL fields are limited to letters, numbers, and underscores.'
|
||||
)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
canvas.getByText('Any spaces are converted to underscores.')
|
||||
).toBeVisible();
|
||||
|
||||
await expect(canvas.getAllByText('Clear All Fields')).toHaveLength(1);
|
||||
|
||||
await expect(canvas.getByLabelText('Custom Function Name')).toBeVisible();
|
||||
await expect(
|
||||
canvas.getByLabelText('Function name root field')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
canvas.getByLabelText('Function aggregate root field')
|
||||
).toBeVisible();
|
||||
|
||||
const customNameInput = canvas.getByPlaceholderText('custom_name');
|
||||
await expect(customNameInput).toBeVisible();
|
||||
await userEvent.type(customNameInput, 'my_custom_name');
|
||||
await expect(customNameInput).toHaveValue('my_custom_name');
|
||||
|
||||
const functionNameInput = canvas.getByPlaceholderText('function_name');
|
||||
await expect(functionNameInput).toBeVisible();
|
||||
await userEvent.type(functionNameInput, 'my_function_name');
|
||||
await expect(functionNameInput).toHaveValue('my_function_name');
|
||||
|
||||
const functionAggregateInput =
|
||||
canvas.getByPlaceholderText('function_aggregate');
|
||||
await expect(functionAggregateInput).toBeVisible();
|
||||
await userEvent.type(functionAggregateInput, 'my_function_aggregate');
|
||||
await expect(functionAggregateInput).toHaveValue('my_function_aggregate');
|
||||
|
||||
await expect(canvas.getByText('Cancel')).toBeVisible();
|
||||
await expect(canvas.getByText('Save')).toBeVisible();
|
||||
|
||||
await userEvent.click(canvas.getByText('Clear All Fields'));
|
||||
await expect(customNameInput).toHaveValue('');
|
||||
await expect(functionNameInput).toHaveValue('');
|
||||
await expect(functionAggregateInput).toHaveValue('');
|
||||
|
||||
await userEvent.click(canvas.getByText('Cancel'));
|
||||
|
||||
await expect(
|
||||
canvas.getByText('No custom fields are currently set.')
|
||||
).toBeVisible();
|
||||
},
|
||||
};
|
@ -0,0 +1,159 @@
|
||||
import { useState } from 'react';
|
||||
import { Driver } from '../../../../../../dataSources';
|
||||
import { RootField } from '../../../../../../features/Data/ModifyTable/components/TableRootFields';
|
||||
import {
|
||||
MetadataSelectors,
|
||||
useMetadata,
|
||||
} from '../../../../../../features/hasura-metadata-api';
|
||||
import { QualifiedFunction } from '../../../../../../features/hasura-metadata-types';
|
||||
import { Button } from '../../../../../../new-components/Button';
|
||||
import { Dialog } from '../../../../../../new-components/Dialog';
|
||||
import { LearnMoreLink } from '../../../../../../new-components/LearnMoreLink';
|
||||
import { hasuraToast } from '../../../../../../new-components/Toasts';
|
||||
import { IconTooltip } from '../../../../../../new-components/Tooltip';
|
||||
import { isObject } from '../../../../../Common/utils/jsUtils';
|
||||
import {
|
||||
CustomFunctionFieldsForm,
|
||||
CustomFunctionFieldsFormValues,
|
||||
} from './CustomFunctionFieldsForm';
|
||||
import { useSetFunctionCustomization } from './hooks/useSetFunctionCustomization';
|
||||
|
||||
export type FunctionGraphQLCustomizationProps = {
|
||||
driver: Driver;
|
||||
dataSourceName: string;
|
||||
qualifiedFunction: QualifiedFunction;
|
||||
};
|
||||
|
||||
const getFunctionName = (fn: QualifiedFunction) => {
|
||||
if (isObject(fn) && 'name' in fn) {
|
||||
return fn.name as string;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const FunctionGraphQLCustomization = ({
|
||||
driver,
|
||||
dataSourceName,
|
||||
qualifiedFunction,
|
||||
}: FunctionGraphQLCustomizationProps) => {
|
||||
const [showCustomModal, setShowCustomModal] = useState(false);
|
||||
|
||||
const { data: metadataFunction } = useMetadata(
|
||||
MetadataSelectors.findFunction(dataSourceName, qualifiedFunction)
|
||||
);
|
||||
|
||||
const formDefaultValues: CustomFunctionFieldsFormValues = {
|
||||
custom_name: metadataFunction?.configuration?.custom_name ?? '',
|
||||
function:
|
||||
metadataFunction?.configuration?.custom_root_fields?.function ?? '',
|
||||
function_aggregate:
|
||||
metadataFunction?.configuration?.custom_root_fields?.function_aggregate ??
|
||||
'',
|
||||
};
|
||||
|
||||
const isCustomized =
|
||||
metadataFunction?.configuration?.custom_name ||
|
||||
(metadataFunction?.configuration?.custom_root_fields &&
|
||||
Object.keys(metadataFunction?.configuration?.custom_root_fields)
|
||||
.length === 0);
|
||||
|
||||
const functionName = getFunctionName(metadataFunction?.function);
|
||||
|
||||
const { onSetFunctionCustomization, isLoading } = useSetFunctionCustomization(
|
||||
{
|
||||
onSuccess: () => {
|
||||
hasuraToast({
|
||||
title: 'Success!',
|
||||
message: 'Custom function fields updated successfully',
|
||||
type: 'success',
|
||||
});
|
||||
setShowCustomModal(false);
|
||||
},
|
||||
onError: error => {
|
||||
hasuraToast({
|
||||
title: 'Error',
|
||||
message:
|
||||
error?.message ?? 'Error while updating custom function fields',
|
||||
type: 'error',
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const onSaveCustomFields = (data: CustomFunctionFieldsFormValues) => {
|
||||
const areCustomRootFieldsDefined = data.function || data.function_aggregate;
|
||||
const customRootFields = {
|
||||
...(data.function ? { function: data.function } : {}),
|
||||
...(data.function_aggregate
|
||||
? { function_aggregate: data.function_aggregate }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const configuration = {
|
||||
...(data.custom_name ? { custom_name: data.custom_name } : {}),
|
||||
...(areCustomRootFieldsDefined
|
||||
? { custom_root_fields: customRootFields }
|
||||
: {}),
|
||||
};
|
||||
|
||||
return onSetFunctionCustomization({
|
||||
driver,
|
||||
dataSourceName,
|
||||
functionName,
|
||||
configuration,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full sm:w-6/12 mb-md">
|
||||
<h4 className="flex items-center text-gray-600 font-semibold mb-formlabel">
|
||||
Custom Field Names{' '}
|
||||
<IconTooltip message="Customize function root names for GraphQL operations." />
|
||||
<span className="ml-xs">
|
||||
<LearnMoreLink href="https://hasura.io/docs/latest/graphql/core/schema/custom-functions.html#custom-function-root-fields" />
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div>
|
||||
<Button onClick={() => setShowCustomModal(true)} className="mb-2">
|
||||
{isCustomized ? 'Edit Custom Field Names' : 'Add Custom Field Names'}
|
||||
</Button>
|
||||
<div className="p-2">
|
||||
{!isCustomized && (
|
||||
<div className="text-gray-500 mx-2">
|
||||
No custom fields are currently set.
|
||||
</div>
|
||||
)}
|
||||
{metadataFunction?.configuration?.custom_name && (
|
||||
<RootField
|
||||
property="custom_name"
|
||||
value={metadataFunction.configuration.custom_name}
|
||||
/>
|
||||
)}
|
||||
{Object.entries(
|
||||
metadataFunction?.configuration?.custom_root_fields ?? {}
|
||||
).map(([key, value]) => (
|
||||
<RootField key={key} property={key} value={value} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{showCustomModal && (
|
||||
<Dialog
|
||||
hasBackdrop
|
||||
title={functionName}
|
||||
description={''}
|
||||
onClose={() => setShowCustomModal(false)}
|
||||
titleTooltip="Customize function name and root fields for GraphQL operations."
|
||||
>
|
||||
<CustomFunctionFieldsForm
|
||||
onSubmit={onSaveCustomFields}
|
||||
onClose={() => setShowCustomModal(false)}
|
||||
isLoading={isLoading}
|
||||
defaultValues={formDefaultValues}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,69 @@
|
||||
import { Driver } from '../../../../../../../dataSources';
|
||||
import { getDriverPrefix } from '../../../../../../../features/DataSource';
|
||||
import {
|
||||
allowedMetadataTypes,
|
||||
useMetadataMigration,
|
||||
} from '../../../../../../../features/MetadataAPI';
|
||||
import { useAppDispatch } from '../../../../../../../storeHooks';
|
||||
import { updateSchemaInfo } from '../../../../DataActions';
|
||||
|
||||
type Configuration = {
|
||||
custom_root_fields?: {
|
||||
function_aggregate?: string;
|
||||
function?: string;
|
||||
};
|
||||
custom_name?: string;
|
||||
};
|
||||
|
||||
type OnSetFunctionCustomizationArgs = {
|
||||
driver: Driver;
|
||||
dataSourceName: string;
|
||||
functionName: string;
|
||||
configuration: Configuration;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
};
|
||||
|
||||
export const useSetFunctionCustomization = ({ onSuccess, onError }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const mutation = useMetadataMigration({
|
||||
onSuccess: () => {
|
||||
dispatch(updateSchemaInfo()).then(() => {
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const onSetFunctionCustomization = ({
|
||||
driver,
|
||||
dataSourceName,
|
||||
functionName,
|
||||
configuration,
|
||||
}: OnSetFunctionCustomizationArgs) => {
|
||||
const requestBody = {
|
||||
type: `${getDriverPrefix(
|
||||
driver
|
||||
)}_set_function_customization` as allowedMetadataTypes,
|
||||
args: {
|
||||
source: dataSourceName,
|
||||
function: functionName,
|
||||
configuration,
|
||||
},
|
||||
};
|
||||
return mutation.mutate({
|
||||
query: requestBody,
|
||||
});
|
||||
};
|
||||
|
||||
return { ...mutation, onSetFunctionCustomization };
|
||||
};
|
@ -0,0 +1,74 @@
|
||||
import { rest } from 'msw';
|
||||
import { Metadata } from '../../../../../../../features/hasura-metadata-types';
|
||||
import { ServerConfig } from '../../../../../../../hooks';
|
||||
|
||||
export const createDefaultInitialData = (): Metadata => ({
|
||||
resource_version: 1,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'aPostgres',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'Album',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
function: {
|
||||
name: 'search_album',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: 'postgres://postgres:pass@postgres:5432/chinook',
|
||||
isolation_level: 'read-committed',
|
||||
use_prepared_statements: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
type HandlersOptions = {
|
||||
delay?: number;
|
||||
initialData?: Metadata | (() => Metadata);
|
||||
config?: Partial<ServerConfig>;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const defaultOptions: HandlersOptions = {
|
||||
delay: 0,
|
||||
config: {},
|
||||
url: 'http://localhost:8080',
|
||||
initialData: createDefaultInitialData,
|
||||
};
|
||||
|
||||
export const handlers = (options?: HandlersOptions) => {
|
||||
const { url } = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
|
||||
return [
|
||||
rest.post(`${url}/v1/metadata`, async (req, res, ctx) => {
|
||||
const body = (await req.json()) as Record<string, any>;
|
||||
|
||||
if (body.type === 'pg_set_function_customization') {
|
||||
return res(ctx.json({}));
|
||||
}
|
||||
|
||||
const response = createDefaultInitialData();
|
||||
|
||||
return res(ctx.json(response));
|
||||
}),
|
||||
];
|
||||
};
|
@ -30,8 +30,8 @@ import {
|
||||
} from '../../../../Common/utils/routesUtils';
|
||||
import SessionVarSection from './SessionVarSection';
|
||||
import RawSqlButton from '../../Common/Components/RawSqlButton';
|
||||
|
||||
import { isFeatureSupported } from '../../../../../dataSources';
|
||||
import { isFeatureSupported, currentDriver } from '../../../../../dataSources';
|
||||
import { FunctionGraphQLCustomization } from './GraphQLCustomization/FunctionGraphQLCustomization';
|
||||
|
||||
export const pageTitle = 'Custom Function';
|
||||
|
||||
@ -254,6 +254,15 @@ class ModifyCustomFunction extends React.Component {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FunctionGraphQLCustomization
|
||||
driver={currentDriver}
|
||||
dataSourceName={currentSource}
|
||||
qualifiedFunction={{
|
||||
name: functionName,
|
||||
schema,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="w-full sm:w-6/12 mb-md">
|
||||
<h4 className="flex items-center text-gray-600 font-semibold mb-formlabel">
|
||||
Function Definition:
|
||||
@ -303,8 +312,7 @@ const mapStateToProps = state => ({
|
||||
currentSchema: state.tables.currentSchema,
|
||||
});
|
||||
|
||||
const modifyCustomFnConnector = connect(mapStateToProps);
|
||||
const ConnectedModifyCustomFunction =
|
||||
modifyCustomFnConnector(ModifyCustomFunction);
|
||||
connect(mapStateToProps)(ModifyCustomFunction);
|
||||
|
||||
export default ConnectedModifyCustomFunction;
|
||||
|
@ -151,8 +151,8 @@ export const CustomFieldNamesForm: React.VFC<
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/*
|
||||
Implementing a custom footer here because there's no way to submit the form from the footer buttons otherwise.
|
||||
{/*
|
||||
Implementing a custom footer here because there's no way to submit the form from the footer buttons otherwise.
|
||||
Since this creates buttons within the form, the form submit is automatically triggered when these buttons are clicked.
|
||||
The only other approach would be to wrap the form around the entire dialog.
|
||||
However, if this is done, the form stays rendered in memory when the dialog is opened and closed and must be manually reset and reinit'd each time it happens
|
||||
|
@ -7,7 +7,7 @@ import Skeleton from 'react-loading-skeleton';
|
||||
import { useUpdateTableConfiguration } from '../hooks';
|
||||
import { ModifyTableProps } from '../ModifyTable';
|
||||
|
||||
const RootField: React.VFC<{ property: string; value: string }> = ({
|
||||
export const RootField: React.VFC<{ property: string; value: string }> = ({
|
||||
property,
|
||||
value,
|
||||
}) => (
|
||||
|
@ -27,6 +27,7 @@ export const allowedMetadataTypesArr = [
|
||||
'pg_drop_delete_permission',
|
||||
'pg_set_permission_comment',
|
||||
'pg_track_table',
|
||||
'pg_set_function_customization',
|
||||
'mssql_create_insert_permission',
|
||||
'mssql_drop_insert_permission',
|
||||
'mssql_create_select_permission',
|
||||
|
@ -2,10 +2,10 @@ import {
|
||||
LogicalModelWithSource,
|
||||
NativeQueryWithSource,
|
||||
} from '../Data/LogicalModels/types';
|
||||
import { Metadata, Table } from '../hasura-metadata-types';
|
||||
import { Metadata, QualifiedFunction, Table } from '../hasura-metadata-types';
|
||||
import * as utils from './utils';
|
||||
|
||||
/*
|
||||
/*
|
||||
|
||||
How do I implement my custom selector apart from the ones provided here?
|
||||
|
||||
@ -34,6 +34,11 @@ export const findTable =
|
||||
(dataSourceName: string, table: Table) => (m: Metadata) =>
|
||||
utils.findMetadataTable(dataSourceName, table, m);
|
||||
|
||||
export const findFunction =
|
||||
(dataSourceName: string, qualifiedFunction: QualifiedFunction) =>
|
||||
(m: Metadata) =>
|
||||
utils.findMetadataFunction(dataSourceName, qualifiedFunction, m);
|
||||
|
||||
export const resourceVersion = () => (m: Metadata) => m?.resource_version;
|
||||
|
||||
export const extractModelsAndQueriesFromMetadata = (
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { Metadata, Source, Table } from '../hasura-metadata-types';
|
||||
import {
|
||||
Metadata,
|
||||
QualifiedFunction,
|
||||
Source,
|
||||
Table,
|
||||
} from '../hasura-metadata-types';
|
||||
import { areTablesEqual } from './areTablesEqual';
|
||||
|
||||
/*
|
||||
@ -36,5 +41,14 @@ export const findMetadataTable = (
|
||||
areTablesEqual(t.table, table)
|
||||
);
|
||||
|
||||
export const findMetadataFunction = (
|
||||
dataSourceName: string,
|
||||
qualifiedFunction: QualifiedFunction,
|
||||
m: Metadata
|
||||
) =>
|
||||
findMetadataSource(dataSourceName, m)?.functions?.find(fn =>
|
||||
areTablesEqual(fn.function, qualifiedFunction)
|
||||
);
|
||||
|
||||
export const getSupportsForeignKeys = (source: Source | undefined) =>
|
||||
source?.kind !== 'bigquery';
|
||||
|
Loading…
Reference in New Issue
Block a user