"use client";
import { useLogger } from "@pythnetwork/app-logger";
import { Badge } from "@pythnetwork/component-library/Badge";
import { Button } from "@pythnetwork/component-library/Button";
import { Card } from "@pythnetwork/component-library/Card";
import { Paginator } from "@pythnetwork/component-library/Paginator";
import { SearchInput } from "@pythnetwork/component-library/SearchInput";
import { Select } from "@pythnetwork/component-library/Select";
import { SingleToggleGroup } from "@pythnetwork/component-library/SingleToggleGroup";
import {
type RowConfig,
type SortDescriptor,
Table,
} from "@pythnetwork/component-library/Table";
import { useQueryState, parseAsStringEnum, parseAsBoolean } from "nuqs";
import { type ReactNode, Suspense, useMemo, useCallback } from "react";
import { useFilter, useCollator } from "react-aria";
import styles from "./index.module.scss";
import { useQueryParamFilterPagination } from "../../hooks/use-query-param-filter-pagination";
import { Cluster } from "../../services/pyth";
import {
type StatusName,
STATUS_NAMES,
Status as StatusType,
statusNameToStatus,
} from "../../status";
import { Explain } from "../Explain";
import { EvaluationTime } from "../Explanations";
import { FormattedNumber } from "../FormattedNumber";
import { LivePrice, LiveConfidence, LiveComponentValue } from "../LivePrices";
import { NoResults } from "../NoResults";
import rootStyles from "../Root/index.module.scss";
import { Score } from "../Score";
import { Status as StatusComponent } from "../Status";
const SCORE_WIDTH = 32;
type Props = {
className?: string | undefined;
priceComponents: PriceComponent[];
metricsTime?: Date | undefined;
nameLoadingSkeleton: ReactNode;
label: string;
searchPlaceholder: string;
onPriceComponentAction: (component: PriceComponent) => void;
};
type PriceComponent = {
id: string;
score: number | undefined;
symbol: string;
uptimeScore: number | undefined;
deviationScore: number | undefined;
stalledScore: number | undefined;
cluster: Cluster;
status: StatusType;
feedKey: string;
publisherKey: string;
name: ReactNode;
nameAsString: string;
};
export const PriceComponentsCard = ({
priceComponents,
onPriceComponentAction,
...props
}: Props) => (
}>
);
export const ResolvedPriceComponentsCard = ({
priceComponents,
onPriceComponentAction,
...props
}: Props) => {
const logger = useLogger();
const collator = useCollator();
const filter = useFilter({ sensitivity: "base", usage: "search" });
const [status, setStatus] = useQueryState(
"status",
parseAsStringEnum(["", ...Object.values(STATUS_NAMES)]).withDefault(""),
);
const [showQuality, setShowQuality] = useQueryState(
"showQuality",
parseAsBoolean.withDefault(false),
);
const statusType = useMemo(() => statusNameToStatus(status), [status]);
const componentsFilteredByStatus = useMemo(
() =>
statusType === undefined
? priceComponents
: priceComponents.filter(
(component) => component.status === statusType,
),
[statusType, priceComponents],
);
const {
search,
sortDescriptor,
page,
pageSize,
updateSearch,
updateSortDescriptor,
updatePage,
updatePageSize,
paginatedItems,
numResults,
numPages,
mkPageLink,
} = useQueryParamFilterPagination(
componentsFilteredByStatus,
(component, search) => filter.contains(component.nameAsString, search),
(a, b, { column, direction }) => {
switch (column) {
case "score":
case "uptimeScore":
case "deviationScore":
case "stalledScore": {
if (a[column] === undefined && b[column] === undefined) {
return 0;
} else if (a[column] === undefined) {
return direction === "descending" ? 1 : -1;
} else if (b[column] === undefined) {
return direction === "descending" ? -1 : 1;
} else {
return (
(direction === "descending" ? -1 : 1) * (a[column] - b[column])
);
}
}
case "name": {
return (
(direction === "descending" ? -1 : 1) *
collator.compare(a.nameAsString, b.nameAsString)
);
}
case "status": {
const resultByStatus = b.status - a.status;
const result =
resultByStatus === 0
? collator.compare(a.nameAsString, b.nameAsString)
: resultByStatus;
return (direction === "descending" ? -1 : 1) * result;
}
default: {
return 0;
}
}
},
{
defaultPageSize: 20,
defaultSort: "name",
defaultDescending: false,
},
);
const rows = useMemo(
() =>
paginatedItems.map((component) => ({
id: component.id,
data: {
name: (
{component.name}
{component.cluster === Cluster.PythtestConformance && (
test
)}
),
...(showQuality
? {
score: component.score !== undefined && (
),
uptimeScore: component.uptimeScore !== undefined && (
),
deviationScore: component.deviationScore !== undefined && (
),
stalledScore: component.stalledScore !== undefined && (
),
}
: {
slot: (
),
price: (
),
confidence: (
),
}),
status: ,
},
onAction: () => {
onPriceComponentAction(component);
},
})),
[paginatedItems, showQuality, onPriceComponentAction],
);
const updateStatus = useCallback(
(newStatus: StatusName | "") => {
updatePage(1);
setStatus(newStatus).catch((error: unknown) => {
logger.error("Failed to update status", error);
});
},
[updatePage, setStatus, logger],
);
const updateShowQuality = useCallback(
(newValue: boolean) => {
setShowQuality(newValue).catch((error: unknown) => {
logger.error("Failed to update show quality", error);
});
},
[setShowQuality, logger],
);
return (
);
};
type PriceComponentsCardProps = Pick<
Props,
| "className"
| "metricsTime"
| "nameLoadingSkeleton"
| "label"
| "searchPlaceholder"
> &
(
| { isLoading: true }
| {
isLoading?: false;
numResults: number;
search: string;
sortDescriptor: SortDescriptor;
numPages: number;
page: number;
pageSize: number;
onSearchChange: (newSearch: string) => void;
onSortChange: (newSort: SortDescriptor) => void;
onPageSizeChange: (newPageSize: number) => void;
onPageChange: (newPage: number) => void;
mkPageLink: (page: number) => string;
status: StatusName | "";
onStatusChange: (newStatus: StatusName | "") => void;
showQuality: boolean;
setShowQuality: (newValue: boolean) => void;
rows: RowConfig[];
}
);
export const PriceComponentsCardContents = ({
className,
metricsTime,
nameLoadingSkeleton,
label,
searchPlaceholder,
...props
}: PriceComponentsCardProps) => {
const collator = useCollator();
return (
{label}
{!props.isLoading && (
{props.numResults}
)}
>
}
toolbar={
<>