mirror of
https://github.com/codex-storage/codex-marketplace-ui.git
synced 2025-02-23 21:28:26 +00:00
Add purchase history
This commit is contained in:
parent
8dfe43f754
commit
23dc9b1083
10
src/assets/icons/purchase-history-outline.svg
Normal file
10
src/assets/icons/purchase-history-outline.svg
Normal 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 |
11
src/assets/icons/purchases-state-pending.svg
Normal file
11
src/assets/icons/purchases-state-pending.svg
Normal 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 |
@ -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));
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
67
src/components/Files/PurchaseHistory.tsx
Normal file
67
src/components/Files/PurchaseHistory.tsx
Normal 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 "";
|
||||
}
|
16
src/components/StorageRequestSetup/purchase.util.ts
Normal file
16
src/components/StorageRequestSetup/purchase.util.ts
Normal 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())
|
||||
},
|
||||
}
|
@ -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: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user