Skip to content

Commit

Permalink
Frontend code refactoring (#504)
Browse files Browse the repository at this point in the history
* Add dynamic routing for BuildPage

* WIP

* Code cleanup

* Code refactoring

* Update deployment instructions

* WIP: Refactoring

* WIP

* Add tests

* packages upgraded

---------

Co-authored-by: Davor Runje <[email protected]>
  • Loading branch information
harishmohanraj and davorrunje authored Jul 8, 2024
1 parent 1f703cf commit 1a5a48b
Show file tree
Hide file tree
Showing 11 changed files with 1,094 additions and 314 deletions.
361 changes: 50 additions & 311 deletions app/src/client/components/DynamicFormBuilder.tsx

Large diffs are not rendered by default.

149 changes: 149 additions & 0 deletions app/src/client/components/form/DynamicForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React from 'react';
import _ from 'lodash';
import { TextInput } from './TextInput';
import { SelectInput } from './SelectInput';
import { TextArea } from './TextArea';
import { NumericStepperWithClearButton } from './NumericStepperWithClearButton';
import { SECRETS_TO_MASK } from '../../utils/constants';
import { JsonSchema } from '../../interfaces/BuildPageInterfaces';
import { FormData } from '../../hooks/useForm';
import AgentConversationHistory from '../AgentConversationHistory';

interface DynamicFormProps {
jsonSchema: JsonSchema;
formData: FormData;
handleChange: (key: string, value: any) => void;
formErrors: Record<string, string>;
refValues: Record<string, any>;
isLoading: boolean;
onMissingDependencyClick: (e: any, type: string) => void;
updateExistingModel: any;
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
instructionForDeployment: Record<string, string> | null;
onCancelCallback: (event: React.FormEvent) => void;
cancelButtonRef: React.RefObject<HTMLButtonElement>;
onDeleteCallback: (data: any) => void;
}

const DynamicForm: React.FC<DynamicFormProps> = ({
jsonSchema,
formData,
handleChange,
formErrors,
refValues,
isLoading,
onMissingDependencyClick,
updateExistingModel,
handleSubmit,
instructionForDeployment,
onCancelCallback,
cancelButtonRef,
onDeleteCallback,
}) => {
return (
<form onSubmit={handleSubmit} className='px-6.5 py-2'>
{Object.entries(jsonSchema.properties).map(([key, property]) => {
if (key === 'uuid') {
return null;
}
const inputValue = formData[key] || '';
let missingDependencyForKey = null;
let formElementsObject = property;
if (_.has(property, '$ref') || _.has(property, 'anyOf') || _.has(property, 'allOf')) {
if (refValues[key]) {
formElementsObject = refValues[key].htmlSchema;
missingDependencyForKey = refValues[key].missingDependency;
missingDependencyForKey.label = formElementsObject.title;
}
}
// return formElementsObject?.enum?.length === 1 ? null : (
return (
<div key={key} className='w-full mt-2'>
<label htmlFor={key}>{formElementsObject.title}</label>
{formElementsObject.enum ? (
formElementsObject.type === 'numericStepperWithClearButton' ? (
<div>
<NumericStepperWithClearButton
id={key}
value={inputValue}
formElementObject={formElementsObject}
onChange={(value) => handleChange(key, value)}
/>
</div>
) : (
<SelectInput
id={key}
value={inputValue}
options={formElementsObject.enum}
onChange={(value) => handleChange(key, value)}
missingDependency={missingDependencyForKey}
onMissingDependencyClick={onMissingDependencyClick}
/>
)
) : key === 'system_message' ? (
<TextArea
id={key}
value={inputValue}
placeholder={formElementsObject.description || ''}
onChange={(value) => handleChange(key, value)}
/>
) : (
<TextInput
id={key}
type={_.includes(SECRETS_TO_MASK, key) && typeof inputValue === 'string' ? 'password' : 'text'}
value={inputValue}
placeholder={formElementsObject.description || ''}
onChange={(value) => handleChange(key, value)}
/>
)}
{formErrors[key] && <div style={{ color: 'red' }}>{formErrors[key]}</div>}
</div>
);
})}
{instructionForDeployment && instructionForDeployment.instruction && (
<div className='w-full mt-8'>
<AgentConversationHistory
agentConversationHistory={instructionForDeployment.instruction}
isDeploymentInstructions={true}
containerTitle='Deployment Details and Next Steps'
/>
</div>
)}
<div className='col-span-full mt-7'>
<div className='float-right'>
<button
className='rounded-md px-3.5 py-2.5 text-sm border border-airt-error text-airt-primary hover:bg-opacity-10 hover:bg-airt-error shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
disabled={isLoading}
data-testid='form-cancel-button'
onClick={onCancelCallback}
ref={cancelButtonRef}
>
Cancel
</button>
<button
type='submit'
className='ml-3 rounded-md px-3.5 py-2.5 text-sm bg-airt-primary text-airt-font-base hover:bg-opacity-85 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
disabled={isLoading}
data-testid='form-submit-button'
>
Save
</button>
</div>

{updateExistingModel && (
<button
type='button'
className='float-left rounded-md px-3.5 py-2.5 text-sm border bg-airt-error text-airt-font-base hover:bg-opacity-80 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
disabled={isLoading}
data-testid='form-cancel-button'
onClick={onDeleteCallback}
>
Delete
</button>
)}
</div>
</form>
);
};

export default DynamicForm;
23 changes: 23 additions & 0 deletions app/src/client/hooks/useDeploymentInstructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect } from 'react';
import { DEPLOYMENT_INSTRUCTIONS } from '../utils/constants';

