Joystream Stats 3 年 前
コミット
a7f1f27b8d

+ 4 - 4
src/components/Dashboard/index.tsx

@@ -28,10 +28,7 @@ const Dashboard = (props: IProps) => {
   const userLink = `${domain}/#/members/joystreamstats`;
   return (
     <div className="w-100 flex-grow-1 d-flex align-items-center justify-content-center d-flex flex-column">
-      <div
-        className="box position-fixed bg-warning d-flex flex-column"
-        style={{ top: "0px", right: "0px" }}
-      >
+      <div className="back bg-warning d-flex flex-column p-2">
         <Link to={`/calendar`}>Calendar</Link>
         <Link to={`/timeline`}>Timeline</Link>
         <Link to={`/tokenomics`}>Reports</Link>
@@ -64,6 +61,9 @@ const Dashboard = (props: IProps) => {
           <Link className="m-3 text-light" to={"/proposals"}>
             All
           </Link>
+          <Link className="m-3 text-light" to={`/spending`}>
+            Spending
+          </Link>
           <Link className="m-3 text-light" to={"/councils"}>
             Votes
           </Link>

+ 79 - 0
src/components/Proposals/Spending.tsx

@@ -0,0 +1,79 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import { IState, ProposalDetail } from "../../types";
+import Back from "../Back";
+
+const amount = (amount: number) => (amount / 1000000).toFixed(2);
+
+const getRound = (block: number): number =>
+  Math.round((block - 57600) / 201600);
+
+const executionFailed = (result: string, executed: any) => {
+  if (result !== "Approved") return result;
+  if (!executed || !Object.keys(executed)) return;
+  if (executed.Approved.ExecutionFailed)
+    return executed.Approved.ExecutionFailed.error;
+  return false;
+};
+
+const Spending = (props: IState) => {
+  const spending = props.proposals.filter(
+    (p: ProposalDetail) => p && p.type === "Spending"
+  );
+
+  const rounds: ProposalDetail[][] = [];
+  let unknown = 0;
+  let sum = 0;
+  let sums: number[] = [];
+  spending.forEach((p) => {
+    const r = getRound(p.finalizedAt);
+    rounds[r] = rounds[r] ? rounds[r].concat(p) : [p];
+    if (!sums[r]) sums[r] = 0;
+    if (!p.detail) return unknown++;
+    if (executionFailed(p.result, p.executed)) return;
+    sum += p.detail.Spending[0];
+    sums[r] += p.detail.Spending[0];
+  });
+
+  return (
+    <div className="box text-left">
+      <div className="back position-fixed">
+        <Back history={props.history} />
+      </div>
+      <h1 className="text-left">
+        Total: {amount(sum)}
+        {unknown ? `*` : ``} M tJOY
+      </h1>
+      {unknown ? `* subject to change until all details are available` : ``}
+      {rounds.map((proposals, i: number) => (
+        <div key={`round-${i}`} className="bg-secondary p-1 my-2">
+          <h2 className="text-left mt-3">
+            Round {i} <small>{amount(sums[i])} M</small>
+          </h2>
+          {proposals.map((p) => (
+            <ProposalLine key={p.id} {...p} />
+          ))}
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default Spending;
+
+const ProposalLine = (props: any) => {
+  const { id, title, detail, author, executed, result } = props;
+  const failed = executionFailed(result, executed);
+  return (
+    <div key={id} className={failed ? "bg-danger" : "bg-warn"}>
+      <span
+        className={`bg-${failed ? "danger" : "warning"} text-body p-1 mr-2`}
+      >
+        {detail ? amount(detail.Spending[0]) : `?`} M
+      </span>
+      <Link to={`/proposals/${id}`}>{title}</Link> (
+      <Link to={`/members/${author}`}>{author}</Link>)
+      {failed ? ` - ${failed}` : ""}
+    </div>
+  );
+};

+ 12 - 1
src/components/Routes/index.tsx

@@ -12,6 +12,7 @@ import {
   Timeline,
   Tokenomics,
   Validators,
+  Spending,
 } from "..";
 import { IState } from "../../types";
 
@@ -34,9 +35,19 @@ const Routes = (props: IProps) => {
           />
         )}
       />
+      <Route
+        path="/spending"
+        render={(routeprops) => <Spending {...routeprops} {...props} />}
+      />
       <Route
         path="/proposals/:id"
-        render={(routeprops) => <Proposal {...routeprops} {...props} />}
+        render={(routeprops) => (
+          <Proposal
+            fetchProposal={props.fetchProposal}
+            {...routeprops}
+            {...props}
+          />
+        )}
       />
       <Route path="/proposals" render={() => <Proposals {...props} />} />
       <Route

+ 1 - 0
src/components/index.ts

@@ -6,6 +6,7 @@ export { default as Councils } from "./Councils";
 export { default as Dashboard } from "./Dashboard";
 export { default as Forum } from "./Forum";
 export { default as Mint } from "./Mint";
+export { default as Spending } from "./Proposals/Spending";
 export { default as Proposals } from "./Proposals";
 export { default as ProposalLink } from "./Proposals/ProposalLink";
 export { default as Proposal } from "./Proposals/Proposal";

+ 11 - 4
src/lib/announcements.ts

@@ -199,13 +199,20 @@ export const proposals = async (
     }
   }
 
+  const getLabel = (executed: any) => {
+    if (!executed || !Object.keys(executed)) return `executed`;
+    if (Object.keys(executed)[0] === "ExecutionFailed")
+      return executed[Object.keys(executed)[0]].error;
+    return Object.keys(executed)[0];
+  };
+
   for (const id of executing) {
     const proposal = await proposalDetail(api, id);
-    const { exec, finalizedAt, message, parameters } = proposal;
-    const execStatus = exec ? Object.keys(exec)[0] : "";
-    const label = execStatus === "Executed" ? "has been" : "failed to be";
+    const { executed, finalizedAt, message, parameters } = proposal;
     const block = +finalizedAt + parameters.gracePeriod.toNumber();
-    const msg = `Proposal ${id} <b>${label} executed</b> at block ${block}.\r\n${message}`;
+    const msg = `Proposal ${id} <b>${getLabel(
+      executed
+    )}</b> at block ${block}.\r\n${message}`;
     sendMessage(msg);
     executing = executing.filter((e) => e !== id);
   }

+ 8 - 3
src/lib/getters.ts

@@ -115,6 +115,11 @@ const getProposalType = async (api: Api, id: number): Promise<string> => {
   return type;
 };
 
+const isExecuted = (proposalStatus : any) => {
+  if (!proposalStatus) return null
+  if (proposalStatus.Approved) return proposalStatus.Approved.toJSON()
+  return proposalStatus.toJSON()
+}
 export const proposalDetail = async (
   api: Api,
   id: number
@@ -131,12 +136,12 @@ export const proposalDetail = async (
       (proposalStatus.isSlashed && "Slashed") ||
       (proposalStatus.isVetoed && "Vetoed")
     : "Pending";
-  const exec = proposalStatus ? proposalStatus["Approved"] : null;
-
+  const executed = isExecuted(proposalStatus)
   const { description, parameters, proposerId, votingResults } = proposal;
   const author: string = await memberHandle(api, proposerId);
   const title: string = proposal.title.toString();
   const type: string = await getProposalType(api, id);
+  // TODO catch ExecutionFailed
   const args: string[] = [String(id), title, type, stage, result, author];
   const message: string = formatProposalMessage(args);
   const createdAt: number = proposal.createdAt.toNumber();
@@ -150,7 +155,7 @@ export const proposalDetail = async (
     message,
     stage,
     result,
-    exec,
+    executed,
     description,
     votes: votingResults,
     type,

+ 3 - 3
src/types.ts

@@ -19,7 +19,7 @@ export interface IState {
   //gethandle: (account: AccountId | string)  => string;
   connecting: boolean;
   now: number;
-  era:number;
+  era: number;
   block: number;
   blocks: Block[];
   nominators: string[];
@@ -100,7 +100,7 @@ export interface ProposalDetail {
   parameters: ProposalParameters;
   stage: any;
   result: string;
-  exec: any;
+  executed?: { Executed: null } | { ExecutionFailed: { error: string } };
   id: number;
   title: string;
   description: any;
@@ -109,7 +109,7 @@ export interface ProposalDetail {
   votesByAccount?: Vote[];
   author: string;
   authorId: number;
-  detail? : any
+  detail?: any;
 }
 
 export interface Vote {