Skip to content

Commit a979e73

Browse files
wzrdxaledefra
andauthored
Redeploy failed paid job drafts, Monitor page + claim funds (#31)
* feat: Paid/unpaid job drafts status UI * feat: Mark paid job drafts as paid together with the running job id, Delete only successful job drafts on deployment * feat: Split onPayAndDeploy into two functions, only pay for the unpaid job drafts, exclude the payment step if not needed * chore: Logs and styling for DeeployInfoAlert * fix: Total amount due, display which jobs failed, DeeployFlowModal UX * chore: Display draft job status in DraftJobsList + small UI fixes * feat: Job draft editing flow for paid jobs * feat: Unlink payment from job draft * feat: Monitoring page initial implementation * feat Link monitored jobs to job drafts, implement confirmation for jobs with job drafts * chore: Add list header for Monitoring * feat: Refresh monitored jobs using throttle * feat: If a job's funds are claimed, unlink its payment. * fix: Sort months in BillingMonthSelect * feat: Implement redeemUnusedJob in the Monitoring page * feat: Implement SigningModal in the Monitoring page * refactor: Monitoring into Monitor * fix: Monitor refreshing jobs twice, Possible race condition in Payment's payJobDrafts function * fix: Monitor page job refreshing * fix: typos --------- Co-authored-by: Alessandro <defranceschi_a@yahoo.com>
1 parent 4bc708e commit a979e73

33 files changed

+1022
-280
lines changed

src/components/account/invoicing/BillingMonthSelect.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default function BillingMonthSelect({
4242
value: 'font-medium text-slate-600!',
4343
selectorIcon: 'mt-0.5 mr-0.5',
4444
}}
45+
aria-label="Billing month"
4546
listboxProps={{
4647
itemClasses: {
4748
base: [

src/components/account/invoicing/Invoicing.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ function Invoicing() {
4040
}
4141

4242
const months: string[] = _(drafts)
43-
.orderBy('creationTimestamp', 'desc')
4443
.map((draft) => draft.creationTimestamp.slice(0, 7))
4544
.uniq()
46-
.value();
45+
.value()
46+
.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
4747

4848
const obj = {};
4949

src/components/create-job/JobFormWrapper.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ function JobFormWrapper({ projectName, draftJobsCount }) {
198198
...data.deployment,
199199
jobAlias: data.deployment.jobAlias.toLowerCase(),
200200
},
201+
paid: false,
201202
};
202203

203204
if (data.jobType === JobType.Native) {

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import DeeployInfoAlert from '@shared/jobs/DeeployInfoAlert';
12
import { JobType } from '@typedefs/deeploys';
23
import { useEffect } from 'react';
34
import { useFormContext } from 'react-hook-form';
@@ -6,11 +7,13 @@ import NativeSpecifications from './specifications/NativeSpecifications';
67
import ServiceSpecifications from './specifications/ServiceSpecifications';
78

89
function Specifications({
9-
isEditingRunningJob,
10+
isEditingRunningJob = false,
11+
isJobPaid = false,
1012
initialTargetNodesCount,
1113
onTargetNodesCountDecrease,
1214
}: {
1315
isEditingRunningJob?: boolean;
16+
isJobPaid?: boolean;
1417
initialTargetNodesCount?: number;
1518
onTargetNodesCountDecrease?: (blocked: boolean) => void;
1619
}) {
@@ -28,6 +31,7 @@ function Specifications({
2831
return (
2932
<GenericSpecifications
3033
isEditingRunningJob={isEditingRunningJob}
34+
disablePaymentAffectingControls={isJobPaid}
3135
initialTargetNodesCount={initialTargetNodesCount}
3236
onTargetNodesCountDecrease={onTargetNodesCountDecrease}
3337
/>
@@ -37,20 +41,38 @@ function Specifications({
3741
return (
3842
<NativeSpecifications
3943
isEditingRunningJob={isEditingRunningJob}
44+
disablePaymentAffectingControls={isJobPaid}
4045
initialTargetNodesCount={initialTargetNodesCount}
4146
onTargetNodesCountDecrease={onTargetNodesCountDecrease}
4247
/>
4348
);
4449

4550
case JobType.Service:
46-
return <ServiceSpecifications isEditingRunningJob={isEditingRunningJob} />;
51+
return <ServiceSpecifications disablePaymentAffectingControls={isJobPaid} />;
4752

4853
default:
4954
return <div>Error: Unknown specifications type</div>;
5055
}
5156
};
5257

53-
return getComponent();
58+
return (
59+
<div className="col gap-6">
60+
{isJobPaid && (
61+
<DeeployInfoAlert
62+
variant="green"
63+
title={<div className="font-medium">Job already paid</div>}
64+
description={
65+
<div>
66+
This job draft has been <span className="font-medium">paid but not yet deployed</span>. Some fields
67+
cannot be edited after successful payment.
68+
</div>
69+
}
70+
/>
71+
)}
72+
73+
{getComponent()}
74+
</div>
75+
);
5476
}
5577

5678
export default Specifications;

src/components/create-job/steps/specifications/GenericSpecifications.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import SpecsNodesSection from '@shared/jobs/SpecsNodesSection';
77
import { JobType } from '@typedefs/deeploys';
88

99
export default function GenericSpecifications({
10-
isEditingRunningJob,
10+
isEditingRunningJob = false,
11+
disablePaymentAffectingControls = false,
1112
initialTargetNodesCount,
1213
onTargetNodesCountDecrease,
1314
}: {
1415
isEditingRunningJob?: boolean;
16+
disablePaymentAffectingControls?: boolean;
1517
initialTargetNodesCount?: number;
1618
onTargetNodesCountDecrease?: (blocked: boolean) => void;
1719
}) {
@@ -22,17 +24,18 @@ export default function GenericSpecifications({
2224
name="specifications.containerType"
2325
label="Container Type"
2426
options={genericContainerTypes}
25-
isDisabled={isEditingRunningJob}
27+
isDisabled={disablePaymentAffectingControls}
2628
/>
2729

28-
<SelectGPU jobType={JobType.Generic} isDisabled={isEditingRunningJob} />
30+
<SelectGPU jobType={JobType.Generic} isDisabled={disablePaymentAffectingControls} />
2931

3032
<ContainerResourcesInfo name="specifications.containerType" options={genericContainerTypes} />
3133
</SlateCard>
3234

3335
<SpecsNodesSection
3436
jobType={JobType.Generic}
3537
isEditingRunningJob={isEditingRunningJob}
38+
disablePaymentAffectingControls={disablePaymentAffectingControls}
3639
initialTargetNodesCount={initialTargetNodesCount}
3740
onTargetNodesCountDecrease={onTargetNodesCountDecrease}
3841
/>

src/components/create-job/steps/specifications/NativeSpecifications.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import SpecsNodesSection from '@shared/jobs/SpecsNodesSection';
77
import { JobType } from '@typedefs/deeploys';
88

99
export default function NativeSpecifications({
10-
isEditingRunningJob,
10+
isEditingRunningJob = false,
11+
disablePaymentAffectingControls = false,
1112
initialTargetNodesCount,
1213
onTargetNodesCountDecrease,
1314
}: {
1415
isEditingRunningJob?: boolean;
16+
disablePaymentAffectingControls?: boolean;
1517
initialTargetNodesCount?: number;
1618
onTargetNodesCountDecrease?: (blocked: boolean) => void;
1719
}) {
@@ -22,17 +24,18 @@ export default function NativeSpecifications({
2224
name="specifications.workerType"
2325
label="Worker Type"
2426
options={nativeWorkerTypes}
25-
isDisabled={isEditingRunningJob}
27+
isDisabled={disablePaymentAffectingControls}
2628
/>
2729

28-
<SelectGPU jobType={JobType.Native} isDisabled={isEditingRunningJob} />
30+
<SelectGPU jobType={JobType.Native} isDisabled={disablePaymentAffectingControls} />
2931

3032
<ContainerResourcesInfo name="specifications.workerType" options={nativeWorkerTypes} />
3133
</SlateCard>
3234

3335
<SpecsNodesSection
3436
jobType={JobType.Native}
3537
isEditingRunningJob={isEditingRunningJob}
38+
disablePaymentAffectingControls={disablePaymentAffectingControls}
3639
initialTargetNodesCount={initialTargetNodesCount}
3740
onTargetNodesCountDecrease={onTargetNodesCountDecrease}
3841
/>

src/components/create-job/steps/specifications/ServiceSpecifications.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ import SelectContainerOrWorkerType from '@shared/jobs/SelectContainerOrWorkerTyp
55
import JobTags from '@shared/jobs/target-nodes/JobTags';
66
import NumberInputWithLabel from '@shared/NumberInputWithLabel';
77

8-
export default function ServiceSpecifications({ isEditingRunningJob }: { isEditingRunningJob?: boolean }) {
8+
export default function ServiceSpecifications({
9+
disablePaymentAffectingControls = false,
10+
}: {
11+
disablePaymentAffectingControls?: boolean;
12+
}) {
913
return (
1014
<div className="col gap-6">
1115
<SlateCard title="Service Resources">
1216
<SelectContainerOrWorkerType
1317
name="specifications.containerType"
1418
label="Container Type"
1519
options={serviceContainerTypes}
16-
isDisabled={isEditingRunningJob}
20+
isDisabled={disablePaymentAffectingControls}
1721
/>
1822

1923
<ContainerResourcesInfo name="specifications.containerType" options={serviceContainerTypes} />
@@ -22,12 +26,6 @@ export default function ServiceSpecifications({ isEditingRunningJob }: { isEditi
2226
<SlateCard>
2327
<div className="flex gap-4">
2428
<NumberInputWithLabel name="specifications.targetNodesCount" label="Target Nodes Count" isDisabled />
25-
26-
{/* <SelectWithLabel
27-
name="specifications.applicationType"
28-
label="Application Type"
29-
options={APPLICATION_TYPES}
30-
/> */}
3129
</div>
3230

3331
<JobTags />

src/components/deeploys/Running.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ListHeader from '@shared/ListHeader';
77
import { RunningJobWithDetails } from '@typedefs/deeploys';
88
import _ from 'lodash';
99
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
10+
import toast from 'react-hot-toast';
1011
import { RiDraftLine } from 'react-icons/ri';
1112
import { usePublicClient } from 'wagmi';
1213
import RunningCard from './RunningCard';
@@ -54,13 +55,18 @@ const Running = forwardRef<
5455
const getProjectsWithJobs = async () => {
5556
setLoading(true);
5657

57-
const jobsWithDetails: RunningJobWithDetails[] = await fetchRunningJobsWithDetails();
58-
const projectsWithJobs = _.groupBy(jobsWithDetails, 'projectHash');
59-
60-
setProjects(projectsWithJobs);
61-
setProjectsCount(Object.keys(projectsWithJobs).length);
62-
63-
setLoading(false);
58+
try {
59+
const { runningJobsWithDetails } = await fetchRunningJobsWithDetails();
60+
const projectsWithJobs = _.groupBy(runningJobsWithDetails, 'projectHash');
61+
62+
setProjects(projectsWithJobs);
63+
setProjectsCount(Object.keys(projectsWithJobs).length);
64+
} catch (error) {
65+
console.error(error);
66+
toast.error('Failed to fetch running jobs.');
67+
} finally {
68+
setLoading(false);
69+
}
6470
};
6571

6672
const expandAll = () => {

src/components/deeploys/RunningCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export default function RunningCard({
134134
const daysLeftUntilNextPayment = getDaysLeftUntilNextPayment(job);
135135

136136
return (
137-
<div key={job.id} className="row">
137+
<div key={`${job.id}-${index}`} className="row">
138138
<div className="row flex-1 gap-6">
139139
<div className="row gap-1.5">
140140
{/* Tree Line */}

src/components/draft/DraftEditFormWrapper.tsx

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,6 @@ import { FieldErrors, FormProvider, useForm } from 'react-hook-form';
1919
import { useNavigate } from 'react-router-dom';
2020
import z from 'zod';
2121

22-
const DEFAULT_STEPS: {
23-
title: string;
24-
validationName?: string;
25-
}[] = [
26-
{ title: 'Specifications', validationName: 'specifications' },
27-
{ title: 'Cost & Duration', validationName: 'costAndDuration' },
28-
{ title: 'Deployment', validationName: 'deployment' },
29-
];
30-
3122
export default function DraftEditFormWrapper({
3223
job,
3324
onSubmit,
@@ -39,7 +30,13 @@ export default function DraftEditFormWrapper({
3930

4031
const navigate = useNavigate();
4132

42-
const [steps, setSteps] = useState(DEFAULT_STEPS);
33+
const [steps, setSteps] = useState<
34+
| {
35+
title: string;
36+
validationName?: string;
37+
}[]
38+
| undefined
39+
>();
4340

4441
const getBaseSchemaDeploymentDefaults = () => ({
4542
jobAlias: job.deployment.jobAlias,
@@ -200,8 +197,21 @@ export default function DraftEditFormWrapper({
200197
});
201198

202199
useEffect(() => {
203-
if (job.jobType === JobType.Native) {
204-
setSteps([...DEFAULT_STEPS, { title: 'Plugins' }]);
200+
if (job) {
201+
const defaultSteps: {
202+
title: string;
203+
validationName?: string;
204+
}[] = [
205+
{ title: 'Specifications', validationName: 'specifications' },
206+
...(job.paid ? [] : [{ title: 'Cost & Duration', validationName: 'costAndDuration' }]),
207+
{ title: 'Deployment', validationName: 'deployment' },
208+
];
209+
210+
if (job.jobType === JobType.Native) {
211+
defaultSteps.push({ title: 'Plugins' });
212+
}
213+
214+
setSteps(defaultSteps);
205215
}
206216
}, [job]);
207217

@@ -214,27 +224,39 @@ export default function DraftEditFormWrapper({
214224
<form onSubmit={form.handleSubmit(onSubmit, onError)} key={`${job.jobType}-draft-edit`}>
215225
<div className="w-full flex-1">
216226
<div className="mx-auto max-w-[626px]">
217-
<div className="col gap-6">
218-
<JobFormHeaderInterface
219-
steps={steps.map((step) => step.title)}
220-
onCancel={() => {
221-
navigate(-1);
222-
}}
223-
>
224-
<div className="big-title">Edit Job Draft</div>
225-
</JobFormHeaderInterface>
226-
227-
{step === 0 && <Specifications />}
228-
{step === 1 && <CostAndDuration />}
229-
{step === 2 && <Deployment />}
230-
{step === 3 && <Plugins />}
231-
232-
<JobFormButtons
233-
steps={steps}
234-
cancelLabel="Project"
235-
customSubmitButton={<SubmitButton label="Update Draft" />}
236-
/>
237-
</div>
227+
{!!steps && (
228+
<div className="col gap-6">
229+
<JobFormHeaderInterface
230+
steps={steps.map((step) => step.title)}
231+
onCancel={() => {
232+
navigate(-1);
233+
}}
234+
>
235+
<div className="big-title">Edit Job Draft</div>
236+
</JobFormHeaderInterface>
237+
238+
{job.paid ? (
239+
<>
240+
{step === 0 && <Specifications isJobPaid />}
241+
{step === 1 && <Deployment />}
242+
{step === 2 && <Plugins />}
243+
</>
244+
) : (
245+
<>
246+
{step === 0 && <Specifications />}
247+
{step === 1 && <CostAndDuration />}
248+
{step === 2 && <Deployment />}
249+
{step === 3 && <Plugins />}
250+
</>
251+
)}
252+
253+
<JobFormButtons
254+
steps={steps}
255+
cancelLabel="Project"
256+
customSubmitButton={<SubmitButton label="Update Draft" />}
257+
/>
258+
</div>
259+
)}
238260
</div>
239261
</div>
240262
</form>

0 commit comments

Comments
 (0)