瀏覽代碼

update Councils Leaderboard

Joystream Stats 3 年之前
父節點
當前提交
4906a1ef91

+ 30 - 9
src/App.tsx

@@ -64,14 +64,6 @@ class App extends React.Component<IProps, IState> {
     );
     this.fetchMints(api, [2, 3, 4]);
     this.updateStatus(api);
-
-    let { status } = this.state;
-    let blockHash = await api.rpc.chain.getBlockHash(1);
-    if (blockHash)
-      status.startTime = (
-        await api.query.timestamp.now.at(blockHash)
-      ).toNumber();
-    this.save("status", status);
   }
 
   async fetchMints(api: Api, ids: number[]) {
@@ -170,11 +162,15 @@ class App extends React.Component<IProps, IState> {
 
     let { status, councils } = this.state;
     status.era = await this.updateEra(api);
-
+    status.election = await this.updateElection(api);
     councils.forEach((c) => {
       if (c.round > status.council) status.council = c;
     });
 
+    let hash: string = await api.rpc.chain.getBlockHash(1);
+    if (hash)
+      status.startTime = (await api.query.timestamp.now.at(hash)).toNumber();
+
     const nextMemberId = await await api.query.members.nextMemberId();
     status.members = nextMemberId - 1;
     status.proposals = await get.proposalCount(api);
@@ -200,6 +196,31 @@ class App extends React.Component<IProps, IState> {
     return era;
   }
 
+  async updateElection(api: Api) {
+    console.debug(`Updating council`);
+    const round = Number((await api.query.councilElection.round()).toJSON());
+    const termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
+    const stage = (await api.query.councilElection.stage()).toJSON();
+    let stageEndsAt = 0;
+    if (stage) {
+      const key = Object.keys(stage)[0];
+      stageEndsAt = stage[key];
+    }
+
+    const stages = [
+      "announcingPeriod",
+      "votingPeriod",
+      "revealingPeriod",
+      "newTermDuration",
+    ];
+
+    let durations = await Promise.all(
+      stages.map((s) => api.query.councilElection[s]())
+    ).then((stages) => stages.map((stage) => stage.toJSON()));
+    durations.push(durations.reduce((a, b) => a + b, 0));
+    return { round, stageEndsAt, termEndsAt, stage, durations };
+  }
+
   async fetchCouncils() {
     const { data } = await axios.get(
       `https://api.joystreamstats.live/api/v1/councils`

+ 45 - 0
src/components/Councils/ConsulRow.tsx

@@ -0,0 +1,45 @@
+import React from "react";
+import VotesTooltip from "./VotesTooltip";
+import { Consul } from "../../types";
+
+// display number of votes per term
+
+const ConsulProposalVotes = (props: {
+  handle: string;
+  percent: number;
+  proposalsPerRound: number[];
+  rank: number;
+  terms: Consul[];
+}) => {
+  const { handle, percent, proposals, rank, terms, votes } = props;
+
+  // associate terms to rounds
+  let rounds: Consul[] = [];
+  for (let round = 0; round < props.proposalsPerRound.length; round++) {
+    const term = terms.find((t) => t.councilRound === round + 1);
+    rounds[round] = term || null;
+  }
+
+  return (
+    <tr key={handle}>
+      <td>{rank + 1}</td>
+      <td>{handle}</td>
+      <td>
+        {votes} / {proposals} ({percent}%)
+      </td>
+      {rounds.map((term, round: number) =>
+        term ? (
+          <VotesTooltip
+            key={`votes-${handle}-${term.councilRound}`}
+            proposals={props.proposalsPerRound[term.councilRound - 1]}
+            {...term}
+          />
+        ) : (
+          <td key={`votes-${handle}-${round + 1}`} />
+        )
+      )}
+    </tr>
+  );
+};
+
+export default ConsulProposalVotes;

+ 21 - 31
src/components/Councils/CouncilVotes.tsx

@@ -1,7 +1,6 @@
 import React, { Component } from "react";
 import { Table } from "react-bootstrap";
-import ProposalOverlay from "../Proposals/ProposalOverlay";
-import { VoteButton } from "..";
+import { ProposalOverlay, VoteButton } from "..";
 import { Member, ProposalDetail, Seat } from "../../types";
 
 interface IProps {
@@ -33,16 +32,9 @@ class CouncilVotes extends Component<IProps, IState> {
   }
 
   render() {
-    const { block, council, members, proposals, round } = this.props;
+    const { block, round, consuls, proposals } = this.props;
     const { expand } = this.state;
 
-    let councilMembers: Member[] = [];
-    council.forEach((seat) => {
-      const member = members.find((m) => m.account === seat.member);
-      if (!member) return;
-      councilMembers.push(member);
-    });
-
     const fail = "btn btn-outline-danger";
     const styles: { [key: string]: string } = {
       Approved: "btn btn-success",
@@ -56,7 +48,12 @@ class CouncilVotes extends Component<IProps, IState> {
       <Table className="text-light text-center">
         <thead onClick={this.toggleExpand}>
           <tr>
-            <th colSpan={council.length + 1}>Round {round}</th>
+            <th className="text-left" colSpan={consuls.length - 1}>
+              Round {round}
+            </th>
+            <th className="text-right" colSpan={2}>
+              {proposals.length} Proposals
+            </th>
           </tr>
         </thead>
         <tbody>
@@ -71,26 +68,19 @@ class CouncilVotes extends Component<IProps, IState> {
                     <ProposalOverlay block={block} {...p} />
                   </td>
 
-                  {p.votesByAccount ? (
-                    council.map((seat) => {
-                      if (!p.votesByAccount || !members) return <td />;
-                      const member = members.find(
-                        (m) => m.account === seat.member
-                      );
-                      if (!member) return <td />;
-                      const vote = p.votesByAccount.find(
-                        (v) => v.handle === member.handle
-                      );
-                      if (!vote) return <td />;
-                      return (
-                        <td key={member.handle}>
-                          <VoteButton handle={member.handle} vote={vote.vote} />
-                        </td>
-                      );
-                    })
-                  ) : (
-                    <td>Loading ..</td>
-                  )}
+                  {consuls.map((c) => {
+                    const { handle } = c.member;
+                    if (!p.votes) return <td key={handle} />;
+                    const vote = p.votes.find(
+                      ({ member }) => member.handle === handle
+                    );
+                    if (!vote) return <td key={handle} />;
+                    return (
+                      <td key={handle}>
+                        <VoteButton handle={handle} vote={vote.vote} />
+                      </td>
+                    );
+                  })}
                 </tr>
               ))}
         </tbody>

+ 79 - 170
src/components/Councils/Leaderboard.tsx

@@ -1,190 +1,99 @@
 import React from "react";
-import { Table, OverlayTrigger, Tooltip } from "react-bootstrap";
-import SeatBackers from "./SeatBackers";
-import { Member, ProposalDetail, Seat } from "../../types";
+import { Table } from "react-bootstrap";
+import ConsulProposalVotes from "./ConsulRow";
+import { Council, ProposalDetail } from "../../types";
 
-interface CouncilMember {
+interface ConsulTerms {
   handle: string;
+  memberId: number;
+  terms: Consul[];
   votes: number;
-  proposalCount: number;
-  percentage: number;
-  seat: Seat;
+  percent: number;
 }
 
-interface CouncilVotes {
-  proposalCount: number;
-  members: CouncilMember[];
-}
+const getConsulsTerms = (
+  councils: Council[],
+  proposals: ProposalDetail[]
+): ConsulTerm[] => {
+  // TODO from backend
+  let consuls: { [key: string]: ConsulTerms[] } = {};
+
+  councils
+    .sort((a, b) => a.round - b.round)
+    .forEach((council) => {
+      council.consuls.forEach((term) => {
+        const { memberId, member } = term;
+        const { handle } = member;
+
+        // update or create consul
+        const [terms, votes] = consuls[handle]
+          ? [
+              consuls[handle].terms.concat(term),
+              consuls[handle].votes + term.votes.length,
+            ]
+          : [[term], term.votes.length];
+        const percent = proposals.length
+          ? ((100 * votes) / proposals.length).toFixed()
+          : 0;
+        consuls[handle] = { handle, memberId, terms, votes, percent };
+      });
+    });
+  return Object.values(consuls);
+};
 
 const LeaderBoard = (props: {
   proposals: ProposalDetail[];
-  members: Member[];
-  councils: Seat[][];
-  stages: number[];
+  councils: Council[];
 }) => {
-  const { members, proposals, stages } = props;
-  const councils = props.councils.filter((c) => c);
-  const summarizeVotes = (handle: string, propList: ProposalDetail[]) => {
-    let votes = 0;
-    propList.forEach((p) => {
-      if (!p || !p.votesByAccount) return;
-      const vote = p.votesByAccount.find((v) => v.handle === handle);
-      if (vote && vote.vote !== "") votes++;
-    });
-    return votes;
-  };
+  const { councils, proposals } = props;
 
-  let councilMembers: Member[] = [];
+  // prepare data to show:
+  // - how often voted a consul per term (ProposalVote)
+  // - stakes (own + voters) (CouncilVote)
+  // council rounds as columns with consul`s votes per term as rows
 
-  const councilVotes: CouncilVotes[] = councils
+  const consuls: ConsulTerms[] = getConsulsTerms(councils, proposals);
+  const proposalsPerRound: number[] = councils
+    .sort((a, b) => a.round - b.round)
     .map(
-      (council, i: number): CouncilVotes => {
-        const start =
-          stages[0] + stages[1] + stages[2] + stages[3] + i * stages[4];
-        const end =
-          stages[0] + stages[1] + stages[2] + stages[3] + (i + 1) * stages[4];
-        const proposalsRound = proposals.filter(
-          (p) => p && p.createdAt > start && p.createdAt < end
-        );
-        const proposalCount = proposalsRound.length;
-        if (!proposalCount) return null;
-
-        const members: CouncilMember[] = council.map(
-          (seat): CouncilMember => {
-            const member = props.members.find((m) => m.account === seat.member);
-            if (!member)
-              return {
-                handle: ``,
-                seat,
-                votes: 0,
-                proposalCount,
-                percentage: 0,
-              };
-
-            councilMembers.find((m) => m.id === member.id) ||
-              councilMembers.push(member);
-
-            let votes = summarizeVotes(member.handle, proposalsRound);
-            const percentage = Math.round((100 * votes) / proposalCount);
-            return {
-              handle: member.handle,
-              seat,
-              votes,
-              proposalCount,
-              percentage,
-            };
-          }
-        );
-
-        return { proposalCount, members };
-      }
-    )
-    .filter((c) => c);
-
-  councilMembers = councilMembers
-    .map((m) => {
-      return { ...m, id: summarizeVotes(m.handle, props.proposals) };
-    })
-    .sort((a, b) => b.id - a.id);
+      ({ round }) => proposals.filter((p) => p.councilRound === round).length
+    );
 
   return (
-    <div className={`text-light m-3`}>
-      <h2 className="w-100 text-center text-light">Votes per Council Member</h2>
-      <Table className={`text-light`}>
-        <thead>
-          <tr>
-            <th>Council Member</th>
-            <th>Total Votes</th>
-            {councilVotes.map((c, i: number) => (
-              <th key={`round-${i + 1}`}>Round {1 + i}</th>
-            ))}
-          </tr>
-        </thead>
-        <tbody>
-          {councilMembers.map((member: Member) => (
-            <MemberRow
-              key={member.handle}
-              member={member}
-              members={members}
-              votes={councilVotes}
+    <Table className={`text-light`}>
+      <thead>
+        <tr>
+          <th>Rank</th>
+          <th>Consul</th>
+          <th>Total Votes</th>
+          {councils.map((c, i: number) => (
+            <th key={`round-${i + 1}`}>Round {1 + i}</th>
+          ))}
+        </tr>
+      </thead>
+      <tbody>
+        {consuls
+          .sort((a, b) => b.percent - a.percent)
+          .slice(0, 25)
+          .map((c: ConsulTerms, rank: number) => (
+            <ConsulProposalVotes
+              key={c.handle}
+              proposals={proposals.length}
+              proposalsPerRound={proposalsPerRound}
+              rank={rank}
+              {...c}
             />
           ))}
-          <tr>
-            <td>Proposals</td>
-            <td>{proposals.length}</td>
-            {councilVotes.map((round, i: number) => (
-              <td key={`round-${i + 1}`}>{round.proposalCount}</td>
-            ))}
-          </tr>
-        </tbody>
-      </Table>
-    </div>
-  );
-};
-
-const MemberRow = (props: {
-  member: Member;
-  members: Member[];
-  votes: CouncilVotes[];
-}) => {
-  const { votes, members } = props;
-  const { handle } = props.member;
-  let totalVotes = 0;
-  let totalProposals = 0;
-
-  votes.forEach((c) => {
-    const m = c.members.find((member) => member.handle === handle);
-    if (!m) return;
-    totalVotes += m.votes;
-    totalProposals += m.proposalCount;
-  });
-
-  const totalPercent = Math.round((100 * totalVotes) / totalProposals);
-
-  return (
-    <tr key={handle}>
-      <td>{handle}</td>
-      <td>
-        {totalVotes} / {totalProposals} ({totalPercent}%)
-      </td>
-      {props.votes.map((c, round: number) => (
-        <RoundVotes
-          key={`round-${round + 1}-${handle}`}
-          member={c.members.find((member) => member.handle === handle)}
-          members={members}
-        />
-      ))}
-    </tr>
-  );
-};
-
-const RoundVotes = (props: { member?: CouncilMember; members: Member[] }) => {
-  if (!props.member || props.member.handle === "") return <td />;
-  const { votes, percentage, seat } = props.member;
-
-  const getHandle = (account: string) => {
-    const member = props.members.find((m) => m.account === account);
-    return member ? member.handle : account;
-  };
-
-  return (
-    <OverlayTrigger
-      placement="left"
-      overlay={
-        <Tooltip id={seat.member}>
-          <h4>Stakes</h4>
-          <div className="d-flex flex-row justify-content-between">
-            <div className="mr-2">Own</div>
-            <div>{seat.stake}</div>
-          </div>
-          <SeatBackers backers={seat.backers} getHandle={getHandle} />
-        </Tooltip>
-      }
-    >
-      <td>
-        {votes} ({percentage}%)
-      </td>
-    </OverlayTrigger>
+        <tr>
+          <td>Proposals</td>
+          <td></td>
+          <td>{proposals.length}</td>
+          {proposalsPerRound.map((count: number, round: number) => (
+            <td key={`round-${round}`}>{count}</td>
+          ))}
+        </tr>
+      </tbody>
+    </Table>
   );
 };
 

+ 13 - 14
src/components/Councils/SeatBackers.tsx

@@ -1,24 +1,23 @@
 import React from "react";
 import { Backer } from "../../types";
 
-const SeatBackers = (props: {
-  getHandle: (account: string) => string;
-  backers: Backer[];
-}) => {
-  const { getHandle, backers } = props;
+const SeatBackers = (props: { backers: Backer[] }) => {
+  const { backers } = props;
   if (!backers.length) return <span />;
 
   return (
     <div>
-      {backers.map((backer) => (
-        <div
-          key={`${backer.member}-${backer.stake}`}
-          className="d-flex flex-row justify-content-between"
-        >
-          <div className="mr-2">{getHandle(backer.member)}</div>
-          <div>{backer.stake}</div>
-        </div>
-      ))}
+      {backers
+        .sort((a, b) => b.stake - a.stake)
+        .map((backer) => (
+          <div
+            key={`${backer.consulId}-${backer.memberId}`}
+            className="d-flex flex-row justify-content-between"
+          >
+            <div className="mr-2">{backer.member.handle}</div>
+            <div>{backer.stake}</div>
+          </div>
+        ))}
     </div>
   );
 };

+ 42 - 0
src/components/Councils/TermLengths.jsx

@@ -0,0 +1,42 @@
+import React from "react";
+import { Table } from "react-bootstrap";
+
+const Terms = (props: { councils: Council[], stage: number[] }) => {
+  const { councils, stage } = props;
+  return (
+    <Table className="w-100 text-light">
+      <thead>
+        <tr>
+          <th>Round</th>
+          <th>Announced</th>
+          <th>Voted</th>
+          <th>Revealed</th>
+          <th>Term start</th>
+          <th>Term end</th>
+        </tr>
+      </thead>
+      <tbody>
+        {councils
+          .sort((a, b) => a.round - b.round)
+          .map((council, i: number) => (
+            <tr key={i + 1}>
+              <td>{i + 1}</td>
+              <td>{1 + i * stage[4]}</td>
+              <td>
+                {council.start} {stage[0] + i * stage[4]}
+              </td>
+              <td>
+                {council.start + stage[1]} {stage[0] + stage[1] + i * stage[4]}
+              </td>
+              <td>
+                {council.start + stage[1] + stage[2]}{" "}
+                {stage[0] + stage[2] + stage[3] + i * stage[4]}
+              </td>
+              <td>{(1 + i) * stage[4]}</td>
+            </tr>
+          ))}
+      </tbody>
+    </Table>
+  );
+};
+export default Terms;

+ 34 - 0
src/components/Councils/VotesTooltip.tsx

@@ -0,0 +1,34 @@
+import React from "react";
+import { InfoTooltip } from "..";
+import { Consul } from "../../types";
+import Voters from "./SeatBackers";
+
+// display stakes tooltip and votes and percentage of proposals voted on
+
+const VotesTooltip = (props: Consul) => {
+  const { member, stake, votes, voters, proposals = 0 } = props;
+  const percent = proposals ? ((100 * votes.length) / proposals).toFixed() : 0;
+  const totalStake = voters.reduce((a, b) => a + b.stake, stake);
+  const tag = proposals ? `${votes.length} (${percent}%)` : `-`;
+
+  return (
+    <InfoTooltip
+      placement="bottom"
+      id={`stakes-${member.handle}`}
+      title={
+        <>
+          <h4>Stakes: {(totalStake / 1000000).toFixed(1)} M</h4>
+          <div className="d-flex flex-row justify-content-between">
+            <div className="mr-2">Own</div>
+            <div>{stake}</div>
+          </div>
+          <Voters backers={voters} />
+        </>
+      }
+    >
+      <td>{tag}</td>
+    </InfoTooltip>
+  );
+};
+
+export default VotesTooltip;

+ 26 - 53
src/components/Councils/index.tsx

@@ -1,77 +1,50 @@
 import React from "react";
-import { Table } from "react-bootstrap";
 import LeaderBoard from "./Leaderboard";
 import CouncilVotes from "./CouncilVotes";
-import { Member, ProposalDetail, Seat, Status } from "../../types";
+import Terms from "./TermLengths";
+import { Loading } from "..";
+
+import { Council, ProposalDetail, Status } from "../../types";
 
 const Rounds = (props: {
   block: number;
-  members: Member[];
-  councils: Seat[][];
-  proposals: any;
-  history: any;
+  councils: Council[];
+  proposals: ProposalDetail[];
   status: Status;
 }) => {
-  const { block, councils, members, proposals, status } = props;
-  if (!status.council) return <div />;
-  const stage = status.council.durations;
+  const { block, councils, proposals, status } = props;
+  if (!status.election) return <Loading target="election status" />;
+  const stage: number[] = status.election.durations;
+
   return (
     <div className="w-100">
-      <Table className="w-100 text-light">
-        <thead>
-          <tr>
-            <th>Round</th>
-            <th>Announced</th>
-            <th>Voted</th>
-            <th>Revealed</th>
-            <th>Term start</th>
-            <th>Term end</th>
-          </tr>
-        </thead>
-        <tbody>
-          {councils.map((council, i: number) => (
-            <tr key={i + 1}>
-              <td>{i + 1}</td>
-              <td>{1 + i * stage[4]}</td>
-              <td>{stage[0] + i * stage[4]}</td>
-              <td>{stage[0] + stage[1] + i * stage[4]}</td>
-              <td>{stage[0] + stage[2] + stage[3] + i * stage[4]}</td>
-              <td>
-                {stage[0] + stage[1] + stage[2] + stage[3] + +i * stage[4]}
-              </td>
-            </tr>
-          ))}
-        </tbody>
-      </Table>
-
+      <h2 className="w-100 text-center text-light">Leaderboard</h2>
       <LeaderBoard
-        stages={stage}
+        stages={status.election?.stage}
         councils={councils}
-        members={members}
         proposals={proposals}
+        status={status}
       />
 
-      <h2 className="w-100 text-center text-light">Votes per Council</h2>
+      <h2 className="w-100 text-center text-light">Proposal Votes</h2>
       {councils
-        .filter((c) => c)
-        .map((council, i: number) => (
+        .sort((a, b) => b.round - a.round)
+        .map((council) => (
           <CouncilVotes
-            key={i}
-            expand={i === councils.length - 1}
+            key={council.round}
+            {...council}
+            expand={council.round === councils.length - 1}
             block={block}
-            round={i + 1}
-            council={council}
-            members={props.members}
-            proposals={props.proposals.filter(
-              (p: ProposalDetail) =>
-                p &&
-                p.createdAt >
-                  stage[0] + stage[1] + stage[2] + stage[3] + i * stage[4] &&
-                p.createdAt <
-                  stage[0] + stage[1] + stage[2] + stage[3] + (i + 1) * stage[4]
+            proposals={proposals.filter(
+              (p) =>
+                p.createdAt > council.start + stage[1] + stage[2] &&
+                p.createdAt < council.end + stage[0] + stage[1] + stage[2]
             )}
           />
         ))}
+
+      <h2 className="w-100 text-center text-light">Term Durations</h2>
+      <Terms councils={councils} stage={stage} />
     </div>
   );
 };

+ 31 - 1
src/types.ts

@@ -9,9 +9,39 @@ export interface Api {
   derive: any;
 }
 
+export interface ProposalVote {
+  id: number;
+  consulId: number;
+  memberId: number;
+  proposalId: number;
+  proposal: { title: string };
+  vote: string;
+}
+
+export interface CouncilVote {
+  id: number;
+  stake: number;
+  memberId: number;
+  member: { handle: string };
+}
+
+export interface Consul {
+  consulId: number;
+  councilRound: number;
+  memberId: number;
+  member: { handle: string };
+  stake: number;
+  voters: CouncilVote[];
+  votes: ProposalVote[];
+}
+
 export interface Council {
   round: number;
-  consuls: { member: { handle: string } }[];
+  start: number;
+  end: number;
+  startDate: string;
+  endDate: string;
+  consuls: Consul[];
 }
 
 export interface Status {