diff --git a/package-lock.json b/package-lock.json index 35ea7eb..420c11e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/package.json b/package.json index 6117fdc..b08d812 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 91f7f40..375a06f 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -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 = { settings: , files: , logs: , + availabilies: , }; const descriptions: Record = { @@ -32,6 +34,7 @@ const descriptions: Record = { 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) { diff --git a/src/components/Availability/AvailabilitiesTable.css b/src/components/Availability/AvailabilitiesTable.css deleted file mode 100644 index 23b14f0..0000000 --- a/src/components/Availability/AvailabilitiesTable.css +++ /dev/null @@ -1,8 +0,0 @@ -.availabilityTable-chevron { - cursor: pointer; - transition: transform 0.35s; -} - -.availabilityTable-chevron--open { - transform: rotate(180deg); -} diff --git a/src/components/Availability/AvailabilitiesTable.tsx b/src/components/Availability/AvailabilitiesTable.tsx index 78c156d..f31e300 100644 --- a/src/components/Availability/AvailabilitiesTable.tsx +++ b/src/components/Availability/AvailabilitiesTable.tsx @@ -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 ( {hasSlots ? ( ) : ( - + "" )} , - , + , {PrettyBytes(a.totalSize)}, {Times.pretty(a.duration)}, {a.minPrice.toString()}, @@ -69,11 +66,11 @@ export function AvailabilitiesTable({ availabilities, space }: Props) { ]}> {a.slots.map((slot) => ( - + id={slot.id}> ))} ); diff --git a/src/components/Availability/AvailabilityActionsCell.css b/src/components/Availability/AvailabilityActionsCell.css index 3e3a39c..37c5352 100644 --- a/src/components/Availability/AvailabilityActionsCell.css +++ b/src/components/Availability/AvailabilityActionsCell.css @@ -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; + } } diff --git a/src/components/Availability/AvailabilityActionsCell.tsx b/src/components/Availability/AvailabilityActionsCell.tsx index 3872139..b233e82 100644 --- a/src/components/Availability/AvailabilityActionsCell.tsx +++ b/src/components/Availability/AvailabilityActionsCell.tsx @@ -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 (
- - - + onDetails(content.cid)} + Icon={EditIcon}>
); diff --git a/src/components/Availability/AvailabilityDiskRow.css b/src/components/Availability/AvailabilityDiskRow.css deleted file mode 100644 index d27469c..0000000 --- a/src/components/Availability/AvailabilityDiskRow.css +++ /dev/null @@ -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; -} diff --git a/src/components/Availability/AvailabilityDiskRow.tsx b/src/components/Availability/AvailabilityDiskRow.tsx index 247bef6..56c82bc 100644 --- a/src/components/Availability/AvailabilityDiskRow.tsx +++ b/src/components/Availability/AvailabilityDiskRow.tsx @@ -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 ( + , - -
- + +
+
-
- Node -
- - {PrettyBytes(bytes)} allocated for the node - + Node + {PrettyBytes(bytes)} allocated for the node
, ]}> ); } - -const HardDrive = () => ( - - - - - - - - - -); diff --git a/src/components/Availability/AvailabilityEdit.css b/src/components/Availability/AvailabilityEdit.css index 6cb96ca..246f5dd 100644 --- a/src/components/Availability/AvailabilityEdit.css +++ b/src/components/Availability/AvailabilityEdit.css @@ -1,3 +1,10 @@ +.availability-edit { + .button div { + width: 40px; + height: 40px; + color: black; + } +} @media (min-width: 801px) { .availabilityCreate .stepper-body { width: 600px; diff --git a/src/components/Availability/AvailabilityEdit.tsx b/src/components/Availability/AvailabilityEdit.tsx index 5c8a8ed..f785ce4 100644 --- a/src/components/Availability/AvailabilityEdit.tsx +++ b/src/components/Availability/AvailabilityEdit.tsx @@ -152,12 +152,12 @@ export function AvailabilityEdit({ const nextLabel = state.step === steps.current.length - 1 ? "Finish" : "Next"; return ( - <> +
); } diff --git a/src/components/Availability/AvailabilityIdCell.css b/src/components/Availability/AvailabilityIdCell.css deleted file mode 100644 index 263df35..0000000 --- a/src/components/Availability/AvailabilityIdCell.css +++ /dev/null @@ -1,5 +0,0 @@ -.availabilityIdCell { - display: flex; - align-items: center; - gap: 1rem; -} diff --git a/src/components/Availability/AvailabilityIdCell.tsx b/src/components/Availability/AvailabilityIdCell.tsx index 41768e0..c39a806 100644 --- a/src/components/Availability/AvailabilityIdCell.tsx +++ b/src/components/Availability/AvailabilityIdCell.tsx @@ -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 ( -
- +
+
{value.name || Strings.shortId(value.id)} @@ -22,36 +20,12 @@ export function AvailabilityIdCell({ value, index }: Props) { {PrettyBytes(value.totalSize)} allocated for the availability -
- - Max collateral {value.maxCollateral} | Min price {value.minPrice} - -
+
+ + Max collateral {value.maxCollateral} | Min price {value.minPrice} +
); } - -const Folder = ({ color }: { color: string }) => ( - - - - - -); diff --git a/src/components/Availability/AvailabilitySlotRow.css b/src/components/Availability/AvailabilitySlotRow.css deleted file mode 100644 index 5dd2217..0000000 --- a/src/components/Availability/AvailabilitySlotRow.css +++ /dev/null @@ -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; -} diff --git a/src/components/Availability/AvailabilitySlotRow.tsx b/src/components/Availability/AvailabilitySlotRow.tsx deleted file mode 100644 index ee99d96..0000000 --- a/src/components/Availability/AvailabilitySlotRow.tsx +++ /dev/null @@ -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 ( - - - , - -
- -
-
- Slot {id} -
- - {PrettyBytes(bytes)} allocated for the slot - -
-
-
, - ]}>
- ); -} - -const SlotIcon = () => ( - - - - - -); diff --git a/src/components/Availability/AvailabilitySunburst.css b/src/components/Availability/AvailabilitySunburst.css deleted file mode 100644 index 86c8e96..0000000 --- a/src/components/Availability/AvailabilitySunburst.css +++ /dev/null @@ -1,5 +0,0 @@ -.activity-sunburst { - height: 600px; - width: 600px; - margin: auto; -} diff --git a/src/components/Availability/ChevronDown.tsx b/src/components/Availability/ChevronDown.tsx new file mode 100644 index 0000000..a078628 --- /dev/null +++ b/src/components/Availability/ChevronDown.tsx @@ -0,0 +1,20 @@ +type Props = { + onClick?: () => void; +}; + +export function ChevronDown({ onClick, ...rest }: Props) { + return ( + + + + ); +} diff --git a/src/components/Availability/EditIcon.tsx b/src/components/Availability/EditIcon.tsx new file mode 100644 index 0000000..092381f --- /dev/null +++ b/src/components/Availability/EditIcon.tsx @@ -0,0 +1,15 @@ +export function EditIcon() { + return ( + + + + ); +} diff --git a/src/components/Availability/FolderIcon.tsx b/src/components/Availability/FolderIcon.tsx new file mode 100644 index 0000000..548dae9 --- /dev/null +++ b/src/components/Availability/FolderIcon.tsx @@ -0,0 +1,15 @@ +export function FolderIcon() { + return ( + + + + ); +} diff --git a/src/components/Availability/HostIcon.tsx b/src/components/Availability/HostIcon.tsx new file mode 100644 index 0000000..0a28e28 --- /dev/null +++ b/src/components/Availability/HostIcon.tsx @@ -0,0 +1,15 @@ +export function HostIcon() { + return ( + + + + ); +} diff --git a/src/components/Availability/SlotIcon.tsx b/src/components/Availability/SlotIcon.tsx new file mode 100644 index 0000000..16df62f --- /dev/null +++ b/src/components/Availability/SlotIcon.tsx @@ -0,0 +1,15 @@ +export function SlotIcon() { + return ( + + + + ); +} diff --git a/src/components/Availability/SlotRow.css b/src/components/Availability/SlotRow.css new file mode 100644 index 0000000..f9763f1 --- /dev/null +++ b/src/components/Availability/SlotRow.css @@ -0,0 +1,9 @@ +.slot-row { + transition: + visibility 0.35s, + max-height 0.35s; + + &.slot-row--inactive { + visibility: collapse; + } +} diff --git a/src/components/Availability/SlotRow.tsx b/src/components/Availability/SlotRow.tsx new file mode 100644 index 0000000..27301d2 --- /dev/null +++ b/src/components/Availability/SlotRow.tsx @@ -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 ( + + + , + +
+ +
+ Slot {id} + {PrettyBytes(bytes)} allocated for the slot +
+
+
, + ]}>
+ ); +} diff --git a/src/components/Availability/Sunburst.css b/src/components/Availability/Sunburst.css new file mode 100644 index 0000000..80d6ecd --- /dev/null +++ b/src/components/Availability/Sunburst.css @@ -0,0 +1,5 @@ +.sunburst { + height: 350px; + width: 350px; + margin: auto; +} diff --git a/src/components/Availability/AvailabilitySunburst.tsx b/src/components/Availability/Sunburst.tsx similarity index 87% rename from src/components/Availability/AvailabilitySunburst.tsx rename to src/components/Availability/Sunburst.tsx index 5179d4a..e9f55f5 100644 --- a/src/components/Availability/AvailabilitySunburst.tsx +++ b/src/components/Availability/Sunburst.tsx @@ -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(null); const chart = useRef(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
; + return
; } diff --git a/src/components/Availability/availability.colors.ts b/src/components/Availability/availability.colors.ts index 478b5f9..be484b2 100644 --- a/src/components/Availability/availability.colors.ts +++ b/src/components/Availability/availability.colors.ts @@ -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", ]; \ No newline at end of file diff --git a/src/components/Card/Card.css b/src/components/Card/Card.css new file mode 100644 index 0000000..543f0d2 --- /dev/null +++ b/src/components/Card/Card.css @@ -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; + } + } +} diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx new file mode 100644 index 0000000..db16fd8 --- /dev/null +++ b/src/components/Card/Card.tsx @@ -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 ( +
+
+
+ {icon} +
{title}
+
+ {buttonLabel && ( + + )} +
+ {children} +
+ ); +} diff --git a/src/components/Files/FileDetails.tsx b/src/components/Files/FileDetails.tsx index 745325c..1222ea7 100644 --- a/src/components/Files/FileDetails.tsx +++ b/src/components/Files/FileDetails.tsx @@ -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) {
- {Files.isImage(details.manifest.mimetype) ? ( + {FilesUtils.isImage(details.manifest.mimetype) ? ( ) : (
diff --git a/src/components/Files/FileFilters.tsx b/src/components/Files/FileFilters.tsx index fd98c44..bc91451 100644 --- a/src/components/Files/FileFilters.tsx +++ b/src/components/Files/FileFilters.tsx @@ -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"; } diff --git a/src/components/Files/Files.css b/src/components/Files/Files.css index b12183f..f59bb95 100644 --- a/src/components/Files/Files.css +++ b/src/components/Files/Files.css @@ -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; -} diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index 1ea646c..d17fcd5 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -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: () => , - }); - - 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() { ]}> )) || []; + tabs.unshift({ + label: "All", + Icon: () => , + }); + return (
@@ -282,13 +232,7 @@ export function Files() { selected={selectedFilters} /> -
- - +
diff --git a/src/components/Files/files.utils.test.ts b/src/components/Files/files.utils.test.ts new file mode 100644 index 0000000..355d92e --- /dev/null +++ b/src/components/Files/files.utils.test.ts @@ -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]]); + }); +}) \ No newline at end of file diff --git a/src/components/Files/files.utils.ts b/src/components/Files/files.utils.ts new file mode 100644 index 0000000..3cffb64 --- /dev/null +++ b/src/components/Files/files.utils.ts @@ -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; +}; diff --git a/src/components/HealthChecks/HealthChecks.tsx b/src/components/HealthChecks/HealthChecks.tsx index cfa6684..976ae6b 100644 --- a/src/components/HealthChecks/HealthChecks.tsx +++ b/src/components/HealthChecks/HealthChecks.tsx @@ -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 = { diff --git a/src/components/HealthChecks/health-check.util.test.ts b/src/components/HealthChecks/health-check.utils.test.ts similarity index 98% rename from src/components/HealthChecks/health-check.util.test.ts rename to src/components/HealthChecks/health-check.utils.test.ts index 534a851..4565603 100644 --- a/src/components/HealthChecks/health-check.util.test.ts +++ b/src/components/HealthChecks/health-check.utils.test.ts @@ -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 () => { diff --git a/src/components/HealthChecks/health-check.util.ts b/src/components/HealthChecks/health-check.utils.ts similarity index 100% rename from src/components/HealthChecks/health-check.util.ts rename to src/components/HealthChecks/health-check.utils.ts diff --git a/src/components/NodeSpace/NodeSpace.tsx b/src/components/NodeSpace/NodeSpace.tsx index dc8c48c..0c65a20 100644 --- a/src/components/NodeSpace/NodeSpace.tsx +++ b/src/components/NodeSpace/NodeSpace.tsx @@ -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 ( -
-
-
- -
Storage
-
- -
-
-
Disk
+
+
Disk
- -
-
+ + ); } diff --git a/src/components/Peers/PeerCountryCell.tsx b/src/components/Peers/PeerCountryCell.tsx index 1232280..e1a2873 100644 --- a/src/components/Peers/PeerCountryCell.tsx +++ b/src/components/Peers/PeerCountryCell.tsx @@ -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; diff --git a/src/components/Peers/Peers.tsx b/src/components/Peers/Peers.tsx index 11cd5ce..9657b83 100644 --- a/src/components/Peers/Peers.tsx +++ b/src/components/Peers/Peers.tsx @@ -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"; diff --git a/src/components/Peers/PeersCard.tsx b/src/components/Peers/PeersCard.tsx index a705426..759aa6e 100644 --- a/src/components/Peers/PeersCard.tsx +++ b/src/components/Peers/PeersCard.tsx @@ -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"; diff --git a/src/components/Peers/PeersMap.tsx b/src/components/Peers/PeersMap.tsx index 7391302..cc6fa09 100644 --- a/src/components/Peers/PeersMap.tsx +++ b/src/components/Peers/PeersMap.tsx @@ -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"; diff --git a/src/components/Peers/PeersPin.ts b/src/components/Peers/PeersPin.ts index d74c596..76c1149 100644 --- a/src/components/Peers/PeersPin.ts +++ b/src/components/Peers/PeersPin.ts @@ -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 = { diff --git a/src/components/Peers/peers.util.test.ts b/src/components/Peers/peers.utils.test.ts similarity index 97% rename from src/components/Peers/peers.util.test.ts rename to src/components/Peers/peers.utils.test.ts index 326a728..74cc315 100644 --- a/src/components/Peers/peers.util.test.ts +++ b/src/components/Peers/peers.utils.test.ts @@ -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 () => { diff --git a/src/components/Peers/peers.util.ts b/src/components/Peers/peers.utils.ts similarity index 100% rename from src/components/Peers/peers.util.ts rename to src/components/Peers/peers.utils.ts diff --git a/src/components/Versions/Versions.tsx b/src/components/Versions/Versions.tsx index 08e8ea8..4d2d984 100644 --- a/src/components/Versions/Versions.tsx +++ b/src/components/Versions/Versions.tsx @@ -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; diff --git a/src/components/Versions/versions.util.test.ts b/src/components/Versions/versions.utils.test.ts similarity index 85% rename from src/components/Versions/versions.util.test.ts rename to src/components/Versions/versions.utils.test.ts index 63b1805..56a5115 100644 --- a/src/components/Versions/versions.util.test.ts +++ b/src/components/Versions/versions.utils.test.ts @@ -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 () => { diff --git a/src/components/Versions/versions.util.ts b/src/components/Versions/versions.utils.ts similarity index 100% rename from src/components/Versions/versions.util.ts rename to src/components/Versions/versions.utils.ts diff --git a/src/routes/dashboard/availabilities.css b/src/routes/dashboard/availabilities.css index e65ce40..a70a824 100644 --- a/src/routes/dashboard/availabilities.css +++ b/src/routes/dashboard/availabilities.css @@ -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 { diff --git a/src/routes/dashboard/availabilities.tsx b/src/routes/dashboard/availabilities.tsx index 83b87e4..fc80b5c 100644 --- a/src/routes/dashboard/availabilities.tsx +++ b/src/routes/dashboard/availabilities.tsx @@ -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 ( +
+ +
+ ); + } return ( -
-
- {isPending ? ( -
- -
- ) : ( - <> -
- - - -
- -
- -
- - )} +
+
+
+
- ) + ); } } -export const Route = createFileRoute('/dashboard/availabilities')({ +export const Route = createFileRoute("/dashboard/availabilities")({ component: () => ( ( - )} - > + )}> ), -}) +}); diff --git a/src/routes/dashboard/files.css b/src/routes/dashboard/files.css index 2bbaf15..cf12930 100644 --- a/src/routes/dashboard/files.css +++ b/src/routes/dashboard/files.css @@ -8,7 +8,7 @@ flex: 1 1 67%; } - .column { + aside { display: flex; flex-direction: column; gap: 16px; diff --git a/src/routes/dashboard/files.tsx b/src/routes/dashboard/files.tsx index abe7b44..1a481d5 100644 --- a/src/routes/dashboard/files.tsx +++ b/src/routes/dashboard/files.tsx @@ -12,7 +12,7 @@ export const Route = createFileRoute("/dashboard/files")({
-
+
+
), }); diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 96275eb..24a678a 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -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." /> )}> - + } + title="Storage" + buttonLabel="Details"> + +
diff --git a/src/utils/files.ts b/src/utils/files.ts deleted file mode 100644 index 22c0b07..0000000 --- a/src/utils/files.ts +++ /dev/null @@ -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; -};