Joystream Stats пре 2 година
родитељ
комит
25a8712c9e

+ 1 - 1
src/App.tsx

@@ -74,7 +74,7 @@ class App extends React.Component<IProps, IState> {
     status.categories = await get.currentCategoryId(api);
     status.proposalPosts = await api.query.proposalsDiscussion.postCount();
     await this.updateEra(api, status.era).then(async (era) => {
-      status.era = era.toNumber();
+      status.era = era;
       status.lastReward = await getLastReward(api, era);
       status.validatorStake = await getTotalStake(api, era);
       this.save("status", status);

+ 7 - 6
src/components/AppBar/config.ts

@@ -2,12 +2,12 @@ import { createStyles, makeStyles, Theme } from "@material-ui/core";
 
 export const routes = {
   dashboard: "Dashboard",
-  calendar: "Calendar",
-  timeline: "Timeline",
+  election: "Election",
   tokenomics: "Tokenomics",
   validators: "Validators",
   "validator-report": "Validator Report",
   storage: "Storage",
+  distribution: "Distribution",
   spending: "Spending",
   transactions: "Transfers",
   burners: "Top Burners",
@@ -16,7 +16,8 @@ export const routes = {
   //faq: "FAQ",
   //survey: "Survey",
   issues: "Issues",
-  election: "Election",
+  calendar: "Calendar",
+  timeline: "Timeline",
 } as { [key: string]: string };
 
 export const useStyles = makeStyles((theme: Theme) =>
@@ -28,14 +29,14 @@ export const useStyles = makeStyles((theme: Theme) =>
     },
     drawer: {
       color: "#000",
-      '& .MuiDrawer-paper': {
+      "& .MuiDrawer-paper": {
         background: "#000",
         width: 200,
       },
     },
     menuButton: {
       margin: 0,
-      color: "#fff"
+      color: "#fff",
     },
     select: {
       color: "#fff",
@@ -63,7 +64,7 @@ export const useStyles = makeStyles((theme: Theme) =>
       color: "#4038ff",
       "&:hover": {
         background: "#111",
-      }
+      },
     },
   })
 );

+ 23 - 0
src/components/Distribution/Bag.tsx

@@ -0,0 +1,23 @@
+import { useEffect, useState } from "react";
+import { Badge } from "react-bootstrap";
+import { Object, Operator } from ".types";
+import { gb, testBag } from "./util";
+
+const Bag = (props: { id: number; operator: Operator; objects: Object[] }) => {
+  const { id, objects, operator } = props;
+  const [color, setColor] = useState(`warning`);
+  const endpoint = operator?.metadata?.nodeEndpoint;
+  useEffect(
+    () => testBag(endpoint, objects).then((color) => setColor(color)),
+    [endpoint, objects]
+  );
+  const channelId = id.split(":")[2];
+  const size = objects?.reduce((o, sum: number) => sum + +o.size, 0) || 0;
+  return (
+    <Badge variant={color} title={gb(size)}>
+      {channelId}
+    </Badge>
+  );
+};
+
+export default Bag;

+ 28 - 0
src/components/Distribution/Bags.tsx

@@ -0,0 +1,28 @@
+import { useEffect, useState } from "react";
+import BagBubble from "./Bag";
+import { getBucketObjects } from "./util";
+
+const Bags = (props: { show: boolean; bags: Bag[]; operator: Operator }) => {
+  const { bucketId, bags, operator } = props;
+  const [bagsWithObjects, setBags] = useState([]);
+  useEffect(
+    () =>
+      getBucketObjects(bucketId).then((bags) => bags.length && setBags(bags)),
+    [bucketId]
+  );
+  const findBag = (id: string) => bagsWithObjects.find((b) => b.id === id);
+  return (
+    <div>
+      {bags.map((b) => (
+        <BagBubble
+          key={b.id}
+          {...b}
+          objects={findBag(b.id)?.objects}
+          operator={operator}
+        />
+      ))}
+    </div>
+  );
+};
+
+export default Bags;

+ 43 - 0
src/components/Distribution/Bucket.tsx

@@ -0,0 +1,43 @@
+import { useState } from "react";
+import { Badge } from "react-bootstrap";
+import Metadata from "./Metadata";
+import Bags from "./Bags";
+import StatusBadge from "./StatusBadge";
+import { Operator, Bucket } from "./types";
+
+const BucketRow = (props: { bucket: Bucket }) => {
+  const [show, setShow] = useState(false);
+  const { id, distributing, acceptingNewBags, bags, operators } = props.bucket;
+  return (
+    <>
+      <div key={id} className="d-flex flex-row" onClick={() => setShow(!show)}>
+        <h3>{id}</h3>
+        <StatusBadge status={distributing} label={"D"} title={"distributing"} />
+        <StatusBadge
+          status={acceptingNewBags}
+          label={"A"}
+          title={"accepting new bags"}
+        />
+        <div className="col-1 p-2">{bags.length} bags</div>
+        <OperatorFields operator={operators[0]} />
+      </div>
+      {show ? <Bags bucketId={id} bags={bags} operator={operators[0]} /> : ``}
+    </>
+  );
+};
+
+export default BucketRow;
+
+const OperatorFields = (props: { operator: Operator }) => {
+  if (!props.operator) return <div className="col-7" />;
+  const { workerId, member, metadata } = props.operator;
+  return (
+    <>
+      <div className="col-1" title={`worker ${workerId}`}>
+        {member ? <Badge>{member.handle}</Badge> : ``}
+      </div>
+      <Badge className="col-3 text-left">{metadata?.extra}</Badge>
+      <Metadata metadata={metadata} />
+    </>
+  );
+};

+ 10 - 0
src/components/Distribution/Metadata.tsx

@@ -0,0 +1,10 @@
+import Status from "./Status";
+
+const Metadata = (props: { metadata }) => {
+  if (!props.metadata) return <div />;
+  const { nodeEndpoint } = props.metadata;
+
+  return <Status endpoint={nodeEndpoint} />;
+};
+
+export default Metadata;

+ 38 - 0
src/components/Distribution/Status.tsx

@@ -0,0 +1,38 @@
+import { useState, useEffect } from "react";
+import { Badge } from "react-bootstrap";
+import axios from "axios";
+import moment from "moment";
+const gb = (bytes: number) => (bytes / 1024 ** 3).toFixed();
+
+const Status = (props: { endpoint: string }) => {
+  const { endpoint } = props;
+  const [status, setStatus] = useState({});
+  const updateStatus = (url: string) => {
+    console.debug(`udating status`, url);
+    axios
+      .get(url)
+      .then(({ data }) => setStatus(data))
+      .catch((e) => console.log(`status`, url, e.message));
+  };
+  useEffect(() => updateStatus(endpoint + `api/v1/status`), [endpoint]);
+
+  if (!status.id) return <div />;
+  const { id, objectsInCache, storageLimit, storageUsed, uptime } = status;
+  const upSince = moment().subtract(uptime * 1000);
+  return (
+    <>
+      <Badge className="col-1" title="GB used / limit">
+        {gb(storageUsed)}/{gb(storageLimit)}
+      </Badge>
+      <Badge className="col-1" title="Objects (downloading)">
+        {objectsInCache} ({status.downloadsInProgress})
+      </Badge>
+      <Badge className="col-1" title="up since">
+        {upSince.fromNow()}
+      </Badge>
+      {id}
+    </>
+  );
+};
+
+export default Status;

+ 17 - 0
src/components/Distribution/StatusBadge.tsx

@@ -0,0 +1,17 @@
+import { Button } from "react-bootstrap";
+
+const StatusBadge = (props: {
+  label: string;
+  title: string;
+  status: boolean;
+}) => {
+  const { label, title, status } = props;
+  const variant = status ? `success` : `danger`;
+  return (
+    <Button variant={variant} className="p-1 m-1" title={title}>
+      {label}
+    </Button>
+  );
+};
+
+export default StatusBadge;

+ 29 - 0
src/components/Distribution/index.tsx

@@ -0,0 +1,29 @@
+import { useState, useEffect } from "react";
+import { Loading } from "..";
+import Bucket from "./Bucket";
+import { getDistributionBuckets } from "./util";
+
+const Distribution = (props: {
+  workers: { distributionWorkingGroup?: Worker[] };
+}) => {
+  const [buckets, setBuckets] = useState([]);
+  useEffect(
+    () =>
+      getDistributionBuckets(props.workers?.distributionWorkingGroup).then(
+        (buckets) => buckets.length && setBuckets(buckets)
+      ),
+    [props.workers?.distributionWorkingGroup]
+  );
+  return (
+    <div className="m-2 p-2 bg-light">
+      <h2>Distribution Providers</h2>
+      {buckets.length ? (
+        buckets.map((bucket) => <Bucket key={bucket.id} bucket={bucket} />)
+      ) : (
+        <Loading target="buckets" />
+      )}
+    </div>
+  );
+};
+
+export default Distribution;

+ 35 - 0
src/components/Distribution/types.ts

@@ -0,0 +1,35 @@
+export interface Operator {
+  workerId: number;
+  metadata: {
+    extra: string;
+    nodeEndpoint: string;
+  };
+}
+
+export interface Bucket {
+  id: string;
+  createdAt: string;
+  distributing: boolean;
+  acceptingNewBags: boolean;
+  operators: Operator[];
+  bags: Bag[];
+}
+
+export interface Bag {
+  id: number;
+  objects: Object[];
+}
+
+export interface Object {
+  id: nubmer;
+  size: number;
+}
+
+interface StatusData {
+  id: string;
+  objectsInCache: number;
+  storageLimite: number;
+  storageUsed: number;
+  uptime: number;
+  downloadsInProgress: number;
+}

+ 70 - 0
src/components/Distribution/util.ts

@@ -0,0 +1,70 @@
+import axios from "axios";
+import { queryNode } from "../../config";
+import { Bucket } from "./types";
+import { qnDistributionBuckets, qnBucketObjects } from "./queries";
+
+export const gb = (bytes: number) => (bytes / 1024 ** 3).toFixed() + `gb`;
+
+const fail = (msg: string) => {
+  console.log(`getQN: ${string}`);
+  return [];
+};
+export const postQN = (query) =>
+  axios
+    .post(queryNode, { query })
+    .then(({ data }) => {
+      if (data.error) return fail(data.error);
+      console.debug(`postQN`, query, data.data);
+      return data.data;
+    })
+    .catch((e) => fail(e.message));
+
+export const testBag = async (
+  endpoint: string,
+  objects: Object[] | null
+): Promise<string> => {
+  if (!objects) return `warning`;
+  if (!objects.length) return ``;
+  return axios
+    .head(endpoint + `api/v1/assets/${objects[0].id}`)
+    .then((data) => `success`)
+    .catch((e) => {
+      console.error(`testBag: ${e.message}`);
+      return `danger`;
+    });
+};
+
+export const getBucketObjects = async (bucketId: number) =>
+  postQN(qnBucketObjects(bucketId)).then(({ distributionBuckets }) => {
+    if (!distributionBuckets) {
+      console.error(`getBucketObjects: received empty distributionBuckets`);
+      return [];
+    }
+    const bags = distributionBuckets[0].bags;
+    console.debug(`received objects of bucket`, bucketId, bags);
+    return bags;
+  });
+
+export const getDistributionBuckets = async (
+  workers?: Worker[]
+): Promise<Bucket[]> =>
+  postQN(qnDistributionBuckets).then(({ distributionBuckets }) =>
+    sortBuckets(
+      distributionBuckets.map((b) => addWorkerMemberships(b, workers))
+    )
+  );
+
+// TODO OPTIMIZE sort by bucketIndex
+const sortBuckets = (buckets: Bucket[]): Bucket[] =>
+  buckets.sort((a, b) => a.id.split(":")[0] - b.id.split(":")[0]);
+
+export const addWorkerMemberships = (
+  bucket: Bucket,
+  workers?: Worker[]
+): Bucket => {
+  const operators = bucket.operators.map((operator) => {
+    const member = workers?.find((w) => w.id === operator.workerId);
+    return { ...operator, member };
+  });
+  return { ...bucket, operators };
+};

+ 7 - 0
src/components/Routes/index.tsx

@@ -20,6 +20,7 @@ const Timeline = React.lazy(() => import("../Timeline"));
 const Tokenomics = React.lazy(() => import("../Tokenomics"));
 const Validators = React.lazy(() => import("../Validators"));
 const Storage = React.lazy(() => import("../Storage"));
+const Distribution = React.lazy(() => import("../Distribution"));
 const Transactions = React.lazy(() => import("../Transactions"));
 const Bounties = React.lazy(() => import("../Bounties"));
 const Burners = React.lazy(() => import("../Burners"));
@@ -133,6 +134,12 @@ const Routes = (props: IProps) => {
                 path="/storage"
                 render={(routeprops) => <Storage {...routeprops} {...props} />}
               />
+              <Route
+                path="/distribution"
+                render={(routeprops) => (
+                  <Distribution {...routeprops} {...props} />
+                )}
+              />
               <Route
                 path="/transactions"
                 render={(routeprops) => (

+ 1 - 0
src/components/index.ts

@@ -26,6 +26,7 @@ export { default as MemberBox } from "./Members/MemberBox";
 export { default as MemberOverlay } from "./Members/MemberOverlay";
 export { default as Members } from "./Members";
 export { default as Storage } from "./Storage";
+export { default as Distribution } from "./Distribution";
 export { default as Tokenomics } from "./Tokenomics";
 export { default as Transactions } from "./Transactions";
 export { default as Burners } from "./Burners";

+ 1 - 0
src/config.ts

@@ -4,6 +4,7 @@ export const wsLocation = "wss://joystreamstats.live:9945";
 export const apiLocation = "https://api.joystreamstats.live/api"
 export const socketLocation = "/socket.io"
 export const hydraLocation = "https://hydra.joystream.org/graphql"
+export const queryNode= "https://hydra.joystream.org/graphql"
 //export const alternativeBackendApis = "http://localhost:3000"
 export const alternativeBackendApis = "https://validators.joystreamstats.live"
 export const tasksEndpoint = "https://api.joystreamstats.live/tasks"

+ 3 - 3
src/lib/queries.ts

@@ -1,6 +1,6 @@
 import { apiLocation } from "../config";
 import { Tokenomics } from "../types";
-import { getAssets, getStorageProviders } from "./storage";
+//import { getAssets, getStorageProviders } from "./storage";
 import axios from "axios";
 
 export const queryJstats = (route: string) => {
@@ -79,8 +79,8 @@ export const bootstrap = (save: (key: string, data: any) => {}) => {
     { posts: () => queryJstats(`/v1/posts`) },
     { threads: () => queryJstats(`/v1/threads`) },
     { categories: () => queryJstats(`/v1/categories`) },
-    { providers: () => getStorageProviders() },
-    { assets: () => getAssets() },
+    //{ providers: () => getStorageProviders() },
+    //{ assets: () => getAssets() },
   ].reduce(async (promise, request) => {
     //promise.then(async () => {
     const key = Object.keys(request)[0];