Browse Source

feat: feed report

Alexandru Cambose 2 months ago
parent
commit
0236b35f88

+ 2 - 2
apps/insights/src/components/Publisher/conformance-reports.module.scss → apps/insights/src/components/PriceFeed/conformance-reports.module.scss

@@ -1,4 +1,4 @@
 .conformanceReports {
   display: flex;
-  gap: .5rem;
-}
+  gap: 0.5rem;
+}

+ 122 - 0
apps/insights/src/components/PriceFeed/conformance-reports.tsx

@@ -0,0 +1,122 @@
+"use client";
+
+import { Download } from "@phosphor-icons/react/dist/ssr/Download";
+import { Button } from "@pythnetwork/component-library/Button";
+import { Select } from "@pythnetwork/component-library/Select";
+import { Skeleton } from "@pythnetwork/component-library/Skeleton";
+import { useAlert } from "@pythnetwork/component-library/useAlert";
+import { useState } from "react";
+
+import styles from "./conformance-reports.module.scss";
+
+const PYTHTEST_CONFORMANCE_REFERENCE_PUBLISHER =
+  "HUZu4xMSHbxTWbkXR6jkGdjvDPJLjrpSNXSoUFBRgjWs";
+
+const download = (blob: Blob, filename: string) => {
+  const url = globalThis.URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = filename;
+  document.body.append(a);
+  a.click();
+  a.remove();
+};
+
+type ConformanceReportsProps =
+  | { isLoading: true }
+  | {
+      isLoading?: false | undefined;
+      symbol: string;
+      cluster: string;
+      publisher: string;
+    };
+
+const ConformanceReports = (props: ConformanceReportsProps) => {
+  const [timeframe, setTimeframe] = useState("24H");
+  const [isGeneratingReport, setIsGeneratingReport] = useState(false);
+  const { open } = useAlert();
+
+  const downloadReport = async () => {
+    if (props.isLoading) {
+      return;
+    }
+    const url = new URL(
+      "/pyth/metrics/conformance",
+      "https://web-api.pyth.network/",
+    );
+    url.searchParams.set("symbol", props.symbol);
+    url.searchParams.set("range", timeframe);
+    url.searchParams.set("cluster", "pythnet");
+    url.searchParams.set("publisher", props.publisher);
+
+    if (props.cluster === "pythtest-conformance") {
+      url.searchParams.set(
+        "pythnet_aggregate_publisher",
+        PYTHTEST_CONFORMANCE_REFERENCE_PUBLISHER,
+      );
+    }
+
+    const response = await fetch(url, {
+      headers: new Headers({
+        Accept: "application/octet-stream",
+      }),
+    });
+    const blob = await response.blob();
+    download(
+      blob,
+      `${props.publisher}-${props.symbol
+        .split("/")
+        .join("")}-${timeframe}-${props.cluster}-conformance-report.tsv`,
+    );
+  };
+
+  const handleReport = () => {
+    setIsGeneratingReport(true);
+    try {
+      downloadReport().catch(() => {
+        open({
+          title: "Error",
+          contents: "Error generating conformance report",
+        });
+      });
+    } finally {
+      setIsGeneratingReport(false);
+    }
+  };
+  if (props.isLoading) {
+    return <Skeleton width={100} />;
+  }
+  return (
+    <div className={styles.conformanceReports}>
+      <Select
+        options={[
+          { id: "24H" },
+          { id: "48H" },
+          { id: "72H" },
+          { id: "1W" },
+          { id: "1M" },
+        ]}
+        placement="bottom end"
+        selectedKey={timeframe}
+        onSelectionChange={(value) => {
+          setTimeframe(value);
+        }}
+        size="sm"
+        label="Timeframe"
+        variant="outline"
+        hideLabel
+      />
+      <Button
+        variant="outline"
+        size="sm"
+        onClick={handleReport}
+        afterIcon={<Download key="download" />}
+        isPending={isGeneratingReport}
+      >
+        Report
+      </Button>
+    </div>
+  );
+};
+
+export default ConformanceReports;

+ 14 - 5
apps/insights/src/components/PriceFeed/header.tsx

@@ -5,23 +5,24 @@ import { Skeleton } from "@pythnetwork/component-library/Skeleton";
 import { StatCard } from "@pythnetwork/component-library/StatCard";
 import { Suspense } from "react";
 
