mirror of
https://github.com/codex-storage/codex-marketplace-ui.git
synced 2025-02-23 21:28:26 +00:00
Move to sunburst component and inprove table
This commit is contained in:
parent
c8411eb96a
commit
9fb5892212
1222
package-lock.json
generated
1222
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,12 +23,15 @@
|
||||
"React"
|
||||
],
|
||||
"dependencies": {
|
||||
"@codex-storage/marketplace-ui-components": "0.0.14",
|
||||
"@codex-storage/marketplace-ui-components": "0.0.15",
|
||||
"@codex-storage/sdk-js": "0.0.6",
|
||||
"@nivo/sunburst": "^0.87.0",
|
||||
"@sentry/browser": "^8.32.0",
|
||||
"@sentry/react": "^8.31.0",
|
||||
"@tanstack/react-query": "^5.51.15",
|
||||
"@tanstack/react-router": "^1.58.7",
|
||||
"chart.js": "^4.4.4",
|
||||
"echarts": "^5.5.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lucide-react": "^0.445.0",
|
||||
"react": "^18.3.1",
|
||||
@ -46,6 +49,7 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"prettier": "^3.3.3",
|
||||
"sass-embedded": "^1.79.4",
|
||||
"typescript": "5.5.4",
|
||||
"vite": "^5.4.7"
|
||||
},
|
||||
|
8
src/components/Availability/AvailabilitiesTable.css
Normal file
8
src/components/Availability/AvailabilitiesTable.css
Normal file
@ -0,0 +1,8 @@
|
||||
.availabilityTable-chevron {
|
||||
cursor: pointer;
|
||||
transition: transform 0.35s;
|
||||
}
|
||||
|
||||
.availabilityTable-chevron--open {
|
||||
transform: rotate(180deg);
|
||||
}
|
@ -1,22 +1,33 @@
|
||||
import { Cell, Table } from "@codex-storage/marketplace-ui-components";
|
||||
import { TruncateCell } from "../TruncateCell/TruncateCell";
|
||||
import { Cell, Row, Table } from "@codex-storage/marketplace-ui-components";
|
||||
import { PrettyBytes } from "../../utils/bytes";
|
||||
import { AvailabilityActionsCell } from "./AvailabilityActionsCell";
|
||||
import { CodexAvailability } from "@codex-storage/sdk-js/async";
|
||||
import { CodexAvailability, CodexNodeSpace } from "@codex-storage/sdk-js/async";
|
||||
import { Times } from "../../utils/times";
|
||||
import { useState } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { AvailabilityReservations } from "./AvailabilityReservations";
|
||||
import { AvailabilityIdCell } from "./AvailabilityIdCell";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import "./AvailabilitiesTable.css";
|
||||
import { Arrays } from "../../utils/arrays";
|
||||
import { AvailabilitySlotRow } from "./AvailabilitySlotRow";
|
||||
import { classnames } from "../../utils/classnames";
|
||||
import { AvailabilityWithSlots } from "./types";
|
||||
import { AvailabilityDiskRow } from "./AvailabilityDiskRow";
|
||||
|
||||
type Props = {
|
||||
// onEdit: () => void;
|
||||
availabilities: CodexAvailability[];
|
||||
space: CodexNodeSpace;
|
||||
availabilities: AvailabilityWithSlots[];
|
||||
};
|
||||
|
||||
export function AvailabilitiesTable({ availabilities }: Props) {
|
||||
export function AvailabilitiesTable({ availabilities, space }: Props) {
|
||||
const [availability, setAvailability] = useState<CodexAvailability | null>(
|
||||
null
|
||||
);
|
||||
const [details, setDetails] = useState<string[]>([]);
|
||||
|
||||
const headers = [
|
||||
"",
|
||||
"id",
|
||||
"total size",
|
||||
"duration",
|
||||
@ -25,28 +36,56 @@ export function AvailabilitiesTable({ availabilities }: Props) {
|
||||
"actions",
|
||||
];
|
||||
|
||||
const onReservationsShow = (a: CodexAvailability) => setAvailability(a);
|
||||
|
||||
const onReservationsClose = () => setAvailability(null);
|
||||
|
||||
const cells =
|
||||
availabilities.map((a) => {
|
||||
return [
|
||||
<TruncateCell value={a.id} />,
|
||||
<Cell value={PrettyBytes(a.totalSize)} />,
|
||||
<Cell value={Times.pretty(a.duration)} />,
|
||||
<Cell value={a.minPrice.toString()} />,
|
||||
<Cell value={a.maxCollateral.toString()} />,
|
||||
<AvailabilityActionsCell
|
||||
availability={a}
|
||||
onReservationsShow={onReservationsShow}
|
||||
/>,
|
||||
];
|
||||
}) || [];
|
||||
const rows = availabilities.map((a, index) => {
|
||||
const showDetails = details.includes(a.id);
|
||||
|
||||
const onShowDetails = () => setDetails(Arrays.toggle(details, a.id));
|
||||
const hasSlots = a.slots.length > 0;
|
||||
|
||||
return (
|
||||
<Fragment key={a.id + a.duration}>
|
||||
<Row
|
||||
cells={[
|
||||
<Cell>
|
||||
{hasSlots ? (
|
||||
<ChevronDown
|
||||
className={classnames(
|
||||
["availabilityTable-chevron"],
|
||||
["availabilityTable-chevron--open", showDetails]
|
||||
)}
|
||||
onClick={onShowDetails}></ChevronDown>
|
||||
) : (
|
||||
<span></span>
|
||||
)}
|
||||
</Cell>,
|
||||
<AvailabilityIdCell value={a} index={index} />,
|
||||
<Cell>{PrettyBytes(a.totalSize)}</Cell>,
|
||||
<Cell>{Times.pretty(a.duration)}</Cell>,
|
||||
<Cell>{a.minPrice.toString()}</Cell>,
|
||||
<Cell>{a.maxCollateral.toString()}</Cell>,
|
||||
<AvailabilityActionsCell availability={a} />,
|
||||
]}></Row>
|
||||
|
||||
{a.slots.map((slot) => (
|
||||
<AvailabilitySlotRow
|
||||
key={slot.id}
|
||||
active={showDetails}
|
||||
bytes={parseFloat(slot.size)}
|
||||
id={slot.id}></AvailabilitySlotRow>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
rows.unshift(
|
||||
<AvailabilityDiskRow bytes={space.quotaMaxBytes}></AvailabilityDiskRow>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table headers={headers} cells={cells} />
|
||||
<Table headers={headers} rows={rows} />
|
||||
<AvailabilityReservations
|
||||
availability={availability}
|
||||
onClose={onReservationsClose}
|
||||
|
@ -1,18 +1,15 @@
|
||||
import { StretchHorizontal } from "lucide-react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import "./AvailabilityActionsCell.css";
|
||||
import { CodexAvailability } from "@codex-storage/sdk-js/async";
|
||||
import { Cell } from "@codex-storage/marketplace-ui-components";
|
||||
|
||||
type Props = {
|
||||
availability: CodexAvailability;
|
||||
// onEdit: () => void;
|
||||
onReservationsShow: (availability: CodexAvailability) => void;
|
||||
};
|
||||
|
||||
export function AvailabilityActionsCell({
|
||||
availability,
|
||||
// onEdit,
|
||||
onReservationsShow,
|
||||
}: Props) {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export function AvailabilityActionsCell(_: Props) {
|
||||
// const onEditClick = async () => {
|
||||
// const unit = availability.totalSize >= 1_000_000_000 ? "gb" : "mb";
|
||||
// const totalSize =
|
||||
@ -29,23 +26,20 @@ export function AvailabilityActionsCell({
|
||||
// onEdit();
|
||||
// };
|
||||
|
||||
const onReservationsClick = () => onReservationsShow(availability);
|
||||
|
||||
return (
|
||||
<div className="availability-actions">
|
||||
{/* <a
|
||||
<Cell>
|
||||
<div className="availability-actions">
|
||||
{/* <a
|
||||
className="cell--action availability-action"
|
||||
title="Edit"
|
||||
onClick={onEditClick}>
|
||||
<Pen width={"1.25rem"} />
|
||||
</a> */}
|
||||
|
||||
<a
|
||||
className="cell--action availability-action"
|
||||
title="Reservations"
|
||||
onClick={onReservationsClick}>
|
||||
<StretchHorizontal width={"1.25rem"} />
|
||||
</a>
|
||||
</div>
|
||||
<a className="cell--action availability-action" title="Reservations">
|
||||
<Pencil width={"1.25rem"} />
|
||||
</a>
|
||||
</div>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
4
src/components/Availability/AvailabilityContext.ts
Normal file
4
src/components/Availability/AvailabilityContext.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { createContext } from "react";
|
||||
import { AvailabilityWithSlots } from "./types";
|
||||
|
||||
export const AvailabilityContext = createContext<AvailabilityWithSlots | null>(null);
|
10
src/components/Availability/AvailabilityDiskRow.css
Normal file
10
src/components/Availability/AvailabilityDiskRow.css
Normal file
@ -0,0 +1,10 @@
|
||||
.availabilityDiskRow {
|
||||
border-bottom: 5px solid var(--codex-border-color);
|
||||
background-color: var(--codex-background-light);
|
||||
}
|
||||
|
||||
.availabilityDiskRow-cell-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
74
src/components/Availability/AvailabilityDiskRow.tsx
Normal file
74
src/components/Availability/AvailabilityDiskRow.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import {
|
||||
Cell,
|
||||
Row,
|
||||
SimpleText,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { PrettyBytes } from "../../utils/bytes";
|
||||
import "./AvailabilityDiskRow.css";
|
||||
import { classnames } from "../../utils/classnames";
|
||||
|
||||
type Props = {
|
||||
bytes: number;
|
||||
};
|
||||
|
||||
export function AvailabilityDiskRow({ bytes }: Props) {
|
||||
return (
|
||||
<Row
|
||||
className={classnames(["availabilityDiskRow"])}
|
||||
cells={[
|
||||
<Cell className=" availabilityDiskRow-cell">
|
||||
<span></span>
|
||||
</Cell>,
|
||||
<Cell colSpan={6} className={classnames([" availabilityDiskRow-cell"])}>
|
||||
<div className={classnames(["availabilityDiskRow-cell-content"])}>
|
||||
<HardDrive />
|
||||
<div>
|
||||
<div>
|
||||
<b>Node</b>
|
||||
</div>
|
||||
<SimpleText size="small" variant="light">
|
||||
{PrettyBytes(bytes)} allocated for the node
|
||||
</SimpleText>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>,
|
||||
]}></Row>
|
||||
);
|
||||
}
|
||||
|
||||
const HardDrive = () => (
|
||||
<svg
|
||||
width="30"
|
||||
viewBox="0 0 60 80"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M55 0H5C2.23858 0 0 2.23858 0 5V75C0 77.7614 2.23858 80 5 80H55C57.7614 80 60 77.7614 60 75V5C60 2.23858 57.7614 0 55 0Z"
|
||||
fill="#46484C"
|
||||
/>
|
||||
<path
|
||||
d="M30 60C43.8071 60 55 48.8071 55 35C55 21.1929 43.8071 10 30 10C16.1929 10 5 21.1929 5 35C5 48.8071 16.1929 60 30 60Z"
|
||||
fill="#9494D1"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 10C8.88071 10 10 8.88071 10 7.5C10 6.11929 8.88071 5 7.5 5C6.11929 5 5 6.11929 5 7.5C5 8.88071 6.11929 10 7.5 10Z"
|
||||
fill="#95989D"
|
||||
/>
|
||||
<path
|
||||
d="M52.5 10C53.8807 10 55 8.88071 55 7.5C55 6.11929 53.8807 5 52.5 5C51.1193 5 50 6.11929 50 7.5C50 8.88071 51.1193 10 52.5 10Z"
|
||||
fill="#95989D"
|
||||
/>
|
||||
<path
|
||||
d="M52.5 75C53.8807 75 55 73.8807 55 72.5C55 71.1193 53.8807 70 52.5 70C51.1193 70 50 71.1193 50 72.5C50 73.8807 51.1193 75 52.5 75Z"
|
||||
fill="#95989D"
|
||||
/>
|
||||
<path
|
||||
d="M30 40C32.7614 40 35 37.7614 35 35C35 32.2386 32.7614 30 30 30C27.2386 30 25 32.2386 25 35C25 37.7614 27.2386 40 30 40Z"
|
||||
fill="#46484C"
|
||||
/>
|
||||
<path
|
||||
d="M28.0697 41.4744C29.1749 42.165 29.4965 43.8048 28.8334 45.3439L19.8787 68.3287C19.7944 68.5884 19.6948 68.8452 19.5795 69.0978L19.4531 69.4169L19.4313 69.4035C19.3383 69.5843 19.2371 69.7626 19.1275 69.938C16.9697 73.3912 12.3729 74.4111 8.86014 72.2161C5.34741 70.0211 4.24902 65.4424 6.40681 61.9892C6.51642 61.8138 6.63231 61.6447 6.75405 61.4819L6.7324 61.4681L6.94941 61.2322C7.13484 61.0056 7.33205 60.7926 7.53964 60.5934L24.2571 42.4843C25.3497 41.2136 26.9645 40.7838 28.0697 41.4744Z"
|
||||
fill="#CDCED0"
|
||||
/>
|
||||
</svg>
|
||||
);
|
186
src/components/Availability/AvailabilityEdit.tsx
Normal file
186
src/components/Availability/AvailabilityEdit.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import {
|
||||
Stepper,
|
||||
useStepperReducer,
|
||||
Button,
|
||||
Modal,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AvailabilityForm } from "./AvailabilityForm";
|
||||
import { Pencil, Plus } from "lucide-react";
|
||||
import { CodexNodeSpace } from "@codex-storage/sdk-js";
|
||||
import { AvailabilityConfirm } from "./AvailabilityConfirmation";
|
||||
import { WebStorage } from "../../utils/web-storage";
|
||||
import { AvailabilityState } from "./types";
|
||||
import { STEPPER_DURATION } from "../../utils/constants";
|
||||
import { useAvailabilityMutation } from "./useAvailabilityMutation";
|
||||
import { AvailabilitySuccess } from "./AvailabilitySuccess";
|
||||
import { AvailabilityError } from "./AvailabilityError";
|
||||
import "./AvailabilityEdit.css";
|
||||
|
||||
type Props = {
|
||||
space: CodexNodeSpace;
|
||||
hasLabel?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CONFIRM_STATE = 2;
|
||||
|
||||
const defaultAvailabilityData: AvailabilityState = {
|
||||
totalSize: 1,
|
||||
duration: 1,
|
||||
minPrice: 0,
|
||||
maxCollateral: 0,
|
||||
totalSizeUnit: "gb",
|
||||
durationUnit: "days",
|
||||
};
|
||||
|
||||
export function AvailabilityEdit({
|
||||
space,
|
||||
className = "",
|
||||
hasLabel = true,
|
||||
}: Props) {
|
||||
const steps = useRef(["Sale", "Confirmation", "Success"]);
|
||||
const [availability, setAvailability] = useState<AvailabilityState>(
|
||||
defaultAvailabilityData
|
||||
);
|
||||
const { state, dispatch } = useStepperReducer();
|
||||
const { mutateAsync, error } = useAvailabilityMutation(dispatch, state);
|
||||
const [availabilityId, setAvailabilityId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
WebStorage.get<number>("availability-step"),
|
||||
WebStorage.get<AvailabilityState>("availability"),
|
||||
]).then(([s, a]) => {
|
||||
if (s) {
|
||||
dispatch({
|
||||
type: "next",
|
||||
step: s,
|
||||
});
|
||||
}
|
||||
|
||||
if (a) {
|
||||
setAvailability(a);
|
||||
}
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
// We use a custom event to not re render the sunburst component
|
||||
useEffect(() => {
|
||||
const onAvailabilityIdChange = (e: Event) => {
|
||||
const custom = e as CustomEvent;
|
||||
setAvailabilityId(custom.detail);
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
"codexavailabilityid",
|
||||
onAvailabilityIdChange,
|
||||
false
|
||||
);
|
||||
|
||||
return () =>
|
||||
document.removeEventListener(
|
||||
"codexavailabilityid",
|
||||
onAvailabilityIdChange
|
||||
);
|
||||
}, []);
|
||||
|
||||
const components = [
|
||||
AvailabilityForm,
|
||||
AvailabilityConfirm,
|
||||
error ? AvailabilityError : AvailabilitySuccess,
|
||||
];
|
||||
|
||||
const onNextStep = async (step: number) => {
|
||||
if (step === components.length) {
|
||||
setAvailability(defaultAvailabilityData);
|
||||
|
||||
dispatch({
|
||||
step: 0,
|
||||
type: "next",
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: "close",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
WebStorage.set("availability-step", step);
|
||||
|
||||
if (step == CONFIRM_STATE) {
|
||||
mutateAsync(availability);
|
||||
} else {
|
||||
dispatch({
|
||||
step,
|
||||
type: "next",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onAvailabilityChange = (data: Partial<AvailabilityState>) => {
|
||||
const val = { ...availability, ...data };
|
||||
|
||||
WebStorage.set("availability", val);
|
||||
|
||||
setAvailability(val);
|
||||
};
|
||||
|
||||
const onOpen = () => {
|
||||
if (availability.id) {
|
||||
WebStorage.set("availability-step", 0);
|
||||
WebStorage.set("availability", defaultAvailabilityData);
|
||||
|
||||
setAvailability(defaultAvailabilityData);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "open",
|
||||
});
|
||||
|
||||
dispatch({
|
||||
step: 0,
|
||||
type: "next",
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = () => dispatch({ type: "close" });
|
||||
|
||||
const Body = components[state.step] || (() => <span />);
|
||||
const backLabel = state.step ? "Back" : "Close";
|
||||
const nextLabel = state.step === steps.current.length - 1 ? "Finish" : "Next";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={hasLabel ? "Sale" : ""}
|
||||
Icon={!availabilityId ? Plus : Pencil}
|
||||
onClick={onOpen}
|
||||
variant="primary"
|
||||
className={className}
|
||||
/>
|
||||
|
||||
<Modal open={state.open} onClose={onClose} displayCloseButton={false}>
|
||||
<Stepper
|
||||
className="availabilityCreate"
|
||||
titles={steps.current}
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
duration={STEPPER_DURATION}
|
||||
onNextStep={onNextStep}
|
||||
backLabel={backLabel}
|
||||
nextLabel={nextLabel}>
|
||||
<Body
|
||||
dispatch={dispatch}
|
||||
state={state}
|
||||
onAvailabilityChange={onAvailabilityChange}
|
||||
availability={availability}
|
||||
space={space}
|
||||
error={error}
|
||||
/>
|
||||
</Stepper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -17,3 +17,15 @@
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.availabilityForm-itemInput-maxSize {
|
||||
color: var(--codex-color-primary);
|
||||
padding-right: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: 0.35s opacity;
|
||||
}
|
||||
|
||||
.availabilityForm-itemInput-maxSize:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { Input, InputGroup } from "@codex-storage/marketplace-ui-components";
|
||||
import { ChangeEvent, useEffect } from "react";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import "./AvailabilityForm.css";
|
||||
import { AvailabilityComponentProps } from "./types";
|
||||
import { classnames } from "../../utils/classnames";
|
||||
import { AvailabilitySpaceAllocation } from "./AvailabilitySpaceAllocation";
|
||||
import { availabilityMax, isAvailabilityValid } from "./availability.domain";
|
||||
import {
|
||||
availabilityMax,
|
||||
availabilityUnit,
|
||||
isAvailabilityValid,
|
||||
} from "./availability.domain";
|
||||
|
||||
export function AvailabilityForm({
|
||||
dispatch,
|
||||
@ -12,6 +16,12 @@ export function AvailabilityForm({
|
||||
availability,
|
||||
space,
|
||||
}: AvailabilityComponentProps) {
|
||||
const [availabilityValue, setAvailabilityValue] = useState(
|
||||
(
|
||||
availability.totalSize / availabilityUnit(availability.totalSizeUnit)
|
||||
).toFixed(2)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const max = availabilityMax(space);
|
||||
const isValid = isAvailabilityValid(availability, max);
|
||||
@ -44,9 +54,12 @@ export function AvailabilityForm({
|
||||
const onAvailablityChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const element = e.currentTarget;
|
||||
const v = element.value;
|
||||
const unit = availabilityUnit(availability.totalSizeUnit);
|
||||
|
||||
setAvailabilityValue(v);
|
||||
|
||||
onAvailabilityChange({
|
||||
[element.name]: v,
|
||||
[element.name]: parseFloat(v) * unit,
|
||||
});
|
||||
};
|
||||
|
||||
@ -54,13 +67,31 @@ export function AvailabilityForm({
|
||||
const element = e.currentTarget;
|
||||
|
||||
onAvailabilityChange({
|
||||
[element.name]: parseFloat(element.value),
|
||||
[element.name]:
|
||||
element.name === "name" ? element.value : parseFloat(element.value),
|
||||
});
|
||||
};
|
||||
|
||||
const max = availabilityMax(space);
|
||||
const isValid = isAvailabilityValid(availability, max);
|
||||
// const domain = new AvailabilityDomain(space, availability);
|
||||
|
||||
const onMaxSize = () => {
|
||||
const available =
|
||||
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes;
|
||||
|
||||
const unit = availabilityUnit(availability.totalSizeUnit);
|
||||
|
||||
setAvailabilityValue((available / unit).toFixed(2));
|
||||
|
||||
onAvailabilityChange({
|
||||
totalSize: available,
|
||||
});
|
||||
};
|
||||
|
||||
const available =
|
||||
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes;
|
||||
const isValid = available >= availability.totalSize;
|
||||
const unit = availabilityUnit(availability.totalSizeUnit);
|
||||
const max = available / unit;
|
||||
const helper = isValid
|
||||
? "Total size of sale's storage in bytes"
|
||||
: "The total size cannot exceed the space available.";
|
||||
@ -84,13 +115,18 @@ export function AvailabilityForm({
|
||||
max={max.toFixed(2)}
|
||||
onChange={onAvailablityChange}
|
||||
onGroupChange={onTotalSizeUnitChange}
|
||||
value={availability.totalSize.toString()}
|
||||
value={availabilityValue}
|
||||
step={"0.01"}
|
||||
group={[
|
||||
["gb", "GB"],
|
||||
["tb", "TB"],
|
||||
]}
|
||||
groupValue={availability.totalSizeUnit}
|
||||
extra={
|
||||
<a onClick={onMaxSize} className="availabilityForm-itemInput-maxSize">
|
||||
Use max size
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="availabilityForm-item">
|
||||
@ -143,6 +179,21 @@ export function AvailabilityForm({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="availabilityForm-item">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="string"
|
||||
label="Nickname"
|
||||
max={9}
|
||||
helper="You can add a custom name to easily retrieve your sale."
|
||||
inputClassName="availabilityForm-itemInput"
|
||||
onChange={onInputChange}
|
||||
value={availability.name?.toString()}
|
||||
maxLength={9}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
5
src/components/Availability/AvailabilityIdCell.css
Normal file
5
src/components/Availability/AvailabilityIdCell.css
Normal file
@ -0,0 +1,5 @@
|
||||
.availabilityIdCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
62
src/components/Availability/AvailabilityIdCell.tsx
Normal file
62
src/components/Availability/AvailabilityIdCell.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import "./AvailabilityIdCell.css";
|
||||
import { Strings } from "../../utils/strings";
|
||||
import { Cell, SimpleText } from "@codex-storage/marketplace-ui-components";
|
||||
import { PrettyBytes } from "../../utils/bytes";
|
||||
import { availabilityColors } from "./availability.colors";
|
||||
import { AvailabilityWithSlots } from "./types";
|
||||
|
||||
type Props = {
|
||||
value: AvailabilityWithSlots;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export function AvailabilityIdCell({ value, index }: Props) {
|
||||
return (
|
||||
<Cell>
|
||||
<div className="availabilityIdCell" id={value.id}>
|
||||
<Folder color={availabilityColors[index]} />
|
||||
<div>
|
||||
<div>
|
||||
<b>{value.name || Strings.shortId(value.id)}</b>
|
||||
</div>
|
||||
<SimpleText size="small" variant="light">
|
||||
{PrettyBytes(value.totalSize)} allocated for the availability
|
||||
</SimpleText>
|
||||
<div>
|
||||
{/* <div>
|
||||
<SimpleText size="small" variant="light">
|
||||
{a.id}
|
||||
</SimpleText>
|
||||
</div> */}
|
||||
<SimpleText size="small" variant="light">
|
||||
Max collateral {value.maxCollateral} | Min price {value.minPrice}
|
||||
</SimpleText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
const Folder = ({ color }: { color: string }) => (
|
||||
<svg
|
||||
width="30"
|
||||
viewBox="0 0 65 60"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M60.9133 4H24.9133C24.9133 1.8 23.1133 0 20.9133 0H4.91333C2.71333 0 0.91333 1.8 0.91333 4V16C0.91333 18.2 2.71333 20 4.91333 20H60.9133C63.1133 20 64.9133 18.2 64.9133 16V8C64.9133 5.8 63.1133 4 60.9133 4Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M56.9133 8H8.91333C6.71333 8 4.91333 9.8 4.91333 12V16C4.91333 18.2 6.71333 20 8.91333 20H56.9133C59.1133 20 60.9133 18.2 60.9133 16V12C60.9133 9.8 59.1133 8 56.9133 8Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M60.9133 12H4.91333C2.71333 12 0.91333 13.8 0.91333 16V56C0.91333 58.2 2.71333 60 4.91333 60H60.9133C63.1133 60 64.9133 58.2 64.9133 56V16C64.9133 13.8 63.1133 12 60.9133 12Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
@ -11,13 +11,14 @@ import { Promises } from "../../utils/promises";
|
||||
import { CodexAvailability } from "@codex-storage/sdk-js";
|
||||
import { useEffect } from "react";
|
||||
import { ErrorPlaceholder } from "../ErrorPlaceholder/ErrorPlaceholder";
|
||||
import { availabilityColors } from "./availability.colors";
|
||||
|
||||
type Props = {
|
||||
availability: CodexAvailability | null;
|
||||
open: boolean;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
// TODO remove this
|
||||
export function AvailabilityReservations({
|
||||
availability,
|
||||
onClose,
|
||||
@ -91,13 +92,15 @@ export function AvailabilityReservations({
|
||||
const totalSize = availability.totalSize;
|
||||
const totalUsed = data.reduce((acc, val) => acc + parseInt(val.size, 10), 0);
|
||||
const spaceData = [
|
||||
...data.map((val) => ({
|
||||
...data.map((val, index) => ({
|
||||
title: val.id,
|
||||
size: parseInt(val.size, 10),
|
||||
color: availabilityColors[index],
|
||||
})),
|
||||
{
|
||||
title: "Availability remaining",
|
||||
size: totalSize - totalUsed,
|
||||
color: availabilityColors[availabilityColors.length - 1],
|
||||
},
|
||||
];
|
||||
const isEmpty = !data.length;
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
Stepper,
|
||||
useStepperReducer,
|
||||
Button,
|
||||
Modal,
|
||||
Sheets,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AvailabilityForm } from "./AvailabilityForm";
|
||||
@ -19,6 +19,8 @@ import "./AvailabilityCreate.css";
|
||||
|
||||
type Props = {
|
||||
space: CodexNodeSpace;
|
||||
hasLabel?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CONFIRM_STATE = 2;
|
||||
@ -32,7 +34,11 @@ const defaultAvailabilityData: AvailabilityState = {
|
||||
durationUnit: "days",
|
||||
};
|
||||
|
||||
export function AvailabilityCreate({ space }: Props) {
|
||||
export function AvailabilitySheetCreate({
|
||||
space,
|
||||
className = "",
|
||||
hasLabel = true,
|
||||
}: Props) {
|
||||
const steps = useRef(["Sale", "Confirmation", "Success"]);
|
||||
const [availability, setAvailability] = useState<AvailabilityState>(
|
||||
defaultAvailabilityData
|
||||
@ -126,9 +132,15 @@ export function AvailabilityCreate({ space }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button label="Sale" Icon={Plus} onClick={onOpen} variant="primary" />
|
||||
<Button
|
||||
label={hasLabel ? "Sale" : ""}
|
||||
Icon={Plus}
|
||||
onClick={onOpen}
|
||||
variant="primary"
|
||||
className={className}
|
||||
/>
|
||||
|
||||
<Modal open={state.open} onClose={onClose} displayCloseButton={false}>
|
||||
<Sheets open={state.open} onClose={onClose} mode="bottom">
|
||||
<Stepper
|
||||
className="availabilityCreate"
|
||||
titles={steps.current}
|
||||
@ -147,7 +159,7 @@ export function AvailabilityCreate({ space }: Props) {
|
||||
error={error}
|
||||
/>
|
||||
</Stepper>
|
||||
</Modal>
|
||||
</Sheets>
|
||||
</>
|
||||
);
|
||||
}
|
34
src/components/Availability/AvailabilitySlotRow.css
Normal file
34
src/components/Availability/AvailabilitySlotRow.css
Normal file
@ -0,0 +1,34 @@
|
||||
.availabilitySlotRow {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.availabilitySlotRow--active {
|
||||
border-bottom: 1px solid var(--codex-border-color);
|
||||
}
|
||||
|
||||
.availabilitySlotRow-cell {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.availabilitySlotRow--inactive {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.availabilitySlotRow-cell-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: height 0.35s;
|
||||
will-change: height;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.availabilitySlotRow--active .availabilitySlotRow-cell-content {
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
.availabilitySlotRow--closing .availabilitySlotRow-cell-content {
|
||||
height: 0;
|
||||
}
|
86
src/components/Availability/AvailabilitySlotRow.tsx
Normal file
86
src/components/Availability/AvailabilitySlotRow.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import {
|
||||
Cell,
|
||||
Row,
|
||||
SimpleText,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { PrettyBytes } from "../../utils/bytes";
|
||||
import "./AvailabilitySlotRow.css";
|
||||
import { classnames } from "../../utils/classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
bytes: number;
|
||||
id: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export function AvailabilitySlotRow({ bytes, active, id }: Props) {
|
||||
const [className, setClassName] = useState("availabilitySlotRow--inactive");
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
setClassName("availabilitySlotRow--opening");
|
||||
|
||||
setTimeout(() => {
|
||||
setClassName("availabilitySlotRow--active");
|
||||
}, 15);
|
||||
} else {
|
||||
setClassName("availabilitySlotRow--closing");
|
||||
|
||||
setTimeout(() => {
|
||||
setClassName("availabilitySlotRow--inactive");
|
||||
}, 350);
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<Row
|
||||
className={classnames(["availabilitySlotRow"], [className])}
|
||||
cells={[
|
||||
<Cell className="availabilitySlotRow-cell">
|
||||
<span></span>
|
||||
</Cell>,
|
||||
<Cell
|
||||
colSpan={6}
|
||||
className={classnames(
|
||||
["availabilitySlotRow-cell"],
|
||||
["availabilitySlotRow-cell--main"]
|
||||
)}>
|
||||
<div className={classnames(["availabilitySlotRow-cell-content"])}>
|
||||
<SlotIcon />
|
||||
<div>
|
||||
<div>
|
||||
<b>Slot {id}</b>
|
||||
</div>
|
||||
<SimpleText size="small" variant="light">
|
||||
{PrettyBytes(bytes)} allocated for the slot
|
||||
</SimpleText>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>,
|
||||
]}></Row>
|
||||
);
|
||||
}
|
||||
|
||||
const SlotIcon = () => (
|
||||
<svg
|
||||
width="30"
|
||||
viewBox="0 0 65 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M60.9133 0H4.91333C2.71333 0 0.91333 1.8 0.91333 4V60C0.91333 62.2 2.71333 64 4.91333 64H60.9133C63.1133 64 64.9133 62.2 64.9133 60V4C64.9133 1.8 63.1133 0 60.9133 0Z"
|
||||
fill="#B59B77"
|
||||
/>
|
||||
<path
|
||||
d="M26.9133 0V22C26.9133 23.1 27.8133 24 28.9133 24H36.9133C38.0133 24 38.9133 23.1 38.9133 22V0H26.9133Z"
|
||||
fill="#D5B98B"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M56.3133 44.6L50.3133 38.6C49.9133 38.2 49.5133 38 48.9133 38C48.3133 38 47.9133 38.2 47.5133 38.6L41.5133 44.6C41.1133 45 40.9133 45.4 40.9133 46C40.9133 47.1 41.8133 48 42.9133 48C43.5133 48 43.9133 47.8 44.3133 47.4L46.9133 44.8V54C46.9133 55.1 47.8133 56 48.9133 56C49.9133 56 50.9133 55.1 50.9133 54V44.8L53.5133 47.5C53.9133 47.8 54.3133 48 54.9133 48C56.0133 48 56.9133 47.1 56.9133 46C56.9133 45.4 56.7133 45 56.3133 44.6Z"
|
||||
fill="#865F3B"
|
||||
/>
|
||||
</svg>
|
||||
);
|
@ -2,7 +2,7 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js";
|
||||
import { AvailabilityState } from "./types";
|
||||
import { SpaceAllocation } from "@codex-storage/marketplace-ui-components";
|
||||
import "./AvailabilitySpaceAllocation.css";
|
||||
import { availabilityUnit } from "./availability.domain";
|
||||
import { nodeSpaceAllocationColors } from "../NodeSpaceAllocation/nodeSpaceAllocation.domain";
|
||||
|
||||
type Props = {
|
||||
space: CodexNodeSpace;
|
||||
@ -10,28 +10,31 @@ type Props = {
|
||||
};
|
||||
|
||||
export function AvailabilitySpaceAllocation({ availability, space }: Props) {
|
||||
const unit = availabilityUnit(availability.totalSizeUnit);
|
||||
const { quotaMaxBytes, quotaReservedBytes } = space;
|
||||
const size = availability.totalSize * unit;
|
||||
const { quotaMaxBytes, quotaReservedBytes, quotaUsedBytes } = space;
|
||||
const isUpdating = !!availability.id;
|
||||
const allocated = isUpdating ? quotaReservedBytes - size : quotaReservedBytes;
|
||||
const allocated = isUpdating
|
||||
? quotaReservedBytes - availability.totalSize + quotaUsedBytes
|
||||
: quotaReservedBytes + quotaUsedBytes;
|
||||
const remaining =
|
||||
size > quotaMaxBytes - allocated
|
||||
availability.totalSize > quotaMaxBytes - allocated
|
||||
? quotaMaxBytes - allocated
|
||||
: quotaMaxBytes - allocated - size;
|
||||
: quotaMaxBytes - allocated - availability.totalSize;
|
||||
|
||||
const spaceData = [
|
||||
{
|
||||
title: "Space allocated",
|
||||
size: Math.trunc(allocated),
|
||||
color: nodeSpaceAllocationColors[0],
|
||||
},
|
||||
{
|
||||
title: "New space allocation",
|
||||
size: Math.trunc(size),
|
||||
size: Math.trunc(availability.totalSize),
|
||||
color: nodeSpaceAllocationColors[1],
|
||||
},
|
||||
{
|
||||
title: "Remaining space",
|
||||
size: Math.trunc(remaining),
|
||||
color: nodeSpaceAllocationColors[nodeSpaceAllocationColors.length - 1],
|
||||
},
|
||||
];
|
||||
|
||||
|
5
src/components/Availability/AvailabilitySunburst.css
Normal file
5
src/components/Availability/AvailabilitySunburst.css
Normal file
@ -0,0 +1,5 @@
|
||||
.activity-sunburst {
|
||||
height: 600px;
|
||||
width: 600px;
|
||||
margin: auto;
|
||||
}
|
183
src/components/Availability/AvailabilitySunburst.tsx
Normal file
183
src/components/Availability/AvailabilitySunburst.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import { CodexNodeSpace } from "@codex-storage/sdk-js";
|
||||
import { Times } from "../../utils/times";
|
||||
import { Strings } from "../../utils/strings";
|
||||
import { PrettyBytes } from "../../utils/bytes";
|
||||
import { useRef, useState } from "react";
|
||||
import { CallbackDataParams, ECBasicOption } from "echarts/types/dist/shared";
|
||||
import * as echarts from "echarts";
|
||||
import { availabilityColors } from "./availability.colors";
|
||||
import { AvailabilityWithSlots } from "./types";
|
||||
import "./AvailabilitySunburst.css";
|
||||
|
||||
type Props = {
|
||||
availabilities: AvailabilityWithSlots[];
|
||||
space: CodexNodeSpace;
|
||||
};
|
||||
|
||||
export function AvailabilitySunburst({ availabilities, space }: Props) {
|
||||
const div = useRef<HTMLDivElement>(null);
|
||||
const chart = useRef<echarts.EChartsType | null>(null);
|
||||
const [, setRefresher] = useState(Date.now());
|
||||
|
||||
if (div.current && !chart.current) {
|
||||
chart.current = echarts.init(div.current);
|
||||
setRefresher(Date.now());
|
||||
}
|
||||
|
||||
const data = availabilities.map((a, index) => {
|
||||
return {
|
||||
name: Strings.shortId(a.id),
|
||||
value: a.totalSize,
|
||||
itemStyle: {
|
||||
color: availabilityColors[index],
|
||||
borderColor: "var(--codex-background)",
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "#333",
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
color: "white",
|
||||
formatter: (params: CallbackDataParams) => {
|
||||
return (
|
||||
params.marker +
|
||||
a.id +
|
||||
"<br/>" +
|
||||
"Duration " +
|
||||
Times.pretty(a.duration) +
|
||||
"<br/>" +
|
||||
"Max collateral " +
|
||||
a.maxCollateral +
|
||||
"<br/>" +
|
||||
"Min price " +
|
||||
a.minPrice +
|
||||
"<br/>" +
|
||||
"Size " +
|
||||
PrettyBytes(a.totalSize)
|
||||
);
|
||||
},
|
||||
},
|
||||
children: a.slots.map((slot) => ({
|
||||
name: "",
|
||||
value: parseFloat(slot.size),
|
||||
children: [],
|
||||
itemStyle: {
|
||||
color: availabilityColors[index],
|
||||
borderColor: "var(--codex-background)",
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "#333",
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
formatter: (params: CallbackDataParams) => {
|
||||
return (
|
||||
params.marker +
|
||||
"Slot " +
|
||||
slot.id +
|
||||
PrettyBytes(parseFloat(slot.size))
|
||||
);
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const option: ECBasicOption = {
|
||||
series: {
|
||||
type: "sunburst",
|
||||
data: [
|
||||
...data,
|
||||
{
|
||||
name: "Space remaining",
|
||||
value:
|
||||
space.quotaMaxBytes -
|
||||
space.quotaReservedBytes -
|
||||
space.quotaUsedBytes,
|
||||
children: [],
|
||||
itemStyle: {
|
||||
color: "#ccc",
|
||||
borderColor: "var(--codex-background)",
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "#333",
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
formatter: (params: CallbackDataParams) => {
|
||||
return (
|
||||
params.marker +
|
||||
" Space remaining " +
|
||||
PrettyBytes(
|
||||
space.quotaMaxBytes -
|
||||
space.quotaReservedBytes -
|
||||
space.quotaUsedBytes
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderRadius: 7,
|
||||
borderWidth: 1,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
levels: [
|
||||
{},
|
||||
{
|
||||
r0: "35%",
|
||||
r: "70%",
|
||||
label: {
|
||||
align: "right",
|
||||
},
|
||||
},
|
||||
{
|
||||
r0: "75%",
|
||||
r: "85%",
|
||||
itemStyle: {
|
||||
shadowBlur: 80,
|
||||
shadowColor: "#ccc",
|
||||
},
|
||||
label: {
|
||||
position: "outside",
|
||||
textShadowBlur: 5,
|
||||
textShadowColor: "#333",
|
||||
},
|
||||
downplay: {
|
||||
label: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
tooltip: {
|
||||
// type: "item",
|
||||
},
|
||||
};
|
||||
|
||||
if (chart.current) {
|
||||
chart.current.setOption(option);
|
||||
chart.current.off("click");
|
||||
chart.current.on("click", function (params) {
|
||||
// console.info(params.componentIndex);
|
||||
// console.info(params.dataIndex);
|
||||
|
||||
const index = params.dataIndex;
|
||||
|
||||
const detail =
|
||||
params.dataIndex === 0 ? null : availabilities[index - 1].id;
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("codexavailabilityid", {
|
||||
detail,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return <div id="chart" ref={div} className="activity-sunburst"></div>;
|
||||
}
|
33
src/components/Availability/availability.colors.ts
Normal file
33
src/components/Availability/availability.colors.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export const availabilityColors = [
|
||||
"#004d00", // Very Dark Green
|
||||
"#1B5E20", // Dark Green
|
||||
"#2E7D32", // Medium Dark Green
|
||||
"#388E3C", // Medium Green
|
||||
"#43A047", // Bright Forest Green
|
||||
"#4CAF50", // Green
|
||||
"#5CB85C", // Medium Green
|
||||
"#66BB6A", // Light Green
|
||||
"#76FF03", // Bright Green
|
||||
"#A5D6A7", // Soft Green
|
||||
"#007A33", // Darker Green
|
||||
"#009639", // Vivid Green
|
||||
"#3B8A3B", // Medium Olive Green
|
||||
"#4E9F3D", // Olive Green
|
||||
"#5CBA3D", // Olive Drab
|
||||
"#6BBE45", // Light Olive Green
|
||||
"#7ED957", // Bright Olive
|
||||
"#8BC34A", // Light Olive
|
||||
"#A4D65E", // Olive Green
|
||||
"#B2DFDB", // Soft Mint Green
|
||||
"#C8E6C9", // Pale Green
|
||||
"#AEEA00", // Lime Green
|
||||
"#B9FBC0", // Soft Mint
|
||||
"#C5E1A5", // Soft Light Green
|
||||
"#DCE775", // Light Lime
|
||||
"#A4D65E", // Olive Green
|
||||
"#4CAF50", // Green
|
||||
"#66BB6A", // Light Green
|
||||
"#007A33", // Darker Green
|
||||
"#009639", // Vivid Green
|
||||
"#3B8A3B", // Medium Olive Green
|
||||
];
|
@ -2,6 +2,42 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js";
|
||||
import { GB, TB } from "../../utils/constants";
|
||||
import { AvailabilityState } from "./types";
|
||||
|
||||
export class AvailabilityDomain {
|
||||
space: CodexNodeSpace
|
||||
state: AvailabilityState
|
||||
|
||||
constructor(space: CodexNodeSpace, availability: AvailabilityState) {
|
||||
this.space = space
|
||||
this.state = availability
|
||||
}
|
||||
|
||||
get unitInBytes() {
|
||||
return this.state.totalSizeUnit === "gb" ? GB : TB;
|
||||
}
|
||||
|
||||
get unit() {
|
||||
return this.state.totalSizeUnit;
|
||||
}
|
||||
|
||||
get sizeInUnit() {
|
||||
return this.state.totalSize
|
||||
}
|
||||
|
||||
get maxInBytes() {
|
||||
return this.space.quotaMaxBytes - this.space.quotaReservedBytes - this.space.quotaUsedBytes
|
||||
}
|
||||
|
||||
get maxInUnit() {
|
||||
return this.maxInBytes / this.unitInBytes / this.unitInBytes
|
||||
}
|
||||
|
||||
isValid() {
|
||||
const size = this.state.totalSize * this.unitInBytes;
|
||||
|
||||
return size > 0 && size <= this.maxInBytes;
|
||||
}
|
||||
}
|
||||
|
||||
export const availabilityUnit = (unit: "gb" | "tb") =>
|
||||
unit === "gb" ? GB : TB;
|
||||
|
||||
@ -11,9 +47,6 @@ export const availabilityMax = (space: CodexNodeSpace) =>
|
||||
export const isAvailabilityValid = (
|
||||
availability: AvailabilityState,
|
||||
max: number
|
||||
) => {
|
||||
const unit = availabilityUnit(availability.totalSizeUnit);
|
||||
const size = parseFloat(availability.totalSize.toString()) * unit;
|
||||
) => availability.totalSize > 0 && availability.totalSize <= max;
|
||||
|
||||
|
||||
return size > 0 && size <= max;
|
||||
};
|
||||
|
@ -2,7 +2,11 @@ import {
|
||||
StepperAction,
|
||||
StepperState,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { CodexNodeSpace } from "@codex-storage/sdk-js";
|
||||
import {
|
||||
CodexAvailability,
|
||||
CodexNodeSpace,
|
||||
CodexReservation,
|
||||
} from "@codex-storage/sdk-js";
|
||||
import { Dispatch } from "react";
|
||||
|
||||
export type AvailabilityState = {
|
||||
@ -13,6 +17,7 @@ export type AvailabilityState = {
|
||||
minPrice: number;
|
||||
maxCollateral: number;
|
||||
totalSizeUnit: "gb" | "tb";
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type AvailabilityComponentProps = {
|
||||
@ -23,3 +28,8 @@ export type AvailabilityComponentProps = {
|
||||
availability: AvailabilityState;
|
||||
error: Error | null;
|
||||
};
|
||||
|
||||
export type AvailabilityWithSlots = CodexAvailability & {
|
||||
name: string;
|
||||
slots: CodexReservation[];
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { GB, TB } from "../../utils/constants";
|
||||
import { Promises } from "../../utils/promises";
|
||||
import { WebStorage } from "../../utils/web-storage";
|
||||
import { AvailabilityState } from "./types";
|
||||
@ -10,6 +9,8 @@ import {
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { Times } from "../../utils/times";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import { AvailabilityStorage } from "../../utils/availabilities-storage";
|
||||
import { CodexAvailabilityCreateResponse } from "@codex-storage/sdk-js";
|
||||
|
||||
export function useAvailabilityMutation(
|
||||
dispatch: Dispatch<StepperAction>,
|
||||
@ -19,25 +20,26 @@ export function useAvailabilityMutation(
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const { mutateAsync } = useMutation({
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
mutationFn: ({
|
||||
totalSize,
|
||||
totalSizeUnit,
|
||||
duration,
|
||||
durationUnit = "days",
|
||||
name,
|
||||
...input
|
||||
}: AvailabilityState) => {
|
||||
const unit = totalSizeUnit === "gb" ? GB : TB;
|
||||
const marketplace = CodexSdk.marketplace;
|
||||
const time = Times.toSeconds(duration, durationUnit);
|
||||
|
||||
const fn: (
|
||||
input: Omit<AvailabilityState, "totalSizeUnit" | "durationUnit">
|
||||
) => Promise<unknown> = input.id
|
||||
? (input) =>
|
||||
) => Promise<CodexAvailabilityCreateResponse | ""> = input.id
|
||||
? (input) =>
|
||||
marketplace
|
||||
.updateAvailability({ ...input, id: input.id || "" })
|
||||
.then((s) => Promises.rejectOnError(s))
|
||||
: (input) =>
|
||||
: (input) =>
|
||||
marketplace
|
||||
.createAvailability(input)
|
||||
.then((s) => Promises.rejectOnError(s));
|
||||
@ -45,16 +47,20 @@ export function useAvailabilityMutation(
|
||||
return fn({
|
||||
...input,
|
||||
duration: time,
|
||||
totalSize: Math.trunc(totalSize * unit),
|
||||
totalSize: Math.trunc(totalSize),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (res, body) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["availabilities"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["space"] });
|
||||
|
||||
WebStorage.delete("availability");
|
||||
WebStorage.delete("availability-step");
|
||||
|
||||
if (typeof res === "object" && body.name) {
|
||||
AvailabilityStorage.add(res.id, body.name)
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
dispatch({
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CheckCircle, CircleDashed, ShieldAlert } from "lucide-react";
|
||||
import "./CustomStateCellRender.css";
|
||||
import { Tooltip } from "@codex-storage/marketplace-ui-components";
|
||||
import { Cell, Tooltip } from "@codex-storage/marketplace-ui-components";
|
||||
|
||||
type Props = {
|
||||
state: string;
|
||||
@ -29,20 +29,22 @@ export const CustomStateCellRender = ({ state, message }: Props) => {
|
||||
const Icon = icons[state as keyof typeof icons] || CircleDashed;
|
||||
|
||||
return (
|
||||
<p
|
||||
className={
|
||||
"cell-state cell-state--custom cell-state--" +
|
||||
states[state as keyof typeof states]
|
||||
}>
|
||||
{message ? (
|
||||
<Tooltip message={message}>
|
||||
<Cell>
|
||||
<p
|
||||
className={
|
||||
"cell-state cell-state--custom cell-state--" +
|
||||
states[state as keyof typeof states]
|
||||
}>
|
||||
{message ? (
|
||||
<Tooltip message={message}>
|
||||
<Icon size={"1rem"} className="cell-stateIcon" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Icon size={"1rem"} className="cell-stateIcon" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Icon size={"1rem"} className="cell-stateIcon" />
|
||||
)}
|
||||
)}
|
||||
|
||||
<span>{state}</span>
|
||||
</p>
|
||||
<span>{state}</span>
|
||||
</p>
|
||||
</Cell>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tooltip, WebFileIcon } from "@codex-storage/marketplace-ui-components";
|
||||
import {
|
||||
Cell,
|
||||
Tooltip,
|
||||
WebFileIcon,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import "./FileCell.css";
|
||||
import { FileMetadata, FilesStorage } from "../../utils/file-storage";
|
||||
import { PurchaseStorage } from "../../utils/purchases-storage";
|
||||
@ -44,7 +48,7 @@ export function FileCell({ requestId, purchaseCid }: Props) {
|
||||
const cidTruncated = cid.slice(0, 10) + "...";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Cell>
|
||||
<div className="fileCell">
|
||||
<WebFileIcon type={metadata.mimetype} />
|
||||
<div>
|
||||
@ -58,6 +62,6 @@ export function FileCell({ requestId, purchaseCid }: Props) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import Loader from "../../assets/loader.svg";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import { SpaceAllocation } from "@codex-storage/marketplace-ui-components";
|
||||
import { Promises } from "../../utils/promises";
|
||||
import { nodeSpaceAllocationColors } from "./nodeSpaceAllocation.domain";
|
||||
|
||||
const defaultSpace = {
|
||||
quotaMaxBytes: 0,
|
||||
@ -42,14 +43,18 @@ export function NodeSpaceAllocation() {
|
||||
{
|
||||
title: "Maximum storage space used by the node",
|
||||
size: quotaMaxBytes,
|
||||
color: nodeSpaceAllocationColors[0],
|
||||
},
|
||||
{
|
||||
title: "Amount of storage space currently in use",
|
||||
size: quotaUsedBytes,
|
||||
color: nodeSpaceAllocationColors[1],
|
||||
},
|
||||
{
|
||||
title: "Amount of storage space reserved",
|
||||
size: quotaReservedBytes,
|
||||
color:
|
||||
nodeSpaceAllocationColors[nodeSpaceAllocationColors.length - 1],
|
||||
},
|
||||
]}></SpaceAllocation>
|
||||
);
|
||||
|
@ -0,0 +1,5 @@
|
||||
export const nodeSpaceAllocationColors = [
|
||||
"var(--codex-color-primary)",
|
||||
"#f9fa93",
|
||||
"#ccc",
|
||||
]
|
@ -5,3 +5,22 @@
|
||||
.truncateCell .tooltip:hover:after {
|
||||
left: -33%;
|
||||
}
|
||||
|
||||
.truncateCell-point {
|
||||
height: 0.5rem;
|
||||
width: 3rem;
|
||||
border-radius: var(--codex-border-radius);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.truncateCell--ellipsis {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.table-tbodyTr:hover .truncateCell--ellipsis {
|
||||
white-space: unset;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Tooltip } from "@codex-storage/marketplace-ui-components";
|
||||
import { Cell } from "@codex-storage/marketplace-ui-components";
|
||||
import "./TruncateCell.css";
|
||||
|
||||
type Props = {
|
||||
@ -7,14 +7,16 @@ type Props = {
|
||||
|
||||
export function TruncateCell({ value }: Props) {
|
||||
if (value.length <= 10) {
|
||||
return <span>{value}</span>;
|
||||
return <span id={value}>{value}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="truncateCell">
|
||||
<Tooltip message={value}>
|
||||
<span>{value.slice(0, 10) + "..."}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Cell>
|
||||
<div className="truncateCell" id={value}>
|
||||
<div className="truncateCell--ellipsis">
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
@ -14,89 +14,142 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.availabilities-create {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
border-radius: 100%;
|
||||
height: 6rem;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.availabilities-create .button-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.availabilities-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.availabilities-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.availabilities-table {
|
||||
order: 2;
|
||||
.nodeSpaceAllocation-bar {
|
||||
background-color: var(--codex-background-light);
|
||||
padding: 0.25rem;
|
||||
border-radius: var(--codex-border-radius);
|
||||
}
|
||||
|
||||
.availabilities-space {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
order: 1;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.availabilities-space .nodeSpaceAllocation-legendRow,
|
||||
.availabilities-space .nodeSpaceAllocation-barItem {
|
||||
.availabilities-space-allocation .nodeSpaceAllocation-legendRow,
|
||||
.availabilities-space-allocation .nodeSpaceAllocation-barItem {
|
||||
transition: opacity 0.35s;
|
||||
opacity: 0.8;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.availabilities-table:has(.table-tbodyTr:first-child:hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-quota-0,
|
||||
.availabilities-table:has(.table-tbodyTr:first-child:hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-legendRow:first-child {
|
||||
.availabilities-space-allocation .nodeSpaceAllocation-barItem:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(2):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-quota-1,
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(2):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-legendRow:nth-child(2) {
|
||||
opacity: 1;
|
||||
.availabilities-space-allocation {
|
||||
flex: 1;
|
||||
}
|
||||
/*
|
||||
// This isn't the best approach, but it will suffice for now.
|
||||
// The issue is that there is no sibling index to create a generic rule.
|
||||
// Additionally, rerendering the components with React on hover feels like overkill.
|
||||
// We are also uncertain about the number of availabilities that will be in the table,
|
||||
// so this workaround is acceptable for the time being.
|
||||
// @for $i from 1 through 30 {
|
||||
// .availabilities-table:has(.table-tbodyTr:nth-child(#{$i}):hover)
|
||||
// + .availabilities-space
|
||||
// .nodeSpaceAllocation-barItem:nth-child(#{$i}),
|
||||
// .availabilities-table:has(.table-tbodyTr:nth-child(#{$i}):hover)
|
||||
// + .availabilities-space
|
||||
// .nodeSpaceAllocation-legendRow:nth-child(#{$i}) {
|
||||
// opacity: 1;
|
||||
// }
|
||||
|
||||
// .availabilities-table:has(.table-tbodyTr:nth-child(#{$i}):hover)
|
||||
// + .availabilities-space
|
||||
// .nodeSpaceAllocation-barItem:nth-child(#{$i})::after {
|
||||
// opacity: 1;
|
||||
// z-index: 1;
|
||||
// }
|
||||
|
||||
// .availabilities-table:has(
|
||||
// ~ .availabilities-space
|
||||
// .nodeSpaceAllocation-barItem:nth-child(#{$i}):hover
|
||||
// )
|
||||
// .table-tbodyTr:nth-child(#{$i}) {
|
||||
// background-color: var(--codex-background-light);
|
||||
// }
|
||||
// }
|
||||
*/
|
||||
|
||||
.plus {
|
||||
border-radius: 50%;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
margin: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(3):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-quota-2,
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(3):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-legendRow:nth-child(3) {
|
||||
opacity: 1;
|
||||
.plus .button-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(4):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-quota-3,
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(4):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-legendRow:nth-child(4) {
|
||||
opacity: 1;
|
||||
.progress {
|
||||
border: 1px solid var(--codex-border-color);
|
||||
height: 8px;
|
||||
width: 200px;
|
||||
border-radius: var(--codex-border-radius);
|
||||
background-color: var(--codex-background);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(5):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-quota-4,
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(5):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-legendRow:nth-child(5) {
|
||||
opacity: 1;
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--codex-progress-bar);
|
||||
display: inline-block;
|
||||
border-radius: var(--codex-border-radius);
|
||||
}
|
||||
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(6):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-quota-5,
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(6):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-legendRow:nth-child(6) {
|
||||
opacity: 1;
|
||||
.progress-container {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(7):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-quota-6,
|
||||
.availabilities-table:has(.table-tbodyTr:nth-child(7):hover)
|
||||
+ .availabilities-space
|
||||
.nodeSpaceAllocation-legendRow:nth-child(7) {
|
||||
opacity: 1;
|
||||
.slot {
|
||||
background-color: transparent;
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
transparent,
|
||||
transparent 1rem,
|
||||
rgb(var(--codex-color-primary-rgb)) 1rem,
|
||||
rgb(var(--codex-color-primary-rgb)) 1.5rem
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
animation: barberpole 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes barberpole {
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,23 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
|
||||
import { Spinner } from "@codex-storage/marketplace-ui-components";
|
||||
import {
|
||||
SpaceAllocationItem,
|
||||
Spinner,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Promises } from "../../utils/promises";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import "./availabilities.css";
|
||||
import "./availabilities.scss";
|
||||
import { AvailabilitiesTable } from "../../components/Availability/AvailabilitiesTable";
|
||||
import { AvailabilityCreate } from "../../components/Availability/AvailabilityCreate";
|
||||
import { AvailabilityEdit } from "../../components/Availability/AvailabilityEdit";
|
||||
import { Strings } from "../../utils/strings";
|
||||
import { PrettyBytes } from "../../utils/bytes";
|
||||
import { AvailabilitySunburst } from "../../components/Availability/AvailabilitySunburst";
|
||||
import { Errors } from "../../utils/errors";
|
||||
import { availabilityColors } from "../../components/Availability/availability.colors";
|
||||
import { AvailabilityStorage } from "../../utils/availabilities-storage";
|
||||
import { AvailabilityWithSlots } from "../../components/Availability/types";
|
||||
|
||||
const defaultSpace = {
|
||||
quotaMaxBytes: 0,
|
||||
@ -19,15 +29,42 @@ const defaultSpace = {
|
||||
export function Availabilities() {
|
||||
{
|
||||
// Error will be catched in ErrorBounday
|
||||
const { data: availabilities = [], isPending } = useQuery({
|
||||
const { data: availabilities = [], isPending } = useQuery<
|
||||
AvailabilityWithSlots[]
|
||||
>({
|
||||
queryFn: () =>
|
||||
CodexSdk.marketplace
|
||||
.availabilities()
|
||||
.then((s) => Promises.rejectOnError(s))
|
||||
.then((res) => res.sort((a, b) => b.totalSize - a.totalSize)),
|
||||
.then((res) => res.sort((a, b) => b.totalSize - a.totalSize))
|
||||
.then((data) =>
|
||||
Promise.all(
|
||||
data.map((a) =>
|
||||
CodexSdk.marketplace
|
||||
.reservations(a.id)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
Errors.report(res);
|
||||
return { ...a, slots: [] };
|
||||
}
|
||||
|
||||
return { ...a, slots: res.data };
|
||||
})
|
||||
.then((data) =>
|
||||
AvailabilityStorage.get(data.id).then((n) => ({
|
||||
...data,
|
||||
name: n || "",
|
||||
}))
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
queryKey: ["availabilities"],
|
||||
initialData: [],
|
||||
|
||||
// .then((res) =>
|
||||
// res.error ? res : { ...data, slots: res.data }
|
||||
// )
|
||||
// No need to retry because if the connection to the node
|
||||
// is back again, all the queries will be invalidated.
|
||||
retry: false,
|
||||
@ -67,12 +104,22 @@ export function Availabilities() {
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
// const allocation = availabilities
|
||||
// .map((a) => ({
|
||||
// title: Strings.shortId(a.id),
|
||||
// size: a.totalSize,
|
||||
// }))
|
||||
// .slice(0, 6);
|
||||
const allocation: SpaceAllocationItem[] = availabilities.map(
|
||||
(a, index) => ({
|
||||
title: Strings.shortId(a.id),
|
||||
size: a.totalSize,
|
||||
tooltip: a.id + "\u000D\u000A" + PrettyBytes(a.totalSize),
|
||||
color: availabilityColors[index],
|
||||
})
|
||||
);
|
||||
|
||||
allocation.push({
|
||||
title: "Space remaining",
|
||||
// TODO move this to domain
|
||||
size:
|
||||
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes,
|
||||
color: "transparent",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
@ -82,20 +129,28 @@ export function Availabilities() {
|
||||
<Spinner width="3rem" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="availabilities-table">
|
||||
<AvailabilitiesTable
|
||||
// onEdit={onOpen}
|
||||
availabilities={availabilities}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
<div className="availabilities-header">
|
||||
<AvailabilitySunburst
|
||||
availabilities={availabilities}
|
||||
space={space}></AvailabilitySunburst>
|
||||
|
||||
<div className="availabilities-space">
|
||||
<div>{/* <SpaceAllocation data={allocation} /> */}</div>
|
||||
<div>
|
||||
<AvailabilityCreate space={space} />
|
||||
</div>
|
||||
</div>
|
||||
<AvailabilityEdit
|
||||
space={space}
|
||||
className="availabilities-create"
|
||||
hasLabel={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="availabilities-table">
|
||||
<AvailabilitiesTable
|
||||
space={space}
|
||||
// onEdit={onOpen}
|
||||
availabilities={availabilities}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,12 @@
|
||||
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 {
|
||||
Cell,
|
||||
Row,
|
||||
Spinner,
|
||||
Table,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { StorageRequestCreate } from "../../components/StorageRequestSetup/StorageRequestCreate";
|
||||
import "./purchases.css";
|
||||
import { FileCell } from "../../components/FileCellRender/FileCell";
|
||||
@ -13,7 +18,7 @@ import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceho
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
|
||||
const Purchases = () => {
|
||||
const { data, isPending, error } = useQuery({
|
||||
const { data, isPending } = useQuery({
|
||||
queryFn: () =>
|
||||
CodexSdk.marketplace.purchases().then((s) => Promises.rejectOnError(s)),
|
||||
queryKey: ["purchases"],
|
||||
@ -54,21 +59,28 @@ const Purchases = () => {
|
||||
"state",
|
||||
];
|
||||
|
||||
const cells = data.map((p, index) => {
|
||||
const rows = data.map((p, index) => {
|
||||
const r = p.request;
|
||||
const ask = p.request.ask;
|
||||
const duration = parseInt(p.request.ask.duration, 10);
|
||||
const pf = parseInt(p.request.ask.proofProbability, 10);
|
||||
|
||||
return [
|
||||
<FileCell requestId={r.id} purchaseCid={r.content.cid} index={index} />,
|
||||
<TruncateCell value={r.id} />,
|
||||
<Cell value={Times.pretty(duration)} />,
|
||||
<Cell value={ask.slots.toString()} />,
|
||||
<Cell value={ask.reward + " CDX"} />,
|
||||
<Cell value={pf.toString()} />,
|
||||
<CustomStateCellRender state={p.state} message={p.error} />,
|
||||
];
|
||||
return (
|
||||
<Row
|
||||
cells={[
|
||||
<FileCell
|
||||
requestId={r.id}
|
||||
purchaseCid={r.content.cid}
|
||||
index={index}
|
||||
/>,
|
||||
<TruncateCell value={r.id} />,
|
||||
<Cell>{Times.pretty(duration)}</Cell>,
|
||||
<Cell>{ask.slots.toString()}</Cell>,
|
||||
<Cell>{ask.reward + " CDX"}</Cell>,
|
||||
<Cell>{pf.toString()}</Cell>,
|
||||
<CustomStateCellRender state={p.state} message={p.error} />,
|
||||
]}></Row>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
@ -77,7 +89,7 @@ const Purchases = () => {
|
||||
<StorageRequestCreate />
|
||||
</div>
|
||||
|
||||
<Table headers={headers} cells={cells} />
|
||||
<Table headers={headers} rows={rows} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
4
src/utils/arrays.ts
Normal file
4
src/utils/arrays.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const Arrays = {
|
||||
toggle: <T>(arr: Array<T>, value: T) =>
|
||||
arr.includes(value) ? arr.filter(i => i !== value) : [...arr, value]
|
||||
}
|
17
src/utils/availabilities-storage.ts
Normal file
17
src/utils/availabilities-storage.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { createStore, del, get, set } from "idb-keyval";
|
||||
|
||||
const store = createStore("availabilities", "availabilities");
|
||||
|
||||
export const AvailabilityStorage = {
|
||||
get(key: string) {
|
||||
return get<string>(key, store);
|
||||
},
|
||||
|
||||
delete(key: string) {
|
||||
return del(key, store);
|
||||
},
|
||||
|
||||
async add(key: string, value: string) {
|
||||
return set(key, value, store);
|
||||
},
|
||||
};
|
33
src/utils/errors.ts
Normal file
33
src/utils/errors.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { isCodexOnline } from "../components/NodeIndicator/NodeIndicator";
|
||||
import { CodexError } from "@codex-storage/sdk-js";
|
||||
|
||||
// It would be preferable to completely ignore the error
|
||||
// when the node is not connected. However, during the
|
||||
// initial load, we lack this information until the
|
||||
// SPR response is completed. In the meantime, other
|
||||
// requests may be initiated, so if the node is not
|
||||
// connected, we should set the level to 'log'.
|
||||
const getLogLevel = () => {
|
||||
switch (isCodexOnline) {
|
||||
case true:
|
||||
return "error";
|
||||
case null:
|
||||
return "info";
|
||||
case false:
|
||||
return "log";
|
||||
}
|
||||
};
|
||||
|
||||
export const Errors = {
|
||||
report(safe: { error: true, data: CodexError }) {
|
||||
Sentry.captureException(safe.data, {
|
||||
extra: {
|
||||
code: safe.data.code,
|
||||
errors: safe.data.errors,
|
||||
sourceStack: safe.data.sourceStack,
|
||||
level: getLogLevel(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@ -1,36 +1,11 @@
|
||||
import { SafeValue } from "@codex-storage/sdk-js";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { isCodexOnline } from "../components/NodeIndicator/NodeIndicator";
|
||||
|
||||
// It would be preferable to completely ignore the error
|
||||
// when the node is not connected. However, during the
|
||||
// initial load, we lack this information until the
|
||||
// SPR response is completed. In the meantime, other
|
||||
// requests may be initiated, so if the node is not
|
||||
// connected, we should set the level to 'log'.
|
||||
const getLogLevel = () => {
|
||||
switch (isCodexOnline) {
|
||||
case true:
|
||||
return "error";
|
||||
case null:
|
||||
return "info";
|
||||
case false:
|
||||
return "log";
|
||||
}
|
||||
};
|
||||
import { Errors } from "./errors";
|
||||
|
||||
export const Promises = {
|
||||
rejectOnError: <T>(safe: SafeValue<T>, report = true) => {
|
||||
if (safe.error) {
|
||||
if (report) {
|
||||
Sentry.captureException(safe.data, {
|
||||
extra: {
|
||||
code: safe.data.code,
|
||||
errors: safe.data.errors,
|
||||
sourceStack: safe.data.sourceStack,
|
||||
level: getLogLevel(),
|
||||
},
|
||||
});
|
||||
Errors.report(safe)
|
||||
}
|
||||
|
||||
return Promise.reject(safe.data);
|
||||
|
Loading…
x
Reference in New Issue
Block a user