Add purchase history

This commit is contained in:
Arnaud 2024-11-07 14:49:24 +01:00
parent 8dfe43f754
commit 23dc9b1083
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
9 changed files with 200 additions and 82 deletions

View File

@ -0,0 +1,10 @@
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M10.5726 3.8999L12.3726 5.6999H20.1C20.5968 5.6999 21 6.1031 21 6.5999V19.1999C21 19.6967 20.5968 20.0999 20.1 20.0999H3.9C3.4032 20.0999 3 19.6967 3 19.1999V4.7999C3 4.3031 3.4032 3.8999 3.9 3.8999H10.5726ZM9.8274 5.6999H4.8V18.2999H19.2V7.4999H11.6274L9.8274 5.6999ZM12.9 9.2999V12.8999H15.6V14.6999H11.1V9.2999H12.9Z"
fill="#969696" />
</svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@ -0,0 +1,11 @@
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M14.8105 9.25L16.3105 10.75H22.75C23.164 10.75 23.5 11.086 23.5 11.5V22C23.5 22.414 23.164 22.75 22.75 22.75H9.25C8.836 22.75 8.5 22.414 8.5 22V10C8.5 9.586 8.836 9.25 9.25 9.25H14.8105ZM16.75 13.75H15.25V18.25H19V16.75H16.75V13.75Z"
fill="#969696"
fill-opacity="0.6" />
</svg>

After

Width:  |  Height:  |  Size: 407 B

View File

@ -1,34 +0,0 @@
.cell-state--custom {
display: inline-flex;
}
.cell-stateIcon {
position: relative;
top: 2px;
margin-right: 0.5rem;
}
.cell-state {
border-radius: var(--codex-border-radius);
padding: 0.5rem;
}
.cell-state--error {
background-color: rgba(var(--codex-color-error), 0.2);
color: rgb(var(--codex-color-error));
}
.cell-state--success {
background-color: rgba(var(--codex-color-success), 0.2);
color: rgb(var(--codex-color-success));
}
.cell-state--warning {
background-color: rgba(var(--codex-color-warning), 0.2);
color: rgb(var(--codex-color-warning));
}
.cell-state--loading {
background-color: rgba(var(--codex-color-grey), 0.2);
color: rgb(var(--codex-color-grey));
}

View File

@ -1,6 +1,7 @@
import { CheckCircle, CircleDashed, ShieldAlert } from "lucide-react";
import "./CustomStateCellRender.css";
import { Cell, Tooltip } from "@codex-storage/marketplace-ui-components";
import PurchaseStateIcon from "../../assets/icons/purchases-state-pending.svg?react";
import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react";
import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react";
type Props = {
state: string;
@ -9,12 +10,12 @@ type Props = {
export const CustomStateCellRender = ({ state, message }: Props) => {
const icons = {
pending: CircleDashed,
submitted: CircleDashed,
started: CircleDashed,
finished: CheckCircle,
cancelled: ShieldAlert,
errored: ShieldAlert,
pending: PurchaseStateIcon,
submitted: PurchaseStateIcon,
started: PurchaseStateIcon,
finished: SuccessCircleIcon,
cancelled: ErrorCircleIcon,
errored: ErrorCircleIcon,
};
const states = {
@ -26,7 +27,7 @@ export const CustomStateCellRender = ({ state, message }: Props) => {
finished: "success",
};
const Icon = icons[state as keyof typeof icons] || CircleDashed;
const Icon = icons[state as keyof typeof icons] || PurchaseStateIcon;
return (
<Cell>
@ -37,13 +38,11 @@ export const CustomStateCellRender = ({ state, message }: Props) => {
}>
{message ? (
<Tooltip message={message}>
<Icon size={"1rem"} className="cell-stateIcon" />
<Icon width={32} className="cell-stateIcon" />
</Tooltip>
) : (
<Icon size={"1rem"} className="cell-stateIcon" />
<Icon width={32} className="cell-stateIcon" />
)}
<span>{state}</span>
</p>
</Cell>
);

View File

@ -35,7 +35,7 @@
.preview {
background-color: #14141499;
border: 1px solid #69696933;
height: 200px;
height: 150px;
margin: auto;
border-radius: 10px;
margin-bottom: 16px;
@ -119,23 +119,37 @@
flex: 1;
}
}
}
.fileDetails-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
display: grid;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--codex-border-color);
}
.purchases {
padding-bottom: 16px;
.fileDetails-gridColumn {
grid-column: span 2 / span 2;
color: var(--codex-text-contrast);
}
header {
margin-top: 16px;
display: block;
border-bottom: 1px solid #96969633;
padding-bottom: 16px;
.fileDetails-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.75rem 1.5rem;
> span {
display: flex;
gap: 16px;
}
}
thead tr th {
border-top: 1px solid #96969633;
border-bottom: 1px solid #96969633;
&:first-child {
border-left: 1px solid #96969633;
}
&:last-child {
border-right: 1px solid #96969633;
}
}
.truncateCell {
width: 100px;
}
}
}

View File

