Skip to content

Commit

Permalink
Basic NIS dashboard (polkadot-js#9437)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacogr authored May 14, 2023
1 parent e198af9 commit 93c7c0e
Show file tree
Hide file tree
Showing 20 changed files with 534 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Contributed:
Changes:

- Cater for current Substrate bonding extrinsic arguments
- Add basic NIS dashboard
- Disable unreachable endpoints


Expand Down
4 changes: 3 additions & 1 deletion packages/apps-routing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import gilt from './gilt.js';
import js from './js.js';
import membership from './membership.js';
import nfts from './nfts.js';
import nis from './nis.js';
import parachains from './parachains.js';
import poll from './poll.js';
import preimages from './preimages.js';
Expand Down Expand Up @@ -74,10 +75,11 @@ export default function create (t: TFunction): Routes {
bounties(t),
// others
parachains(t),
gilt(t),
assets(t),
nfts(t),
society(t),
nis(t),
gilt(t),
scheduler(t),
calendar(t),
contracts(t),
Expand Down
23 changes: 23 additions & 0 deletions packages/apps-routing/src/nis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2017-2023 @polkadot/apps-routing authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { TFunction } from 'i18next';
import type { Route } from './types.js';

import Component from '@polkadot/app-nis';

export default function create (t: TFunction): Route {
return {
Component,
display: {
needsApi: [
'tx.nis.placeBid',
'query.proxy.proxies'
]
},
group: 'network',
icon: 'leaf',
name: 'nis',
text: t<string>('nav.nis', 'Non-interactive Staking', { ns: 'apps-routing' })
};
}
Empty file added packages/page-nis/.skip-build
Empty file.
Empty file added packages/page-nis/.skip-npm
Empty file.
1 change: 1 addition & 0 deletions packages/page-nis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @polkadot/app-nis
26 changes: 26 additions & 0 deletions packages/page-nis/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"bugs": "https://github.com/polkadot-js/apps/issues",
"engines": {
"node": ">=16"
},
"homepage": "https://github.com/polkadot-js/apps/tree/master/packages/page-nis#readme",
"license": "Apache-2.0",
"name": "@polkadot/app-nis",
"private": true,
"repository": {
"directory": "packages/page-nis",
"type": "git",
"url": "https://github.com/polkadot-js/apps.git"
},
"sideEffects": false,
"type": "module",
"version": "0.130.2-1-x",
"dependencies": {
"@polkadot/react-components": "^0.130.2-1-x"
},
"peerDependencies": {
"react": "*",
"react-dom": "*",
"react-is": "*"
}
}
132 changes: 132 additions & 0 deletions packages/page-nis/src/Overview/BidAdd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2017-2023 @polkadot/app-nis authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { BN } from '@polkadot/util';

import React, { useMemo, useState } from 'react';

import { Button, InputAddress, InputBalance, InputNumber, Modal, TxButton } from '@polkadot/react-components';
import { useApi, useToggle } from '@polkadot/react-hooks';
import { Available } from '@polkadot/react-query';
import { BN_ONE } from '@polkadot/util';

import { useTranslation } from '../translate.js';

interface Props {
className?: string;
isDisabled?: boolean;
proxies: Record<string, string[]>;
}

function Bid ({ className, isDisabled, proxies }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [isOpen, toggleOpen] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
const [proxyId, setProxyId] = useState<string | null>(null);
const [amount, setAmount] = useState<BN | undefined>();
const [duration, setDuration] = useState<BN | undefined>();

const tx = useMemo(
() => accountId && amount && duration
? api.tx.proxy.proxy(accountId, null, api.tx.nis.placeBid(amount, duration))
: null,
[api, accountId, amount, duration]
);

const proxiedAccounts = Object.keys(proxies);
const isAmountError = !amount || amount.isZero() || amount.lt(api.consts.nis.minBid);
const isDurationError = !duration || !duration.gte(BN_ONE) || duration.gt(api.consts.nis.queueCount);

return (
<>
<Button
icon='plus'
isDisabled={!proxiedAccounts.length || isDisabled}
label={t<string>('Bid via Proxy')}
onClick={toggleOpen}
/>
{isOpen && (
<Modal
className={className}
header={t<string>('submit nis bid')}
onClose={toggleOpen}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t<string>('This account will make the bid for the nis and pay all associated fees.')}>
<InputAddress
filter={proxiedAccounts}
label={t<string>('use proxied account')}
labelExtra={
<Available
label={<span className='label'>{t<string>('transferrable')}</span>}
params={accountId}
/>
}
onChange={setAccountId}
type='account'
/>
{accountId && (
<InputAddress
filter={proxies[accountId]}
label={t<string>('send via proxy')}
onChange={setProxyId}
type='account'
/>
)}
</Modal.Columns>
<Modal.Columns hint={t<string>('The amount you wish to lock for the duration. It needs to be more than the nis minimum.')}>
<InputBalance
autoFocus
defaultValue={api.consts.nis.minBid}
isError={isAmountError}
isZeroable={false}
label={t<string>('bid amount')}
onChange={setAmount}
/>
<InputBalance
defaultValue={api.consts.nis.minBid}
isDisabled
label={t<string>('minimum bid amount')}
/>
</Modal.Columns>
<Modal.Columns hint={t<string>('The number of periods this bid is to be locked for, less than the maximum period.')}>
<InputNumber
defaultValue={BN_ONE}
isError={isDurationError}
isZeroable={false}
label={t<string>('lock periods')}
onChange={setDuration}
/>
<InputNumber
defaultValue={api.consts.nis.queueCount}
isDisabled
label={t<string>('maximum lock periods')}
/>
{!isDurationError && (
<InputNumber
defaultValue={api.consts.nis.basePeriod.mul(duration)}
isDisabled
label={t<string>('length of lock (blocks, calculated)')}
/>
)}
</Modal.Columns>
</Modal.Content>
<Modal.Actions>
<TxButton
accountId={proxyId}
extrinsic={tx}
icon='check'
isDisabled={isAmountError || isDurationError || !accountId}
label={t<string>('Bid')}
onStart={toggleOpen}
/>
</Modal.Actions>
</Modal>
)}
</>
);
}

