Bladeren bron

Dashboard design improvements

Joystream Stats 3 jaren geleden
bovenliggende
commit
f009d4580a

+ 0 - 56
src/components/Council/ElectionStatus.tsx

@@ -1,56 +0,0 @@
-import React from "react";
-import { Status } from "../../types";
-
-const timeLeft = (blocks: number) => {
-  const seconds = blocks * 6;
-  const days = Math.floor(seconds / 86400);
-  const hours = Math.floor((seconds - days * 86400) / 3600);
-  const minutes = Math.floor(seconds / 60);
-  return days ? `${days}d` : hours ? `${hours}h` : `${minutes}min`;
-};
-
-const ElectionStage = (props: {
-  block: number;
-  council: { stage; termEndsAt: number };
-  domain: string;
-}) => {
-  const { block, council, domain } = props;
-  if (!council) return <span>Loading..</span>;
-  const { stage, termEndsAt } = council;
-
-  if (!stage) {
-    if (!block || !termEndsAt) return <span />;
-    const left = timeLeft(termEndsAt - block);
-    return <span>election starts in {left}</span>;
-  }
-
-  let stageString = Object.keys(stage)[0];
-  const left = timeLeft(stage[stageString] - block);
-  if (stageString === "announcing")
-    return <a style={{color: "#fff"}} href={`${domain}/#/council/applicants`}>{left} to apply</a>;
-
-  if (stageString === "voting")
-    return <a style={{color: "#fff"}} href={`${domain}/#/council/applicants`}>{left} to vote</a>;
-
-  if (stageString === "revealing")
-    return <a style={{color: "#fff"}} href={`${domain}/#/council/votes`}>{left} to reveal votes</a>;
-
-  return <span>{JSON.stringify(stage)}</span>;
-};
-
-const ElectionStatus = (props: { block; domain: string; status: Status }) => {
-  const { domain, status } = props;
-  if (!status.block || !status.council) return <div />;
-
-  return (
-    <div className="text-center text-white">
-      <ElectionStage
-        block={status.block.id}
-        council={status.council}
-        domain={domain}
-      />
-    </div>
-  );
-};
-
-export default ElectionStatus;

+ 3 - 5
src/components/Councils/index.tsx

