Browse Source

Merge pull request #711 from Lezek123/storage-group-overview

Storage group overview
Mokhtar Naamani 4 years ago
parent
commit
2afbfe2b86

+ 2 - 2
pioneer/packages/joy-roles/src/elements.tsx

@@ -102,7 +102,7 @@ export type GroupLead = {
   roleAccount: GenericAccountId;
   profile: IProfile;
   title: string;
-  stage: LeadRoleState;
+  stage?: LeadRoleState;
 }
 
 type inset = {
@@ -131,7 +131,7 @@ export function GroupLeadView (props: GroupLead & inset) {
         <Card.Description>
           <Label color='teal' ribbon={fluid}>
             <Icon name="shield" />
-          Content Lead
+            { props.title }
             <Label.Detail>{/* ... */}</Label.Detail>
           </Label>
         </Card.Description>

+ 0 - 1
pioneer/packages/joy-roles/src/index.sass

@@ -4,7 +4,6 @@
 @import 'styles/icons'
 @import 'styles/countdown'
 
-@import 'tabs/WorkingGroup'
 @import 'tabs/Opportunities'
 @import 'tabs/MyRoles'
 @import 'flows/apply'

+ 1 - 4
pioneer/packages/joy-roles/src/tabs.stories.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { withKnobs } from '@storybook/addon-knobs';
 import { Container, Tab } from 'semantic-ui-react';
-import { ContentCuratorsSection, StorageProvidersSection } from './tabs/WorkingGroup.stories';
+import { ContentCuratorsSection } from './tabs/WorkingGroup.stories';
 import { OpportunitySandbox } from './tabs/Opportunities.stories';
 import { ApplicationSandbox } from './flows/apply.stories';
 import { MyRolesSandbox } from './tabs/MyRoles.stories';
@@ -17,9 +17,6 @@ export const RolesPage = () => {
       <Container className="outer">
         <ContentCuratorsSection />
       </Container>
-      <Container>
-        <StorageProvidersSection />
-      </Container>
     </Container>
   );
 

+ 33 - 13
pioneer/packages/joy-roles/src/tabs/WorkingGroup.controller.tsx

@@ -7,15 +7,18 @@ import { ITransport } from '../transport';
 import {
   ContentCurators,
   WorkingGroupMembership,
-  StorageAndDistributionMembership,
   GroupLeadStatus,
-  ContentLead
+  StorageProviders
 } from './WorkingGroup';
 
+import { WorkingGroups } from '../working_groups';
+import styled from 'styled-components';
+
 type State = {
   contentCurators?: WorkingGroupMembership;
-  storageProviders?: StorageAndDistributionMembership;
-  groupLeadStatus?: GroupLeadStatus;
+  storageProviders?: WorkingGroupMembership;
+  contentLeadStatus?: GroupLeadStatus;
+  storageLeadStatus?: GroupLeadStatus;
 }
 
 export class WorkingGroupsController extends Controller<State, ITransport> {
@@ -23,7 +26,8 @@ export class WorkingGroupsController extends Controller<State, ITransport> {
     super(transport, {});
     this.getCurationGroup();
     this.getStorageGroup();
-    this.getGroupLeadStatus();
+    this.getCuratorLeadStatus();
+    this.getStorageLeadStatus();
   }
 
   getCurationGroup () {
@@ -34,25 +38,41 @@ export class WorkingGroupsController extends Controller<State, ITransport> {
   }
 
   getStorageGroup () {
-    this.transport.storageGroup().then((value: StorageAndDistributionMembership) => {
+    this.transport.storageGroup().then((value: WorkingGroupMembership) => {
       this.setState({ storageProviders: value });
       this.dispatch();
     });
   }
 
-  getGroupLeadStatus () {
-    this.transport.groupLeadStatus().then((value: GroupLeadStatus) => {
-      this.setState({ groupLeadStatus: value });
+  getCuratorLeadStatus () {
+    this.transport.groupLeadStatus(WorkingGroups.ContentCurators).then((value: GroupLeadStatus) => {
+      this.setState({ contentLeadStatus: value });
+      this.dispatch();
+    });
+  }
+
+  getStorageLeadStatus () {
+    this.transport.groupLeadStatus(WorkingGroups.StorageProviders).then((value: GroupLeadStatus) => {
+      this.setState({ storageLeadStatus: value });
       this.dispatch();
     });
   }
 }
 
+const WorkingGroupsOverview = styled.div`
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-gap: 2rem;
+  @media screen and (max-width: 1199px) {
+    grid-template-columns: 1fr;
+  }
+`;
+
 export const WorkingGroupsView = View<WorkingGroupsController, State>(
   (state) => (
-    <div>
-      <ContentCurators {...state.contentCurators!} />
-      <ContentLead {...state.groupLeadStatus!} />
-    </div>
+    <WorkingGroupsOverview>
+      <ContentCurators {...state.contentCurators} leadStatus={state.contentLeadStatus}/>
+      <StorageProviders {...state.storageProviders} leadStatus={state.storageLeadStatus}/>
+    </WorkingGroupsOverview>
   )
 );

+ 0 - 21
pioneer/packages/joy-roles/src/tabs/WorkingGroup.sass

@@ -1,21 +0,0 @@
-#storage-providers 
-  margin-top: 3em
-  .container
-    margin-right: 1em
-
-  .balance span, .memo span
-    font-weight: bold
-
-  .balance
-    font-size: 0.8em
-
-  .button
-    color: green !important
-
-#content-curators 
-  .staked-card
-    margin-right: 1.2em !important
-
-  .cards
-    margin-top: 1em
-    margin-bottom: 1em

+ 3 - 39
pioneer/packages/joy-roles/src/tabs/WorkingGroup.stories.tsx

@@ -1,13 +1,11 @@
 import React from 'react';
 import { boolean, number, text, withKnobs } from '@storybook/addon-knobs';
 
-import { Balance } from '@polkadot/types/interfaces';
-import { Text, u128, GenericAccountId } from '@polkadot/types';
+import { u128, GenericAccountId } from '@polkadot/types';
 
-import { Actor } from '@joystream/types/roles';
-import { IProfile, MemberId } from '@joystream/types/members';
+import { MemberId } from '@joystream/types/members';
 
-import { ContentCurators, StorageAndDistribution } from '@polkadot/joy-roles/tabs/WorkingGroup';
+import { ContentCurators } from '@polkadot/joy-roles/tabs/WorkingGroup';
 import { GroupMember } from '../elements';
 
 import { mockProfile } from '../mocks';
@@ -80,37 +78,3 @@ export function ContentCuratorsSection () {
     <ContentCurators members={members} rolesAvailable={boolean('Roles available', true)} />
   );
 }
-
-export const StorageProvidersSection = () => {
-  const balances = new Map<string, Balance>([
-    ['5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp', new u128(101)]
-  ]);
-
-  const memos = new Map<string, Text>([
-    ['5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp', new Text('This is a memo')]
-  ]);
-
-  const profiles = new Map<number, IProfile>([
-    [1, mockProfile('bwhm0')],
-    [2, mockProfile(
-      'benholdencrowther',
-      'https://www.benholdencrowther.com/wp-content/uploads/2019/03/Hanging_Gardens_of_Babylon.jpg'
-    )]
-  ]);
-
-  const storageProviders: Actor[] = [
-    new Actor({ member_id: 1, account: '5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp' }),
-    new Actor({ member_id: 2, account: '5DQqNWRFPruFs9YKheVMqxUbqoXeMzAWfVfcJgzuia7NA3D3' })
-  ];
-
-  return (
-    <div>
-      <StorageAndDistribution
-        actors={storageProviders}
-        balances={balances}
-        memos={memos}
-        profiles={profiles}
-      />
-    </div>
-  );
-};

+ 120 - 100
pioneer/packages/joy-roles/src/tabs/WorkingGroup.tsx

@@ -1,135 +1,155 @@
 import React from 'react';
-import { Button, Card, Icon, Message, SemanticICONS, Table } from 'semantic-ui-react';
+import { Button, Card, Icon, Message, SemanticICONS } from 'semantic-ui-react';
 import { Link } from 'react-router-dom';
 
-import { Balance } from '@polkadot/types/interfaces';
-import { Actor } from '@joystream/types/roles';
-import { IProfile } from '@joystream/types/members';
-import { Text } from '@polkadot/types';
-
-import { ActorDetailsView, MemberView, GroupMemberView, GroupLeadView, GroupMember, GroupLead } from '../elements';
-
+import { GroupLeadView, GroupMember, GroupMemberView, GroupLead } from '../elements';
 import { Loadable } from '@polkadot/joy-utils/index';
 
+import { WorkingGroups } from '../working_groups';
+import styled from 'styled-components';
+import _ from 'lodash';
+
 export type WorkingGroupMembership = {
   members: GroupMember[];
   rolesAvailable: boolean;
 }
 
-export const ContentCurators = Loadable<WorkingGroupMembership>(
-  ['members'],
-  props => {
-    let message = (
-      <Message>
-        <Message.Header>No open roles at the moment</Message.Header>
-        <p>The team is full at the moment, but we intend to expand. Check back for open roles soon!</p>
-      </Message>
-    );
+const NoRolesAvailable = () => (
+  <Message>
+    <Message.Header>No open roles at the moment</Message.Header>
+    <p>The team is full at the moment, but we intend to expand. Check back for open roles soon!</p>
+  </Message>
+);
 
-    if (props.rolesAvailable) {
-      message = (
-        <Message positive>
-          <Message.Header>Join us and get paid to curate!</Message.Header>
-          <p>
-            There are openings for new content curators. This is a great way to support Joystream!
-          </p>
-          <Link to="/working-groups/opportunities/curators">
-            <Button icon labelPosition="right" color="green" positive>
-              Find out more
-              <Icon name={'right arrow' as SemanticICONS} />
-            </Button>
-          </Link>
-        </Message>
-      );
-    }
+type JoinRoleProps = {
+  group: WorkingGroups;
+  title: string;
+  description: string;
+};
+
+const JoinRole = ({ group, title, description }: JoinRoleProps) => (
+  <Message positive>
+    <Message.Header>{title}</Message.Header>
+    <p>{description}</p>
+    <Link to={`/working-groups/opportunities/${group}`}>
+      <Button icon labelPosition="right" color="green" positive>
+        Find out more
+        <Icon name={'right arrow' as SemanticICONS} />
+      </Button>
+    </Link>
+  </Message>
+);
+
+const GroupOverviewSection = styled.section`
+  padding: 2rem;
+  background: #fff;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+
+  & .staked-card {
+    margin-right: 1.2em !important;
+  }
+
+  & .cards {
+    margin-top: 1em;
+    margin-bottom: 1em;
+  }
+`;
 
+type GroupOverviewOuterProps = Partial<WorkingGroupMembership> & {
+  leadStatus?: GroupLeadStatus;
+}
+
+type GroupOverviewProps = GroupOverviewOuterProps & {
+  group: WorkingGroups;
+  description: string;
+  customGroupName?: string;
+  customJoinTitle?: string;
+  customJoinDesc?: string;
+}
+
+const GroupOverview = Loadable<GroupOverviewProps>(
+  ['members', 'leadStatus'],
+  ({
+    group,
+    description,
+    members,
+    leadStatus,
+    rolesAvailable,
+    customGroupName,
+    customJoinTitle,
+    customJoinDesc
+  }: GroupOverviewProps) => {
+    const groupName = customGroupName || _.startCase(group);
+    const joinTitle = customJoinTitle || `Join the ${groupName} group!`;
+    const joinDesc = customJoinDesc || `There are openings for new ${groupName}. This is a great way to support Joystream!`;
     return (
-      <section id="content-curators">
-        <h2>Content curators</h2>
-        <p>
-          Content Curators are responsible for ensuring that all content is uploaded correctly and in line with the terms of service.
-        </p>
+      <GroupOverviewSection>
+        <h2>{ groupName }</h2>
+        <p>{ description }</p>
         <Card.Group>
-          {props.members.map((member, key) => (
+          { members!.map((member, key) => (
             <GroupMemberView key={key} {...member} />
-          ))}
+          )) }
         </Card.Group>
-        {message}
-      </section>
+        { rolesAvailable
+          ? <JoinRole group={group} title={joinTitle} description={joinDesc} />
+          : <NoRolesAvailable /> }
+        { leadStatus && <CurrentLead groupName={groupName} {...leadStatus}/> }
+      </GroupOverviewSection>
     );
   }
 );
 
-export type StorageAndDistributionMembership = {
-  actors: Actor[];
-  balances: Map<string, Balance>;
-  memos: Map<string, Text>;
-  profiles: Map<number, IProfile>;
-}
+export const ContentCurators = (props: GroupOverviewOuterProps) => (
+  <GroupOverview
+    group={WorkingGroups.ContentCurators}
+    description={
+      'Content Curators are responsible for ensuring that all content is uploaded correctly ' +
+      'and in line with the terms of service.'
+    }
+    {...props}
+  />
+);
 
-export const StorageAndDistribution = Loadable<StorageAndDistributionMembership>(
-  ['actors'],
-  props => {
-    return (
-      <section id="storage-providers">
-        <h2>Storage and distribution</h2>
-        <Table basic='very'>
-          <Table.Header>
-            <Table.Row>
-              <Table.HeaderCell>Member</Table.HeaderCell>
-              <Table.HeaderCell>Details</Table.HeaderCell>
-            </Table.Row>
-          </Table.Header>
-          <Table.Body>
-            {props.actors.map((actor, key) => (
-              <Table.Row key={key}>
-                <Table.Cell>
-                  <MemberView
-                    actor={actor}
-                    balance={props.balances.get(actor.account.toString())}
-                    profile={props.profiles.get(actor.member_id.toNumber()) as IProfile}
-                  />
-                </Table.Cell>
-                <Table.Cell>
-                  <ActorDetailsView
-                    actor={actor}
-                    balance={props.balances.get(actor.account.toString())}
-                    memo={props.memos.get(actor.account.toString())}
-                  />
-                </Table.Cell>
-              </Table.Row>
-            ))}
-          </Table.Body>
-        </Table>
-      </section>
-    );
-  }
+export const StorageProviders = (props: GroupOverviewOuterProps) => (
+  <GroupOverview
+    group={WorkingGroups.StorageProviders}
+    description={
+      'Storage Providers are responsible for storing and providing platform content!'
+    }
+    {...props}
+  />
 );
 
+const LeadSection = styled.div`
+  margin-top: 1rem;
+`;
+
 export type GroupLeadStatus = {
   lead?: GroupLead;
   loaded: boolean;
 }
 
-export const ContentLead = Loadable<GroupLeadStatus>(
+type CurrentLeadProps = GroupLeadStatus & {
+  groupName: string;
+  customLeadDesc?: string;
+};
+
+export const CurrentLead = Loadable<CurrentLeadProps>(
   ['loaded'],
-  props => {
+  ({ customLeadDesc, groupName, lead }: CurrentLeadProps) => {
+    const leadDesc = customLeadDesc || `This role is responsible for hiring ${groupName}.`;
     return (
-      <section id='lead'>
-        <br/>
+      <LeadSection>
         <Message positive>
-          <Message.Header>Content Lead</Message.Header>
-          <p>
-          This role is responsible for hiring curators, and is assigned by the platform.
-          </p>
-          {props.lead
-            ? <Card.Group>
-              <GroupLeadView {...props.lead} />
-            </Card.Group>
-            : 'There is no active Content Lead assigned.'}
+          <Message.Header>{ groupName } Lead</Message.Header>
+          <p>{ leadDesc }</p>
+          {lead
+            ? <Card.Group><GroupLeadView {...lead} /></Card.Group>
+            : `There is no active ${groupName} Lead assigned.` }
         </Message>
-
-      </section>
+      </LeadSection>
     );
   }
 );

+ 20 - 25
pioneer/packages/joy-roles/src/transport.mock.ts

@@ -1,12 +1,12 @@
 import { Observable } from 'rxjs';
 import { Balance } from '@polkadot/types/interfaces';
-import { Option, Text, u32, u128, GenericAccountId } from '@polkadot/types';
+import { Option, u32, u128, GenericAccountId } from '@polkadot/types';
 
 import { Subscribable, Transport as TransportBase } from '@polkadot/joy-utils/index';
 
 import { ITransport } from './transport';
-import { IProfile, MemberId, Role } from '@joystream/types/members';
-import { Actor } from '@joystream/types/roles';
+
+import { Role, MemberId } from '@joystream/types/members';
 import {
   Opening,
   AcceptingApplications,
@@ -15,7 +15,7 @@ import {
   StakingPolicy
 } from '@joystream/types/hiring';
 
-import { WorkingGroupMembership, StorageAndDistributionMembership, GroupLeadStatus } from './tabs/WorkingGroup';
+import { WorkingGroupMembership, GroupLeadStatus } from './tabs/WorkingGroup';
 import { CuratorId } from '@joystream/types/content-working-group';
 import { WorkingGroupOpening } from './tabs/Opportunities';
 import { ActiveRole, OpeningApplication } from './tabs/MyRoles';
@@ -46,7 +46,7 @@ export class Transport extends TransportBase implements ITransport {
     );
   }
 
-  groupLeadStatus (): Promise<GroupLeadStatus> {
+  groupLeadStatus (group: WorkingGroups = WorkingGroups.ContentCurators): Promise<GroupLeadStatus> {
     return this.simulateApiResponse<GroupLeadStatus>({
       loaded: true
     });
@@ -112,28 +112,23 @@ export class Transport extends TransportBase implements ITransport {
     });
   }
 
-  storageGroup (): Promise<StorageAndDistributionMembership> {
-    return this.simulateApiResponse<StorageAndDistributionMembership>(
-      {
-        balances: new Map<string, Balance>([
-          ['5DfJWGbBAH8hLAg8rcRYZW5BEZbE4BJeCQKoxUeqoyewLSew', new u128(101)]
-        ]),
-        memos: new Map<string, Text>([
-          ['5DfJWGbBAH8hLAg8rcRYZW5BEZbE4BJeCQKoxUeqoyewLSew', new Text('This is a memo')]
-        ]),
-        profiles: new Map<number, IProfile>([
-          [1, mockProfile('bwhm0')],
-          [2, mockProfile(
+  storageGroup (): Promise<WorkingGroupMembership> {
+    return this.simulateApiResponse<WorkingGroupMembership>({
+      rolesAvailable: true,
+      members: [
+        {
+          memberId: new MemberId(1),
+          roleAccount: new GenericAccountId('5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp'),
+          profile: mockProfile(
             'benholdencrowther',
             'https://www.benholdencrowther.com/wp-content/uploads/2019/03/Hanging_Gardens_of_Babylon.jpg'
-          )]
-        ]),
-        actors: [
-          new Actor({ member_id: 1, account: '5DfJWGbBAH8hLAg8rcRYZW5BEZbE4BJeCQKoxUeqoyewLSew' }),
-          new Actor({ member_id: 2, account: '5DQqNWRFPruFs9YKheVMqxUbqoXeMzAWfVfcJgzuia7NA3D3' })
-        ]
-      }
-    );
+          ),
+          title: 'Storage provider',
+          stake: new u128(10101),
+          earned: new u128(347829)
+        }
+      ]
+    });
   }
 
   currentOpportunities (): Promise<Array<WorkingGroupOpening>> {

+ 145 - 78
pioneer/packages/joy-roles/src/transport.substrate.ts

@@ -4,6 +4,7 @@ import { map, switchMap } from 'rxjs/operators';
 import ApiPromise from '@polkadot/api/promise';
 import { Balance } from '@polkadot/types/interfaces';
 import { GenericAccountId, Option, u32, u128, Vec } from '@polkadot/types';
+import { Constructor } from '@polkadot/types/types';
 import { Moment } from '@polkadot/types/interfaces/runtime';
 import { QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
@@ -17,23 +18,26 @@ import { GroupMember } from './elements';
 import {
   Curator, CuratorId,
   CuratorApplication, CuratorApplicationId,
-  CuratorInduction,
   CuratorRoleStakeProfile,
   CuratorOpening, CuratorOpeningId,
   Lead, LeadId
 } from '@joystream/types/content-working-group';
 
 import {
-  WorkerApplication, WorkerApplicationId, WorkerOpening, WorkerOpeningId
+  WorkerApplication, WorkerApplicationId,
+  WorkerOpening, WorkerOpeningId,
+  Worker, WorkerId,
+  WorkerRoleStakeProfile,
+  Lead as LeadOf
 } from '@joystream/types/bureaucracy';
 
-import { Application, Opening } from '@joystream/types/hiring';
+import { Application, Opening, OpeningId } from '@joystream/types/hiring';
 import { Stake, StakeId } from '@joystream/types/stake';
-import { Recipient, RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
+import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
 import { ActorInRole, Profile, MemberId, Role, RoleKeys, ActorId } from '@joystream/types/members';
 import { createAccount, generateSeed } from '@polkadot/joy-utils/accounts';
 
-import { WorkingGroupMembership, StorageAndDistributionMembership, GroupLeadStatus } from './tabs/WorkingGroup';
+import { WorkingGroupMembership, GroupLeadStatus } from './tabs/WorkingGroup';
 import { WorkingGroupOpening } from './tabs/Opportunities';
 import { ActiveRole, OpeningApplication } from './tabs/MyRoles';
 
@@ -47,6 +51,7 @@ import {
 } from './classifiers';
 import { WorkingGroups, AvailableGroups } from './working_groups';
 import { Sort, Sum, Zero } from './balances';
+import _ from 'lodash';
 
 type WorkingGroupPair<HiringModuleType, WorkingGroupType> = {
   hiringModule: HiringModuleType;
@@ -58,31 +63,52 @@ type StakePair<T = Balance> = {
   role: T;
 }
 
-interface IRoleAccounter {
-  role_account: GenericAccountId;
-  induction?: CuratorInduction;
-  role_stake_profile?: Option<CuratorRoleStakeProfile>;
-  reward_relationship: Option<RewardRelationshipId>;
-}
-
-type WGApiMethodType = 'nextOpeningId' | 'openingById' | 'nextApplicationId' | 'applicationById';
+type WGApiMethodType =
+  'nextOpeningId'
+  | 'openingById'
+  | 'nextApplicationId'
+  | 'applicationById'
+  | 'nextWorkerId'
+  | 'workerById';
 type WGApiMethodsMapping = { [key in WGApiMethodType]: string };
-type WGToApiMethodsMapping = { [key in WorkingGroups]: { module: string; methods: WGApiMethodsMapping } };
 
 type GroupApplication = CuratorApplication | WorkerApplication;
 type GroupApplicationId = CuratorApplicationId | WorkerApplicationId;
 type GroupOpening = CuratorOpening | WorkerOpening;
 type GroupOpeningId = CuratorOpeningId | WorkerOpeningId;
+type GroupWorker = Worker | Curator;
+type GroupWorkerId = CuratorId | WorkerId;
+type GroupWorkerStakeProfile = WorkerRoleStakeProfile | CuratorRoleStakeProfile;
+type GroupLead = Lead | LeadOf;
+type GroupLeadWithMemberId = {
+  lead: GroupLead;
+  memberId: MemberId;
+}
+
+type WGApiMapping = {
+  [key in WorkingGroups]: {
+    module: string;
+    methods: WGApiMethodsMapping;
+    openingType: Constructor<GroupOpening>;
+    applicationType: Constructor<GroupApplication>;
+    workerType: Constructor<GroupWorker>;
+  }
+};
 
-const wgApiMethodsMapping: WGToApiMethodsMapping = {
+const workingGroupsApiMapping: WGApiMapping = {
   [WorkingGroups.StorageProviders]: {
     module: 'storageBureaucracy',
     methods: {
       nextOpeningId: 'nextWorkerOpeningId',
       openingById: 'workerOpeningById',
       nextApplicationId: 'nextWorkerApplicationId',
-      applicationById: 'workerApplicationById'
-    }
+      applicationById: 'workerApplicationById',
+      nextWorkerId: 'nextWorkerId',
+      workerById: 'workerById'
+    },
+    openingType: WorkerOpening,
+    applicationType: WorkerApplication,
+    workerType: Worker
   },
   [WorkingGroups.ContentCurators]: {
     module: 'contentWorkingGroup',
@@ -90,8 +116,13 @@ const wgApiMethodsMapping: WGToApiMethodsMapping = {
       nextOpeningId: 'nextCuratorOpeningId',
       openingById: 'curatorOpeningById',
       nextApplicationId: 'nextCuratorApplicationId',
-      applicationById: 'curatorApplicationById'
-    }
+      applicationById: 'curatorApplicationById',
+      nextWorkerId: 'nextCuratorId',
+      workerById: 'curatorById'
+    },
+    openingType: CuratorOpening,
+    applicationType: CuratorApplication,
+    workerType: Curator
   }
 };
 
@@ -108,8 +139,8 @@ export class Transport extends TransportBase implements ITransport {
   }
 
   cachedApiMethodByGroup (group: WorkingGroups, method: WGApiMethodType) {
-    const apiModule = wgApiMethodsMapping[group].module;
-    const apiMethod = wgApiMethodsMapping[group].methods[method];
+    const apiModule = workingGroupsApiMapping[group].module;
+    const apiMethod = workingGroupsApiMapping[group].methods[method];
 
     return this.cachedApi.query[apiModule][apiMethod];
   }
@@ -133,24 +164,18 @@ export class Transport extends TransportBase implements ITransport {
     return stake.value.value;
   }
 
-  protected async curatorStake (stakeProfile: CuratorRoleStakeProfile): Promise<Balance> {
+  protected async workerStake (stakeProfile: GroupWorkerStakeProfile): Promise<Balance> {
     return this.stakeValue(stakeProfile.stake_id);
   }
 
-  protected async curatorTotalReward (relationshipId: RewardRelationshipId): Promise<Balance> {
+  protected async workerTotalReward (relationshipId: RewardRelationshipId): Promise<Balance> {
     const relationship = new SingleLinkedMapEntry<RewardRelationship>(
       RewardRelationship,
       await this.cachedApi.query.recurringRewards.rewardRelationships(
         relationshipId
       )
     );
-    const recipient = new SingleLinkedMapEntry<Recipient>(
-      Recipient,
-      await this.cachedApi.query.recurringRewards.rewardRelationships(
-        relationship.value.recipient
-      )
-    );
-    return recipient.value.total_reward_received;
+    return relationship.value.total_reward_received;
   }
 
   protected async memberIdFromRoleAndActorId (role: Role, id: ActorId): Promise<MemberId> {
@@ -180,9 +205,15 @@ export class Transport extends TransportBase implements ITransport {
     );
   }
 
-  protected async groupMember (id: CuratorId, curator: IRoleAccounter): Promise<GroupMember> {
-    const roleAccount = curator.role_account;
-    const memberId = await this.memberIdFromCuratorId(id);
+  protected async groupMember (
+    group: WorkingGroups,
+    id: GroupWorkerId,
+    worker: GroupWorker
+  ): Promise<GroupMember> {
+    const roleAccount = worker.role_account;
+    const memberId = group === WorkingGroups.ContentCurators
+      ? await this.memberIdFromCuratorId(id)
+      : (worker as Worker).member_id;
 
     const profile = await this.cachedApi.query.members.memberProfile(memberId) as Option<Profile>;
     if (profile.isNone) {
@@ -190,41 +221,41 @@ export class Transport extends TransportBase implements ITransport {
     }
 
     let stakeValue: Balance = new u128(0);
-    if (curator.role_stake_profile && curator.role_stake_profile.isSome) {
-      stakeValue = await this.curatorStake(curator.role_stake_profile.unwrap());
+    if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
+      stakeValue = await this.workerStake(worker.role_stake_profile.unwrap());
     }
 
     let earnedValue: Balance = new u128(0);
-    if (curator.reward_relationship && curator.reward_relationship.isSome) {
-      earnedValue = await this.curatorTotalReward(curator.reward_relationship.unwrap());
+    if (worker.reward_relationship && worker.reward_relationship.isSome) {
+      earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
     }
 
     return ({
       roleAccount,
       memberId,
       profile: profile.unwrap(),
-      title: 'Content curator',
+      title: _.startCase(group).slice(0, -1), // FIXME: Temporary solution (just removes "s" at the end)
       stake: stakeValue,
       earned: earnedValue
     });
   }
 
-  protected async areAnyCuratorRolesOpen (): Promise<boolean> {
-    const nextId = await this.cachedApi.query.contentWorkingGroup.nextCuratorOpeningId() as CuratorId;
+  protected async areAnyGroupRolesOpen (group: WorkingGroups): Promise<boolean> {
+    const nextId = await this.cachedApiMethodByGroup(group, 'nextOpeningId')() as GroupOpeningId;
 
     // This is chain specfic, but if next id is still 0, it means no openings have been added yet
     if (nextId.eq(0)) {
       return false;
     }
 
-    const curatorOpenings = new MultipleLinkedMapEntry<CuratorOpeningId, CuratorOpening>(
-      CuratorOpeningId,
-      CuratorOpening,
-      await this.cachedApi.query.contentWorkingGroup.curatorOpeningById()
+    const groupOpenings = new MultipleLinkedMapEntry<GroupOpeningId, GroupOpening>(
+      OpeningId,
+      workingGroupsApiMapping[group].openingType,
+      await this.cachedApiMethodByGroup(group, 'openingById')()
     );
 
-    for (let i = 0; i < curatorOpenings.linked_values.length; i++) {
-      const opening = await this.opening(curatorOpenings.linked_values[i].opening_id.toNumber());
+    for (let i = 0; i < groupOpenings.linked_values.length; i++) {
+      const opening = await this.opening(groupOpenings.linked_values[i].opening_id.toNumber());
       if (opening.is_active) {
         return true;
       }
@@ -233,30 +264,64 @@ export class Transport extends TransportBase implements ITransport {
     return false;
   }
 
-  async groupLeadStatus (): Promise<GroupLeadStatus> {
+  protected async areAnyCuratorRolesOpen (): Promise<boolean> {
+    // Backward compatibility
+    return this.areAnyGroupRolesOpen(WorkingGroups.ContentCurators);
+  }
+
+  protected async currentCuratorLead (): Promise<GroupLeadWithMemberId | null> {
     const optLeadId = (await this.cachedApi.query.contentWorkingGroup.currentLeadId()) as Option<LeadId>;
 
-    if (optLeadId.isSome) {
-      const leadId = optLeadId.unwrap();
-      const lead = new SingleLinkedMapEntry<Lead>(
-        Lead,
-        await this.cachedApi.query.contentWorkingGroup.leadById(leadId)
-      );
+    if (!optLeadId.isSome) {
+      return null;
+    }
+
+    const leadId = optLeadId.unwrap();
+    const lead = new SingleLinkedMapEntry<Lead>(
+      Lead,
+      await this.cachedApi.query.contentWorkingGroup.leadById(leadId)
+    );
 
-      const memberId = await this.memberIdFromLeadId(leadId);
+    const memberId = await this.memberIdFromLeadId(leadId);
+
+    return {
+      lead: lead.value,
+      memberId
+    };
+  }
+
+  protected async currentStorageLead (): Promise <GroupLeadWithMemberId | null> {
+    const optLead = (await this.cachedApi.query.storageBureaucracy.currentLead()) as Option<LeadOf>;
+
+    if (!optLead.isSome) {
+      return null;
+    }
+
+    return {
+      lead: optLead.unwrap(),
+      memberId: optLead.unwrap().member_id
+    };
+  }
+
+  async groupLeadStatus (group: WorkingGroups = WorkingGroups.ContentCurators): Promise<GroupLeadStatus> {
+    const currentLead = group === WorkingGroups.ContentCurators
+      ? await this.currentCuratorLead()
+      : await this.currentStorageLead();
+
+    if (currentLead !== null) {
+      const profile = await this.cachedApi.query.members.memberProfile(currentLead.memberId) as Option<Profile>;
 
-      const profile = await this.cachedApi.query.members.memberProfile(memberId) as Option<Profile>;
       if (profile.isNone) {
-        throw new Error('no profile found');
+        throw new Error(`${group} lead profile not found!`);
       }
 
       return {
         lead: {
-          memberId,
-          roleAccount: lead.value.role_account,
+          memberId: currentLead.memberId,
+          roleAccount: currentLead.lead.role_account_id,
           profile: profile.unwrap(),
-          title: 'Content Lead',
-          stage: lead.value.stage
+          title: _.startCase(group) + ' Lead',
+          stage: group === WorkingGroups.ContentCurators ? (currentLead.lead as Lead).stage : undefined
         },
         loaded: true
       };
@@ -267,10 +332,10 @@ export class Transport extends TransportBase implements ITransport {
     }
   }
 
-  async curationGroup (): Promise<WorkingGroupMembership> {
-    const rolesAvailable = await this.areAnyCuratorRolesOpen();
+  async groupOverview (group: WorkingGroups): Promise<WorkingGroupMembership> {
+    const rolesAvailable = await this.areAnyGroupRolesOpen(group);
 
-    const nextId = await this.cachedApi.query.contentWorkingGroup.nextCuratorId() as CuratorId;
+    const nextId = await this.cachedApiMethodByGroup(group, 'nextWorkerId')() as GroupWorkerId;
 
     // This is chain specfic, but if next id is still 0, it means no curators have been added yet
     if (nextId.eq(0)) {
@@ -280,27 +345,29 @@ export class Transport extends TransportBase implements ITransport {
       };
     }
 
-    const values = new MultipleLinkedMapEntry<CuratorId, Curator>(
-      CuratorId,
-      Curator,
-      await this.cachedApi.query.contentWorkingGroup.curatorById()
+    const values = new MultipleLinkedMapEntry<GroupWorkerId, GroupWorker>(
+      ActorId,
+      workingGroupsApiMapping[group].workerType,
+      await this.cachedApiMethodByGroup(group, 'workerById')() as GroupWorker
     );
 
-    const members = values.linked_values.filter(value => value.is_active).reverse();
-    const memberIds = values.linked_keys.filter((v, k) => values.linked_values[k].is_active).reverse();
+    const workers = values.linked_values.filter(value => value.is_active).reverse();
+    const workerIds = values.linked_keys.filter((v, k) => values.linked_values[k].is_active).reverse();
 
     return {
       members: await Promise.all(
-        members.map((member, k) => this.groupMember(memberIds[k], member))
+        workers.map((worker, k) => this.groupMember(group, workerIds[k], worker))
       ),
       rolesAvailable
     };
   }
 
-  storageGroup (): Promise<StorageAndDistributionMembership> {
-    return this.promise<StorageAndDistributionMembership>(
-      {} as StorageAndDistributionMembership
-    );
+  curationGroup (): Promise<WorkingGroupMembership> {
+    return this.groupOverview(WorkingGroups.ContentCurators);
+  }
+
+  storageGroup (): Promise<WorkingGroupMembership> {
+    return this.groupOverview(WorkingGroups.StorageProviders);
   }
 
   async opportunitiesByGroup (group: WorkingGroups): Promise<WorkingGroupOpening[]> {
@@ -344,7 +411,7 @@ export class Transport extends TransportBase implements ITransport {
     const nextAppid = (await this.cachedApiMethodByGroup(group, 'nextApplicationId')()) as GroupApplicationId;
     for (let i = 0; i < nextAppid.toNumber(); i++) {
       const cApplication = new SingleLinkedMapEntry<GroupApplication>(
-        group === WorkingGroups.ContentCurators ? CuratorApplication : WorkerApplication,
+        workingGroupsApiMapping[group].applicationType,
         await this.cachedApiMethodByGroup(group, 'applicationById')(i)
       );
 
@@ -382,7 +449,7 @@ export class Transport extends TransportBase implements ITransport {
     }
 
     const groupOpening = new SingleLinkedMapEntry<GroupOpening>(
-      group === WorkingGroups.ContentCurators ? CuratorOpening : WorkerOpening,
+      workingGroupsApiMapping[group].openingType,
       await this.cachedApiMethodByGroup(group, 'openingById')(id)
     );
 
@@ -598,12 +665,12 @@ export class Transport extends TransportBase implements ITransport {
         .map(async (curator, key) => {
           let stakeValue: Balance = new u128(0);
           if (curator.role_stake_profile && curator.role_stake_profile.isSome) {
-            stakeValue = await this.curatorStake(curator.role_stake_profile.unwrap());
+            stakeValue = await this.workerStake(curator.role_stake_profile.unwrap());
           }
 
           let earnedValue: Balance = new u128(0);
           if (curator.reward_relationship && curator.reward_relationship.isSome) {
-            earnedValue = await this.curatorTotalReward(curator.reward_relationship.unwrap());
+            earnedValue = await this.workerTotalReward(curator.reward_relationship.unwrap());
           }
 
           return {

+ 3 - 3
pioneer/packages/joy-roles/src/transport.ts

@@ -3,7 +3,7 @@ import { Balance } from '@polkadot/types/interfaces';
 
 import { Role } from '@joystream/types/members';
 
-import { WorkingGroupMembership, StorageAndDistributionMembership, GroupLeadStatus } from './tabs/WorkingGroup';
+import { WorkingGroupMembership, GroupLeadStatus } from './tabs/WorkingGroup';
 import { WorkingGroupOpening } from './tabs/Opportunities';
 import { keyPairDetails } from './flows/apply';
 import { ActiveRole, OpeningApplication } from './tabs/MyRoles';
@@ -11,9 +11,9 @@ import { WorkingGroups } from './working_groups';
 
 export interface ITransport {
   roles: () => Promise<Array<Role>>;
-  groupLeadStatus: () => Promise<GroupLeadStatus>;
+  groupLeadStatus: (group: WorkingGroups) => Promise<GroupLeadStatus>;
   curationGroup: () => Promise<WorkingGroupMembership>;
-  storageGroup: () => Promise<StorageAndDistributionMembership>;
+  storageGroup: () => Promise<WorkingGroupMembership>;
   currentOpportunities: () => Promise<Array<WorkingGroupOpening>>;
   groupOpening: (group: WorkingGroups, id: number) => Promise<WorkingGroupOpening>;
   curationGroupOpening: (id: number) => Promise<WorkingGroupOpening>;