瀏覽代碼

Proposal stats

Joystream Stats 4 年之前
父節點
當前提交
bb55275472
共有 7 個文件被更改,包括 177 次插入28 次删除
  1. 2 3
      src/App.tsx
  2. 3 0
      src/components/Dashboard/index.tsx
  3. 161 18
      src/components/Proposals/index.tsx
  4. 2 2
      src/components/Routes/index.tsx
  5. 0 4
      src/index.css
  6. 2 0
      src/lib/getters.ts
  7. 7 1
      src/types.ts

+ 2 - 3
src/App.tsx

@@ -88,9 +88,7 @@ class App extends React.Component<IProps, IState> {
 
   async fetchProposals(api: Api) {
     const proposalCount = await get.proposalCount(api);
-    for (let i = proposalCount; i > 0; i--) {
-      this.fetchProposal(api, i);
-    }
+    for (let i = proposalCount; i > 0; i--) this.fetchProposal(api, i);
   }
   async fetchProposal(api: Api, id: number) {
     let { proposals } = this.state;
@@ -98,6 +96,7 @@ class App extends React.Component<IProps, IState> {
 
     const proposal = await get.proposalDetail(api, id);
     if (!proposal) return;
+
     proposals[id] = proposal;
     this.save("proposals", proposals);
   }

+ 3 - 0
src/components/Dashboard/index.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+import { Link } from "react-router-dom";
 import { ActiveProposals, Council } from "..";
 import Nominators from "./Nominators";
 import Validators from "./Validators";
@@ -20,6 +21,8 @@ const Dashboard = (props: IState) => {
       <div className="box">
         <h3>Active Proposals</h3>
         <ActiveProposals block={props.block} proposals={props.proposals} />
+        <hr />
+        <Link to={`/proposals`}>Show all</Link>
       </div>
       <Council council={props.council} handles={props.handles} />
       <div className="d-flex flex-row">

+ 161 - 18
src/components/Proposals/index.tsx

@@ -1,30 +1,173 @@
 import React from "react";
+import { Button, OverlayTrigger, Tooltip, Table } from "react-bootstrap";
 import { Link } from "react-router-dom";
-import Proposal from "./Proposal";
 import { ProposalDetail } from "../../types";
+import moment from "moment";
 
-const Proposals = (props: { proposals: ProposalDetail[] }) => {
-  const { proposals } = props;
+const formatTime = (time: number) => {
+  return moment(time).format("DD/MM/YYYY HH:mm");
+};
+
+const Proposals = (props: {
+  now: number;
+  block: number;
+  proposals: ProposalDetail[];
+}) => {
+  const { block, now } = props;
+
+  const proposals = props.proposals
+    .filter((p) => p)
+    .sort((a, b) => b.id - a.id);
+  if (!proposals.length) return <div>No proposals found.</div>;
 
-  const active = proposals.filter((p) => p.stage === "Active");
-  const executing = proposals.filter((p) => p.exec);
+  const startTime = now - block * 6000;
+  const durations: any = proposals
+    .map((p) => (p.finalizedAt ? p.finalizedAt - p.createdAt : null))
+    .filter((p) => p); // TODO typescript doesnt understand filter
+  const avgBlocks =
+    durations.reduce((a: number, b: number) => a + b) / durations.length;
+  const avgDays = avgBlocks ? Math.floor(avgBlocks / 14400) : 0;
+  const avgHours = avgBlocks
+    ? Math.floor((avgBlocks - avgDays * 14400) / 600)
+    : 0;
 
   return (
-    <div className="d-flex flex-column">
-      <div className="d-flex flex-row">
-        {(active.length &&
-          active.map((p, key: number) => (
-            <Link to={`/proposal/${p.id}`}>{p.id}</Link>
-          ))) || <div className="box">No active proposals.</div>}
-      </div>
-      <div className="d-flex flex-row">
-        {(executing.length &&
-          executing.map((p, key: number) => (
-            <Link to={`/proposal/${p.id}`}>{p.id}</Link>
-          ))) || <div className="box">No executing proposals.</div>}
-      </div>
+    <div className="bg-light text-center">
+      <Link to={`/`}>
+        <Button variant="secondary" className="p-1 m-3">
+          back
+        </Button>
+      </Link>
+      <h1>Joystream Proposals</h1>
+      <Table>
+        <thead>
+          <tr>
+            <td>ID</td>
+            <td className="text-right">Proposal</td>
+            <td>Result</td>
+            <td>
+              Voting Duration
+              <br />
+              Average: {avgDays ? `${avgDays}d` : ""}{" "}
+              {avgHours ? `${avgHours}h` : ""}
+            </td>
+            <td className="text-right">Created</td>
+            <td className="text-left">Finalized</td>
+          </tr>
+        </thead>
+        <tbody>
+          {proposals.map((p) => (
+            <ProposalRow
+              key={p.id}
+              startTime={startTime}
+              block={block}
+              {...p}
+            />
+          ))}
+        </tbody>
+      </Table>
     </div>
   );
 };
 
+const Bar = (props: {
+  id: number;
+  blocks: number | null;
+  duration: string;
+  period: number;
+}) => {
+  const { blocks, duration, id, period } = props;
+  const percent = blocks ? 100 * (blocks / period) : 0;
+  return (
+    <OverlayTrigger
+      key={id}
+      placement="right"
+      overlay={
+        <Tooltip id={String(id)}>
+          {Math.floor(percent)}% of {period} blocks
+          <br />
+          {duration}
+        </Tooltip>
+      }
+    >
+      <div
+        className="bg-dark mr-2"
+        style={{ height: `30px`, width: `${percent}%` }}
+      ></div>
+    </OverlayTrigger>
+  );
+};
+
+const colors: { [key: string]: string } = {
+  Approved: "bg-success text-light",
+  Rejected: "bg-danger text-light",
+  Canceled: "bg-danger text-light",
+  Expired: "bg-warning text-dark",
+  Pending: "",
+};
+
+const ProposalRow = (props: any) => {
+  const { block, createdAt, description, finalizedAt, id, type } = props;
+
+  const url = `https://pioneer.joystreamstats.live/#/proposals/${id}`;
+  let result: string = props.result ? props.result : props.stage;
+  if (props.exec) result = "Executing";
+  const color = colors[result];
+
+  const created = formatTime(props.startTime + createdAt * 6000);
+  const finalized =
+    finalizedAt && formatTime(props.startTime + finalizedAt * 6000);
+
+  let blocks = finalizedAt ? finalizedAt - createdAt : block - createdAt;
+  if (blocks < 0) blocks = 0; // TODO make sure block is defined
+  const days = blocks ? Math.floor(blocks / 14400) : 0;
+  const hours = blocks ? Math.floor((blocks - days * 14400) / 600) : 0;
+  const daysStr = days ? `${days}d` : "";
+  const hoursStr = hours ? `${hours}h` : "";
+  const duration = blocks ? `${daysStr} ${hoursStr} / ${blocks} blocks` : "";
+
+  const { abstensions, approvals, rejections, slashes } = props.votes;
+
+  return (
+    <tr key={id}>
+      <td>{id}</td>
+
+      <td className="text-right">
+        <OverlayTrigger
+          key={id}
+          placement="right"
+          overlay={<Tooltip id={String(id)}>{description}</Tooltip>}
+        >
+          <a href={url}>
+            {props.title} ({type})
+          </a>
+        </OverlayTrigger>
+      </td>
+
+      <td className={color}>
+        <b>{result}</b>
+        <br />
+        {abstensions} / {approvals} / {rejections} / {slashes}
+      </td>
+      <td className="text-left  justify-content-center">
+        <Bar
+          id={id}
+          blocks={blocks}
+          period={props.parameters.votingPeriod}
+          duration={duration}
+        />
+      </td>
+
+      <td className="text-right">{created}</td>
+      <td className="text-left">
+        {finalized || (
+          <Button variant="danger">
+            <a href={url}>Vote!</a>
+          </Button>
+        )}
+      </td>
+    </tr>
+  );
+};
+
 export default Proposals;

+ 2 - 2
src/components/Routes/index.tsx

@@ -5,11 +5,11 @@ import { IState } from "../../types";
 const Routes = (props: IState) => {
   return (
     <Switch>
-      <Route path="/proposals" render={() => <Proposals {...props} />} />
       <Route
-        path="/proposal/:id"
+        path="/proposals/:id"
         render={(routeprops) => <Proposal {...routeprops} {...props} />}
       />
+      <Route path="/proposals" render={() => <Proposals {...props} />} />
       <Route path="/council" render={() => <Council {...props} />} />
       <Route path="/" render={() => <Dashboard {...props} />} />
     </Switch>

+ 0 - 4
src/index.css

@@ -54,7 +54,3 @@ h3 {
 .user {
   min-width: 75px;
 }
-
-.tooltip {
-  width: 400px !important;
-}

+ 2 - 0
src/lib/getters.ts

@@ -135,6 +135,8 @@ export const proposalDetail = async (
     result,
     exec,
     description,
+    votes: proposal.votingResults,
+    type,
   };
 };
 

+ 7 - 1
src/types.ts

@@ -1,6 +1,10 @@
 import { ApiPromise } from "@polkadot/api";
 import { MemberId } from "@joystream/types/members";
-import { ProposalParameters, ProposalStatus } from "@joystream/types/proposals";
+import {
+  ProposalParameters,
+  ProposalStatus,
+  VotingResults,
+} from "@joystream/types/proposals";
 import { Nominations } from "@polkadot/types/interfaces";
 import { Option } from "@polkadot/types/codec";
 import { StorageKey } from "@polkadot/types/primitive";
@@ -53,6 +57,8 @@ export interface ProposalDetail {
   id: number;
   title: string;
   description: any;
+  votes: VotingResults;
+  type: string;
 }
 
 export type ProposalArray = number[];