mirror of
https://github.com/logos-storage/logos-storage-marketplace-ui.git
synced 2026-01-04 06:23:08 +00:00
Add availabilities UI
This commit is contained in:
parent
4a5a6aee78
commit
cfc5015ecc
52
package-lock.json
generated
52
package-lock.json
generated
@ -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"
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
.availabilityTable-chevron {
|
||||
cursor: pointer;
|
||||
transition: transform 0.35s;
|
||||
}
|
||||
|
||||
.availabilityTable-chevron--open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
.availability-edit {
|
||||
.button div {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
@media (min-width: 801px) {
|
||||
.availabilityCreate .stepper-body {
|
||||
width: 600px;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
.availabilityIdCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
@ -1,5 +0,0 @@
|
||||
.activity-sunburst {
|
||||
height: 600px;
|
||||
width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
20
src/components/Availability/ChevronDown.tsx
Normal file
20
src/components/Availability/ChevronDown.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
src/components/Availability/EditIcon.tsx
Normal file
15
src/components/Availability/EditIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
src/components/Availability/FolderIcon.tsx
Normal file
15
src/components/Availability/FolderIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
src/components/Availability/HostIcon.tsx
Normal file
15
src/components/Availability/HostIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
src/components/Availability/SlotIcon.tsx
Normal file
15
src/components/Availability/SlotIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
src/components/Availability/SlotRow.css
Normal file
9
src/components/Availability/SlotRow.css
Normal file
@ -0,0 +1,9 @@
|
||||
.slot-row {
|
||||
transition:
|
||||
visibility 0.35s,
|
||||
max-height 0.35s;
|
||||
|
||||
&.slot-row--inactive {
|
||||
visibility: collapse;
|
||||
}
|
||||
}
|
||||
56
src/components/Availability/SlotRow.tsx
Normal file
56
src/components/Availability/SlotRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/components/Availability/Sunburst.css
Normal file
5
src/components/Availability/Sunburst.css
Normal file
@ -0,0 +1,5 @@
|
||||
.sunburst {
|
||||
height: 350px;
|
||||
width: 350px;
|
||||
margin: auto;
|
||||
}
|
||||
@ -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>;
|
||||
}
|
||||
@ -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",
|
||||
];
|
||||
32
src/components/Card/Card.css
Normal file
32
src/components/Card/Card.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/components/Card/Card.tsx
Normal file
39
src/components/Card/Card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
295
src/components/Files/files.utils.test.ts
Normal file
295
src/components/Files/files.utils.test.ts
Normal 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]]);
|
||||
});
|
||||
})
|
||||
98
src/components/Files/files.utils.ts
Normal file
98
src/components/Files/files.utils.ts
Normal 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;
|
||||
};
|
||||
@ -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 = {
|
||||
|
||||
@ -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 () => {
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 () => {
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
),
|
||||
})
|
||||
});
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
flex: 1 1 67%;
|
||||
}
|
||||
|
||||
.column {
|
||||
aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
@ -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>
|
||||
),
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user