Add availabilities UI

This commit is contained in:
Arnaud 2024-11-06 02:33:58 +01:00
parent 4a5a6aee78
commit cfc5015ecc
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
54 changed files with 1025 additions and 680 deletions

52
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.7",
"license": "MIT",
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.35",
"@codex-storage/marketplace-ui-components": "^0.0.36",
"@codex-storage/sdk-js": "^0.0.15",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",
@ -48,6 +48,50 @@
"node": ">=18"
}
},
"../storybook": {
"name": "@codex-storage/marketplace-ui-components",
"version": "0.0.36",
"extraneous": true,
"license": "MIT",
"dependencies": {
"lucide-react": "^0.453.0"
},
"devDependencies": {
"@chromatic-com/storybook": "^2.0.2",
"@codex-storage/sdk-js": "^0.0.15",
"@storybook/addon-essentials": "^8.2.9",
"@storybook/addon-interactions": "^8.2.9",
"@storybook/addon-links": "^8.2.9",
"@storybook/addon-onboarding": "^8.2.9",
"@storybook/blocks": "^8.2.9",
"@storybook/react": "^8.2.9",
"@storybook/react-vite": "^8.2.9",
"@storybook/test": "^8.2.9",
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"glob": "^9.3.5",
"prettier": "^3.3.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"storybook": "^8.2.9",
"typescript": "5.5.2",
"vite-plugin-dts": "^4.0.3",
"vite-plugin-lib-inject-css": "^2.1.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@codex-storage/sdk-js": ">=0.0.14",
"postcss-nesting": "^13.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"dev": true,
@ -379,9 +423,9 @@
"peer": true
},
"node_modules/@codex-storage/marketplace-ui-components": {
"version": "0.0.35",
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.35.tgz",
"integrity": "sha512-wHh+oDRAt2NoIDxehzogDxowKEOwg6xxDHT3KkXfwawBhcmZIrfff9melJ4/hdAXRPo9IPTO/Zpi6Yj0KmlyWQ==",
"version": "0.0.36",
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.36.tgz",
"integrity": "sha512-dVkGhipSa8tBT85a+dAobFJTbtmvnFAnFhxVKRlC94/Ol7RpGUdcxVxIO7mGQBzVgxQz12vWA9j/7UXgQUmyrQ==",
"dependencies": {
"lucide-react": "^0.453.0"
},

View File

@ -25,7 +25,7 @@
"React"
],
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.35",
"@codex-storage/marketplace-ui-components": "^0.0.36",
"@codex-storage/sdk-js": "^0.0.15",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",

View File

@ -13,6 +13,7 @@ import { PeersIcon } from "../Menu/PeersIcon";
import { SettingsIcon } from "../Menu/SettingsIcon";
import { FilesIcon } from "../FilesIcon/FilesIcon";
import { LogsIcon } from "../Menu/LogsIcon";
import { HostIcon } from "../Menu/HostIcon";
type Props = {
onIconClick: () => void;
@ -24,6 +25,7 @@ const icons: Record<string, ReactElement> = {
settings: <SettingsIcon />,
files: <FilesIcon />,
logs: <LogsIcon />,
availabilies: <HostIcon />,
};
const descriptions: Record<string, string> = {
@ -32,6 +34,7 @@ const descriptions: Record<string, string> = {
settings: "Manage your Codex Vault.",
files: "Manage your files in your local vault.",
logs: "Manage your logs and debug console.",
availabilies: "Manage your storage requests.",
};
export function AppBar({ onIconClick }: Props) {

View File

@ -1,8 +0,0 @@
.availabilityTable-chevron {
cursor: pointer;
transition: transform 0.35s;
}
.availabilityTable-chevron--open {
transform: rotate(180deg);
}

View File

@ -6,13 +6,12 @@ import { Times } from "../../utils/times";
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 { SlotRow } from "./SlotRow";
import { AvailabilityWithSlots } from "./types";
import { AvailabilityDiskRow } from "./AvailabilityDiskRow";
import { ChevronDown } from "./ChevronDown";
import { attributes } from "../../utils/attributes";
type Props = {
// onEdit: () => void;
@ -38,7 +37,7 @@ export function AvailabilitiesTable({ availabilities, space }: Props) {
const onReservationsClose = () => setAvailability(null);
const rows = availabilities.map((a, index) => {
const rows = availabilities.map((a) => {
const showDetails = details.includes(a.id);
const onShowDetails = () => setDetails(Arrays.toggle(details, a.id));
@ -47,20 +46,18 @@ export function AvailabilitiesTable({ availabilities, space }: Props) {
return (
<Fragment key={a.id + a.duration}>
<Row
className="availabilty-row"
cells={[
<Cell>
{hasSlots ? (
<ChevronDown
className={classnames(
["availabilityTable-chevron"],
["availabilityTable-chevron--open", showDetails]
)}
{...attributes({ "aria-expanded": showDetails })}
onClick={onShowDetails}></ChevronDown>
) : (
<span></span>
""
)}
</Cell>,
<AvailabilityIdCell value={a} index={index} />,
<AvailabilityIdCell value={a} />,
<Cell>{PrettyBytes(a.totalSize)}</Cell>,
<Cell>{Times.pretty(a.duration)}</Cell>,
<Cell>{a.minPrice.toString()}</Cell>,
@ -69,11 +66,11 @@ export function AvailabilitiesTable({ availabilities, space }: Props) {
]}></Row>
{a.slots.map((slot) => (
<AvailabilitySlotRow
<SlotRow
key={slot.id}
active={showDetails}
bytes={parseFloat(slot.size)}
id={slot.id}></AvailabilitySlotRow>
id={slot.id}></SlotRow>
))}
</Fragment>
);

View File

@ -1,15 +1,17 @@
.availability-actions {
display: flex;
gap: 1rem;
display: inline-flex;
align-items: center;
}
border: 1px solid var(--codex-border-color);
border-radius: var(--codex-border-radius);
padding: 0.5rem;
background-color: #14141499;
gap: 8px;
padding: 10px;
.availability-action {
cursor: pointer;
color: var(--codex-color-primary);
transition: opacity 0.35s;
}
.availability-action:hover {
opacity: 0.7;
.button-icon {
width: 40px;
height: 40px;
background-color: #2f2f2f;
border: 1px solid #96969633;
}
}

View File

@ -1,7 +1,7 @@
import { Pencil } from "lucide-react";
import "./AvailabilityActionsCell.css";
import { CodexAvailability } from "@codex-storage/sdk-js/async";
import { Cell } from "@codex-storage/marketplace-ui-components";
import { ButtonIcon, Cell } from "@codex-storage/marketplace-ui-components";
import { EditIcon } from "./EditIcon";
type Props = {
availability: CodexAvailability;
@ -29,9 +29,10 @@ export function AvailabilityActionsCell(_: Props) {
return (
<Cell>
<div className="availability-actions">
<a className="cell--action availability-action" title="Reservations">
<Pencil width={"1.25rem"} />
</a>
<ButtonIcon
variant="small"
// onClick={() => onDetails(content.cid)}
Icon={EditIcon}></ButtonIcon>
</div>
</Cell>
);

View File

@ -1,10 +0,0 @@
.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;
}

View File

@ -1,7 +1,7 @@
import { Cell, Row } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import "./AvailabilityDiskRow.css";
import { classnames } from "../../utils/classnames";
import { HostIcon } from "./HostIcon";
type Props = {
bytes: number;
@ -10,61 +10,19 @@ type Props = {
export function AvailabilityDiskRow({ bytes }: Props) {
return (
<Row
className={classnames(["availabilityDiskRow"])}
cells={[
<Cell className=" availabilityDiskRow-cell">
<Cell>
<span></span>
</Cell>,
<Cell colSpan={6} className={classnames([" availabilityDiskRow-cell"])}>
<div className={classnames(["availabilityDiskRow-cell-content"])}>
<HardDrive />
<Cell colSpan={6}>
<div className={classnames(["row gap"])}>
<HostIcon />
<div>
<div>
<b>Node</b>
</div>
<small className="text--light">
{PrettyBytes(bytes)} allocated for the node
</small>
<b>Node</b>
<small>{PrettyBytes(bytes)} allocated for the node</small>
</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>
);

View File

@ -1,3 +1,10 @@
.availability-edit {
.button div {
width: 40px;
height: 40px;
color: black;
}
}
@media (min-width: 801px) {
.availabilityCreate .stepper-body {
width: 600px;

View File

@ -152,12 +152,12 @@ export function AvailabilityEdit({
const nextLabel = state.step === steps.current.length - 1 ? "Finish" : "Next";
return (
<>
<div className="availability-edit">
<Button
label={hasLabel ? "Sale" : ""}
Icon={!availabilityId ? Plus : Pencil}
Icon={!availabilityId ? () => <Plus width={40} height={40} /> : Pencil}
onClick={onOpen}
variant="primary"
variant="outline"
className={className}
/>
@ -181,6 +181,6 @@ export function AvailabilityEdit({
/>
</Stepper>
</Modal>
</>
</div>
);
}

View File

@ -1,5 +0,0 @@
.availabilityIdCell {
display: flex;
align-items: center;
gap: 1rem;
}

View File

@ -1,20 +1,18 @@
import "./AvailabilityIdCell.css";
import { Strings } from "../../utils/strings";
import { Cell } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import { availabilityColors } from "./availability.colors";
import { AvailabilityWithSlots } from "./types";
import { FolderIcon } from "./FolderIcon";
type Props = {
value: AvailabilityWithSlots;
index: number;
};
export function AvailabilityIdCell({ value, index }: Props) {
export function AvailabilityIdCell({ value }: Props) {
return (
<Cell>
<div className="availabilityIdCell" id={value.id}>
<Folder color={availabilityColors[index]} />
<div className="row gap" id={value.id}>
<FolderIcon />
<div>
<div>
<b>{value.name || Strings.shortId(value.id)}</b>
@ -22,36 +20,12 @@ export function AvailabilityIdCell({ value, index }: Props) {
<small className="text--light">
{PrettyBytes(value.totalSize)} allocated for the availability
</small>
<div>
<small className="text--light">
Max collateral {value.maxCollateral} | Min price {value.minPrice}
</small>
</div>
<br />
<small className="text--light">
Max collateral {value.maxCollateral} | Min price {value.minPrice}
</small>
</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>
);

View File

@ -1,35 +0,0 @@
.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-left: 3rem;
padding-right: 1rem;
}
.availabilitySlotRow--active .availabilitySlotRow-cell-content {
height: 65px;
}
.availabilitySlotRow--closing .availabilitySlotRow-cell-content {
height: 0;
}

View File

@ -1,82 +0,0 @@
import { Cell, Row } 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>
<small className="text--light">
{PrettyBytes(bytes)} allocated for the slot
</small>
</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>
);

View File

@ -1,5 +0,0 @@
.activity-sunburst {
height: 600px;
width: 600px;
margin: auto;
}

View File

@ -0,0 +1,20 @@
type Props = {
onClick?: () => void;
};
export function ChevronDown({ onClick, ...rest }: Props) {
return (
<svg
width={20}
onClick={onClick}
{...rest}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M9.99999 10.8785L13.7125 7.16602L14.773 8.22652L9.99999 12.9995L5.22699 8.22652L6.28749 7.16602L9.99999 10.8785Z"
fill="#969696"
/>
</svg>
);
}

View File

@ -0,0 +1,15 @@
export function EditIcon() {
return (
<svg
width="14"
height="16"
viewBox="0 0 14 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M2.8105 10.9996L10.417 3.39307L9.3565 2.33257L1.75 9.93908V10.9996H2.8105ZM3.43225 12.4996H0.25V9.31733L8.82625 0.741074C8.9669 0.600471 9.15763 0.521484 9.3565 0.521484C9.55537 0.521484 9.7461 0.600471 9.88675 0.741074L12.0085 2.86282C12.1491 3.00347 12.2281 3.1942 12.2281 3.39307C12.2281 3.59195 12.1491 3.78268 12.0085 3.92332L3.43225 12.4996ZM0.25 13.9996H13.75V15.4996H0.25V13.9996Z"
fill="#969696"
/>
</svg>
);
}

View File

@ -0,0 +1,15 @@
export function FolderIcon() {
return (
<svg
width="26"
height="24"
viewBox="0 0 26 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M1.75 0.75C1.05965 0.75 0.5 1.30965 0.5 2V5.75H9.98224L13 2.73224L11.0177 0.75H1.75ZM16.0177 3.25L11.0177 8.25H0.5V22C0.5 22.6904 1.05965 23.25 1.75 23.25H24.25C24.9404 23.25 25.5 22.6904 25.5 22V4.5C25.5 3.80965 24.9404 3.25 24.25 3.25H16.0177Z"
fill="#969696"
/>
</svg>
);
}

View File

@ -0,0 +1,15 @@
export function HostIcon() {
return (
<svg
width="28"
height="30"
viewBox="0 0 28 30"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M27.5 1.5V28.5C27.5 28.8978 27.342 29.2794 27.0607 29.5607C26.7794 29.842 26.3978 30 26 30H2C1.60218 30 1.22064 29.842 0.93934 29.5607C0.658035 29.2794 0.5 28.8978 0.5 28.5V1.5C0.5 1.10218 0.658035 0.720644 0.93934 0.43934C1.22064 0.158035 1.60218 0 2 0H26C26.3978 0 26.7794 0.158035 27.0607 0.43934C27.342 0.720644 27.5 1.10218 27.5 1.5ZM3.5 21V27H24.5V21H3.5ZM18.5 22.5H21.5V25.5H18.5V22.5Z"
fill="#969696"
/>
</svg>
);
}

View File

@ -0,0 +1,15 @@
export function SlotIcon() {
return (
<svg
width="20"
height="22"
viewBox="0 0 20 22"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M18.5021 4.92225L10 0L1.49793 4.92225L10 9.8445L18.5021 4.92225ZM0.5 6.6555V16.5L9 21.4211V11.5765L0.5 6.6555ZM11 21.4211L19.5 16.5V6.6555L11 11.5765V21.4211Z"
fill="#969696"
/>
</svg>
);
}

View File

@ -0,0 +1,9 @@
.slot-row {
transition:
visibility 0.35s,
max-height 0.35s;
&.slot-row--inactive {
visibility: collapse;
}
}

View File

@ -0,0 +1,56 @@
import { Cell, Row } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import "./SlotRow.css";
import { classnames } from "../../utils/classnames";
import { SlotIcon } from "./SlotIcon";
import { attributes } from "../../utils/attributes";
type Props = {
bytes: number;
id: string;
active: boolean;
};
export function SlotRow({ bytes, active, id }: Props) {
// const [className, setClassName] = useState("slot-row--inactive");
// useEffect(() => {
// if (active) {
// setClassName("slot-row--opening");
// setTimeout(() => {
// setClassName("slot-row--active");
// }, 15);
// } else {
// setClassName("slot-row--closing");
// setTimeout(() => {
// setClassName("slot-row--inactive");
// }, 350);
// }
// }, [active]);
return (
<Row
{...attributes({ "aria-expanded": active })}
className={classnames(
["slot-row"],
["slot-row--active", active],
["slot-row--inactive", !active]
)}
cells={[
<Cell>
<span></span>
</Cell>,
<Cell colSpan={6}>
<div className={"row gap"}>
<SlotIcon />
<div>
<b>Slot {id}</b>
<small>{PrettyBytes(bytes)} allocated for the slot</small>
</div>
</div>
</Cell>,
]}></Row>
);
}

View File

@ -0,0 +1,5 @@
.sunburst {
height: 350px;
width: 350px;
margin: auto;
}

View File

@ -5,16 +5,16 @@ import { PrettyBytes } from "../../utils/bytes";
import { useEffect, useRef, useState } from "react";
import { CallbackDataParams, ECBasicOption } from "echarts/types/dist/shared";
import * as echarts from "echarts";
import { availabilityColors } from "./availability.colors";
import { availabilityColors, slotColors } from "./availability.colors";
import { AvailabilityWithSlots } from "./types";
import "./AvailabilitySunburst.css";
import "./Sunburst.css";
type Props = {
availabilities: AvailabilityWithSlots[];
space: CodexNodeSpace;
};
export function AvailabilitySunburst({ availabilities, space }: Props) {
export function Sunburst({ availabilities, space }: Props) {
const div = useRef<HTMLDivElement>(null);
const chart = useRef<echarts.EChartsType | null>(null);
const [, setRefresher] = useState(Date.now());
@ -32,7 +32,7 @@ export function AvailabilitySunburst({ availabilities, space }: Props) {
value: a.totalSize,
itemStyle: {
color: availabilityColors[index],
borderColor: "var(--codex-background)",
borderColor: "transparent",
},
tooltip: {
backgroundColor: "#333",
@ -64,8 +64,8 @@ export function AvailabilitySunburst({ availabilities, space }: Props) {
value: parseFloat(slot.size),
children: [],
itemStyle: {
color: availabilityColors[index],
borderColor: "var(--codex-background)",
color: slotColors[index],
borderColor: "transparent",
},
tooltip: {
backgroundColor: "#333",
@ -98,8 +98,8 @@ export function AvailabilitySunburst({ availabilities, space }: Props) {
space.quotaUsedBytes,
children: [],
itemStyle: {
color: "#ccc",
borderColor: "var(--codex-background)",
color: "#2F2F2F",
borderColor: "transparent",
},
tooltip: {
backgroundColor: "#333",
@ -125,7 +125,7 @@ export function AvailabilitySunburst({ availabilities, space }: Props) {
borderWidth: 1,
},
label: {
show: true,
show: false,
},
levels: [
{},
@ -139,10 +139,7 @@ export function AvailabilitySunburst({ availabilities, space }: Props) {
{
r0: "75%",
r: "85%",
itemStyle: {
shadowBlur: 80,
shadowColor: "#ccc",
},
itemStyle: {},
label: {
position: "outside",
textShadowBlur: 5,
@ -181,5 +178,5 @@ export function AvailabilitySunburst({ availabilities, space }: Props) {
});
}
return <div id="chart" ref={div} className="activity-sunburst"></div>;
return <div id="chart" ref={div} className="sunburst"></div>;
}

View File

@ -1,33 +1,19 @@
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
"#34A0FFFF",
"#34A0FFEE",
"#34A0FFDD",
"#34A0FFCC",
"#34A0FFBB",
"#34A0FFAA",
"#34A0FF99",
];
export const slotColors = [
"#D2493CFF",
"#D2493CEE",
"#D2493CDD",
"#D2493CCC",
"#D2493CBB",
"#D2493CAA",
"#D2493C99",
];

View File

@ -0,0 +1,32 @@
.card {
border: 1px solid #96969633;
border-radius: 16px;
padding: 16px;
background-color: #232323;
> header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
svg {
color: #969696;
}
> div {
display: flex;
align-items: center;
gap: 12px;
}
h5 {
font-family: Inter;
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.011em;
}
}
}

View File

@ -0,0 +1,39 @@
import { ReactElement, ReactNode } from "react";
import "./Card.css";
import { Button } from "@codex-storage/marketplace-ui-components";
type Props = {
className?: string;
icon: ReactNode;
buttonLabel?: string;
buttonAction?: () => void;
title?: string;
children: ReactElement;
};
export function Card({
icon,
buttonAction,
buttonLabel,
title,
children,
className = "",
}: Props) {
return (
<div className={"card " + className}>
<header>
<div>
{icon}
<h5>{title}</h5>
</div>
{buttonLabel && (
<Button
label={buttonLabel}
variant="outline"
onClick={buttonAction}></Button>
)}
</header>
{children}
</div>
);
}

View File

@ -10,7 +10,7 @@ import { Dates } from "../../utils/dates";
import { CidCopyButton } from "./CidCopyButton";
import "./FileDetails.css";
import { CodexSdk } from "../../sdk/codex";
import { Files } from "../../utils/files";
import { FilesUtils } from "./files.utils";
import { useEffect, useState } from "react";
import { WebStorage } from "../../utils/web-storage";
import { FileDetailsIcon } from "./FileDetailsIcon";
@ -56,7 +56,7 @@ export function FileDetails({ onClose, details }: Props) {
</header>
<div className="preview">
{Files.isImage(details.manifest.mimetype) ? (
{FilesUtils.isImage(details.manifest.mimetype) ? (
<img src={url + details.cid} />
) : (
<figure>

View File

@ -1,5 +1,5 @@
import { CodexDataContent } from "@codex-storage/sdk-js";
import { Files } from "../../utils/files";
import { FilesUtils } from "./files.utils";
import { classnames } from "../../utils/classnames";
import "./FileFilters.css";
import { ArchiveIcon } from "./ArchiveIcon";
@ -34,15 +34,15 @@ function getIcon(type: string) {
}
function getType(mimetype: string) {
if (Files.isArchive(mimetype)) {
if (FilesUtils.isArchive(mimetype)) {
return "archive";
}
if (Files.isImage(mimetype)) {
if (FilesUtils.isImage(mimetype)) {
return "image";
}
if (Files.isVideo(mimetype)) {
if (FilesUtils.isVideo(mimetype)) {
return "video";
}

View File

@ -25,127 +25,3 @@
background-color: #14141499;
}
}
.files-cell-file {
display: flex;
align-items: center;
gap: 1rem;
}
.files-fileMeta {
display: flex;
align-items: center;
gap: 0.5rem;
}
.files-fileMeta-cid {
word-break: break-all;
}
.files-title {
font-weight: bold;
font-size: 1.125rem;
line-height: 1.75rem;
}
.files-file:not(:last-child) {
padding-bottom: 0.75rem;
}
.files-fileContent {
display: flex;
gap: 0.75rem;
}
.files-file:not(:last-child) .files-fileContent {
border-bottom: 1px solid var(--codex-border-color);
}
.files-file:not(:last-child) .files-fileContent {
padding-bottom: 0.75rem;
}
.files-fileIcon {
padding: 0.5rem;
border: 1px solid var(--codex-border-color);
border-radius: var(--codex-border-radius);
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
}
.files-fileActions {
display: inline-flex;
align-items: center;
border: 1px solid var(--codex-border-color);
border-radius: var(--codex-border-radius);
padding: 0.5rem;
gap: 0.75rem;
}
.files-fileStar {
transition:
fill 0.35s,
stroke 0.35s;
}
.files-fileFavorite {
fill: yellow;
stroke: yellow;
}
.files-header {
margin-bottom: 0.75rem;
display: flex;
}
.files-headerLeft {
flex: 1;
}
.files-headerRight {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.files-folders {
width: 200px;
min-width: 0px;
}
.files-filters {
display: flex;
margin: 1rem 0;
gap: 1rem;
}
.files-filter {
padding: 0.25rem 0.5rem;
background-color: var(--codex-background-light);
border-radius: var(--codex-border-radius);
border: 1px solid var(--codex-border-color);
opacity: 0.5;
display: inline-flex;
align-items: center;
gap: 1rem;
transition: opacity 0.35s;
cursor: pointer;
}
.files-filter--active {
opacity: 1;
}
.fileDetails-imageContainer {
display: flex;
justify-content: center;
}
.fileDetails-image {
max-width: 200px;
max-height: 200px;
margin: auto;
}

View File

@ -18,7 +18,7 @@ import { useData } from "../../hooks/useData.tsx";
import { WebStorage } from "../../utils/web-storage.ts";
import { classnames } from "../../utils/classnames.ts";
import { CodexDataContent } from "@codex-storage/sdk-js";
import { Files as F } from "../../utils/files.ts";
import { FilesUtils } from "./files.utils.ts";
import { FilterFilters } from "./FileFilters.tsx";
import { FileCell } from "./FileCell.tsx";
import { FileActions } from "./FileActions.tsx";
@ -69,7 +69,7 @@ export function Files() {
return;
}
if (folders.find(([folder]) => folder === val)) {
if (FilesUtils.exists(folders, val)) {
setError("This folder already exists");
return;
}
@ -108,22 +108,10 @@ export function Files() {
if (files.includes(cid)) {
WebStorage.folders.deleteFile(folder, cid);
setFolders(
folders.map(([name, files]) =>
name === folder
? [name, files.filter((id) => id !== cid)]
: [name, files]
)
);
setFolders(FilesUtils.removeCidFromFolder(folders, folder, cid));
} else {
WebStorage.folders.addFile(folder, cid);
setFolders(
folders.map(([name, files]) =>
name === folder ? [name, [...files, cid]] : [name, files]
)
);
setFolders(FilesUtils.addCidToFolder(folders, folder, cid));
}
};
@ -151,20 +139,7 @@ export function Files() {
return;
}
setSortFn(
() =>
(
{ manifest: { filename: afilename } }: CodexDataContent,
{ manifest: { filename: bfilename } }: CodexDataContent
) =>
state === "desc"
? (bfilename || "")
.toLocaleLowerCase()
.localeCompare((afilename || "").toLocaleLowerCase())
: (afilename || "")
.toLocaleLowerCase()
.localeCompare((bfilename || "").toLocaleLowerCase())
);
setSortFn(() => FilesUtils.sortByName(state));
};
const onSortBySize = (state: TabSortState) => {
@ -173,12 +148,7 @@ export function Files() {
return;
}
setSortFn(
() => (a: CodexDataContent, b: CodexDataContent) =>
state === "desc"
? b.manifest.datasetSize - a.manifest.datasetSize
: a.manifest.datasetSize - b.manifest.datasetSize
);
setSortFn(() => FilesUtils.sortBySize(state));
};
const onSortByDate = (state: TabSortState) => {
@ -187,30 +157,11 @@ export function Files() {
return;
}
setSortFn(
() => (a: CodexDataContent, b: CodexDataContent) =>
state === "desc"
? new Date(b.manifest.uploadedAt).getTime() -
new Date(a.manifest.uploadedAt).getTime()
: new Date(a.manifest.uploadedAt).getTime() -
new Date(b.manifest.uploadedAt).getTime()
);
setSortFn(() => FilesUtils.sortByDate(state));
};
const onToggleFilter = (filter: string) =>
selectedFilters.includes(filter)
? setSelectedFilters(selectedFilters.filter((f) => f !== filter))
: setSelectedFilters([...selectedFilters, filter]);
tabs.unshift({
label: "All",
Icon: () => <AllFilesIcon></AllFilesIcon>,
});
const items =
index === 0
? files
: files.filter((file) => folders[index - 1][1].includes(file.cid));
setSelectedFilters(FilesUtils.toggleFilters(selectedFilters, filter));
const headers = [
["file", onSortByFilename],
@ -219,14 +170,8 @@ export function Files() {
["actions"],
] satisfies [string, ((state: TabSortState) => void)?][];
const filtered = items.filter(
(item) =>
selectedFilters.length === 0 ||
selectedFilters.includes(F.type(item.manifest.mimetype)) ||
(selectedFilters.includes("archive") &&
F.isArchive(item.manifest.mimetype))
);
const items = FilesUtils.listInFolder(files, folders, index);
const filtered = FilesUtils.applyFilters(items, selectedFilters);
const sorted = sortFn ? [...filtered].sort(sortFn) : filtered;
const rows =
sorted.map((c) => (
@ -243,6 +188,11 @@ export function Files() {
]}></Row>
)) || [];
tabs.unshift({
label: "All",
Icon: () => <AllFilesIcon></AllFilesIcon>,
});
return (
<div className="card files">
<header>
@ -282,13 +232,7 @@ export function Files() {
selected={selectedFilters}
/>
<div>
<Table
headers={headers}
rows={rows.slice(0, 4)}
defaultSortIndex={2}
/>
</div>
<Table headers={headers} rows={rows.slice(0, 4)} defaultSortIndex={2} />
<FileDetails onClose={onClose} details={details} />
</main>

View File

@ -0,0 +1,295 @@
import { assert, describe, it } from "vitest";
import { FilesUtils } from "./files.utils";
describe("files", () => {
it("sorts by name", async () => {
const a = {
cid: "", manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: 0
}
}
const b = {
cid: "", manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "b",
mimetype: null,
uploadedAt: 0
}
}
const items = [a, b,]
const descSorted = items.slice().sort(FilesUtils.sortByName("desc"))
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(FilesUtils.sortByName("asc"))
assert.deepEqual(ascSorted, [a, b]);
});
it("sorts by size", async () => {
const a = {
cid: "", manifest: {
datasetSize: 1000,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: 0
}
}
const b = {
cid: "", manifest: {
datasetSize: 2000,
blockSize: 0,
protected: false,
treeCid: "",
filename: "b",
mimetype: null,
uploadedAt: 0
}
}
const items = [a, b,]
const descSorted = items.slice().sort(FilesUtils.sortBySize("desc"))
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(FilesUtils.sortBySize("asc"))
assert.deepEqual(ascSorted, [a, b]);
});
it("sorts by date", async () => {
const now = new Date()
const a = {
cid: "", manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: now.getTime()
}
}
now.setDate(now.getDate() - 1)
const b = {
cid: "", manifest: {
datasetSize: 2000,
blockSize: 0,
protected: false,
treeCid: "",
filename: "b",
mimetype: null,
uploadedAt: now.getTime()
}
}
const items = [a, b,]
const descSorted = items.slice().sort(FilesUtils.sortBySize("desc"))
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(FilesUtils.sortBySize("asc"))
assert.deepEqual(ascSorted, [a, b]);
});
it("returns true when a file is an image", async () => {
assert.deepEqual(FilesUtils.isImage("image/jpg"), true);
assert.deepEqual(FilesUtils.isImage("video/mp4"), false);
assert.deepEqual(FilesUtils.isImage(null), false);
});
it("returns true when a file is a video", async () => {
assert.deepEqual(FilesUtils.isVideo("video/mp4"), true);
assert.deepEqual(FilesUtils.isVideo("image/jpg"), false);
assert.deepEqual(FilesUtils.isImage(null), false);
});
it("returns true when a file is an archive", async () => {
assert.deepEqual(FilesUtils.isArchive("application/zip"), true);
assert.deepEqual(FilesUtils.isArchive("video/mp4"), false);
assert.deepEqual(FilesUtils.isArchive(null), false);
});
it("gets the type of a file", async () => {
assert.deepEqual(FilesUtils.type("application/zip"), "archive");
});
it("fallbacks to document when the mimetype is not known", async () => {
assert.deepEqual(FilesUtils.type("application/octet-stream"), "document");
});
it("removes a cid from a folder", async () => {
const folders = [["favorites", ["123", "456"]]] satisfies [string, string[]][]
const folder = "favorites"
const cid = "456"
assert.deepEqual(FilesUtils.removeCidFromFolder(folders, folder, cid), [["favorites", ["123"]]]);
});
it("adds a cid from to a folder", async () => {
const folders = [["favorites", ["123"]]] satisfies [string, string[]][]
const folder = "favorites"
const cid = "456"
assert.deepEqual(FilesUtils.addCidToFolder(folders, folder, cid), [["favorites", ["123", cid]]]);
});
it("returns true when the folder exists", async () => {
const folders = [["favorites", []]] satisfies [string, string[]][]
assert.deepEqual(FilesUtils.exists(folders, "favorites"), true);
});
it("toggles filter", async () => {
const filters = FilesUtils.toggleFilters(["images"], "archives")
assert.deepEqual(filters, ["images", "archives"]);
assert.deepEqual(FilesUtils.toggleFilters(filters, "archives"), ["images"]);
});
it("list all files when the first item is selected", async () => {
const folders = [["favorites", ["123"]], ["hello", ["456"]]] satisfies [string, string[]][]
const files = [
{
cid: "123", manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: 0
}
},
{
cid: "456",
manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: 0
}
}
]
assert.deepEqual(FilesUtils.listInFolder(files, folders, 0), files);
});
it("list all files in favorites", async () => {
const folders = [["favorites", ["123"]], ["hello", ["456"]]] satisfies [string, string[]][]
const files = [
{
cid: "123", manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: 0
}
},
{
cid: "456",
manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: 0
}
}
]
assert.deepEqual(FilesUtils.listInFolder(files, folders, 1), [files[0]]);
});
it("returns all files when no filter is selected", async () => {
const files = [
{
cid: "123", manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: 0
}
},
{
cid: "456",
manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: null,
uploadedAt: 0
}
}
]
assert.deepEqual(FilesUtils.applyFilters(files, []), files);
});
it("returns apply filter by mimetype", async () => {
const files = [
{
cid: "123", manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: "image/jpg",
uploadedAt: 0
}
},
{
cid: "456",
manifest: {
datasetSize: 0,
blockSize: 0,
protected: false,
treeCid: "",
filename: "a",
mimetype: "application/zip",
uploadedAt: 0
}
}
]
assert.deepEqual(FilesUtils.applyFilters(files, ["archive"]), [files[1]]);
});
})

View File

@ -0,0 +1,98 @@
import { TabSortState } from "@codex-storage/marketplace-ui-components";
import { CodexDataContent } from "@codex-storage/sdk-js";
const archiveMimetypes = [
"application/zip",
"application/x-rar-compressed",
"application/x-tar",
"application/gzip",
"application/x-7z-compressed",
"application/gzip", // for .tar.gz
"application/x-bzip2",
"application/x-xz",
];
export const FilesUtils = {
isImage(type: string | null) {
return !!type && type.startsWith("image");
},
isVideo(type: string | null) {
return !!type && type.startsWith("video");
},
isArchive(mimetype: string | null) {
return !!mimetype && archiveMimetypes.includes(mimetype)
},
type(mimetype: string | null) {
if (FilesUtils.isArchive(mimetype)) {
return "archive"
}
if (FilesUtils.isVideo(mimetype)) {
return "video"
}
if (FilesUtils.isImage(mimetype)) {
return "image"
}
return "document"
},
sortByName: (state: TabSortState) =>
(a: CodexDataContent, b: CodexDataContent) => {
const { manifest: { filename: afilename } } = a
const { manifest: { filename: bfilename } } = b
return state === "desc"
? (bfilename || "")
.toLocaleLowerCase()
.localeCompare((afilename || "").toLocaleLowerCase())
: (afilename || "")
.toLocaleLowerCase()
.localeCompare((bfilename || "").toLocaleLowerCase())
},
sortBySize: (state: TabSortState) =>
(a: CodexDataContent, b: CodexDataContent) => state === "desc"
? b.manifest.datasetSize - a.manifest.datasetSize
: a.manifest.datasetSize - b.manifest.datasetSize
,
sortByDate: (state: TabSortState) =>
(a: CodexDataContent, b: CodexDataContent) => state === "desc"
? new Date(b.manifest.uploadedAt).getTime() -
new Date(a.manifest.uploadedAt).getTime()
: new Date(a.manifest.uploadedAt).getTime() -
new Date(b.manifest.uploadedAt).getTime()
,
removeCidFromFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] {
return folders.map(([name, files]) =>
name === folder
? [name, files.filter((id) => id !== cid)]
: [name, files]
)
},
addCidToFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] {
return folders.map(([name, files]) =>
name === folder ? [name, [...files, cid]] : [name, files]
)
},
exists(folders: [string, string[]][], name: string) {
return !!folders.find(([folder]) => folder === name)
},
toggleFilters: (filters: string[], filter: string) => filters.includes(filter)
? filters.filter((f) => f !== filter)
: [...filters, filter],
listInFolder(files: CodexDataContent[], folders: [string, string[]][], index: number) {
return index === 0
? files
: files.filter((file) => folders[index - 1][1].includes(file.cid));
},
applyFilters(files: CodexDataContent[], filters: string[]) {
return files.filter(
(file) => filters.length === 0 || filters.includes(this.type(file.manifest.mimetype))
)
}
};
export type CodexFileMetadata = {
type: string;
name: string;
};

View File

@ -12,7 +12,7 @@ import { classnames } from "../../utils/classnames";
import { RefreshIcon } from "../RefreshIcon/RefreshIcon";
import "./HealthChecks.css";
import { CodexSdk } from "../../sdk/codex";
import { HealthCheckUtil } from "./health-check.util";
import { HealthCheckUtil } from "./health-check.utils";
import { PortForwardingUtil } from "../../hooks/port-forwarding.util";
type Props = {

View File

@ -1,5 +1,5 @@
import { assert, describe, it } from "vitest";
import { HealthCheckUtil } from "./health-check.util";
import { HealthCheckUtil } from "./health-check.utils";
describe("health check", () => {
it("remove the port from an url", async () => {

View File

@ -1,12 +1,8 @@
import { useQuery } from "@tanstack/react-query";
import Loader from "../../assets/loader.svg";
import { CodexSdk } from "../../sdk/codex";
import {
Button,
SpaceAllocation,
} from "@codex-storage/marketplace-ui-components";
import { SpaceAllocation } from "@codex-storage/marketplace-ui-components";
import { Promises } from "../../utils/promises";
import { NodesIcon } from "../Menu/NodesIcon";
import "./NodeSpace.css";
const defaultSpace = {
@ -45,36 +41,27 @@ export function NodeSpace() {
const { quotaMaxBytes, quotaReservedBytes, quotaUsedBytes } = space;
return (
<div className="card node-space">
<header>
<div>
<NodesIcon variant="default"></NodesIcon>
<h5>Storage</h5>
</div>
<Button label="Details" variant="outline"></Button>
</header>
<main>
<h6>Disk</h6>
<main className="node-space">
<h6>Disk</h6>
<SpaceAllocation
data={[
{
title: "Allocated",
size: quotaUsedBytes,
color: "#FF6E61",
},
{
title: "Available",
size: quotaReservedBytes,
color: "#34A0FF",
},
{
title: "Free",
size: quotaMaxBytes - quotaReservedBytes - quotaUsedBytes,
color: "#6F6F6F",
},
]}></SpaceAllocation>
</main>
</div>
<SpaceAllocation
data={[
{
title: "Allocated",
size: quotaUsedBytes,
color: "#FF6E61",
},
{
title: "Available",
size: quotaReservedBytes,
color: "#34A0FF",
},
{
title: "Free",
size: quotaMaxBytes - quotaReservedBytes - quotaUsedBytes,
color: "#6F6F6F",
},
]}></SpaceAllocation>
</main>
);
}

View File

@ -1,6 +1,6 @@
import { Cell } from "@codex-storage/marketplace-ui-components";
import "./PeerCountryCell.css";
import { PeerGeo, PeerNode, PeerUtils } from "./peers.util";
import { PeerGeo, PeerNode, PeerUtils } from "./peers.utils";
export type Props = {
node: PeerNode;

View File

@ -10,7 +10,7 @@ import { PeersIcon } from "../Menu/PeersIcon";
import { PeerCountryCell } from "./PeerCountryCell";
import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon";
import "./Peers.css";
import { PeerGeo, PeerNode, PeerSortFn, PeerUtils } from "./peers.util";
import { PeerGeo, PeerNode, PeerSortFn, PeerUtils } from "./peers.utils";
import { PeersMap } from "./PeersMap";
import { useDebug } from "../../hooks/useDebug";
import { PeersQuality } from "./PeersQuality";

View File

@ -2,7 +2,7 @@ import { PeersIcon } from "../Menu/PeersIcon";
import { PeersMap } from "./PeersMap";
import "./PeersCard.css";
import { useDebug } from "../../hooks/useDebug";
import { PeerUtils } from "./peers.util";
import { PeerUtils } from "./peers.utils";
import { PeersChart } from "./PeersChart";
import { PeersQuality } from "./PeersQuality";
import { Button } from "@codex-storage/marketplace-ui-components";

View File

@ -1,6 +1,6 @@
import { getMapJSON } from "dotted-map";
import DottedMap from "dotted-map/without-countries";
import { PeerGeo, PeerNode, PeerUtils } from "./peers.util";
import { PeerGeo, PeerNode, PeerUtils } from "./peers.utils";
import { useCallback, useState } from "react";
import { PeersPin } from "./PeersPin";
import "./PeersMap.css";

View File

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { PeerGeo, PeerNode } from "./peers.util";
import { PeerGeo, PeerNode } from "./peers.utils";
import { useEffect } from "react";
type Props = {

View File

@ -1,5 +1,5 @@
import { assert, describe, it } from "vitest";
import { PeerGeo, PeerUtils } from "./peers.util";
import { PeerGeo, PeerUtils } from "./peers.utils";
describe("peers", () => {
it("sorts by boolean", async () => {

View File

@ -2,7 +2,7 @@ import { useDebug } from "../../hooks/useDebug";
import { AlphaText } from "../AlphaText/AlphaText";
import { AlphaIcon } from "../OnBoarding/AlphaIcon";
import "./Versions.css";
import { VersionsUtil } from "./versions.util";
import { VersionsUtil } from "./versions.utils";
const throwOnError = false;

View File

@ -1,5 +1,5 @@
import { assert, describe, it } from "vitest";
import { VersionsUtil } from "./versions.util";
import { VersionsUtil } from "./versions.utils";
describe("versions", () => {
it("gets the last client version", async () => {

View File

@ -1,5 +1,125 @@
.availabilities {
height: 100%;
display: flex;
flex-wrap: wrap;
gap: 16px;
> .card {
flex: 1 1 60%;
}
.table {
table thead tr th {
background-color: #14141499;
}
table tbody tr.availabilty-row {
td {
background-color: #292929;
padding: 6px 12px;
&:first-child {
cursor: pointer;
transition: transform 0.35s;
& svg {
transition: transform 0.35s;
}
& svg[aria-expanded] {
transform: rotate(-90deg);
}
}
}
}
td {
b {
font-family: Inter;
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: -0.011em;
text-align: left;
display: block;
}
small {
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: 16px;
text-align: left;
color: #ffffffcc;
}
}
}
aside {
display: flex;
width: 400px;
.card {
flex: 1;
}
main {
> div {
position: relative;
.button {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: auto;
border-radius: 100%;
height: 6rem;
width: 6rem;
background-color: white;
}
}
> .button {
width: 100%;
gap: 4px;
}
}
.node-space {
border-bottom: 1px solid #96969633;
padding-bottom: 16px;
h6 {
border-top: none;
}
}
footer {
padding: 16px 0;
b {
display: block;
font-family: Inter;
font-size: 18px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.015em;
text-align: left;
}
small {
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.006em;
text-align: left;
color: #969696cc;
}
}
}
}
.availabilities-actions {
@ -14,24 +134,8 @@
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 {

View File

@ -1,30 +1,34 @@
import { createFileRoute } from '@tanstack/react-router'
import { ErrorBoundary } from '@sentry/react'
import { ErrorPlaceholder } from '../../components/ErrorPlaceholder/ErrorPlaceholder'
import { createFileRoute } from "@tanstack/react-router";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
import {
Button,
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 { AvailabilitiesTable } from '../../components/Availability/AvailabilitiesTable'
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 { AvailabilityWithSlots } from '../../components/Availability/types'
import { WebStorage } from '../../utils/web-storage'
UploadIcon,
} 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 { AvailabilitiesTable } from "../../components/Availability/AvailabilitiesTable";
import { AvailabilityEdit } from "../../components/Availability/AvailabilityEdit";
import { Strings } from "../../utils/strings";
import { PrettyBytes } from "../../utils/bytes";
import { Sunburst } from "../../components/Availability/Sunburst";
import { Errors } from "../../utils/errors";
import { availabilityColors } from "../../components/Availability/availability.colors";
import { AvailabilityWithSlots } from "../../components/Availability/types";
import { WebStorage } from "../../utils/web-storage";
import { NodeSpace } from "../../components/NodeSpace/NodeSpace";
import { PlusCircle } from "lucide-react";
const defaultSpace = {
quotaMaxBytes: 0,
quotaReservedBytes: 0,
quotaUsedBytes: 0,
totalBlocks: 0,
}
};
export function Availabilities() {
{
@ -44,22 +48,22 @@ export function Availabilities() {
.reservations(a.id)
.then((res) => {
if (res.error) {
Errors.report(res)
return { ...a, slots: [] }
Errors.report(res);
return { ...a, slots: [] };
}
return { ...a, slots: res.data }
return { ...a, slots: res.data };
})
.then((data) =>
WebStorage.availabilities.get(data.id).then((n) => ({
...data,
name: n || '',
})),
),
),
),
name: n || "",
}))
)
)
)
),
queryKey: ['availabilities'],
queryKey: ["availabilities"],
initialData: [],
// .then((res) =>
@ -79,7 +83,7 @@ export function Availabilities() {
// Throw the error to the error boundary
throwOnError: true,
})
});
// Error will be catched in ErrorBounday
const { data: space = defaultSpace } = useQuery({
@ -87,7 +91,7 @@ export function Availabilities() {
CodexSdk.data()
.space()
.then((s) => Promises.rejectOnError(s)),
queryKey: ['space'],
queryKey: ["space"],
initialData: defaultSpace,
// No need to retry because if the connection to the node
@ -104,70 +108,86 @@ export function Availabilities() {
// Throw the error to the error boundary
throwOnError: true,
})
});
const allocation: SpaceAllocationItem[] = availabilities.map(
(a, index) => ({
title: Strings.shortId(a.id),
size: a.totalSize,
tooltip: a.id + '\u000D\u000A' + PrettyBytes(a.totalSize),
tooltip: a.id + "\u000D\u000A" + PrettyBytes(a.totalSize),
color: availabilityColors[index],
}),
)
})
);
allocation.push({
title: 'Space remaining',
title: "Space remaining",
// TODO move this to domain
size:
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes,
color: 'transparent',
})
color: "transparent",
});
if (isPending) {
return (
<div className="purchases-loader">
<Spinner width="3rem" />
</div>
);
}
return (
<div className="container">
<div className="availabilities-content">
{isPending ? (
<div className="purchases-loader">
<Spinner width="3rem" />
</div>
) : (
<>
<div className="availabilities-header">
<AvailabilitySunburst
availabilities={availabilities}
space={space}
></AvailabilitySunburst>
<AvailabilityEdit
space={space}
className="availabilities-create"
hasLabel={false}
/>
</div>
<div className="availabilities-table">
<AvailabilitiesTable
space={space}
// onEdit={onOpen}
availabilities={availabilities}
/>
</div>
</>
)}
<div className="availabilities">
<div className="card">
<AvailabilitiesTable
space={space}
// onEdit={onOpen}
availabilities={availabilities}
/>
</div>
<aside>
<div className="card">
<header>
<div>
<UploadIcon width={18}></UploadIcon>
<h5>Host</h5>
</div>
</header>
<main>
<div>
<Sunburst
availabilities={availabilities}
space={space}></Sunburst>
<AvailabilityEdit space={space} hasLabel={false} />
</div>
<Button
Icon={PlusCircle}
label="Create Availability"
variant="outline"></Button>
<NodeSpace></NodeSpace>
</main>
<footer>
<b>Node</b>
<small>
{PrettyBytes(space.quotaMaxBytes)} allocated for the node
</small>
</footer>
</div>
</aside>
</div>
)
);
}
}
export const Route = createFileRoute('/dashboard/availabilities')({
export const Route = createFileRoute("/dashboard/availabilities")({
component: () => (
<ErrorBoundary
fallback={({ error }) => (
<ErrorPlaceholder error={error} subtitle="Cannot retrieve the data." />
)}
>
)}>
<Availabilities />
</ErrorBoundary>
),
})
});

View File

@ -8,7 +8,7 @@
flex: 1 1 67%;
}
.column {
aside {
display: flex;
flex-direction: column;
gap: 16px;

View File

@ -12,7 +12,7 @@ export const Route = createFileRoute("/dashboard/files")({
<div className="files-page">
<Files></Files>
<div className="column">
<aside>
<ErrorBoundary
fallback={({ error }) => (
<ErrorPlaceholder
@ -24,7 +24,7 @@ export const Route = createFileRoute("/dashboard/files")({
</ErrorBoundary>
<Download></Download>
<ManifestFetchCard />
</div>
</aside>
</div>
),
});

View File

@ -12,6 +12,8 @@ import { NodeSpace } from "../../components/NodeSpace/NodeSpace.tsx";
import { UploadCard } from "../../components/UploadCard/UploadCard.tsx";
import { ManifestFetchCard } from "../../components/ManifestFetch/ManifestFetchCard.tsx";
import { PeersCard } from "../../components/Peers/PeersCard.tsx";
import { Card } from "../../components/Card/Card.tsx";
import { NodesIcon } from "../../components/Menu/NodesIcon.tsx";
export const Route = createFileRoute("/dashboard/")({
component: Dashboard,
@ -44,7 +46,12 @@ function Dashboard() {
subtitle="Cannot retrieve the data."
/>
)}>
<NodeSpace></NodeSpace>
<Card
icon={<NodesIcon variant="default"></NodesIcon>}
title="Storage"
buttonLabel="Details">
<NodeSpace></NodeSpace>
</Card>
</ErrorBoundary>
<PeersCard></PeersCard>
</div>

View File

@ -1,31 +0,0 @@
const archiveMimetypes = [
"application/zip",
"application/x-rar-compressed",
"application/x-tar",
"application/gzip",
"application/x-7z-compressed",
"application/gzip", // for .tar.gz
"application/x-bzip2",
"application/x-xz",
];
export const Files = {
isImage(type: string | null) {
return type && type.startsWith("image");
},
isVideo(type: string | null) {
return type && type.startsWith("video");
},
type(mimetype: string | null) {
const [type] = mimetype?.split("/") || []
return type
},
isArchive(mimetype: string | null) {
return mimetype && archiveMimetypes.includes(mimetype)
}
};
export type CodexFileMetadata = {
type: string;
name: string;
};