Allow to create a storage request for days and months

This commit is contained in:
Arnaud 2024-11-27 11:06:02 +01:00
parent dc8b42c03a
commit b90b5bd9fc
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
15 changed files with 203 additions and 76 deletions

View File

@ -76,3 +76,49 @@ test('remove the CID when the file is deleted', async ({ page }) => {
await page.locator('.button-icon--small').nth(1).click();
await expect(page.locator('#cid')).toBeEmpty()
})
test('create a storage request by using decimal values', async ({ page }) => {
await page.goto('/dashboard');
await page.locator('a').filter({ hasText: 'Purchases' }).click();
await page.getByRole('button', { name: 'Storage Request' }).click();
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await expect(page.locator('#cid')).not.toBeEmpty()
await expect(page.getByText('Success, the CID has been')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
const value = (Math.random() * 10);
await page.getByLabel("Full period of the contract").fill(value.toFixed(1))
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Your request is being processed.')).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText('No data.')).not.toBeVisible();
await expect(page.getByText(value.toFixed(1) + " days").first()).toBeVisible();
})
test('create a storage request by using months', async ({ page }) => {
await page.goto('/dashboard');
await page.locator('a').filter({ hasText: 'Purchases' }).click();
await page.getByRole('button', { name: 'Storage Request' }).click();
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await expect(page.locator('#cid')).not.toBeEmpty()
await expect(page.getByText('Success, the CID has been')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await page.getByLabel("Full period of the contract").fill("3")
await page.getByRole('combobox').selectOption('months');
await expect(page.getByLabel("Full period of the contract")).toHaveValue("1")
await page.getByLabel("Full period of the contract").fill("2")
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Your request is being processed.')).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText('No data.')).not.toBeVisible();
await expect(page.getByText("2 months").first()).toBeVisible();
})

View File

@ -29,7 +29,7 @@ const CONFIRM_STATE = 2;
const defaultAvailabilityData: AvailabilityState = {
totalSize: 0.5 * GB,
duration: Times.unitValue("days"),
duration: Times.value("days"),
minPrice: 0,
maxCollateral: 0,
totalSizeUnit: "gb",
@ -62,7 +62,7 @@ export function AvailabilityEdit({
}
if (a) {
setAvailability(a);
setAvailability({ ...defaultAvailabilityData, ...a });
}
});
}, [dispatch]);

View File