@@ -27,18 +27,16 @@ const Rounds = (props: {
       />
 
       <h2 className="w-100 text-center text-light">Proposal Votes</h2>
-      {councils
+      {council
         .sort((a, b) => b.round - a.round)
         .map((council) => (
           <CouncilVotes
             key={council.round}
             {...council}
-            expand={council.round === councils.length - 1}
+            expand={council.round === councils.length}
             block={block}
             proposals={proposals.filter(
-              (p) =>
-                p.createdAt > council.start + stage[1] + stage[2] &&
-                p.createdAt < council.end + stage[0] + stage[1] + stage[2]
+              ({ councilRound }) => councilRound === council.round
             )}
           />
         ))}

+ 28 - 24
src/components/Council/index.tsx → src/components/Dashboard/Council.tsx

@@ -1,7 +1,6 @@
 import React from "react";
-import ElectionStatus from "./ElectionStatus";
-import MemberBox from "../Members/MemberBox";
-import Loading from "../Loading";
+import { MemberBox, Spinner } from "..";
+import ElectionStatus from  "./ElectionStatus"
 import {
   Paper,
   Grid,
@@ -37,12 +36,10 @@ const CouncilGrid = (props: {
   status: Status;
   electionPeriods: number[];
 }) => {
-  const { getMember, councils, posts, proposals, status } = props;
-  const { council } = status;
+  const { getMember, councils, domain, posts, proposals, status } = props;
+  const { council, election } = status;
   const classes = useStyles();
 
-  if (!council) return <Loading target="council" />;
-
   const sortCouncil = (consuls) =>
     consuls.sort((a, b) => a.member.handle.localeCompare(b.member.handle));
 
@@ -64,29 +61,36 @@ const CouncilGrid = (props: {
         <AppBar className={classes.root} position="static">
           <Toolbar>
             <Typography variant="h6" className={classes.title}>
+              <ElectionStatus
+                domain={domain}
+                block={status.block?.id}
+                election={election}
+              />
               Council
             </Typography>
           </Toolbar>
         </AppBar>
         <div className="d-flex flex-wrap justify-content-between mt-2">
-          {sortCouncil(council.consuls).map((c) => (
-            <div key={c.memberId} className="col-12 col-md-4">
-              <MemberBox
-                id={c.memberId}
-                member={getMember(c.member.handle)}
-                councils={councils}
-                council={council}
-                proposals={proposals}
-                placement={"bottom"}
-                posts={posts}
-                startTime={status.startTime}
-                validators={props.validators}
-              />
-            </div>
-          ))}
+          {council?.consuls?.length ? (
+            sortCouncil(council.consuls).map((c) => (
+              <div key={c.memberId} className="col-12 col-md-4">
+                <MemberBox
+                  id={c.memberId}
+                  member={getMember(c.member.handle)}
+                  councils={councils}
+                  council={council}
+                  proposals={proposals}
+                  placement={"bottom"}
+                  posts={posts}
+                  startTime={status.startTime}
+                  validators={props.validators}
+                />
+              </div>
+            ))
+          ) : (
+            <Spinner />
+          )}
         </div>
-        <hr />
-        <ElectionStatus domain={props.domain} status={status} />
       </Paper>
     </Grid>
   );

+ 72 - 0
src/components/Dashboard/ElectionStatus.tsx

@@ -0,0 +1,72 @@
+import React from "react";
+import { Spinner } from "react-bootstrap";
+import { ElectionStage } from "../../types";
+
+const timeLeft = (blocks: number) => {
+  const seconds = blocks * 6;
+  const days = Math.floor(seconds / 86400);
+  const hours = Math.floor((seconds - days * 86400) / 3600);
+  const minutes = Math.floor(seconds / 60);
+  return days ? `${days}d` : hours ? `${hours}h` : `${minutes}min`;
+};
+
+const Stage = (props: {
+  block: number;
+  election: ElectionStage;
+  domain: string;
+}) => {
+  const { block, election, domain } = props;
+  const { stage, termEndsAt } = election;
+
+  if (!stage) {
+    if (!block || !termEndsAt) return <span />;
+    const left = timeLeft(termEndsAt - block);
+    return <span>election starts in {left}</span>;
+  }
+
+  let stageString = Object.keys(stage)[0];
+  console.log(`e`, stageString);
+  const left = timeLeft(stage[stageString] - block);
+  if (stageString === "announcing")
+    return (
+      <a style={{ color: "#fff" }} href={`${domain}/#/council/applicants`}>
+        {left} to apply
+      </a>
+    );
+
+  if (stageString === "voting")
+    return (
+      <a style={{ color: "#fff" }} href={`${domain}/#/council/applicants`}>
+        {left} to vote
+      </a>
+    );
+
+  if (stageString === "revealing")
+    return (
+      <a style={{ color: "#fff" }} href={`${domain}/#/council/votes`}>
+        {left} to reveal votes
+      </a>
+    );
+
+  return <span>{JSON.stringify(stage)}</span>;
+};
+
+const Election = (props: {
+  block: number;
+  domain: string;
+  council: Council;
+}) => {
+  const { domain, election, block } = props;
+
+  return (
+    <div className="text-white float-right">
+      {block && election ? (
+        <Stage block={block} election={election} domain={domain} />
+      ) : (
+        <Spinner animation="border" variant="dark" size="sm" />
+      )}
+    </div>
+  );
+};
+
+export default Election;

+ 37 - 35
src/components/Dashboard/Forum.tsx

@@ -1,17 +1,18 @@
 import React from "react";
-import LatestPost from "../Forum/LatestPost";
-import Loading from "../Loading";
+import { Link } from "react-router-dom";
+import { RefreshCw } from "react-feather";
+import { LatestPost, Spinner } from "..";
 
-import { Handles, Post, Thread } from "../../types";
+import { Post, Thread } from "../../types";
 import {
-    Grid,
-    Paper,
-    Link,
-    makeStyles,
-    Theme,
-    createStyles,
-    Toolbar,
-    AppBar, Typography,
+  Grid,
+  Paper,
+  makeStyles,
+  Theme,
+  createStyles,
+  Toolbar,
+  AppBar,
+  Typography,
 } from "@material-ui/core";
 
 const useStyles = makeStyles((theme: Theme) =>
@@ -29,14 +30,10 @@ const useStyles = makeStyles((theme: Theme) =>
   })
 );
 
-const Forum = (props: {
-  handles: Handles;
-  posts: Post[];
-  threads: Thread[];
-}) => {
+const Forum = (props: { posts: Post[]; threads: Thread[] }) => {
   const { handles, posts, threads, startTime } = props;
   const classes = useStyles();
-  if (!posts.length) return <Loading target="posts" />;
+
   return (
     <Grid
       style={{ textAlign: "center", backgroundColor: "#000", color: "#fff" }}
@@ -55,27 +52,32 @@ const Forum = (props: {
       >
         <AppBar className={classes.root} position="static">
           <Toolbar>
-              <Typography variant="h5" className={classes.title}>
-                  <Link style={{ color: "#fff" }} href={"/forum"}>
-                    Forum
-                  </Link>
-              </Typography>
+            <Typography variant="h5" className={classes.title}>
+              <Link style={{ color: "#fff" }} to={"/forum"}>
+                Posts
+              </Link>
+              <RefreshCw className="ml-2" onClick={props.updateForum} />
+            </Typography>
           </Toolbar>
         </AppBar>
 
-        {props.posts
-          .sort((a, b) => b.id - a.id)
-          .slice(0, 10)
-          .map((post) => (
-            <LatestPost
-              key={post.id}
-              selectThread={() => {}}
-              handles={handles}
-              post={post}
-              thread={threads.find((t) => t.id === post.threadId)}
-              startTime={startTime}
-            />
-          ))}
+        {posts.length ? (
+          posts
+            .sort((a, b) => b.id - a.id)
+            .slice(0, 10)
+            .map((post) => (
+              <LatestPost
+                key={post.id}
+                selectThread={() => {}}
+                handles={handles}
+                post={post}
+                thread={threads.find((t) => t.id === post.threadId)}
+                startTime={startTime}
+              />
+            ))
+        ) : (
+          <Spinner />
+        )}
       </Paper>
     </Grid>
   );

+ 5 - 2
src/components/Dashboard/Proposals.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 import { Link } from "react-router-dom";
+import { RefreshCw } from "react-feather";
 import { ProposalTable } from "..";
 import {
   AppBar,
@@ -66,14 +67,16 @@ const Proposals = (props: {
           textAlign: "center",
           backgroundColor: "#4038FF",
           color: "#fff",
-          height: 500,
+          minHeight: 500,
+          maxHeight: 500,
           overflow: "auto",
         }}
       >
         <AppBar className={classes.root} position="static">
           <Toolbar>
             <Typography variant="h6" className={classes.title}>
-              Active Proposals
+              Active Proposals: {pending.length}
+              <RefreshCw className="ml-2" onClick={props.fetchProposals} />
             </Typography>
             <Link className="m-3 text-light" to={"/proposals"}>
               All

+ 16 - 10
src/components/Dashboard/index.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import Council from "../Council";
+import Council from "./Council";
 import Forum from "./Forum";
 import Proposals from "./Proposals";
 import Validators from "../Validators";
@@ -35,6 +35,17 @@ const Dashboard = (props: IProps) => {
     <div style={{ flexGrow: 1 }}>
       <Container maxWidth="xl">
         <Grid container spacing={3}>
+          <Proposals
+            fetchProposals={props.fetchProposals}
+            block={status.block ? status.block.id : 0}
+            members={members}
+            councils={councils}
+            posts={posts}
+            proposals={proposals}
+            proposalPosts={props.proposalPosts}
+            validators={validators}
+            status={status}
+          />
           <Council
             getMember={getMember}
             councils={councils}
@@ -46,17 +57,12 @@ const Dashboard = (props: IProps) => {
             validators={validators}
             domain={domain}
           />
-          <Proposals
-            block={status.block ? status.block.id : 0}
-            members={members}
-            councils={councils}
+          <Forum
+            updateForum={props.updateForum}
             posts={posts}
-            proposals={proposals}
-            proposalPosts={props.proposalPosts}
-            validators={validators}
-            status={status}
+            threads={threads}
+            startTime={status.startTime}
           />
-          <Forum posts={posts} threads={threads} startTime={status.startTime} />
           <Grid
             style={{
               textAlign: "center",

+ 2 - 1
src/components/Forum/LatestPost.tsx

@@ -15,7 +15,8 @@ const LatestPost = (props: {
 }) => {
   const { selectThread, post, startTime } = props;
   const { author, createdAt, id, thread, text } = post;
-  const created = moment(startTime + createdAt.block * 6000);
+  const created = moment(startTime + createdAt * 6000);
+  if (!created.isValid) console.debug(`created`, createdAt, startTime, created);
   return (
     <div
       key={id}

+ 2 - 1
src/components/Members/MemberBox.tsx

@@ -5,7 +5,8 @@ import { Council, Post, ProposalDetail } from "../../types";
 import { Link } from "react-router-dom";
 import InfoTooltip from "../Tooltip";
 
-const shortName = (key) => `${key.slice(0, 5)}..${key.slice(key.length - 5)}`;
+const shortName = (key = "") =>
+  `${key.slice(0, 5)}..${key.slice(key.length - 5)}`;
 
 const MemberBox = (props: {
   council: Council;

+ 2 - 2
src/components/Proposals/NavButtons.tsx

@@ -8,8 +8,8 @@ const NavButtons = (props: {
   limit: number;
   proposals: number;
 }) => {
-  const { setPage, limit, page, proposals } = props;
-  if (proposals < limit) return <div/>
+  const { show,setPage, limit, page, proposals } = props;
+  if (!show || proposals < limit) return <div/>
   return (
     <div className="text-center my-2">
       <Button

+ 4 - 9
src/components/Proposals/ProposalTable.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { Button } from "react-bootstrap";
+import { Spinner } from "..";
 import Head from "./TableHead";
 import Row from "./Row";
 import NavBar from "./NavBar";
@@ -100,7 +100,6 @@ class ProposalTable extends React.Component<IProps, IState> {
 
   render() {
     const {
-      fetchProposals,
       hideNav,
       block,
       council,
@@ -162,7 +161,6 @@ class ProposalTable extends React.Component<IProps, IState> {
         />
 
         <Head
-          show={!hideNav}
           setKey={this.setKey}
           approved={approved}
           proposals={proposals.length}
@@ -170,6 +168,7 @@ class ProposalTable extends React.Component<IProps, IState> {
           avgHours={avgHours}
         />
         <NavButtons
+          show={!hideNav}
           setPage={this.setPage}
           page={page}
           limit={perPage + 1}
@@ -178,12 +177,7 @@ class ProposalTable extends React.Component<IProps, IState> {
 
         <div className="d-flex flex-column overflow-auto p-2">
           {!proposals.length ? (
-            <div>
-              No proposals cached.{" "}
-              <Button variant="secondary" onClick={fetchProposals}>
-                Fetch
-              </Button>{" "}
-            </div>
+            <Spinner />
           ) : (
             proposals
               .slice((page - 1) * perPage, page * perPage)
@@ -204,6 +198,7 @@ class ProposalTable extends React.Component<IProps, IState> {
           )}
         </div>
         <NavButtons
+          show={!hideNav}
           setPage={this.setPage}
           page={page}
           limit={perPage + 1}

+ 14 - 15
src/components/Proposals/Row.tsx

@@ -8,7 +8,7 @@ import Bar from "./Bar";
 import Posts from "./Posts";
 import Detail from "./Detail";
 import { formatDate } from "../../lib/util";
-
+import { domain } from "../../config";
 import { ProposalParameters, VotingResults } from "@joystream/types/proposals";
 import {
   Council,
@@ -67,7 +67,6 @@ const ProposalRow = (props: {
     block,
     council,
     councils,
-    domain,
     createdAt,
     description,
     executed,
@@ -154,21 +153,21 @@ const ProposalRow = (props: {
           </InfoTooltip>
           <Detail detail={detail} type={type} />
         </div>
+      </div>
 
-        <div className="d-flex flex-wrap p-2">
-          <VotesBubbles votes={votes} />
+      <div className="d-flex flex-wrap p-2">
+        <VotesBubbles votes={votes} />
 
-          {hasToVote.map((c) => (
-            <Button
-              key={c.id}
-              variant="outline-secondary"
-              className="btn-sm p-1"
-              title={`${c.member.handle} did not vote.`}
-            >
-              {c.member.handle}
-            </Button>
-          ))}
-        </div>
+        {hasToVote?.map((c) => (
+          <Button
+            key={c.id}
+            variant="outline-secondary"
+            className="btn-sm p-1"
+            title={`${c.member.handle} did not vote.`}
+          >
+            {c.member.handle}
+          </Button>
+        ))}
       </div>
 
       <Bar

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

@@ -4,7 +4,7 @@ import { AppBar, Spinner } from "..";
 import { IState } from "../../types";
 
 const Calendar = React.lazy(() => import("../Calendar"));
-const Council = React.lazy(() => import("../Council"));
+const { Council } = React.lazy(() => import(".."));
 const Councils = React.lazy(() => import("../Councils"));
 const Curation = React.lazy(() => import("../Curation"));
 const Dashboard = React.lazy(() => import("../Dashboard"));

+ 118 - 0
src/components/Validators/Content.tsx

@@ -0,0 +1,118 @@
+import React, { useState } from "react";
+import { Button } from "react-bootstrap";
+import Stats from "./MinMax";
+import Validator from "./Validator";
+import Waiting from "./Waiting";
+import { Spinner } from "..";
+import { sortValidators } from "./util";
+
+const Content = (props: IState) => {
+  const [sortBy, setSortBy] = useState("totalStake");
+
+  const [showWaiting, setShowWaiting] = useState(false);
+  const [showValidators, setShowValidators] = useState(false);
+
+  const toggleValidators = () => setShowValidators((prev) => !prev);
+
+  const toggleWaiting = () => setShowWaiting((prev) => !prev);
+
+  const {
+    getMember = () => {},
+    councils,
+    members,
+    posts,
+    proposals,
+    validators,
+    nominators,
+    stashes,
+    stars,
+    rewardPoints,
+    status,
+    stakes,
+    tokenomics,
+  } = props;
+
+  if (!status?.block || !validators.length) return <Spinner />;
+
+  const { lastReward, block, era, startTime } = status;
+
+  const issued = tokenomics ? Number(tokenomics.totalIssuance) : 0;
+  const price = tokenomics ? Number(tokenomics.price) : 0;
+
+  const starred = stashes.filter((v) => stars[v]);
+  const unstarred = validators.filter((v) => !stars[v]);
+  const waiting = stashes.filter((s) => !stars[s] && !validators.includes(s));
+
+  return (
+    <>
+      <Stats
+        block={block}
+        era={era}
+        stakes={stakes}
+        issued={issued}
+        price={price}
+        validators={validators}
+        nominators={nominators.length}
+        waiting={waiting.length}
+        reward={status.lastReward}
+      />
+      <div className="d-flex flex-column mt-3">
+        {sortValidators(sortBy, starred, stakes, rewardPoints).map((v) => (
+          <Validator
+            key={v}
+            sortBy={setSortBy}
+            starred={stars[v] ? `teal` : undefined}
+            toggleStar={props.toggleStar}
+            startTime={startTime}
+            validator={v}
+            reward={lastReward / validators.length}
+            councils={councils}
+            council={status.council}
+            member={getMember(v)}
+            posts={posts}
+            proposals={proposals}
+            validators={validators}
+            stakes={stakes}
+            rewardPoints={rewardPoints}
+          />
+        ))}
+
+        <Button onClick={() => toggleValidators()}>
+          Toggle {unstarred.length} validators
+        </Button>
+
+        {showValidators &&
+          sortValidators(sortBy, unstarred, stakes, rewardPoints).map((v) => (
+            <Validator
+              key={v}
+              sortBy={setSortBy}
+              starred={stars[v] ? `teal` : undefined}
+              toggleStar={props.toggleStar}
+              startTime={startTime}
+              validator={v}
+              reward={lastReward / validators.length}
+              councils={councils}
+              council={status.council}
+              member={getMember(v)}
+              posts={posts}
+              proposals={proposals}
+              validators={validators}
+              stakes={stakes}
+              rewardPoints={rewardPoints}
+            />
+          ))}
+
+        <Waiting
+          toggleWaiting={toggleWaiting}
+          show={showWaiting}
+          waiting={waiting}
+          posts={posts}
+          proposals={proposals}
+          members={members}
+        />
+      </div>
+    </>
+  );
+};
+
+export default Content;

+ 4 - 2
src/components/Validators/MinMax.tsx

@@ -37,7 +37,9 @@ const MinMax = (props: {
   const validatorReward = reward ? reward / validators.length : 0;
 
   return (
-    <Table style={{ textAlign: "center", backgroundColor: "#4038FF", color: "#fff" }}>
+    <Table
+      style={{ textAlign: "center", backgroundColor: "#4038FF", color: "#fff" }}
+    >
       <tbody>
         <tr>
           <td {...name}>Validators</td>
@@ -60,7 +62,7 @@ const MinMax = (props: {
           <td {...name}>Staked</td>
           <td {...value}>
             {(sum / 1000000).toFixed(1)} M JOY (
-            <b>{((sum / issued) * 100).toFixed(1)}%</b>)
+            <b>{issued ? ((sum / issued) * 100).toFixed(1) : `-`}%</b>)
           </td>
         </tr>
         <tr>

+ 7 - 186
src/components/Validators/index.tsx

@@ -1,20 +1,6 @@
-import React, { useState } from "react";
-import { Button } from "react-bootstrap";
-import Stats from "./MinMax";
-import Validator from "./Validator";
-import Waiting from "./Waiting";
-import Loading from "../Loading";
-
-import {
-  Member,
-  Post,
-  ProposalDetail,
-  Seat,
-  Stakes,
-  RewardPoints,
-  Status,
-  Tokenomics,
-} from "../../types";
+import React from "react";
+import Content from "./Content";
+import { IState } from "../../types";
 import {
   AppBar,
   createStyles,
@@ -26,22 +12,6 @@ import {
   Typography,
 } from "@material-ui/core";
 
-interface IProps {
-  councils: Seat[][];
-  members: Member[];
-  posts: Post[];
-  proposals: ProposalDetail[];
-  validators: string[];
-  stashes: string[];
-  nominators: string[];
-  stars: { [key: string]: boolean };
-  toggleStar: (account: string) => void;
-  stakes?: { [key: string]: Stakes };
-  rewardPoints?: RewardPoints;
-  tokenomics?: Tokenomics;
-  status: Status;
-}
-
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
     root: {
@@ -55,93 +25,9 @@ const useStyles = makeStyles((theme: Theme) =>
   })
 );
 
-const Validators = (iProps: IProps) => {
-  const [props] = useState(iProps);
-  const [sortBy, setSortBy] = useState("totalStake");
-
-  const [showWaiting, setShowWaiting] = useState(false);
-  const [showValidators, setShowValidators] = useState(false);
-
-  const toggleValidators = () => setShowValidators((prev) => !prev);
-
-  const toggleWaiting = () => setShowWaiting((prev) => !prev);
-
-  const sortValidators = (field: string, validators: string[]) => {
-    const { stakes, rewardPoints } = props;
-    try {
-      if (field === "points" || !stakes)
-        return validators.sort((a, b) =>
-          rewardPoints
-            ? rewardPoints.individual[b] - rewardPoints.individual[a]
-            : 0
-        );
-
-      if (field === "commission")
-        return validators.sort((a, b) =>
-          stakes[a] && stakes[b]
-            ? stakes[a].commission - stakes[b].commission
-            : 0
-        );
-
-      if (field === "ownStake")
-        return validators.sort((a, b) =>
-          stakes[a] && stakes[b] ? stakes[b].own - stakes[a].own : 0
-        );
-
-      if (field === "totalStake")
-        return validators.sort((a, b) =>
-          stakes[a] && stakes[b] ? stakes[b].total - stakes[a].total : 0
-        );
-
-      if (field === "othersStake") {
-        const sumOf = (stakes: { value: number }[]) => {
-          let sum = 0;
-          stakes.forEach((s) => (sum += s.value));
-          return sum;
-        };
-
-        return validators.sort((a, b) =>
-          stakes[a] && stakes[b]
-            ? sumOf(stakes[b].others) - sumOf(stakes[a].others)
-            : 0
-        );
-      }
-    } catch (e) {
-      console.debug(`sorting failed`, e);
-    }
-
-    return validators;
-  };
-
-  const {
-    getMember,
-    councils,
-    members,
-    posts,
-    proposals,
-    validators,
-    nominators,
-    stashes,
-    stars,
-    rewardPoints,
-    status,
-    stakes,
-    tokenomics,
-  } = props;
-
+const Validators = (props: IState) => {
   const classes = useStyles();
 
-  if (!status || !status.block) return <Loading gridSize={12} />;
-
-  const { lastReward, block, era, startTime } = status;
-
-  const issued = tokenomics ? Number(tokenomics.totalIssuance) : 0;
-  const price = tokenomics ? Number(tokenomics.price) : 0;
-
-  const starred = stashes.filter((v) => stars[v]);
-  const unstarred = validators.filter((v) => !stars[v]);
-  const waiting = stashes.filter((s) => !stars[s] && !validators.includes(s));
-
   return (
     <Grid
       style={{
@@ -157,7 +43,8 @@ const Validators = (iProps: IProps) => {
           textAlign: "center",
           backgroundColor: "#4038FF",
           color: "#fff",
-          height: 600,
+          minHeight: 500,
+          maxHeight: 500,
           overflow: "auto",
         }}
       >
@@ -173,73 +60,7 @@ const Validators = (iProps: IProps) => {
           </Toolbar>
         </AppBar>
 
-        <Stats
-          block={block}
-          era={era}
-          stakes={stakes}
-          issued={issued}
-          price={price}
-          validators={validators}
-          nominators={nominators.length}
-          waiting={waiting.length}
-          reward={status.lastReward}
-        />
-
-        <div className="d-flex flex-column mt-3">
-          {sortValidators(sortBy, starred).map((v) => (
-            <Validator
-              key={v}
-              sortBy={setSortBy}
-              starred={stars[v] ? `teal` : undefined}
-              toggleStar={props.toggleStar}
-              startTime={startTime}
-              validator={v}
-              reward={lastReward / validators.length}
-              councils={councils}
-              council={status.council}
-              member={getMember(v)}
-              posts={posts}
-              proposals={proposals}
-              validators={validators}
-              stakes={stakes}
-              rewardPoints={rewardPoints}
-            />
-          ))}
-
-          <Button onClick={() => toggleValidators()}>
-            Toggle {unstarred.length} validators
-          </Button>
-
-          {showValidators &&
-            sortValidators(sortBy, unstarred).map((v) => (
-              <Validator
-                key={v}
-                sortBy={setSortBy}
-                starred={stars[v] ? `teal` : undefined}
-                toggleStar={props.toggleStar}
-                startTime={startTime}
-                validator={v}
-                reward={lastReward / validators.length}
-                councils={councils}
-                council={status.council}
-                member={getMember(v)}
-                posts={posts}
-                proposals={proposals}
-                validators={validators}
-                stakes={stakes}
-                rewardPoints={rewardPoints}
-              />
-            ))}
-
-          <Waiting
-            toggleWaiting={toggleWaiting}
-            show={showWaiting}
-            waiting={waiting}
-            posts={posts}
-            proposals={proposals}
-            members={members}
-          />
-        </div>
+        <Content getMember={props.getMember} {...props} />
       </Paper>
     </Grid>
   );

+ 50 - 0
src/components/Validators/util.ts

@@ -0,0 +1,50 @@
+import { Stakes, RewardPoints } from "../../types";
+
+export const sortValidators = (
+  field: string,
+  validators: string[],
+  stakes: Stakes,
+  points: RewardPoints
+) => {
+  try {
+    if (field === "points" || !stakes)
+      return validators.sort((a, b) =>
+        rewardPoints
+          ? rewardPoints.individual[b] - rewardPoints.individual[a]
+          : 0
+      );
+
+    if (field === "commission")
+      return validators.sort((a, b) =>
+        stakes[a] && stakes[b] ? stakes[a].commission - stakes[b].commission : 0
+      );
+
+    if (field === "ownStake")
+      return validators.sort((a, b) =>
+        stakes[a] && stakes[b] ? stakes[b].own - stakes[a].own : 0
+      );
+
+    if (field === "totalStake")
+      return validators.sort((a, b) =>
+        stakes[a] && stakes[b] ? stakes[b].total - stakes[a].total : 0
+      );
+
+    if (field === "othersStake") {
+      const sumOf = (stakes: { value: number }[]) => {
+        let sum = 0;
+        stakes.forEach((s) => (sum += s.value));
+        return sum;
+      };
+
+      return validators.sort((a, b) =>
+        stakes[a] && stakes[b]
+          ? sumOf(stakes[b].others) - sumOf(stakes[a].others)
+          : 0
+      );
+    }
+  } catch (e) {
+    console.debug(`sorting failed`, e);
+  }
+
+  return validators;
+};

+ 4 - 0
src/components/index.ts

@@ -4,9 +4,12 @@ export { default as Bounties } from "./Bounties";
 export { default as Calendar } from "./Calendar";
 export { default as Routes } from "./Routes";
 export { default as Councils } from "./Councils";
+export { default as Council } from "./Dashboard/Council";
+export { default as ElectionStatus } from "./Dashboard/ElectionStatus";
 export { default as Curation } from "./Curation";
 export { default as Dashboard } from "./Dashboard";
 export { default as Forum } from "./Forum";
+export { default as LatestPost } from "./Forum/LatestPost";
 export { default as InfoTooltip } from "./Tooltip";
 export { default as Mint } from "./Mint";
 export { default as Spending } from "./Proposals/Spending";
@@ -19,6 +22,7 @@ export { default as ActiveProposals } from "./Proposals/Active";
 export { default as Loading } from "./Loading";
 export { default as User } from "./User";
 export { default as Member } from "./Members/Member";
+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";

+ 10 - 1
src/types.ts

@@ -44,6 +44,14 @@ export interface Council {
   consuls: Consul[];
 }
 
+export interface ElectionStage {
+  durations: number[];
+  stage: any;
+  round: number;
+  stageEndsAt: number;
+  termEndsAt: number;
+}
+
 export interface Status {
   now: number;
   block: Block;
@@ -52,7 +60,7 @@ export interface Status {
   connecting: boolean;
   loading: string;
   council?: Council;
-  //{ stage: any; round: number; termEndsAt: number };
+  election: ElectionStage;
   durations: number[];
   issued: number;
   price: number;
@@ -95,6 +103,7 @@ export interface IState {
   rewardPoints?: RewardPoints;
   hideFooter: boolean;
   showStatus: boolean;
+  getMember: (m: string | number) => Member;
 }
 
 export interface RewardPoints {