export const useDeploymentInstructions = (
updateExistingModel: any,
type_name: string,
setInstructionForDeployment: (value: any) => void
) => {
useEffect(() => {
if (updateExistingModel && type_name === 'deployment') {
const msg = DEPLOYMENT_INSTRUCTIONS;

setInstructionForDeployment((prevState: any) => ({
...prevState,
gh_repo_url: updateExistingModel.gh_repo_url,
flyio_app_url: updateExistingModel.flyio_app_url,
instruction: msg
.replaceAll('<gh_repo_url>', updateExistingModel.gh_repo_url)
.replaceAll('<flyio_app_url>', updateExistingModel.flyio_app_url),
}));
}
}, [updateExistingModel, type_name, setInstructionForDeployment]);
};
12 changes: 12 additions & 0 deletions app/src/client/hooks/useEscapeKeyHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useEffect, RefObject } from 'react';

export const useEscapeKeyHandler = (cancelButtonRef: RefObject<HTMLButtonElement>) => {
useEffect(() => {
const keyHandler = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
cancelButtonRef.current?.click();
};
document.addEventListener('keydown', keyHandler);
return () => document.removeEventListener('keydown', keyHandler);
}, [cancelButtonRef]);
};
3 changes: 2 additions & 1 deletion app/src/client/hooks/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ interface UseFormProps {
jsonSchema: JsonSchema;
defaultValues?: SelectedModelSchema | null;
}
interface FormData {

export interface FormData {
[key: string]: any;
}

Expand Down
95 changes: 95 additions & 0 deletions app/src/client/hooks/useFormSubmission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { validateForm } from '../services/commonService';
import {
getFormSubmitValues,
getSecretUpdateFormSubmitValues,
getSecretUpdateValidationURL,
} from '../utils/buildPageUtils';
import { DEPLOYMENT_INSTRUCTIONS } from '../utils/constants';
import { SelectedModelSchema } from '../interfaces/BuildPageInterfaces';
import { parseValidationErrors } from '../app/utils/formHelpers';

interface UseFormSubmissionProps {
type_name: string;
validationURL: string;
updateExistingModel: SelectedModelSchema | null;
onSuccessCallback: (data: any) => void;
setFormErrors: (errors: any) => void;
}

export const useFormSubmission = ({
type_name,
validationURL,
updateExistingModel,
onSuccessCallback,
setFormErrors,
}: UseFormSubmissionProps) => {
const [isLoading, setIsLoading] = useState(false);
const [notification, setNotification] = useState({
message: 'Oops. Something went wrong. Please try again later.',
show: false,
});
const [instructionForDeployment, setInstructionForDeployment] = useState<Record<string, string> | null>(null);
const history = useHistory();
const isDeployment = type_name === 'deployment';

const handleSubmit = async (event: React.FormEvent, formData: any, refValues: Record<string, any>) => {
event.preventDefault();
if (instructionForDeployment && !updateExistingModel) {
return;
}
setIsLoading(true);
const isSecretUpdate = type_name === 'secret' && !!updateExistingModel;
let formDataToSubmit: any = {};
let updatedValidationURL = validationURL;

if (isSecretUpdate) {
formDataToSubmit = getSecretUpdateFormSubmitValues(formData, updateExistingModel);
updatedValidationURL = getSecretUpdateValidationURL(validationURL, updateExistingModel);
} else {
formDataToSubmit = getFormSubmitValues(refValues, formData, false);
}

try {
const response = await validateForm(formDataToSubmit, updatedValidationURL, isSecretUpdate);
const onSuccessCallbackResponse: any = await onSuccessCallback(response);

if (isDeployment && !updateExistingModel) {
setInstructionForDeployment((prevState) => ({
...prevState,
gh_repo_url: response.gh_repo_url,
instruction: DEPLOYMENT_INSTRUCTIONS.replaceAll('<gh_repo_url>', onSuccessCallbackResponse.gh_repo_url),
}));
}
} catch (error: any) {
try {
const errorMsgObj = JSON.parse(error.message);
const errors = parseValidationErrors(errorMsgObj);
setFormErrors(errors);
} catch (e: any) {
setNotification({ message: error.message || notification.message, show: true });
}
} finally {
setIsLoading(false);
}
};

const notificationOnClick = () => {
setNotification({ ...notification, show: false });
};

const onMissingDependencyClick = (e: any, type: string) => {
history.push(`/build/${type}`);
};

return {
isLoading,
notification,
instructionForDeployment,
handleSubmit,
notificationOnClick,
onMissingDependencyClick,
setInstructionForDeployment,
};
};
62 changes: 62 additions & 0 deletions app/src/client/hooks/usePropertyReferenceValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import _ from 'lodash';
import {
getMatchedUserProperties,
constructHTMLSchema,
getAllRefs,
checkForDependency,
getMissingDependencyType,
} from '../utils/buildPageUtils';
import { JsonSchema, SelectedModelSchema } from '../interfaces/BuildPageInterfaces';

interface UsePropertyReferenceValuesProps {
jsonSchema: JsonSchema | null;
allUserProperties: any;
updateExistingModel: SelectedModelSchema | null;
}

export const usePropertyReferenceValues = ({
jsonSchema,
allUserProperties,
updateExistingModel,
}: UsePropertyReferenceValuesProps) => {
const [refValues, setRefValues] = useState<Record<string, any>>({});

useEffect(() => {
async function fetchPropertyReferenceValues() {
if (jsonSchema) {
for (const [key, property] of Object.entries(jsonSchema.properties)) {
const propertyHasRef = _.has(property, '$ref') && property['$ref'];
const propertyHasAnyOf = (_.has(property, 'anyOf') || _.has(property, 'allOf')) && _.has(jsonSchema, '$defs');
if (propertyHasRef || propertyHasAnyOf) {
const allRefList = propertyHasRef ? [property['$ref']] : getAllRefs(property);
const refUserProperties = getMatchedUserProperties(allUserProperties, allRefList);
const missingDependencyList = checkForDependency(refUserProperties, allRefList);
const title: string = property.hasOwnProperty('title') ? property.title || '' : key;
const selectedModelRefValues = _.get(updateExistingModel, key, null);
const htmlSchema = constructHTMLSchema(refUserProperties, title, property, selectedModelRefValues);
let missingDependencyType: null | string = null;
if (missingDependencyList.length > 0) {
missingDependencyType = getMissingDependencyType(jsonSchema.$defs, allRefList);
}
setRefValues((prev) => ({
...prev,
[key]: {
htmlSchema: htmlSchema,
refUserProperties: refUserProperties,
missingDependency: {
type: missingDependencyType,
label: key,
},
},
}));
}
}
}
}

fetchPropertyReferenceValues();
}, [jsonSchema, allUserProperties, updateExistingModel]);

return refValues;
};
12 changes: 12 additions & 0 deletions app/src/client/interfaces/DynamicFormBuilderInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { JsonSchema, SelectedModelSchema } from './BuildPageInterfaces';

export interface DynamicFormBuilderProps {
allUserProperties: any;
type_name: string;
jsonSchema: JsonSchema;
validationURL: string;
updateExistingModel: SelectedModelSchema | null;
onSuccessCallback: (data: any) => void;
onCancelCallback: (event: React.FormEvent) => void;
onDeleteCallback: (data: any) => void;
}
Loading

0 comments on commit 1a5a48b

Please sign in to comment.