@ -46,7 +46,7 @@ export function AvailabilityForm({
const onDurationChange = async (e: ChangeEvent<HTMLInputElement>) => {
const element = e.currentTarget;
const unitValue = Times.unitValue(availability.durationUnit);
const unitValue = Times.value(availability.durationUnit);
onAvailabilityChange({
duration: parseInt(element.value) * unitValue,
@ -56,7 +56,7 @@ export function AvailabilityForm({
const onDurationUnitChange = async (e: ChangeEvent<HTMLSelectElement>) => {
const element = e.currentTarget;
const unit = element.value as "hours" | "days" | "months";
const unitValue = Times.unitValue(unit);
const unitValue = Times.value(unit);
onAvailabilityChange({
duration: unitValue,
@ -108,7 +108,7 @@ export function AvailabilityForm({
availability.totalSizeUnit
).toFixed(2);
const unitValue = Times.unitValue(availability.durationUnit);
const unitValue = Times.value(availability.durationUnit);
const duration = availability.duration / unitValue;
return (

View File

@ -88,5 +88,5 @@ export const AvailabilityUtils = {
"#D2493C22",
"#D2493C11",
"#D2493C00",
]
],
}

View File

@ -294,8 +294,8 @@ describe("files", () => {
});
it("formats date", async () => {
const utcDate = new Date(Date.UTC(2024, 10, 20, 11, 36));
assert.equal(FilesUtils.formatDate(1732102577), "20 Nov 2024, 11:36");
assert.equal(FilesUtils.formatDate(1732102577), "20 Nov 2024, " + utcDate.getHours() + ":" + utcDate.getMinutes());
})
})

View File

@ -0,0 +1,7 @@
.commitment {
--codex-input-group-background-color: transparent;
span {
right: 155px;
}
}

View File

@ -0,0 +1,76 @@
import { InputGroup, Tooltip } from "@codex-storage/marketplace-ui-components";
import "../CardNumbers/CardNumbers.css";
import "./Commitment.css";
import { ChangeEvent, useState } from "react";
import { classnames } from "../../utils/classnames";
import InfoIcon from "../../assets/icons/info.svg?react";
import { attributes } from "../../utils/attributes";
import { Times } from "../../utils/times";
type Props = {
value: string;
onChange: (value: string) => void;
onValidation?: (value: string) => string;
};
export function Commitment({ value, onValidation, onChange }: Props) {
const [error, setError] = useState("");
const unitValue = Times.unit(parseFloat(value));
const val = parseFloat(value) / Times.value(unitValue);
const onValueChange = (e: ChangeEvent<HTMLInputElement>) => {
console.info("e.currentTarget.value", e.currentTarget.value);
onValueOrUnitChange(
(parseFloat(e.currentTarget.value) * Times.value(unitValue)).toFixed(1)
);
};
const onUnitChange = (e: ChangeEvent<HTMLSelectElement>) => {
onValueOrUnitChange(
Times.value(e.currentTarget.value as "days" | "months").toFixed(1)
);
};
const onValueOrUnitChange = (val: string) => {
onChange(val);
const msg = onValidation?.(val);
if (msg) {
setError(msg);
return;
}
setError("");
};
console.info(val);
return (
<div
className={classnames(["card-number cardNumber-container commitment"])}
{...attributes({ "aria-invalid": !!error })}>
<InputGroup
id="duration"
name="duration"
type="number"
label="Full period of the contract"
isInvalid={!!error}
onChange={onValueChange}
onGroupChange={onUnitChange}
value={Number.isInteger(val) ? val.toString() : val.toFixed(1)}
group={[
["days", "days"],
["months", "months"],
]}
groupValue={unitValue}
/>
<Tooltip message={error || "The duration of the request in months"}>
<InfoIcon></InfoIcon>
</Tooltip>
<span>{"Contract duration"}</span>
</div>
);
}

View File

@ -22,8 +22,7 @@ const CONFIRM_STATE = 2;
const defaultStorageRequest: StorageRequest = {
cid: "",
availabilityUnit: "months",
availability: 1,
availability: Times.value("days"),
tolerance: 1,
proofProbability: 1,
nodes: 3,
@ -43,7 +42,7 @@ export function StorageRequestCreate() {
useEffect(() => {
Promise.all([
WebStorage.get<number>("storage-request-step"),
WebStorage.get<StorageRequest>("storage-request"),
WebStorage.get<StorageRequest>("storage-request-2"),
]).then(([s, data]) => {
if (s) {
dispatch({
@ -83,11 +82,10 @@ export function StorageRequestCreate() {
WebStorage.set("storage-request-step", step);
if (step == CONFIRM_STATE) {
const { availabilityUnit, availability, expiration, ...rest } =
storageRequest;
const { availability, expiration, ...rest } = storageRequest;
mutateAsync({
...rest,
duration: Times.toSeconds(availability, availabilityUnit),
duration: availability,
expiry: expiration * 60,
});
} else {
@ -101,7 +99,7 @@ export function StorageRequestCreate() {
const onStorageRequestChange = (data: Partial<StorageRequest>) => {
const val = { ...storageRequest, ...data };
WebStorage.set("storage-request", val);
WebStorage.set("storage-request-2", val);
setStorageRequest(val);
};

View File

@ -14,6 +14,7 @@ import CommitmentIcon from "../../assets/icons/commitment.svg?react";
import RequestDurationIcon from "../../assets/icons/request-duration.svg?react";
import { attributes } from "../../utils/attributes";
import { Strings } from "../../utils/strings";
import { Commitment } from "./Commitment";
type Durability = {
nodes: number;
@ -139,14 +140,6 @@ export function StorageRequestReview({
return error;
}
// if (!unit.endsWith("s")) {
// unit += "s";
// }
// if (!units.includes(unit)) {
// return "Invalid unit must one of: minutes, hours, days, months, years";
// }
return "";
};
@ -165,13 +158,8 @@ export function StorageRequestReview({
const onAvailabilityChange = (value: string) => {
const [availability] = value.split(" ");
// if (!availabilityUnit.endsWith("s")) {
// availabilityUnit += "s";
// }
onStorageRequestChange({
availability: Number(availability),
availabilityUnit: "months",
});
};
@ -184,18 +172,6 @@ export function StorageRequestReview({
const onCollateralChange = (value: string) =>
onStorageRequestChange({ collateral: Number(value) });
// const pluralizeUnit = () => {
// if (data.availability > 1 && !data.availabilityUnit.endsWith("s")) {
// return data.availability + " " +data.availabilityUnit + "s";
// }
// if (data.availability <= 1 && data.availabilityUnit.endsWith("s")) {
// return data.availabilityUnit.slice(0, -1);
// }
// return data.availabilityUnit;
// };
const availability = storageRequest.availability;
return (
@ -286,14 +262,10 @@ export function StorageRequestReview({
</div>
<div className="grid">
<CardNumbers
helper="The duration of the request in months"
id="duration"
title={"Full period of the contract"}
<Commitment
value={availability.toString()}
onChange={onAvailabilityChange}
onValidation={isInvalidAvailability}
unit="Contract duration"></CardNumbers>
onValidation={isInvalidAvailability}></Commitment>
<CardNumbers
helper="Represents how much collateral is asked from hosts that wants to fill a slots"
id="collateral"

View File

@ -16,29 +16,14 @@ export type StoragePriceStepValue = {
expiration: number;
};
export type StorageAvailabilityUnit =
| "days"
| "months"
| "years"
| "minutes"
| "hours";
export type StorageAvailabilityValue = {
value: number;
unit: StorageAvailabilityUnit;
};
export type AvailabilityUnit =
| "days"
| "months"
| "years"
| "minutes"
| "hours";
export type StorageRequest = {
cid: string;
availability: number;
availabilityUnit: AvailabilityUnit;
tolerance: number;
proofProbability: number;
nodes: number;

View File

@ -29,7 +29,7 @@ export function useStorageRequestMutation(
// }
WebStorage.delete("storage-request-step");
WebStorage.delete("storage-request");
WebStorage.delete("storage-request-2");
setError(null);

12
src/utils/bytes.test.ts Normal file
View File

@ -0,0 +1,12 @@
import { assert, describe, it } from "vitest";
import { Bytes } from "./bytes";
import { GB } from "./constants";
describe("bytes", () => {
it("display the bytes", async () => {
assert.equal(Bytes.pretty(0), "0 B");
assert.equal(Bytes.pretty(512), "512.0 B");
assert.equal(Bytes.pretty(1025), "1.0 KB");
assert.equal(Bytes.pretty(GB), "1.0 GB");
});
})

View File

@ -1,14 +1,14 @@
export const Bytes = {
pretty(bytes: number) {
const sizes = ["bytes", "KB", "MB", "GB", "TB"];
const sizes = ["B", "KB", "MB", "GB", "TB"];
if (bytes == 0) {
return "0 b";
return "0 B";
}
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
if (i == 0) {
return bytes + " " + sizes[i];
return bytes.toFixed(1) + " " + sizes[i];
}
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + sizes[i];

28
src/utils/times.test.ts Normal file
View File

@ -0,0 +1,28 @@
import { assert, describe, it } from "vitest";
import { Times } from "./times";
describe("times", () => {
it("display the times", async () => {
assert.equal(Times.pretty(0), "0 second");
assert.equal(Times.pretty(2), "2 seconds");
assert.equal(Times.pretty(60), "1 minute");
assert.equal(Times.pretty(90), "1.5 minutes");
assert.equal(Times.pretty(3600), "1 hour");
assert.equal(Times.pretty(3600 * 2), "2 hours");
assert.equal(Times.pretty(3600 * 24), "1 day");
assert.equal(Times.pretty(3600 * 36), "1.5 days");
assert.equal(Times.pretty(3600 * 24 * 30), "1 month");
});
it("guess the time unit", async () => {
assert.equal(Times.unit(0), "hours");
assert.equal(Times.unit(3600 * 24), "days");
assert.equal(Times.unit(3600 * 24 * 30), "months");
})
it("get the seconds for a time unit given", async () => {
assert.equal(Times.value("hours"), 3600);
assert.equal(Times.value("days"), 3600 * 24);
assert.equal(Times.value("months"), 3600 * 24 * 30);
})
})

View File

@ -6,31 +6,33 @@ export type TimesUnit =
| "hours"
| "seconds";
const plural = (value: number, unit: TimesUnit) =>
value > 1 ? value + ` ${unit}` : value + ` ${unit.slice(0, -1)}`;
const plural = (value: number, unit: TimesUnit) => {
const val = Number.isInteger(value) ? value : value.toFixed(1)
return value > 1 ? val + ` ${unit}` : val + ` ${unit.slice(0, -1)}`;
}
export const Times = {
toSeconds(value: number, unit: TimesUnit) {
let seconds = value;
let val = value;
/* eslint-disable no-fallthrough */
switch (unit) {
// @ts-expect-error - We don't want to break
case "years":
seconds *= 365;
val *= 365;
// @ts-expect-error - We don't want to break
case "months":
seconds *= 30;
val *= 30;
// @ts-expect-error - We don't want to break
case "days":
seconds *= 24;
val *= 24;
// @ts-expect-error - We don't want to break
case "hours":
seconds *= 60;
val *= 60;
case "minutes":
seconds *= 60;
val *= 60;
}
return seconds;
return val;
},
pretty(value: number) {
@ -62,7 +64,7 @@ export const Times = {
return plural(value, "seconds");
},
unit(value: number) {
unit(value: number): "months" | "days" | "hours" {
let seconds = 30 * 24 * 60 * 60;
if (value >= seconds) {
@ -77,7 +79,7 @@ export const Times = {
return "hours"
},
unitValue(unit: "hours" | "days" | "months") {
value(unit: "hours" | "days" | "months") {
switch (unit) {
case "months": {
return 30 * 24 * 60 * 60
@ -90,4 +92,5 @@ export const Times = {
}
}
}
};