Skip to content

Commit

Permalink
NEOS-596:Custom code transformer fe (#1033)
Browse files Browse the repository at this point in the history
  • Loading branch information
evisdrenova authored Jan 4, 2024
1 parent 5c66ebd commit 64bd3ff
Show file tree
Hide file tree
Showing 14 changed files with 452 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ func (s *Service) GetSystemTransformers(
Source: string(TransformJavascript),
Config: &mgmtv1alpha1.TransformerConfig{
Config: &mgmtv1alpha1.TransformerConfig_TransformJavascriptConfig{
TransformJavascriptConfig: &mgmtv1alpha1.TransformJavascript{Code: ""},
TransformJavascriptConfig: &mgmtv1alpha1.TransformJavascript{Code: `let input = value + "test"; return input;"`},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import UserDefinedTransformFloat64Form from './UserDefinedTransformFloat64Form';
import UserDefinedTransformFullNameForm from './UserDefinedTransformFullNameForm';
import UserDefinedTransformInt64Form from './UserDefinedTransformInt64Form';
import UserDefinedTransformIntPhoneNumberForm from './UserDefinedTransformInt64PhoneForm';
import UserDefinedTransformJavascriptForm from './UserDefinedTransformJavascriptForm';
import UserDefinedTransformLastNameForm from './UserDefinedTransformLastNameForm';
import UserDefinedTransformPhoneNumberForm from './UserDefinedTransformPhoneNumberForm';
import UserDefinedTransformStringForm from './UserDefinedTransformStringForm';
Expand Down Expand Up @@ -62,6 +63,8 @@ export function handleUserDefinedTransformerForm(
return <UserDefinedTransformPhoneNumberForm isDisabled={disabled} />;
case 'transform_string':
return <UserDefinedTransformStringForm isDisabled={disabled} />;
case 'transform_javascript':
return <UserDefinedTransformJavascriptForm isDisabled={disabled} />;
default:
<div>No transformer found</div>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
'use client';
import {
FormControl,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form';

import ButtonText from '@/components/ButtonText';
import Spinner from '@/components/Spinner';
import LearnMoreTag from '@/components/labels/LearnMoreTag';
import { useAccount } from '@/components/providers/account-provider';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import Editor from '@monaco-editor/react';
import {
ValidateUserJavascriptCodeRequest,
ValidateUserJavascriptCodeResponse,
} from '@neosync/sdk';
import { CheckCircledIcon, CrossCircledIcon } from '@radix-ui/react-icons';
import { useTheme } from 'next-themes';
import { ReactElement, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import {
CreateUserDefinedTransformerSchema,
UpdateUserDefinedTransformer,
} from '../schema';

interface Props {
isDisabled?: boolean;
}

export type ValidCode = 'valid' | 'invalid' | 'null';

export default function UserDefinedTransformJavascriptForm(
props: Props
): ReactElement {
const fc = useFormContext<
UpdateUserDefinedTransformer | CreateUserDefinedTransformerSchema
>();

const { isDisabled } = props;

const options = {
minimap: { enabled: false },
readOnly: isDisabled,
};

const { resolvedTheme } = useTheme();

const [userCode, setUserCode] = useState<string>(
fc.getValues('config.value.code')
);

const [isValidatingCode, setIsValidatingCode] = useState<boolean>(false);
const [isCodeValid, setIsCodeValid] = useState<ValidCode>('null');

const account = useAccount();

async function handleValidateCode(): Promise<void> {
if (!account) {
return;
}
setIsValidatingCode(true);

try {
const res = await IsUserJavascriptCodeValid(
userCode,
account.account?.id ?? ''
);
setIsValidatingCode(false);
if (res.valid == true) {
setIsCodeValid('valid');
} else {
setIsCodeValid('invalid');
}
} catch (err) {
console.error(err);
setIsValidatingCode(false);
setIsCodeValid('invalid');
}
}

return (
<div className="pt-4">
<FormField
name={`config.value.code`}
control={fc.control}
render={({ field }) => (
<FormItem>
<div className="flex flex-row justify-between">
<div className="space-y-0.5">
<FormLabel>Custom Code</FormLabel>
<div className="text-[0.8rem] text-muted-foreground">
Define your own Transformation below using Javascript. The
input value will be available at the{' '}
<code className="bg-gray-200 px-1 py-0.5 rounded">value</code>{' '}
keyword. Click <b>Validate</b> to check that your code
compiles.{' '}
<LearnMoreTag href="https://docs.neosync.dev/transformers/user-defined" />
</div>
</div>
<div className="flex flex-row gap-2">
{isCodeValid !== 'null' && (
<Badge
variant={isCodeValid == 'valid' ? 'success' : 'destructive'}
className="h-9 px-4 py-2"
>
<ButtonText
leftIcon={
isCodeValid == 'valid' ? (
<CheckCircledIcon />
) : isCodeValid == 'invalid' ? (
<CrossCircledIcon />
) : null
}
text={isCodeValid == 'invalid' ? 'invalid' : 'valid'}
/>
</Badge>
)}
<Button type="button" onClick={handleValidateCode}>
<ButtonText
leftIcon={isValidatingCode ? <Spinner /> : null}
text={'Validate'}
/>
</Button>
</div>
</div>
<FormControl>
<div className="flex flex-col items-center justify-between rounded-lg border dark:border-gray-700 p-3 shadow-sm">
<Editor
height="50vh"
width="100%"
language="javascript"
value={field.value}
theme={resolvedTheme == 'dark' ? 'vs-dark' : 'cobalt'}
defaultValue={field.value}
onChange={(e) => {
field.onChange(e);
setUserCode(e ?? '');
}}
options={options}
/>
</div>
</FormControl>
</FormItem>
)}
/>
</div>
);
}

export async function IsUserJavascriptCodeValid(
code: string,
accountId: string
): Promise<ValidateUserJavascriptCodeResponse> {
const body = new ValidateUserJavascriptCodeRequest({
code: code,
accountId: accountId,
});
const res = await fetch(
`/api/accounts/${accountId}/transformers/user-defined/validate-code`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
}
);
if (!res.ok) {
const body = await res.json();
throw new Error(body.message);
}
return ValidateUserJavascriptCodeResponse.fromJson(await res.json());
}
2 changes: 1 addition & 1 deletion frontend/apps/web/app/[account]/new/transformer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export default function NewTransformer(): ReactElement {
<FormItem>
<FormLabel>Description</FormLabel>
<FormDescription>
The Transformer decription.
The Transformer description.
</FormDescription>
<FormControl>
<Input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import TransformFloatForm from './Sheetforms/TransformFloat64Form';
import TransformFullNameForm from './Sheetforms/TransformFullNameForm';
import TransformInt64Form from './Sheetforms/TransformInt64Form';
import TransformInt64PhoneForm from './Sheetforms/TransformInt64PhoneForm';
import TransformJavascriptForm from './Sheetforms/TransformJavascriptForm';
import TransformLastNameForm from './Sheetforms/TransformLastNameForm';
import TransformPhoneNumberForm from './Sheetforms/TransformPhoneNumberForm';
import TransformStringForm from './Sheetforms/TransformStringForm';
Expand Down Expand Up @@ -250,6 +251,15 @@ function handleTransformerForm(
transformer={transformer}
/>
);
case 'transform_javascript':
return (
<TransformJavascriptForm
index={index}
setIsSheetOpen={setIsSheetOpen}
transformer={transformer}
/>
);

default:
<div>No transformer component found</div>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'use client';
import ButtonText from '@/components/ButtonText';
import Spinner from '@/components/Spinner';
import LearnMoreTag from '@/components/labels/LearnMoreTag';
import { useAccount } from '@/components/providers/account-provider';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
FormControl,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form';
import { Transformer, isUserDefinedTransformer } from '@/shared/transformers';
import { Editor } from '@monaco-editor/react';
import { TransformJavascript } from '@neosync/sdk';
import { CheckCircledIcon, CrossCircledIcon } from '@radix-ui/react-icons';
import { useTheme } from 'next-themes';
import { ReactElement, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import {
IsUserJavascriptCodeValid,
ValidCode,
} from '../../new/transformer/UserDefinedTransformerForms/UserDefinedTransformJavascriptForm';
interface Props {
index?: number;
transformer: Transformer;
setIsSheetOpen?: (val: boolean) => void;
}

export default function TransformJavascriptForm(props: Props): ReactElement {
const { index, setIsSheetOpen, transformer } = props;

const fc = useFormContext();

const codeValue = fc.getValues(
`mappings.${index}.transformer.config.value.code`
);
const [userCode, setUserCode] = useState<string>(codeValue);
const [isValidatingCode, setIsValidatingCode] = useState<boolean>(false);
const [isCodeValid, setIsCodeValid] = useState<ValidCode>('null');
const { resolvedTheme } = useTheme();

const options = {
minimap: { enabled: false },
readOnly: isUserDefinedTransformer(transformer),
};

const handleSubmit = () => {
fc.setValue(
`mappings.${index}.transformer.config.value`,
new TransformJavascript({ code: userCode }),
{
shouldValidate: false,
}
);
setIsSheetOpen!(false);
};

const account = useAccount();

async function handleValidateCode(): Promise<void> {
if (!account) {
return;
}
setIsValidatingCode(true);

try {
const res = await IsUserJavascriptCodeValid(
userCode,
account.account?.id ?? ''
);
setIsValidatingCode(false);
if (res.valid == true) {
setIsCodeValid('valid');
} else {
setIsCodeValid('invalid');
}
} catch (err) {
console.error(err);
setIsValidatingCode(false);
setIsCodeValid('invalid');
}
}

return (
<div className="flex flex-col w-full space-y-4 pt-4">
<FormField
name={`mappings.${index}.transformer.config.value.preserveLength`}
render={() => (
<FormItem>
<div className="flex flex-row justify-between">
<div className="space-y-0.5">
<FormLabel>Custom Code</FormLabel>
<div className="text-[0.8rem] text-muted-foreground">
Define your own Transformation below using Javascript. The
input value will be available at the{' '}
<code className="bg-gray-200 px-1 py-0.5 rounded">value</code>{' '}
keyword.{' '}
<LearnMoreTag href="https://docs.neosync.dev/transformers/user-defined" />
</div>
</div>
<div className="flex flex-row gap-2">
{isCodeValid !== 'null' && (
<Badge
variant={isCodeValid == 'valid' ? 'success' : 'destructive'}
className="h-9 px-4 py-2"
>
<ButtonText
leftIcon={
isCodeValid == 'valid' ? (
<CheckCircledIcon />
) : isCodeValid == 'invalid' ? (
<CrossCircledIcon />
) : null
}
text={isCodeValid == 'invalid' ? 'invalid' : 'valid'}
/>
</Badge>
)}
<Button type="button" onClick={handleValidateCode}>
<ButtonText
leftIcon={isValidatingCode ? <Spinner /> : null}
text={'Validate'}
/>
</Button>
</div>
</div>
<FormControl>
<div className="flex flex-col items-center justify-between rounded-lg border dark:border-gray-700 p-3 shadow-sm">
<Editor
height="50vh"
width="100%"
language="javascript"
value={userCode}
theme={resolvedTheme == 'dark' ? 'vs-dark' : 'cobalt'}
defaultValue={userCode}
onChange={(e) => {
setUserCode(e ?? '');
}}
options={options}
/>
</div>
</FormControl>
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="button" onClick={handleSubmit}>
Save
</Button>
</div>
</div>
);
}
Loading

1 comment on commit 64bd3ff

@vercel
Copy link

@vercel vercel bot commented on 64bd3ff Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.