Body.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import React from "react";
  2. import { Card, Header, Button, Icon, Message } from "semantic-ui-react";
  3. import { ProposalType } from "../runtime/transport";
  4. import { blake2AsHex } from '@polkadot/util-crypto';
  5. import styled from 'styled-components';
  6. import AddressMini from '@polkadot/react-components/AddressMiniJoy';
  7. import TxButton from '@polkadot/joy-utils/TxButton';
  8. import { ProposalId } from "@joystream/types/lib/proposals";
  9. import { MemberId } from "@joystream/types/lib/members";
  10. import ProfilePreview from "@polkadot/joy-utils/MemberProfilePreview";
  11. import { useTransport } from "../runtime";
  12. import { usePromise } from "../utils";
  13. import { Profile } from "@joystream/types/lib/members";
  14. import { Option } from "@polkadot/types/";
  15. import { formatBalance } from "@polkadot/util";
  16. import PromiseComponent from "./PromiseComponent";
  17. type BodyProps = {
  18. title: string;
  19. description: string;
  20. params: any[];
  21. type: ProposalType;
  22. iAmProposer: boolean;
  23. proposalId: number | ProposalId;
  24. proposerId: number | MemberId;
  25. isCancellable: boolean;
  26. cancellationFee: number;
  27. };
  28. function ProposedAddress(props: { address?: string | null }) {
  29. if (props.address === null || props.address === undefined) {
  30. return <>NONE</>;
  31. }
  32. return (
  33. <AddressMini value={props.address} isShort={false} isPadded={false} withAddress={true} style={{ padding: 0 }} />
  34. );
  35. }
  36. function ProposedMember(props: { memberId?: MemberId | number | null }) {
  37. if (props.memberId === null || props.memberId === undefined) {
  38. return <>NONE</>;
  39. }
  40. const memberId: MemberId | number = props.memberId;
  41. const transport = useTransport();
  42. const [ member, error, loading ] = usePromise<Option<Profile> | null>(
  43. () => transport.memberProfile(memberId),
  44. null
  45. );
  46. const profile = member && member.unwrapOr(null);
  47. return (
  48. <PromiseComponent error={error} loading={loading} message="Fetching profile...">
  49. { profile ? (
  50. <ProfilePreview
  51. avatar_uri={ profile.avatar_uri.toString() }
  52. root_account={ profile.root_account.toString() }
  53. handle={ profile.handle.toString() }
  54. link={ true }
  55. />
  56. ) : 'Profile not found' }
  57. </PromiseComponent>
  58. );
  59. }
  60. // The methods for parsing params by Proposal type.
  61. // They take the params as array and return { LABEL: VALUE } object.
  62. const paramParsers: { [x in ProposalType]: (params: any[]) => { [key: string]: string | number | JSX.Element } } = {
  63. Text: ([content]) => ({
  64. Content: content
  65. }),
  66. RuntimeUpgrade: ([wasm]) => {
  67. const buffer: Buffer = Buffer.from(wasm.replace("0x", ""), "hex");
  68. return {
  69. "Blake2b256 hash of WASM code": blake2AsHex(buffer, 256),
  70. "File size": buffer.length + " bytes"
  71. };
  72. },
  73. SetElectionParameters: ([params]) => ({
  74. "Announcing period": params.announcing_period + " blocks",
  75. "Voting period": params.voting_period + " blocks",
  76. "Revealing period": params.revealing_period + " blocks",
  77. "Council size": params.council_size + " members",
  78. "Candidacy limit": params.candidacy_limit + " members",
  79. "New term duration": params.new_term_duration + " blocks",
  80. "Min. council stake": formatBalance(params.min_council_stake),
  81. "Min. voting stake": formatBalance(params.min_voting_stake)
  82. }),
  83. Spending: ([amount, account]) => ({
  84. Amount: formatBalance(amount),
  85. Account: <ProposedAddress address={account} />
  86. }),
  87. SetLead: ([memberId, accountId]) => ({
  88. "Member": <ProposedMember memberId={ memberId } />,
  89. "Account id": <ProposedAddress address={accountId} />
  90. }),
  91. SetContentWorkingGroupMintCapacity: ([capacity]) => ({
  92. "Mint capacity": formatBalance(capacity)
  93. }),
  94. EvictStorageProvider: ([accountId]) => ({
  95. "Storage provider account": <ProposedAddress address={accountId} />
  96. }),
  97. SetValidatorCount: ([count]) => ({
  98. "Validator count": count
  99. }),
  100. SetStorageRoleParameters: ([params]) => ({
  101. "Min. stake": formatBalance(params.min_stake),
  102. // "Min. actors": params.min_actors,
  103. "Max. actors": params.max_actors,
  104. Reward: formatBalance(params.reward),
  105. "Reward period": params.reward_period + " blocks",
  106. // "Bonding period": params.bonding_period + " blocks",
  107. "Unbonding period": params.unbonding_period + " blocks",
  108. // "Min. service period": params.min_service_period + " blocks",
  109. // "Startup grace period": params.startup_grace_period + " blocks",
  110. "Entry request fee": formatBalance(params.entry_request_fee)
  111. })
  112. };
  113. const ProposalParams = styled.div`
  114. display: grid;
  115. font-weight: bold;
  116. grid-template-columns: min-content 1fr;
  117. grid-row-gap: 0.5rem;
  118. @media screen and (max-width: 767px) {
  119. grid-template-columns: 1fr;
  120. }
  121. `;
  122. const ProposalParamName = styled.div`
  123. margin-right: 1rem;
  124. white-space: nowrap;
  125. `;
  126. const ProposalParamValue = styled.div`
  127. color: black;
  128. word-wrap: break-word;
  129. word-break: break-all;
  130. @media screen and (max-width: 767px) {
  131. margin-top: -0.25rem;
  132. }
  133. `;
  134. export default function Body({
  135. type,
  136. title,
  137. description,
  138. params = [],
  139. iAmProposer,
  140. proposalId,
  141. proposerId,
  142. isCancellable,
  143. cancellationFee
  144. }: BodyProps) {
  145. const parseParams = paramParsers[type];
  146. const parsedParams = parseParams(params);
  147. return (
  148. <Card fluid>
  149. <Card.Content>
  150. <Card.Header>
  151. <Header as="h1">{title}</Header>
  152. </Card.Header>
  153. <Card.Description>{description}</Card.Description>
  154. <Header as="h4">Parameters:</Header>
  155. <ProposalParams>
  156. { Object.entries(parsedParams).map(([paramName, paramValue]) => (
  157. <React.Fragment key={paramName}>
  158. <ProposalParamName>{paramName}:</ProposalParamName>
  159. <ProposalParamValue>{paramValue}</ProposalParamValue>
  160. </React.Fragment>
  161. ))}
  162. </ProposalParams>
  163. { iAmProposer && isCancellable && (<>
  164. <Message warning active>
  165. <Message.Content>
  166. <Message.Header>Proposal cancellation</Message.Header>
  167. <p style={{ margin: '0.5em 0', padding: '0' }}>
  168. You can only cancel your proposal while it's still in the Voting Period.
  169. </p>
  170. <p style={{ margin: '0.5em 0', padding: '0' }}>
  171. The cancellation fee for this type of proposal is:&nbsp;
  172. <b>{ cancellationFee ? formatBalance(cancellationFee) : 'NONE' }</b>
  173. </p>
  174. <Button.Group color="red">
  175. <TxButton
  176. params={ [ proposerId, proposalId ] }
  177. tx={ "proposalsEngine.cancelProposal" }
  178. onClick={ sendTx => { sendTx(); } }
  179. className={'icon left labeled'}
  180. >
  181. <Icon name="cancel" inverted />
  182. Withdraw proposal
  183. </TxButton>
  184. </Button.Group>
  185. </Message.Content>
  186. </Message>
  187. </>) }
  188. </Card.Content>
  189. </Card>
  190. );
  191. }