-import styles from "./header.module.scss";
-import { PriceFeedSelect } from "./price-feed-select";
-import { ReferenceData } from "./reference-data";
 import { Cluster } from "../../services/pyth";
 import { AssetClassBadge } from "../AssetClassBadge";
 import { Cards } from "../Cards";
 import { Explain } from "../Explain";
 import { FeedKey } from "../FeedKey";
-import { LivePrice, LiveConfidence, LiveLastUpdated } from "../LivePrices";
+import { LiveConfidence, LiveLastUpdated, LivePrice } from "../LivePrices";
 import {
-  YesterdaysPricesProvider,
   PriceFeedChangePercent,
+  YesterdaysPricesProvider,
 } from "../PriceFeedChangePercent";
 import { PriceFeedIcon } from "../PriceFeedIcon";
 import { PriceFeedTag } from "../PriceFeedTag";
 import { PriceName } from "../PriceName";
+import ConformanceReports from "./conformance-reports";
 import { getFeed } from "./get-feed";
+import styles from "./header.module.scss";
+import { PriceFeedSelect } from "./price-feed-select";
+import { ReferenceData } from "./reference-data";
 
 type Props = {
   params: Promise<{
@@ -174,6 +175,14 @@ const PriceFeedHeaderImpl = (props: PriceFeedHeaderImplProps) => (
         >
           Reference Data
         </Button>
+        <ConformanceReports
+          isLoading={props.isLoading}
+          {...(!props.isLoading && {
+            symbol: props.feed.symbol,
+            cluster: Cluster.Pythnet,
+            publisher: props.feed.product.price_account,
+          })}
+        />
       </div>
     </div>
     <Cards>

+ 0 - 31
apps/insights/src/components/Publisher/conformance-reports.tsx

@@ -1,31 +0,0 @@
-'use client';
-
-import { Download } from '@phosphor-icons/react/dist/ssr/Download';
-import { Button } from '@pythnetwork/component-library/Button';
-import { Select } from '@pythnetwork/component-library/Select';
-import { useState } from 'react';
-import styles from "./conformance-reports.module.scss";
-
-const ConformanceReports = () => {
-  const [timeframe, setTimeframe] = useState("Daily");
-  const handleReport = () => {
-    console.log("Report", timeframe);
-  }
-  return (
-    <div className={styles.conformanceReports}>
-      <Select
-          options={[{ id: "Daily" }, { id: "Weekly" }, { id: "Monthly" }]}
-          placement="bottom end"
-          selectedKey={timeframe}
-          onSelectionChange={(value) => setTimeframe(value as string)}
-          size="sm"
-          label="Timeframe"
-          variant="outline"
-          hideLabel
-        />
-        <Button variant='outline' size='sm' onClick={handleReport} afterIcon={<Download key="download"/>}>Report</Button>
-    </div>
-  )
-}
-
-export default ConformanceReports;

+ 8 - 10
apps/insights/src/components/Publisher/layout.tsx

@@ -45,7 +45,6 @@ import { SemicircleMeter } from "../SemicircleMeter";
 import { TabPanel, TabRoot, Tabs } from "../Tabs";
 import { TokenIcon } from "../TokenIcon";
 import { OisApyHistory } from "./ois-apy-history";
-import ConformanceReports from './conformance-reports';
 
 type Props = {
   children: ReactNode;
@@ -79,16 +78,15 @@ export const PublisherLayout = async ({ children, params }: Props) => {
           </div>
           <div className={styles.titleRow}>
             <PublisherTag
-            cluster={parsedCluster}
-            publisherKey={key}
-            {...(knownPublisher && {
-              name: knownPublisher.name,
-              icon: <PublisherIcon knownPublisher={knownPublisher} />,
-            })}
-          />
-          <ConformanceReports/>
+              cluster={parsedCluster}
+              publisherKey={key}
+              {...(knownPublisher && {
+                name: knownPublisher.name,
+                icon: <PublisherIcon knownPublisher={knownPublisher} />,
+              })}
+            />
           </div>
-          
+
           <Cards className={styles.stats ?? ""}>
             <Suspense fallback={<RankingCardImpl isLoading />}>
               <RankingCard cluster={parsedCluster} publisherKey={key} />