@ -4,18 +4,20 @@ import {
Sheets,
WebFileIcon,
} from "@codex-storage/marketplace-ui-components";
import { CodexDataContent } from "@codex-storage/sdk-js";
import { CodexDataContent, CodexPurchase } from "@codex-storage/sdk-js";
import { PrettyBytes } from "../../utils/bytes";
import { Dates } from "../../utils/dates";
import { CidCopyButton } from "./CidCopyButton";
import "./FileDetails.css";
import { CodexSdk } from "../../sdk/codex";
import { FilesUtils } from "./files.utils";
import { useEffect, useState } from "react";
import { WebStorage } from "../../utils/web-storage";
import DownloadIcon from "../../assets/icons/download-file.svg?react";
import FileDetailsIcon from "../../assets/icons/file-details.svg?react";
import CloseIcon from "../../assets/icons/close.svg?react";
import { useQuery } from "@tanstack/react-query";
import { Promises } from "../../utils/promises";
import { PurchaseHistory } from "./PurchaseHistory";
import { WebStorage } from "../../utils/web-storage";
type Props = {
details: CodexDataContent | null;
@ -23,19 +25,50 @@ type Props = {
};
export function FileDetails({ onClose, details }: Props) {
const [purchases, setPurchases] = useState(0);
const { data: purchases = [] } = useQuery({
queryFn: () =>
CodexSdk.marketplace()
.purchases()
.then(async (res) => {
if (res.error) {
return res;
}
useEffect(() => {
WebStorage.purchases
.entries()
.then((entries) =>
setPurchases(
entries
.filter((e) => e[1] === details?.cid)
.reduce((acc) => acc + 1, 0)
)
);
}, [details?.cid]);
const all: CodexPurchase[] = [];
for (const p of res.data) {
const cid = await WebStorage.purchases.get(p.requestId);
if (cid == details?.cid) {
all.push(p);
}
}
return {
error: false as any,
data: all,
};
})
.then((s) => Promises.rejectOnError(s)),
queryKey: ["purchases"],
enabled: !!details,
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
initialData: [],
// Throw the error to the error boundary
throwOnError: true,
});
const url = CodexSdk.url() + "/api/codex/v1/data/";
@ -103,7 +136,7 @@ export function FileDetails({ onClose, details }: Props) {
<li>
<p>Used:</p>
<p>
<b>{purchases} </b> purchase(s)
<b>{purchases.length} </b> purchase(s)
</p>
</li>
</ul>
@ -117,6 +150,8 @@ export function FileDetails({ onClose, details }: Props) {
variant="outline"
onClick={onDownload}></Button>
</div>
<PurchaseHistory purchases={purchases}></PurchaseHistory>
</div>
)}
</>

View File

@ -0,0 +1,67 @@
import {
Row,
Cell,
Table,
TabSortState,
} from "@codex-storage/marketplace-ui-components";
import { Times } from "../../utils/times";
import { CustomStateCellRender } from "../CustomStateCellRender/CustomStateCellRender";
import { TruncateCell } from "../TruncateCell/TruncateCell";
import { CodexPurchase } from "@codex-storage/sdk-js";
import PurchaseHistoryIcon from "../../assets/icons/purchase-history-outline.svg?react";
import { useState } from "react";
import { PurchaseUtils } from "../StorageRequestSetup/purchase.util";
type Props = {
purchases: CodexPurchase[];
};
type SortFn = (a: CodexPurchase, b: CodexPurchase) => number;
export function PurchaseHistory({ purchases }: Props) {
const [sortFn, setSortFn] = useState<SortFn>(() =>
PurchaseUtils.sortById("desc")
);
const onSortById = (state: TabSortState) =>
setSortFn(() => PurchaseUtils.sortById(state));
const headers = [
["request id", onSortById],
["duration"],
["expiry"],
["status"],
] satisfies [string, ((state: TabSortState) => void)?][];
const sorted = sortFn ? [...purchases].sort(sortFn) : purchases;
const rows = sorted.map((p) => {
const duration = parseInt(p.request.ask.duration, 10);
return (
<Row
cells={[
<TruncateCell value={p.requestId} />,
<Cell>{Times.pretty(duration)}</Cell>,
<Cell>{p.request.expiry}</Cell>,
<CustomStateCellRender state={p.state} message={p.error} />,
]}></Row>
);
});
if (purchases.length > 0) {
return (
<div className="purchases">
<header>
<span>
<PurchaseHistoryIcon></PurchaseHistoryIcon> Purchase history
</span>
</header>
<Table headers={headers} rows={rows} defaultSortIndex={0}></Table>
</div>
);
}
return "";
}

View File

@ -0,0 +1,16 @@
import { TabSortState } from "@codex-storage/marketplace-ui-components"
import { CodexPurchase } from "@codex-storage/sdk-js"
export const PurchaseUtils = {
sortById: (state: TabSortState) =>
(a: CodexPurchase, b: CodexPurchase) => {
return state === "desc"
? b.requestId
.toLocaleLowerCase()
.localeCompare(a.requestId.toLocaleLowerCase())
: a.requestId
.toLocaleLowerCase()
.localeCompare(b.requestId.toLocaleLowerCase())
},
}

View File

@ -106,8 +106,8 @@ export const WebStorage = {
return set(key, cid, this.store);
},
async entries() {
return entries(this.store);
async entries<T>() {
return entries<string, T>(this.store);
},
dates: {