Update UI to take environment variables from user #6

Merged
nabarun merged 6 commits from iv-env-ui into main 2024-10-21 11:05:36 +00:00
5 changed files with 257 additions and 194 deletions
Showing only changes of commit 05a24dcb77 - Show all commits

View File

@ -416,11 +416,11 @@ export class Registry {
}; };
} }
async getCompletedAuctionIds(auctionIds: (string | null | undefined)[]): Promise<string[] | null> { async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
const validAuctionIds = auctionIds.filter((id): id is string => id !== null && id !== undefined); const validAuctionIds = auctionIds.filter((id): id is string => id !== '');
if (!validAuctionIds.length) { if (!validAuctionIds.length) {
return null; return [];
} }
const auctions = await this.registry.getAuctionsByIds(validAuctionIds); const auctions = await this.registry.getAuctionsByIds(validAuctionIds);

View File

@ -313,10 +313,9 @@ export class Service {
return project.deployments.length === 0; return project.deployments.length === 0;
}); });
const auctionIds = projects.map((project) => project.auctionId); if (projects.length > 0) {
const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(auctionIds); const auctionIds = projects.map((project) => project.auctionId!);
const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(auctionIds);
if (completedAuctionIds) {
const projectsToBedeployed = projects.filter((project) => const projectsToBedeployed = projects.filter((project) =>
completedAuctionIds.includes(project.auctionId!) completedAuctionIds.includes(project.auctionId!)
); );

View File

@ -1,5 +1,6 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { FormProvider, FieldValues } from 'react-hook-form';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts'; import { useMediaQuery } from 'usehooks-ts';
import { AuctionParams } from 'gql-client'; import { AuctionParams } from 'gql-client';
@ -14,15 +15,20 @@ import { Select, SelectOption } from 'components/shared/Select';
import { Input } from 'components/shared/Input'; import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { useGQLClient } from '../../../context/GQLClientContext'; import { useGQLClient } from '../../../context/GQLClientContext';
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from 'types/types';
type ConfigureFormValues = { type ConfigureDeploymentFormValues = {
option: string; option: string;
lrn?: string; lrn?: string;
numProviders?: number; numProviders?: number;
maxPrice?: string; maxPrice?: string;
}; };
type ConfigureFormValues = ConfigureDeploymentFormValues & EnvironmentVariablesFormValues;
const Configure = () => { const Configure = () => {
const [isLoading, setIsLoading] = useState(false);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const templateId = searchParams.get('templateId'); const templateId = searchParams.get('templateId');
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
@ -40,19 +46,18 @@ const Configure = () => {
const { toast, dismiss } = useToast(); const { toast, dismiss } = useToast();
const client = useGQLClient(); const client = useGQLClient();
const [isLoading, setIsLoading] = useState(false); const methods = useForm<ConfigureFormValues>({
const { handleSubmit, control, watch } = useForm<ConfigureFormValues>({
defaultValues: { option: 'LRN' }, defaultValues: { option: 'LRN' },
}); });
const selectedOption = watch('option'); const selectedOption = methods.watch('option');
const isTabletView = useMediaQuery('(min-width: 720px)'); // md: const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
const buttonSize = isTabletView ? { size: 'lg' as const } : {}; const buttonSize = isTabletView ? { size: 'lg' as const } : {};
const onSubmit: SubmitHandler<ConfigureFormValues> = useCallback( const createProject = async (data: FieldValues): Promise<string> => {
async (data) => { setIsLoading(true);
setIsLoading(true); let projectId: string | null = null;
try { try {
let lrn: string | undefined; let lrn: string | undefined;
@ -67,69 +72,118 @@ const Configure = () => {
}; };
} }
if (templateId) { if (templateId) {
// Template-based project creation const projectData: any = {
const projectData: any = { templateOwner,
templateOwner, templateRepo,
templateRepo, owner,
owner, name,
name, isPrivate,
isPrivate, };
};
const { addProjectFromTemplate } = await client.addProjectFromTemplate( const { addProjectFromTemplate } = await client.addProjectFromTemplate(
orgSlug!, orgSlug!,
projectData, projectData,
lrn, lrn,
auctionParams auctionParams
); );
projectId = addProjectFromTemplate.id;
} else {
const { addProject } = await client.addProject(
orgSlug!,
{
name: fullName!,
prodBranch: defaultBranch!,
repository: fullName!,
template: 'webapp',
},
lrn,
auctionParams
);
data.option === 'Auction' projectId = addProject.id;
? navigate( }
`/${orgSlug}/projects/create/success/${addProjectFromTemplate.id}?isAuction=true`, } catch (error) {
) console.error('Error creating project:', error);
: navigate( toast({
`/${orgSlug}/projects/create/template/deploy?projectId=${addProjectFromTemplate.id}&templateId=${templateId}` id: 'error-creating-project',
); title: 'Error creating project',
} else { variant: 'error',
const { addProject } = await client.addProject( onDismiss: dismiss,
orgSlug!, });
{ } finally {
name: fullName!, setIsLoading(false);
prodBranch: defaultBranch!, }
repository: fullName!,
template: 'webapp',
},
lrn,
auctionParams
);
data.option === 'Auction' if (projectId) {
? navigate( return projectId;
`/${orgSlug}/projects/create/success/${addProject.id}?isAuction=true` } else {
) throw new Error('Project creation failed');
: navigate( }
`/${orgSlug}/projects/create/deploy?projectId=${addProject.id}` };
);
} const createEnvironmentVariablesHandler = useCallback(
} catch (error) { async (createFormData: FieldValues) => {
console.error('Error creating project:', error); const environmentVariables = createFormData.variables.map((variable: any) => {
return {
key: variable.key,
value: variable.value,
environments: Object.entries(createFormData.environment)
.filter(([, value]) => value === true)
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
};
});
const projectId = await createProject(createFormData);
const { addEnvironmentVariables: isEnvironmentVariablesAdded } =
await client.addEnvironmentVariables(projectId, environmentVariables);
if (isEnvironmentVariablesAdded) {
toast({ toast({
id: 'error-creating-project', id:
title: 'Error creating project', createFormData.variables.length > 1
? 'env_variable_added'
: 'env_variables_added',
title:
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
variant: 'success',
onDismiss: dismiss,
});
} else {
toast({
id: 'env_variables_not_added',
title: 'Environment variables not added',
variant: 'error', variant: 'error',
onDismiss: dismiss, onDismiss: dismiss,
}); });
} finally { }
setIsLoading(false); if (templateId) {
createFormData.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
)
: navigate(
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`
);
} else {
createFormData.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`
)
: navigate(
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`
);
} }
}, },
[client, isPrivate, templateId, navigate, dismiss, toast] [client, createProject, dismiss, toast]
); );
return ( return (
<div className="space-y-7"> <div className="space-y-7 px-4 py-6">
<div className="flex justify-between"> <div className="flex justify-between mb-6">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Heading as="h4" className="md:text-lg font-medium"> <Heading as="h4" className="md:text-lg font-medium">
Configure deployment Configure deployment
@ -142,99 +196,108 @@ const Configure = () => {
</div> </div>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)}> <div className="flex flex-col gap-6 lg:gap-8 w-full">
<div className="flex flex-col gap-4 lg:gap-7 w-full"> <FormProvider {...methods}>
<div className="flex flex-col justify-start gap-3"> <form onSubmit={methods.handleSubmit(createEnvironmentVariablesHandler)}>
<Controller <div className="flex flex-col justify-start gap-4 mb-6">
name="option"
control={control}
render={({ field: { value, onChange } }) => (
<Select
label="Configuration Options"
value={
{
value: value || 'LRN',
label: value === 'Auction' ? 'Create Auction' : 'Deployer LRN',
} as SelectOption
}
onChange={(value) => onChange((value as SelectOption).value)}
options={[
{ value: 'LRN', label: 'Deployer LRN' },
{ value: 'Auction', label: 'Create Auction' },
]}
/>
)}
/>
</div>
{selectedOption === 'LRN' && (
<div className="flex flex-col justify-start gap-3">
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
The app will be deployed by the configured deployer
</Heading>
<span className="text-sm text-elements-high-em">
Enter LRN for deployer
</span>
<Controller <Controller
name="lrn" name="option"
control={control} control={methods.control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Input value={value} onChange={onChange} /> <Select
label="Configuration Options"
value={
{
value: value || 'LRN',
label: value === 'Auction' ? 'Create Auction' : 'Deployer LRN',
} as SelectOption
}
onChange={(value) => onChange((value as SelectOption).value)}
options={[
{ value: 'LRN', label: 'Deployer LRN' },
{ value: 'Auction', label: 'Create Auction' },
]}
/>
)} )}
/> />
</div> </div>
)}
{selectedOption === 'Auction' && ( {selectedOption === 'LRN' && (
<> <div className="flex flex-col justify-start gap-4 mb-6">
<div className="flex flex-col justify-start gap-3">
<Heading as="h5" className="text-sm font-sans text-elements-low-em"> <Heading as="h5" className="text-sm font-sans text-elements-low-em">
Set the number of deployers and maximum price for each deployment The app will be deployed by the configured deployer
</Heading> </Heading>
<span className="text-sm text-elements-high-em"> <span className="text-sm text-elements-high-em">
Number of Deployers Enter LRN for deployer
</span> </span>
<Controller <Controller
name="numProviders" name="lrn"
control={control} control={methods.control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} /> <Input value={value} onChange={onChange} />
)} )}
/> />
</div> </div>
<div className="flex flex-col justify-start gap-3"> )}
<span className="text-sm text-elements-high-em">
Maximum Price (alnt)
</span>
<Controller
name="maxPrice"
control={control}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
</>
)}
<div> {selectedOption === 'Auction' && (
<Button <>
{...buttonSize} <div className="flex flex-col justify-start gap-4 mb-6">
type="submit" <Heading as="h5" className="text-sm font-sans text-elements-low-em">
disabled={isLoading} Set the number of deployers and maximum price for each deployment
rightIcon={ </Heading>
isLoading ? ( <span className="text-sm text-elements-high-em">
<LoadingIcon className="animate-spin" /> Number of Deployers
) : ( </span>
<ArrowRightCircleFilledIcon /> <Controller
) name="numProviders"
} control={methods.control}
> render={({ field: { value, onChange } }) => (
{isLoading ? 'Deploying repo' : 'Deploy repo'} <Input type="number" value={value} onChange={onChange} />
</Button> )}
</div> />
</div> </div>
</form> <div className="flex flex-col justify-start gap-4 mb-6">
<span className="text-sm text-elements-high-em">
Maximum Price (alnt)
</span>
<Controller
name="maxPrice"
control={methods.control}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
</>
)}
<Heading as="h4" className="md:text-lg font-medium mb-3">
Environment Variables
</Heading>
<div className="p-4 bg-slate-100 rounded-lg mb-6">
<EnvironmentVariablesForm />
</div>
<div>
<Button
{...buttonSize}
type="submit"
disabled={isLoading}
rightIcon={
isLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
>
{isLoading ? 'Deploying repo' : 'Deploy repo'}
</Button>
</div>
</form>
</FormProvider>
</div>
</div> </div>
); );
}; };

View File

@ -14,6 +14,8 @@ import { ProjectSettingContainer } from 'components/projects/project/settings/Pr
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { Environment, EnvironmentVariable } from 'gql-client'; import { Environment, EnvironmentVariable } from 'gql-client';
import EnvironmentVariablesForm from './EnvironmentVariablesForm'; import EnvironmentVariablesForm from './EnvironmentVariablesForm';
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
import { Button } from 'components/shared';
export const EnvironmentVariablesTabPanel = () => { export const EnvironmentVariablesTabPanel = () => {
const { id } = useParams(); const { id } = useParams();
@ -26,6 +28,17 @@ export const EnvironmentVariablesTabPanel = () => {
const [createNewVariable, setCreateNewVariable] = useState(false); const [createNewVariable, setCreateNewVariable] = useState(false);
const methods = useForm<EnvironmentVariablesFormValues>({
defaultValues: {
variables: [{ key: '', value: '' }],
environment: {
development: false,
preview: false,
production: false,
},
},
});
const getEnvironmentVariables = useCallback( const getEnvironmentVariables = useCallback(
(environment: Environment) => { (environment: Environment) => {
return environmentVariables.filter( return environmentVariables.filter(
@ -51,8 +64,8 @@ export const EnvironmentVariablesTabPanel = () => {
}, [id]); }, [id]);
const createEnvironmentVariablesHandler = useCallback( const createEnvironmentVariablesHandler = useCallback(
async (createFormData: EnvironmentVariablesFormValues, reset: () => void) => { async (createFormData: FieldValues) => {
const environmentVariables = createFormData.variables.map((variable) => { const environmentVariables = createFormData.variables.map((variable: any) => {
return { return {
key: variable.key, key: variable.key,
value: variable.value, value: variable.value,
@ -66,7 +79,7 @@ export const EnvironmentVariablesTabPanel = () => {
await client.addEnvironmentVariables(id!, environmentVariables); await client.addEnvironmentVariables(id!, environmentVariables);
if (isEnvironmentVariablesAdded) { if (isEnvironmentVariablesAdded) {
reset(); // reset();
setCreateNewVariable((cur) => !cur); setCreateNewVariable((cur) => !cur);
fetchEnvironmentVariables(id); fetchEnvironmentVariables(id);
@ -111,10 +124,18 @@ export const EnvironmentVariablesTabPanel = () => {
</div> </div>
</Heading> </Heading>
<Collapse open={createNewVariable}> <Collapse open={createNewVariable}>
<div className="p-4 bg-slate-100"> <FormProvider {...methods}>
<EnvironmentVariablesForm <form onSubmit={methods.handleSubmit((data) => createEnvironmentVariablesHandler(data))}>
onSubmit={(data, reset) => createEnvironmentVariablesHandler(data, reset)} /> <div className="p-4 bg-slate-100">
</div> <EnvironmentVariablesForm />
</div>
<div className="p-2">
<Button size="md" type="submit">
Save changes
</Button>
</div>
</form>
</FormProvider>
</Collapse> </Collapse>
</div> </div>
<div className="p-2"> <div className="p-2">

View File

@ -1,35 +1,20 @@
import React, { useEffect, useMemo } from 'react'; import { useEffect } from 'react';
import { useForm, useFieldArray } from 'react-hook-form'; import { useFieldArray, useFormContext } from 'react-hook-form';
import { Checkbox } from '@snowballtools/material-tailwind-react-fork'; import { Checkbox } from '@snowballtools/material-tailwind-react-fork';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { InlineNotification } from 'components/shared/InlineNotification'; // import { InlineNotification } from 'components/shared/InlineNotification';
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow'; import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
import { EnvironmentVariablesFormValues } from 'types/types'; import { EnvironmentVariablesFormValues } from 'types/types';
interface EnvironmentVariablesFormProps { const EnvironmentVariablesForm = () => {
onSubmit: (data: EnvironmentVariablesFormValues, reset: () => void) => void;
}
const EnvironmentVariablesForm: React.FC<EnvironmentVariablesFormProps> = ({ onSubmit }) => {
const { const {
handleSubmit,
register, register,
control, control,
reset, reset,
formState: { isSubmitSuccessful, errors }, formState: { isSubmitSuccessful },
} = useForm<EnvironmentVariablesFormValues>({ } = useFormContext<EnvironmentVariablesFormValues>();
defaultValues: {
variables: [{ key: '', value: '' }],
environment: {
development: false,
preview: false,
production: false,
},
},
});
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
name: 'variables', name: 'variables',
control, control,
@ -44,23 +29,23 @@ const EnvironmentVariablesForm: React.FC<EnvironmentVariablesFormProps> = ({ onS
} }
}, [isSubmitSuccessful, reset]); }, [isSubmitSuccessful, reset]);
const isFieldEmpty = useMemo(() => { // const isFieldEmpty = useMemo(() => {
if (errors.variables) { // if (errors.variables) {
return fields.some((_, index) => { // return fields.some((_, index) => {
if ( // if (
errors.variables![index]?.value?.type === 'required' || // errors.variables![index]?.value?.type === 'required' ||
errors.variables![index]?.key?.type === 'required' // errors.variables![index]?.key?.type === 'required'
) { // ) {
return true; // return true;
} // }
}); // });
} // }
return false; // return false;
}, [fields, errors.variables]); // }, [fields, errors.variables]);
return ( return (
<form onSubmit={handleSubmit((data) => onSubmit(data, reset))}> <>
{fields.map((field, index) => ( {fields.map((field, index) => (
<AddEnvironmentVariableRow <AddEnvironmentVariableRow
key={field.id} key={field.id}
@ -75,23 +60,18 @@ const EnvironmentVariablesForm: React.FC<EnvironmentVariablesFormProps> = ({ onS
+ Add variable + Add variable
</Button> </Button>
</div> </div>
{isFieldEmpty && ( {/* {isFieldEmpty && (
<InlineNotification <InlineNotification
title="Please ensure no fields are empty before saving." title="Please ensure no fields are empty before saving."
variant="danger" variant="danger"
/> />
)} )} */}
<div className="flex gap-2 p-2"> <div className="flex gap-2 p-2">
<Checkbox label="Production" {...register('environment.production')} /> <Checkbox label="Production" {...register('environment.production')} />
<Checkbox label="Preview" {...register('environment.preview')} /> <Checkbox label="Preview" {...register('environment.preview')} />
<Checkbox label="Development" {...register('environment.development')} /> <Checkbox label="Development" {...register('environment.development')} />
</div> </div>
<div className="p-2"> </>
<Button size="md" type="submit">
Save changes
</Button>
</div>
</form>
); );
}; };