Merge pull request #24 from codex-storage/bug/fix-node-space-allocation-size-calculation

Bug/fix node space allocation size calculation
This commit is contained in:
Arnaud 2024-09-26 16:07:45 +02:00 committed by GitHub
commit a7349c79ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 297 additions and 213 deletions

View File

@ -1,6 +1,5 @@
import { CodexNodeSpace } from "@codex-storage/sdk-js";
import { AvailabilityState } from "./types";
import { GB, TB } from "../../utils/constants";
import { SpaceAllocation } from "@codex-storage/marketplace-ui-components";
import "./AvailabilitySpaceAllocation.css";
import { availabilityUnit } from "./availability.domain";
@ -24,15 +23,15 @@ export function AvailabilitySpaceAllocation({ availability, space }: Props) {
const spaceData = [
{
title: "Space allocated",
size: allocated,
size: Math.trunc(allocated),
},
{
title: "New space allocation",
size: size,
size: Math.trunc(size),
},
{
title: "Remaining space",
size: remaining,
size: Math.trunc(remaining),
},
];

View File

@ -5,16 +5,15 @@ import { AvailabilityState } from "./types";
export const availabilityUnit = (unit: "gb" | "tb") =>
unit === "gb" ? GB : TB;
export const availabilityMax = (space: CodexNodeSpace, unit: "gb" | "tb") => {
const bytes = availabilityUnit(unit);
return space.quotaMaxBytes / bytes - space.quotaReservedBytes / bytes;
};
export const availabilityMax = (space: CodexNodeSpace) =>
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes;
export const isAvailabilityValid = (
totalSize: string | number,
availability: AvailabilityState,
max: number
) => {
const size = parseFloat(totalSize.toString());
const unit = availabilityUnit(availability.totalSizeUnit);
const size = parseFloat(availability.totalSize.toString()) * unit;
return size > 0 && size <= max;
};

View File

@ -1,100 +0,0 @@
import {
SpaceAllocation,
Spinner,
} from "@codex-storage/marketplace-ui-components";
import { useQuery } from "@tanstack/react-query";
import { Promises } from "../../utils/promises";
import { AvailabilityCreate } from "../Availability/AvailabilityCreate";
import { CodexSdk } from "../../sdk/codex";
import { Strings } from "../../utils/strings";
import { AvailabilitiesTable } from "../Availability/AvailabilitiesTable";
import "./Availabilities.css";
const defaultSpace = {
quotaMaxBytes: 0,
quotaReservedBytes: 0,
quotaUsedBytes: 0,
totalBlocks: 0,
};
export function Availabilities() {
{
// Error will be catched in ErrorBounday
const { data: availabilities = [], isPending } = useQuery({
queryFn: () =>
CodexSdk.marketplace
.availabilities()
.then((s) => Promises.rejectOnError(s))
.then((res) => res.sort((a, b) => b.totalSize - a.totalSize)),
queryKey: ["availabilities"],
initialData: [],
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
});
// Error will be catched in ErrorBounday
const { data: space = defaultSpace } = useQuery({
queryFn: () =>
CodexSdk.data.space().then((s) => Promises.rejectOnError(s)),
queryKey: ["space"],
initialData: defaultSpace,
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
});
const allocation = availabilities
.map((a) => ({
title: Strings.shortId(a.id),
size: a.totalSize,
}))
.slice(0, 6);
return (
<div className="container">
<div className="availabilities-content">
{isPending ? (
<div className="purchases-loader">
<Spinner width="3rem" />
</div>
) : (
<div className="availabilities-table">
<AvailabilitiesTable
// onEdit={onOpen}
availabilities={availabilities}
/>
</div>
)}
<div className="availabilities-space">
<div>
<SpaceAllocation data={allocation} />
</div>
<div>
<AvailabilityCreate space={space} />
</div>
</div>
</div>
</div>
);
}
}

View File

@ -1,5 +1,5 @@
@media (min-width: 801px) {
.storageRequest-stepper {
.storageRequestCreate {
min-width: 700px;
}
}

View File

@ -1,4 +1,4 @@
import { StorageRequestFileChooser } from "../../components/StorageRequestSetup/StorageRequestFileChooser";
import { StorageRequestFileChooser } from "./StorageRequestFileChooser";
import { useEffect, useRef, useState } from "react";
import { WebStorage } from "../../utils/web-storage";
import { STEPPER_DURATION } from "../../utils/constants";
@ -10,11 +10,11 @@ import {
Stepper,
useStepperReducer,
} from "@codex-storage/marketplace-ui-components";
import { StorageRequestDone } from "./StorageRequestDone";
import { StorageRequestSuccess } from "./StorageRequestSuccess";
import { Times } from "../../utils/times";
import { useStorageRequestMutation } from "./useStorageRequestMutation";
import { Plus } from "lucide-react";
import "./StorageRequestStepper.css";
import "./StorageRequestCreate.css";
import { StorageRequestError } from "./StorageRequestError";
const CONFIRM_STATE = 2;
@ -31,7 +31,7 @@ const defaultStorageRequest: StorageRequest = {
expiration: 300,
};
export function StorageRequestStepper() {
export function StorageRequestCreate() {
const [storageRequest, setStorageRequest] = useState<StorageRequest>(
defaultStorageRequest
);
@ -60,7 +60,7 @@ export function StorageRequestStepper() {
const components = [
StorageRequestFileChooser,
StorageRequestReview,
error ? StorageRequestError : StorageRequestDone,
error ? StorageRequestError : StorageRequestSuccess,
];
const onNextStep = async (step: number) => {
@ -133,7 +133,7 @@ export function StorageRequestStepper() {
duration={STEPPER_DURATION}
onNextStep={onNextStep}
backLabel={backLabel}
className="storageRequest-stepper"
className="storageRequestCreate"
nextLabel={nextLabel}>
<Body
dispatch={dispatch}

View File

@ -36,13 +36,7 @@ export function StorageRequestFileChooser({
onStorageRequestChange({ cid: value });
};
const onSuccess = (data: string, file: File) => {
// TODO Move this to proxy object
WebStorage.set(data, {
type: file.type,
name: file.name,
});
const onSuccess = (data: string) => {
onStorageRequestChange({ cid: data });
};

View File

@ -1,11 +1,12 @@
import { Placeholder } from "@codex-storage/marketplace-ui-components";
import { CircleCheck } from "lucide-react";
import "./StorageRequestDone.css";
import "./StorageRequestSuccess.css";
import { StorageRequestComponentProps } from "./types";
import { useEffect } from "react";
// TODO rename
export function StorageRequestDone({ dispatch }: StorageRequestComponentProps) {
export function StorageRequestSuccess({
dispatch,
}: StorageRequestComponentProps) {
useEffect(() => {
dispatch({
type: "toggle-buttons",

View File

@ -1,76 +1,28 @@
import { useQuery } from "@tanstack/react-query";
import { FilesStorage } from "../utils/file-storage";
import { CodexSdk } from "../sdk/codex";
import * as Sentry from "@sentry/browser";
import { CodexDataContent } from "@codex-storage/sdk-js";
import { CodexDataResponse } from "@codex-storage/sdk-js";
import { Promises } from "../utils/promises";
export function useData() {
const { data = [] } = useQuery({
queryFn: (): Promise<CodexDataContent[]> => {
// TODO refactor
return Promise.resolve().then(async () => {
const res = await CodexSdk.data.cids();
const { data = { content: [] } satisfies CodexDataResponse } =
useQuery<CodexDataResponse>({
queryFn: (_) =>
CodexSdk.data.cids().then((res) => Promises.rejectOnError(res)),
queryKey: ["cids"],
if (res.error) {
if (import.meta.env.PROD) {
Sentry.captureException(res.data);
}
return [];
}
initialData: { content: [] } satisfies CodexDataResponse,
const metadata = await FilesStorage.list();
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
return res.data.content.map((content, index) => {
if (content.manifest.filename) {
return content;
}
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
const value = metadata.find(([cid]) => content.cid === cid);
// Don't expect something new when coming back to the UI
refetchOnWindowFocus: false,
});
if (!value) {
return {
cid: content.cid,
manifest: {
...content.manifest,
mimetype: "N/A",
uploadedAt: new Date(0, 0, 0, 0, 0, 0).toJSON(),
filename: "N/A" + index,
},
};
}
const {
mimetype = "",
name = "",
uploadedAt = new Date(0, 0, 0, 0, 0, 0).toJSON(),
} = value[1];
return {
cid: content.cid,
manifest: {
...content.manifest,
mimetype,
filename: name,
uploadedAt: uploadedAt,
},
};
});
});
},
queryKey: ["cids"],
initialData: [],
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Don't expect something new when coming back to the UI
refetchOnWindowFocus: false,
});
return data;
return data.content;
}

147
src/proxy.ts Normal file
View File

@ -0,0 +1,147 @@
import {
CodexData,
CodexDataResponse,
CodexMarketplace,
SafeValue,
UploadResponse,
} from "@codex-storage/sdk-js";
import { CodexSdk as Sdk } from "./sdk/codex";
import { Promises } from "./utils/promises";
import { WebStorage } from "./utils/web-storage";
import * as Sentry from "@sentry/browser";
import { FilesStorage } from "./utils/file-storage";
class CodexDataMock extends CodexData {
override upload(
file: File,
onProgress?: (loaded: number, total: number) => void
): Promise<UploadResponse> {
const res = super.upload(file, onProgress);
return res.then(({ result, abort }) => {
return {
abort,
result: result.then((safe) => {
if (!safe.error) {
return WebStorage.set(safe.data, {
type: file.type,
name: file.name,
}).then(() => safe);
}
return safe;
}),
};
});
}
override async cids(): Promise<SafeValue<CodexDataResponse>> {
const res = await super.cids();
if (res.error) {
return res;
}
const metadata = await FilesStorage.list();
const content = res.data.content.map((content, index) => {
if (content.manifest.filename) {
return content;
}
const value = metadata.find(([cid]) => content.cid === cid);
if (!value) {
return {
cid: content.cid,
manifest: {
...content.manifest,
mimetype: "N/A",
uploadedAt: new Date(0, 0, 0, 0, 0, 0).toJSON(),
filename: "N/A" + index,
},
};
}
const {
mimetype = "",
name = "",
uploadedAt = new Date(0, 0, 0, 0, 0, 0).toJSON(),
} = value[1];
return {
cid: content.cid,
manifest: {
...content.manifest,
mimetype,
filename: name,
uploadedAt: uploadedAt,
},
};
});
return { error: false, data: { content } };
}
}
class CodexMarketplaceMock extends CodexMarketplace {
// override createStorageRequest(
// input: CodexCreateStorageRequestInput
// ): Promise<SafeValue<string>> {
// return Promise.resolve({
// error: true,
// data: {
// message: "C'est balo",
// },
// });
// }
// override createAvailability(): Promise<
// SafeValue<CodexAvailabilityCreateResponse>
// > {
// return Promise.resolve({
// error: true,
// data: {
// message: "C'est balo",
// },
// });
// }
// override reservations(): Promise<SafeValue<CodexReservation[]>> {
// return Promise.resolve({
// error: false,
// data: [
// {
// id: "0x123456789",
// availabilityId: "0x12345678910",
// requestId: "0x1234567891011",
// /**
// * Size in bytes
// */
// size: 500_000_000 + "",
// /**
// * Slot Index as hexadecimal string
// */
// slotIndex: "2",
// },
// {
// id: "0x987654321",
// availabilityId: "0x9876543210",
// requestId: "0x98765432100",
// /**
// * Size in bytes
// */
// size: 500_000_000 + "",
// /**
// * Slot Index as hexadecimal string
// */
// slotIndex: "1",
// },
// ],
// });
// }
}
export const CodexSdk = {
...Sdk,
marketplace: new CodexMarketplaceMock(import.meta.env.VITE_CODEX_API_URL),
data: new CodexDataMock(import.meta.env.VITE_CODEX_API_URL),
};

View File

@ -1,7 +1,106 @@
import { createFileRoute } from "@tanstack/react-router";
import { Availabilities } from "../../components/Availailibities/Availabilities";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
import {
SpaceAllocation,
Spinner,
} from "@codex-storage/marketplace-ui-components";
import { useQuery } from "@tanstack/react-query";
import { Promises } from "../../utils/promises";
import { CodexSdk } from "../../sdk/codex";
import { Strings } from "../../utils/strings";
import "./availabilities.css";
import { AvailabilitiesTable } from "../../components/Availability/AvailabilitiesTable";
import { AvailabilityCreate } from "../../components/Availability/AvailabilityCreate";
const defaultSpace = {
quotaMaxBytes: 0,
quotaReservedBytes: 0,
quotaUsedBytes: 0,
totalBlocks: 0,
};
export function Availabilities() {
{
// Error will be catched in ErrorBounday
const { data: availabilities = [], isPending } = useQuery({
queryFn: () =>
CodexSdk.marketplace
.availabilities()
.then((s) => Promises.rejectOnError(s))
.then((res) => res.sort((a, b) => b.totalSize - a.totalSize)),
queryKey: ["availabilities"],
initialData: [],
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
});
// Error will be catched in ErrorBounday
const { data: space = defaultSpace } = useQuery({
queryFn: () =>
CodexSdk.data.space().then((s) => Promises.rejectOnError(s)),
queryKey: ["space"],
initialData: defaultSpace,
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
});
const allocation = availabilities
.map((a) => ({
title: Strings.shortId(a.id),
size: a.totalSize,
}))
.slice(0, 6);
return (
<div className="container">
<div className="availabilities-content">
{isPending ? (
<div className="purchases-loader">
<Spinner width="3rem" />
</div>
) : (
<div className="availabilities-table">
<AvailabilitiesTable
// onEdit={onOpen}
availabilities={availabilities}
/>
</div>
)}
<div className="availabilities-space">
<div>
<SpaceAllocation data={allocation} />
</div>
<div>
<AvailabilityCreate space={space} />
</div>
</div>
</div>
</div>
);
}
}
export const Route = createFileRoute("/dashboard/availabilities")({
component: () => (

View File

@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { CodexSdk } from "../../sdk/codex";
import { Cell, Spinner, Table } from "@codex-storage/marketplace-ui-components";
import { StorageRequestStepper } from "../../components/StorageRequestSetup/StorageRequestStepper";
import { StorageRequestCreate } from "../../components/StorageRequestSetup/StorageRequestCreate";
import "./purchases.css";
import { FileCell } from "../../components/FileCellRender/FileCell";
import { CustomStateCellRender } from "../../components/CustomStateCellRender/CustomStateCellRender";
@ -66,12 +66,10 @@ const Purchases = () => {
];
}) || [];
// TODO make name uniforms
return (
<div className="container">
<div className="purchases-actions">
<StorageRequestStepper />
<StorageRequestCreate />
</div>
<Table headers={headers} cells={cells} />
@ -79,8 +77,6 @@ const Purchases = () => {
);
};
// TODO make uniforms for availabilities
export const Route = createFileRoute("/dashboard/purchases")({
component: () => (
<ErrorBoundary card={true}>

View File

@ -23,20 +23,11 @@ export const CodexSdk = {
return WebStorage.set("codex-node-url", url);
},
// TODO Change this
get debug() {
return client.debug;
},
debug: client.debug,
get data() {
return client.data;
},
data: client.data,
get node() {
return client.node;
},
node: client.node,
get marketplace() {
return client.marketplace;
},
marketplace: client.marketplace,
};

View File

@ -17,4 +17,10 @@ export default defineConfig({
},
},
},
resolve: {
alias: {
"../sdk/codex": "../proxy",
"../../sdk/codex": "../../proxy",
},
},
});