Skip to content

Commit cdfcb66

Browse files
committed
Squashed commit of the following:
commit 64095dd Author: wzrdx <128477299+wzrdx@users.noreply.github.com> Date: Tue Oct 14 15:14:53 2025 +0300 feat: Implement edit job draft flow commit 04ed7fb Author: wzrdx <128477299+wzrdx@users.noreply.github.com> Date: Tue Oct 14 14:14:30 2025 +0300 fix: JobDraftBreadcrumbs navigation commit 10d43b7 Author: wzrdx <128477299+wzrdx@users.noreply.github.com> Date: Tue Oct 14 14:09:40 2025 +0300 feat: Create EditJobDraft and JobDraftBreadcrumbs commit 38a3af9 Author: wzrdx <128477299+wzrdx@users.noreply.github.com> Date: Tue Oct 14 13:22:30 2025 +0300 chore: Modify isUsingDevAddress in config, Create DraftEditFormWrapper
1 parent 7b7b7c5 commit cdfcb66

File tree

19 files changed

+435
-53
lines changed

19 files changed

+435
-53
lines changed

src/components/create-job/JobFormWrapper.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import toast from 'react-hot-toast';
2424
import { useParams } from 'react-router-dom';
2525
import { z } from 'zod';
2626

27-
const STEPS = [
27+
const STEPS: {
28+
title: string;
29+
validationName?: string;
30+
}[] = [
2831
{ title: 'Specifications', validationName: 'specifications' },
2932
{ title: 'Cost & Duration', validationName: 'costAndDuration' },
3033
{ title: 'Deployment', validationName: 'deployment' },

src/components/create-job/steps/CostAndDuration.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ function CostAndDuration() {
7878
<CostAndDurationInterface
7979
costPerEpoch={costPerEpoch}
8080
summaryItems={summaryItems}
81-
initialDuration={12}
82-
initialPaymentMonthsCount={12}
81+
initialDuration={costAndDuration.duration}
82+
initialPaymentMonthsCount={costAndDuration.paymentMonthsCount}
8383
onDurationChange={onDurationChange}
8484
/>
8585
);

src/components/deeploys/DraftCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default function DraftCard({ project }: { project: DraftProject }) {
4444
};
4545

4646
return (
47-
<Link to={`${routePath.deeploys}/${routePath.draft}/${project.projectHash}`}>
47+
<Link to={`${routePath.deeploys}/${routePath.projectDraft}/${project.projectHash}`}>
4848
<BorderedCard isHoverable>
4949
<div className="row justify-between gap-3 text-sm lg:gap-6">
5050
<div className="min-w-[132px]">
@@ -77,7 +77,7 @@ export default function DraftCard({ project }: { project: DraftProject }) {
7777
{
7878
key: 'delete',
7979
label: 'Delete',
80-
description: 'Removes the project draft from storage',
80+
description: 'Remove the project draft from storage',
8181
onPress: () => onDeleteProject(),
8282
},
8383
]}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import JobFormButtons from '@components/create-job/JobFormButtons';
2+
import CostAndDuration from '@components/create-job/steps/CostAndDuration';
3+
import Deployment from '@components/create-job/steps/Deployment';
4+
import Specifications from '@components/create-job/steps/Specifications';
5+
import { APPLICATION_TYPES } from '@data/applicationTypes';
6+
import { BOOLEAN_TYPES } from '@data/booleanTypes';
7+
import { DYNAMIC_ENV_TYPES } from '@data/dynamicEnvTypes';
8+
import { PIPELINE_INPUT_TYPES } from '@data/pipelineInputTypes';
9+
import { PLUGIN_SIGNATURE_TYPES } from '@data/pluginSignatureTypes';
10+
import { zodResolver } from '@hookform/resolvers/zod';
11+
import { DeploymentContextType, useDeploymentContext } from '@lib/contexts/deployment';
12+
import { jobSchema } from '@schemas/index';
13+
import JobFormHeaderInterface from '@shared/jobs/JobFormHeaderInterface';
14+
import { DraftJob, GenericDraftJob, JobType, NativeDraftJob, ServiceDraftJob } from '@typedefs/deeploys';
15+
import { FieldErrors, FormProvider, useForm } from 'react-hook-form';
16+
import { useNavigate } from 'react-router-dom';
17+
import z from 'zod';
18+
19+
const STEPS: {
20+
title: string;
21+
validationName?: string;
22+
}[] = [
23+
{ title: 'Specifications', validationName: 'specifications' },
24+
{ title: 'Cost & Duration', validationName: 'costAndDuration' },
25+
{ title: 'Deployment', validationName: 'deployment' },
26+
];
27+
28+
export default function DraftEditFormWrapper({
29+
job,
30+
onSubmit,
31+
}: {
32+
job: DraftJob;
33+
onSubmit: (data: z.infer<typeof jobSchema>) => Promise<void>;
34+
}) {
35+
const { step } = useDeploymentContext() as DeploymentContextType;
36+
37+
const navigate = useNavigate();
38+
39+
const cloneKeyValueEntries = (entries?: Array<{ key: string; value: string }>) =>
40+
entries ? entries.map((entry) => ({ key: entry.key, value: entry.value })) : [];
41+
42+
const cloneDynamicEnvEntries = (
43+
entries?: Array<{
44+
key: string;
45+
values: Array<{ type: (typeof DYNAMIC_ENV_TYPES)[number]; value: string }>;
46+
}>,
47+
) =>
48+
entries
49+
? entries.map((entry) => ({
50+
key: entry.key,
51+
values: entry.values.map((value) => ({ type: value.type, value: value.value })),
52+
}))
53+
: [];
54+
55+
const cloneDeploymentNodes = (nodes?: Array<{ address: string }>) =>
56+
nodes && nodes.length ? nodes.map((node) => ({ address: node.address })) : [{ address: '' }];
57+
58+
const getBaseSchemaDefaults = () => ({
59+
jobType: job.jobType,
60+
specifications: {
61+
applicationType: job.specifications.applicationType ?? APPLICATION_TYPES[0],
62+
targetNodesCount: job.specifications.targetNodesCount,
63+
jobTags: [...(job.specifications.jobTags ?? [])],
64+
nodesCountries: [...(job.specifications.nodesCountries ?? [])],
65+
},
66+
costAndDuration: {
67+
duration: job.costAndDuration.duration,
68+
paymentMonthsCount: job.costAndDuration.paymentMonthsCount,
69+
},
70+
deployment: {
71+
autoAssign: job.deployment.autoAssign ?? true,
72+
targetNodes: cloneDeploymentNodes(job.deployment.targetNodes),
73+
spareNodes: cloneDeploymentNodes(job.deployment.spareNodes),
74+
allowReplicationInTheWild: job.deployment.allowReplicationInTheWild ?? true,
75+
enableTunneling: job.deployment.enableTunneling ?? BOOLEAN_TYPES[0],
76+
tunnelingToken: job.deployment.tunnelingToken,
77+
tunnelingLabel: job.deployment.tunnelingLabel,
78+
},
79+
});
80+
81+
const getGenericSchemaDefaults = () => {
82+
const baseDefaults = getBaseSchemaDefaults();
83+
const genericJob = job as GenericDraftJob;
84+
const deployment = genericJob.deployment;
85+
86+
return {
87+
...baseDefaults,
88+
specifications: {
89+
...baseDefaults.specifications,
90+
containerType: genericJob.specifications.containerType,
91+
gpuType: genericJob.specifications.gpuType,
92+
},
93+
deployment: {
94+
...baseDefaults.deployment,
95+
jobAlias: deployment.jobAlias,
96+
deploymentType:
97+
deployment.deploymentType.type === 'image'
98+
? {
99+
...deployment.deploymentType,
100+
crUsername: deployment.deploymentType.crUsername ?? '',
101+
crPassword: deployment.deploymentType.crPassword ?? '',
102+
}
103+
: {
104+
...deployment.deploymentType,
105+
workerCommands: deployment.deploymentType.workerCommands.map((command) => ({
106+
command: command.command,
107+
})),
108+
username: deployment.deploymentType.username ?? '',
109+
accessToken: deployment.deploymentType.accessToken ?? '',
110+
},
111+
port: deployment.port ?? '',
112+
restartPolicy: deployment.restartPolicy,
113+
imagePullPolicy: deployment.imagePullPolicy,
114+
envVars: cloneKeyValueEntries(deployment.envVars),
115+
dynamicEnvVars: cloneDynamicEnvEntries(deployment.dynamicEnvVars),
116+
volumes: cloneKeyValueEntries(deployment.volumes),
117+
fileVolumes: deployment.fileVolumes
118+
? deployment.fileVolumes.map((fileVolume) => ({
119+
name: fileVolume.name,
120+
mountingPoint: fileVolume.mountingPoint,
121+
content: fileVolume.content,
122+
}))
123+
: [],
124+
},
125+
} as z.infer<typeof jobSchema>;
126+
};
127+
128+
const getNativeSchemaDefaults = () => {
129+
const baseDefaults = getBaseSchemaDefaults();
130+
const nativeJob = job as NativeDraftJob;
131+
const deployment = nativeJob.deployment;
132+
133+
return {
134+
...baseDefaults,
135+
specifications: {
136+
...baseDefaults.specifications,
137+
workerType: nativeJob.specifications.workerType,
138+
gpuType: nativeJob.specifications.gpuType,
139+
},
140+
deployment: {
141+
...baseDefaults.deployment,
142+
jobAlias: deployment.jobAlias,
143+
port: deployment.port ?? '',
144+
pluginSignature: deployment.pluginSignature ?? PLUGIN_SIGNATURE_TYPES[0],
145+
customParams: cloneKeyValueEntries(deployment.customParams),
146+
pipelineParams: cloneKeyValueEntries(deployment.pipelineParams),
147+
pipelineInputType: deployment.pipelineInputType ?? PIPELINE_INPUT_TYPES[0],
148+
pipelineInputUri: deployment.pipelineInputUri,
149+
chainstoreResponse: deployment.chainstoreResponse ?? BOOLEAN_TYPES[1],
150+
},
151+
} as z.infer<typeof jobSchema>;
152+
};
153+
154+
const getServiceSchemaDefaults = () => {
155+
const baseDefaults = getBaseSchemaDefaults();
156+
const serviceJob = job as ServiceDraftJob;
157+
const deployment = serviceJob.deployment;
158+
159+
return {
160+
...baseDefaults,
161+
specifications: {
162+
...baseDefaults.specifications,
163+
containerType: serviceJob.specifications.containerType,
164+
},
165+
deployment: {
166+
...baseDefaults.deployment,
167+
jobAlias: deployment.jobAlias,
168+
envVars: cloneKeyValueEntries(deployment.envVars),
169+
dynamicEnvVars: cloneDynamicEnvEntries(deployment.dynamicEnvVars),
170+
volumes: cloneKeyValueEntries(deployment.volumes),
171+
serviceReplica: deployment.serviceReplica ?? '',
172+
},
173+
} as z.infer<typeof jobSchema>;
174+
};
175+
176+
const getDefaultSchemaValues = () => {
177+
switch (job.jobType) {
178+
case JobType.Generic:
179+
return getGenericSchemaDefaults();
180+
181+
case JobType.Native:
182+
return getNativeSchemaDefaults();
183+
184+
case JobType.Service:
185+
return getServiceSchemaDefaults();
186+
187+
default:
188+
return {};
189+
}
190+
};
191+
192+
const form = useForm<z.infer<typeof jobSchema>>({
193+
resolver: zodResolver(jobSchema),
194+
mode: 'onTouched',
195+
defaultValues: getDefaultSchemaValues(),
196+
});
197+
198+
const onError = (errors: FieldErrors<z.infer<typeof jobSchema>>) => {
199+
console.log(errors);
200+
};
201+
202+
return (
203+
<FormProvider {...form}>
204+
<form onSubmit={form.handleSubmit(onSubmit, onError)} key={`${job.jobType}-draft-edit`}>
205+
<div className="w-full flex-1">
206+
<div className="mx-auto max-w-[626px]">
207+
<div className="col gap-6">
208+
<JobFormHeaderInterface
209+
steps={STEPS.map((step) => step.title)}
210+
onCancel={() => {
211+
navigate(-1);
212+
}}
213+
>
214+
<div className="big-title">Edit Job Draft</div>
215+
</JobFormHeaderInterface>
216+
217+
{step === 0 && <Specifications />}
218+
{step === 1 && <CostAndDuration />}
219+
{step === 2 && <Deployment />}
220+
221+
<JobFormButtons steps={STEPS} cancelLabel="Project" />
222+
</div>
223+
</div>
224+
</div>
225+
</form>
226+
</FormProvider>
227+
);
228+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { DeploymentContextType, useDeploymentContext } from '@lib/contexts/deployment';
2+
import { routePath } from '@lib/routes/route-paths';
3+
import db from '@lib/storage/db';
4+
import { SmallTag } from '@shared/SmallTag';
5+
import { DraftJob, DraftProject } from '@typedefs/deeploys';
6+
import { JobTypeOption, jobTypeOptions } from '@typedefs/jobType';
7+
import { useLiveQuery } from 'dexie-react-hooks';
8+
import { useEffect, useState } from 'react';
9+
import { Link, useNavigate } from 'react-router-dom';
10+
11+
export default function JobDraftBreadcrumbs({ jobDraft }: { jobDraft: DraftJob }) {
12+
const { getProjectName, setProjectOverviewTab } = useDeploymentContext() as DeploymentContextType;
13+
const navigate = useNavigate();
14+
15+
const [jobTypeOption, setJobTypeOption] = useState<JobTypeOption | undefined>();
16+
17+
const projectDraft: DraftProject | undefined | null = useLiveQuery(
18+
() => db.projects.get(jobDraft.projectHash),
19+
[jobDraft],
20+
null, // Default value returned while data is loading
21+
);
22+
23+
useEffect(() => {
24+
if (jobDraft) {
25+
setJobTypeOption(jobTypeOptions.find((option) => option.jobType === jobDraft.jobType));
26+
}
27+
}, [jobDraft]);
28+
29+
if (projectDraft === null) {
30+
return <></>;
31+
}
32+
33+
const targetPath = `${routePath.deeploys}/${!projectDraft ? routePath.project : routePath.projectDraft}/${jobDraft.projectHash}`;
34+
35+
return (
36+
<div className="row gap-1.5">
37+
<Link
38+
to={targetPath}
39+
className="hover:underline"
40+
onClick={(event) => {
41+
event.preventDefault();
42+
43+
if (!projectDraft) {
44+
setProjectOverviewTab('draftJobs');
45+
}
46+
47+
navigate(targetPath);
48+
}}
49+
>
50+
<div className="text-xl font-semibold">{projectDraft?.name ?? getProjectName(jobDraft.projectHash)}</div>
51+
</Link>
52+
53+
<div className="mb-0.5 ml-1 text-xl font-semibold text-slate-500">/</div>
54+
55+
<div className="row gap-1.5">
56+
{!!jobTypeOption && <div className={`text-xl ${jobTypeOption.textColorClass}`}>{jobTypeOption.icon}</div>}
57+
<div className="text-xl font-semibold">{jobDraft.deployment.jobAlias}</div>
58+
59+
<SmallTag isLarge>Draft</SmallTag>
60+
</div>
61+
</div>
62+
);
63+
}

src/lib/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,4 @@ export const getDevAddress = (): {
8989
address: import.meta.env.VITE_DEV_ADDRESS,
9090
});
9191

92-
export const isUsingDevAddress = process.env.NODE_ENV === 'development' && true; // TODO: false
92+
export const isUsingDevAddress = process.env.NODE_ENV === 'development' && !!import.meta.env.VITE_DEV_ADDRESS;

src/lib/routes/route-paths.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export const routePath = {
77
dashboard: 'dashboard',
88
legacyRequester: 'legacy-requester',
99
createProject: 'create-project',
10-
draft: 'draft',
10+
projectDraft: 'project-draft',
11+
jobDraft: 'job-draft',
1112
project: 'project',
1213
job: 'job',
1314
//

src/lib/routes/routes.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import NotFound from '@pages/404';
22
import Account from '@pages/Account';
33
import CreateProject from '@pages/deeploys/CreateProject';
44
import Dashboard from '@pages/deeploys/Dashboard';
5-
import Draft from '@pages/deeploys/Draft';
5+
import EditJobDraft from '@pages/deeploys/drafts/EditJobDraft';
6+
import ProjectDraft from '@pages/deeploys/drafts/ProjectDraft';
67
import EditJob from '@pages/deeploys/job/EditJob';
78
import ExtendJob from '@pages/deeploys/job/ExtendJob';
89
import Job from '@pages/deeploys/job/Job';
@@ -64,7 +65,7 @@ export const routeInfo = {
6465
description: 'Create and configure a new project for deployment',
6566
routeTitle: 'Create Project',
6667
},
67-
[`${routePath.deeploys}/${routePath.draft}`]: {
68+
[`${routePath.deeploys}/${routePath.projectDraft}`]: {
6869
title: 'Project Draft',
6970
description: 'Edit, pay and deploy your project draft',
7071
},
@@ -149,8 +150,12 @@ export const routes: AppRoute[] = [
149150
},
150151
// Routes which are not displayed in the main navigation
151152
{
152-
path: `${routePath.deeploys}/${routePath.draft}/:projectHash`,
153-
page: () => <Draft />,
153+
path: `${routePath.deeploys}/${routePath.projectDraft}/:projectHash`,
154+
page: () => <ProjectDraft />,
155+
},
156+
{
157+
path: `${routePath.deeploys}/${routePath.jobDraft}/:draftId`,
158+
page: () => <EditJobDraft />,
154159
},
155160
{
156161
path: `${routePath.deeploys}/${routePath.project}/:projectHash`,

0 commit comments

Comments
 (0)