Joystream Stats 2 лет назад
Родитель
Сommit
7cded0d96a

+ 66 - 1
src/App.tsx

@@ -264,6 +264,14 @@ class App extends React.Component<IProps, IState> {
   }
 
   async fetchWorkingGroups(api: ApiPromise) {
+    const openings = {
+      curators: await this.fetchOpenings(api, "contentDirectory"),
+      storageProviders: await this.fetchOpenings(api, "storage"),
+      operationsGroup: await this.fetchOpenings(api, "operations"),
+      _lastUpdate: moment().valueOf(),
+    };
+    this.save("openings", openings);
+
     const lastUpdate = this.state.workers?._lastUpdate;
     if (lastUpdate && moment() < moment(lastUpdate).add(1, `hour`)) return;
     const workers = {
@@ -277,6 +285,63 @@ class App extends React.Component<IProps, IState> {
     this.save("council", council);
   }
 
+  async fetchOpenings(api: ApiPromise, wg: string) {
+    const group = wg + "WorkingGroup";
+    const count = (
+      (await api.query[group].nextOpeningId()) as OpeningId
+    ).toNumber();
+    console.debug(`Fetching ${count} ${wg} openings`);
+    let openings = [];
+    for (let wgOpeningId = 0; wgOpeningId < count; ++wgOpeningId) {
+      const wgOpening: OpeningOf = (
+        await api.query[group].openingById(wgOpeningId)
+      ).toJSON();
+      const id = wgOpening.hiring_opening_id;
+      const opening = (await api.query.hiring.openingById(id)).toJSON();
+      openings.push({
+        ...opening,
+        id,
+        type: Object.keys(wgOpening.opening_type)[0],
+        applications: await this.fetchApplications(
+          api,
+          group,
+          wgOpening.applications
+        ),
+        policy: wgOpening.policy_commitment,
+      });
+    }
+    console.debug(`${group} openings`, openings);
+    return openings;
+  }
+
+  async fetchApplications(api: ApiPromise, group: string, ids: number[]) {
+    const { members } = this.state;
+    return Promise.all(
+      ids.map(async (wgApplicationId) => {
+        const wgApplication: ApplicationOf = (
+          await api.query[group].applicationById(wgApplicationId)
+        ).toJSON();
+        const account = wgApplication.role_account_id;
+        const openingId = wgApplication.opening_id;
+        const memberId: number = wgApplication.member_id;
+        const member = members.find((m) => +m.id === +memberId);
+        const handle = member ? member.handle : null;
+        const id = wgApplication.application_id;
+        const application = (
+          await api.query.hiring.applicationById(id)
+        ).toJSON();
+        return {
+          id,
+          account,
+          openingId,
+          memberId,
+          member: { handle },
+          application,
+        };
+      })
+    );
+  }
+
   async fetchWorkers(api: ApiPromise, wg: string) {
     const group = wg + "WorkingGroup";
     const { members } = this.state;
@@ -538,7 +603,7 @@ class App extends React.Component<IProps, IState> {
     }
     console.debug(`Loading data`);
     this.loadMembers();
-    "assets providers councils council workers categories channels proposals posts threads  mints tokenomics transactions reports validators nominators stakes stars"
+    "assets providers councils council workers categories channels proposals posts threads  mints openings tokenomics transactions reports validators nominators stakes stars"
       .split(" ")
       .map((key) => this.load(key));
   }

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

@@ -3,6 +3,7 @@ import Council from "./Council";
 import Forum from "./Forum";
 import Proposals from "./Proposals";
 import Validators from "../Validators";
+import Openings from "../Openings";
 import { IState } from "../../types";
 import { Container, Grid } from "@material-ui/core";
 
@@ -18,6 +19,7 @@ const Dashboard = (props: IProps) => {
     councils,
     members,
     nominators,
+    openings,
     posts,
     proposals,
     rewardPoints,
@@ -46,6 +48,7 @@ const Dashboard = (props: IProps) => {
             validators={validators}
             status={status}
           />
+          <Openings openings={openings} />
           <Council
             getMember={getMember}
             councils={councils}

+ 29 - 0
src/components/Openings/Applications.tsx

@@ -0,0 +1,29 @@
+import Details from "./Details";
+import InfoTooltip from "../Tooltip";
+import { domain } from "../../config";
+
+const Applications = (props: { applications: Application[] }) => {
+  const { applications } = props;
+  if (!applications.length) return <span>No applications</span>;
+  const count = applications.length;
+  return (
+    <>
+      <span>
+        {count} application{count !== 1 ? "s" : ""}:
+      </span>
+      {applications.map((a) => (
+        <InfoTooltip
+          key={a.id}
+          placement="bottom"
+          title={
+            <Details object={JSON.parse(a.application.human_readable_text)} />
+          }
+        >
+          <span className="ml-1">{a.member.handle || a.memberId}</span>
+        </InfoTooltip>
+      ))}
+    </>
+  );
+};
+
+export default Applications;

+ 34 - 0
src/components/Openings/Details.tsx

@@ -0,0 +1,34 @@
+import { createStyles, makeStyles, Typography } from "@material-ui/core";
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    heading: {
+      fontSize: theme.typography.pxToRem(16),
+      fontWeight: theme.typography.fontWeightBold,
+    },
+    answer: {
+      fontSize: theme.typography.pxToRem(18),
+      fontWeight: theme.typography.fontWeightRegular,
+      marginBottom: ".5em",
+    },
+  })
+);
+
+const ObjectKeys = (props: { object }) => {
+  const classes = useStyles();
+  const { object } = props;
+  if (!object) return <div />;
+
+  return Object.keys(object).map((key, index: number) =>
+    typeof object[key] === "object" ? (
+      <ObjectKeys key={index} object={object[key]} />
+    ) : (
+      <div key={index}>
+        <Typography className={classes.heading}>{key}</Typography>
+        <Typography className={classes.answer}>{object[key]}</Typography>
+      </div>
+    )
+  );
+};
+
+export default ObjectKeys;

+ 18 - 0
src/components/Openings/Group.tsx

@@ -0,0 +1,18 @@
+import GroupOpening from "./Opening";
+
+import { Opening } from "./types";
+
+const GroupOpenings = (props: { group: string; openings: Opening[] }) => {
+  const { group, openings } = props;
+  if (!openings?.length) return <div />;
+  return (
+    <div className="p-3">
+      <h2>{group}</h2>
+      {openings.map((opening) => (
+        <GroupOpening key={opening.id} opening={opening} />
+      ))}
+    </div>
+  );
+};
+
+export default GroupOpenings;

+ 29 - 0
src/components/Openings/Opening.tsx

@@ -0,0 +1,29 @@
+import Applications from "./Applications";
+import Details from "./Details";
+import InfoTooltip from "../Tooltip";
+import { domain } from "../../config";
+
+import { Opening } from "./types";
+
+const GroupOpening = (props: { group: string; opening: Opening }) => {
+  const { group, opening } = props;
+  const { id, type, applications } = opening;
+  const details = JSON.parse(opening.human_readable_text);
+  return (
+    <div>
+      <InfoTooltip placement="bottom" title={<Details object={details} />}>
+        <a
+          className="font-weight-bold mr-2"
+          href={`${domain}/#/working-groups/opportunities/${group}${
+            opening.type === "leader" ? "/lead" : ""
+          }`}
+        >
+          {opening.type} opening
+        </a>
+      </InfoTooltip>
+      <Applications applications={opening.applications} />
+    </div>
+  );
+};
+
+export default GroupOpening;

+ 71 - 0
src/components/Openings/index.tsx

@@ -0,0 +1,71 @@
+import GroupOpenings from "./Group";
+import {
+  createStyles,
+  makeStyles,
+  Grid,
+  Paper,
+  AppBar,
+  Toolbar,
+  Typography,
+  Theme,
+} from "@material-ui/core";
+
+import { Opening } from "./types";
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    grid: { textAlign: "center", backgroundColor: "#000", color: "#fff" },
+    root: { flexGrow: 1, backgroundColor: "#4038FF" },
+    title: { textAlign: "left", flexGrow: 1 },
+    paper: {
+      textAlign: "left",
+      backgroundColor: "#4038FF",
+      color: "#fff",
+      minHeight: 500,
+      maxHeight: 500,
+      overflow: "auto",
+    },
+  })
+);
+
+const activeOpenings = (openings: Opening[]) =>
+  openings.filter(
+    (o) => Object.keys(o.stage["active"].stage)[0] === "acceptingApplications"
+  );
+
+const Openings = (props: { openings: {} }) => {
+  const classes = useStyles();
+  const { openings } = props;
+  if (!openings) return <div />;
+  const groups = Object.keys(openings).filter((g) => g !== "_lastUpdate");
+  const active = groups.reduce(
+    (sum, group) => sum + activeOpenings(openings[group]),
+    0
+  );
+  if (!active.length) return <div />;
+
+  return (
+    <Grid className={classes.grid} item lg={6}>
+      <Paper className={classes.paper}>
+        <AppBar className={classes.root} position="static">
+          <Toolbar>
+            <Typography variant="h6" className={classes.title}>
+              Openings
+            </Typography>
+          </Toolbar>
+        </AppBar>
+        <div>
+          {groups.map((group) => (
+            <GroupOpenings
+              key={`${group}-openings`}
+              group={group}
+              openings={activeOpenings(openings[group])}
+            />
+          ))}
+        </div>
+      </Paper>
+    </Grid>
+  );
+};
+
+export default Openings;

+ 3 - 0
src/components/Openings/types.ts

@@ -0,0 +1,3 @@
+export interface Opening {}
+
+export interface Application {}