export default React.memo(Bid);
31 changes: 31 additions & 0 deletions packages/page-nis/src/Overview/Queue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2017-2023 @polkadot/app-nis authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { QueueTotal } from './types.js';

import React from 'react';

import { Table } from '@polkadot/react-components';
import { FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';

interface Props {
className?: string;
value: QueueTotal;
}

function Queue ({ className, value: { balance, index, numItems } }: Props): React.ReactElement<Props> {
return (
<tr className={className}>
<Table.Column.Id value={index} />
<td className='number all'>
{formatNumber(numItems)}
</td>
<td className='number'>
<FormatBalance value={balance} />
</td>
</tr>
);
}

export default React.memo(Queue);
43 changes: 43 additions & 0 deletions packages/page-nis/src/Overview/Queues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2017-2023 @polkadot/app-nis authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { QueueTotal } from './types.js';

import React, { useRef } from 'react';

import { Table } from '@polkadot/react-components';

import { useTranslation } from '../translate.js';
import Queue from './Queue.js';

interface Props {
className?: string;
queueTotals?: QueueTotal[];
}

function Queues ({ className, queueTotals }: Props): React.ReactElement<Props> {
const { t } = useTranslation();

const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t<string>('queues'), 'start'],
[t<string>('participants'), 'number'],
[t<string>('balance'), 'number']
]);

return (
<Table
className={className}
empty={queueTotals && t<string>('No active nis queues found.')}
header={headerRef.current}
>
{queueTotals?.map((value) => (
<Queue
key={value.index}
value={value}
/>
))}
</Table>
);
}

export default React.memo(Queues);
55 changes: 55 additions & 0 deletions packages/page-nis/src/Overview/Summary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2017-2023 @polkadot/app-nis authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { NisInfo } from './types.js';

import React from 'react';

import { CardSummary, SummaryBox } from '@polkadot/react-components';
import { useApi, useBestNumber } from '@polkadot/react-hooks';
import { BN_QUINTILL } from '@polkadot/util';

import { useTranslation } from '../translate.js';

interface Props {
className?: string;
info?: NisInfo;
isDisabled?: boolean;
}

function Summary ({ className, info }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const bestNumber = useBestNumber();

return (
<SummaryBox className={className}>
<section>
{bestNumber && (
<CardSummary
label={t<string>('intake')}
progress={{
total: api.consts.nis.intakePeriod,
value: bestNumber.mod(api.consts.nis.intakePeriod),
withTime: true
}}
/>
)}
</section>
<section>
{info?.summary && (
<CardSummary
label={t<string>('proportion')}
progress={{
isPercent: true,
total: BN_QUINTILL,
value: info.summary.proportionOwed
}}
/>
)}
</section>
</SummaryBox>
);
}

export default React.memo(Summary);
41 changes: 41 additions & 0 deletions packages/page-nis/src/Overview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2017-2023 @polkadot/app-nis authors & contributors
// SPDX-License-Identifier: Apache-2.0

import React from 'react';

import { Button } from '@polkadot/react-components';

import { useProxies } from '../useProxies.js';
import BidAdd from './BidAdd.js';
import Queues from './Queues.js';
import Summary from './Summary.js';
import useInfo from './useInfo.js';

interface Props {
className?: string;
}

function Overview ({ className }: Props): React.ReactElement<Props> {
const proxies = useProxies();
const { info } = useInfo();

const isDisabled = !info || !info.summary || info.summary.proportionOwed.isZero();

return (
<div className={className}>
<Summary
info={info}
isDisabled={isDisabled}
/>
<Button.Group>
<BidAdd
isDisabled
proxies={proxies}
/>
</Button.Group>
<Queues queueTotals={info?.queueTotals} />
</div>
);
}

export default React.memo(Overview);
Loading

0 comments on commit 93c7c0e

Please sign in to comment.