Browse Source

Support multiple explorers (#2316)

* Support multiple explorers

* LinkExplorer -> LinkExternal

* Add externals to council, treasury & democracy
Jaco Greeff 5 years ago
parent
commit
a65aa97b5f

+ 2 - 2
packages/app-accounts/src/Account.tsx

@@ -9,7 +9,7 @@ import { RecoveryConfig } from '@polkadot/types/interfaces';
 import React, { useState, useEffect } from 'react';
 import { Label } from 'semantic-ui-react';
 import styled from 'styled-components';
-import { AddressInfo, AddressSmall, Badge, Button, ChainLock, Forget, Icon, IdentityIcon, InputTags, LinkPolkascan, Menu, Popup, Input } from '@polkadot/react-components';
+import { AddressInfo, AddressSmall, Badge, Button, ChainLock, Forget, Icon, IdentityIcon, InputTags, LinkExternal, Menu, Popup, Input } from '@polkadot/react-components';
 import { useApi, useCall, useToggle } from '@polkadot/react-hooks';
 import { Option } from '@polkadot/types';
 import keyring from '@polkadot/ui-keyring';
@@ -414,7 +414,7 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
         </Popup>
       </td>
       <td className='mini top'>
-        <LinkPolkascan
+        <LinkExternal
           className='ui--AddressCard-exporer-link'
           data={address}
           type='address'

+ 2 - 2
packages/app-address-book/src/Address.tsx

@@ -9,7 +9,7 @@ import { ActionStatus } from '@polkadot/react-components/Status/types';
 import React, { useEffect, useState } from 'react';
 import { Label } from 'semantic-ui-react';
 import styled from 'styled-components';
-import { AddressSmall, AddressInfo, Button, ChainLock, Icon, InputTags, Input, LinkPolkascan, Forget, Menu, Popup } from '@polkadot/react-components';
+import { AddressSmall, AddressInfo, Button, ChainLock, Icon, InputTags, Input, LinkExternal, Forget, Menu, Popup } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 import keyring from '@polkadot/ui-keyring';
 import Transfer from '@polkadot/app-accounts/modals/Transfer';
@@ -295,7 +295,7 @@ function Address ({ address, className, filter, isFavorite, toggleFavorite }: Pr
         </Popup>
       </td>
       <td className='mini top'>
-        <LinkPolkascan
+        <LinkExternal
           className='ui--AddressCard-exporer-link'
           data={address}
           type='address'

+ 5 - 1
packages/app-council/src/Motions/Motion.tsx

@@ -5,7 +5,7 @@
 import { DerivedCollectiveProposal } from '@polkadot/api-derive/types';
 
 import React from 'react';
-import { AddressMini } from '@polkadot/react-components';
+import { AddressMini, LinkExternal } from '@polkadot/react-components';
 import ProposalCell from '@polkadot/app-democracy/Overview/ProposalCell';
 import { formatNumber } from '@polkadot/util';
 
@@ -68,6 +68,10 @@ export default function Motion ({ className, isMember, members, motion: { hash,
           members={members}
           proposal={proposal}
         />
+        <LinkExternal
+          data={index}
+          type='council'
+        />
       </td>
     </tr>
   );

+ 5 - 0
packages/app-democracy/src/Overview/DispatchEntry.tsx

@@ -6,6 +6,7 @@ import { AccountId, Balance, BlockNumber, Hash, Proposal, ReferendumIndex } from
 import { ITuple } from '@polkadot/types/types';
 
 import React, { useEffect, useState } from 'react';
+import { LinkExternal } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 import { Bytes, Option } from '@polkadot/types';
 import { formatNumber } from '@polkadot/util';
@@ -54,6 +55,10 @@ export default function DispatchEntry ({ blockNumber, hash, referendumIndex }: P
           isImminent
           proposal={proposal}
         />
+        <LinkExternal
+          data={referendumIndex}
+          type='referendum'
+        />
       </td>
     </tr>
   );

+ 5 - 1
packages/app-democracy/src/Overview/Proposal.tsx

@@ -6,7 +6,7 @@ import { DeriveProposal } from '@polkadot/api-derive/types';
 
 import React from 'react';
 import styled from 'styled-components';
-import { AddressMini, AddressSmall, Button } from '@polkadot/react-components';
+import { AddressMini, AddressSmall, Button, LinkExternal } from '@polkadot/react-components';
 import { FormatBalance } from '@polkadot/react-query';
 import { formatNumber } from '@polkadot/util';
 
@@ -68,6 +68,10 @@ function Proposal ({ className, value: { balance, hash, index, proposal, propose
             proposal={proposal}
           />
         </Button.Group>
+        <LinkExternal
+          data={index}
+          type='proposal'
+        />
       </td>
     </tr>
   );

+ 5 - 1
packages/app-democracy/src/Overview/Referendum.tsx

@@ -9,7 +9,7 @@ import { BlockNumber } from '@polkadot/types/interfaces';
 import BN from 'bn.js';
 import React, { useEffect, useState } from 'react';
 import styled from 'styled-components';
-import { Button } from '@polkadot/react-components';
+import { Button, LinkExternal } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 import { FormatBalance } from '@polkadot/react-query';
 import { formatNumber } from '@polkadot/util';
@@ -126,6 +126,10 @@ function Referendum ({ className, idNumber, value }: Props): React.ReactElement<
             proposal={value.proposal}
           />
         </Button.Group>
+        <LinkExternal
+          data={value.index}
+          type='referendum'
+        />
       </td>
     </tr>
   );

+ 2 - 2
packages/app-explorer/src/BlockHeader.tsx

@@ -8,7 +8,7 @@ import React from 'react';
 import { Link } from 'react-router-dom';
 import styled from 'styled-components';
 import { HeaderExtended } from '@polkadot/api-derive';
-import { AddressMini, LinkPolkascan } from '@polkadot/react-components';
+import { AddressMini, LinkExternal } from '@polkadot/react-components';
 import { formatNumber } from '@polkadot/util';
 
 interface Props extends BareProps {
@@ -75,7 +75,7 @@ function BlockHeader ({ className, isSummary, value, withExplorer, withLink }: P
       }
       {
         withExplorer
-          ? <LinkPolkascan data={hashHex} type='block' />
+          ? <LinkExternal data={hashHex} type='block' />
           : undefined
       }
     </article>

+ 2 - 2
packages/app-explorer/src/BlockInfo/Extrinsics.tsx

@@ -8,7 +8,7 @@ import { I18nProps } from '@polkadot/react-components/types';
 import React from 'react';
 import styled from 'styled-components';
 import { registry } from '@polkadot/react-api';
-import { AddressMini, Call, Column, LinkPolkascan } from '@polkadot/react-components';
+import { AddressMini, Call, Column, LinkExternal } from '@polkadot/react-components';
 import { formatNumber } from '@polkadot/util';
 
 import translate from '../translate';
@@ -73,7 +73,7 @@ function renderExtrinsic (props: Props, extrinsic: Extrinsic, index: number): Re
       </details>
       {
         extrinsic.isSigned
-          ? <LinkPolkascan data={extrinsic.hash.toHex()} type='extrinsic' />
+          ? <LinkExternal data={extrinsic.hash.toHex()} type='extrinsic' />
           : null
       }
     </article>

+ 6 - 2
packages/app-treasury/src/Overview/Proposal.tsx

@@ -5,7 +5,7 @@
 import { DerivedTreasuryProposal } from '@polkadot/api-derive/types';
 
 import React from 'react';
-import { AddressMini, AddressSmall } from '@polkadot/react-components';
+import { AddressMini, AddressSmall, LinkExternal } from '@polkadot/react-components';
 import { FormatBalance } from '@polkadot/react-query';
 import { formatNumber } from '@polkadot/util';
 
@@ -29,7 +29,7 @@ export default function ProposalDisplay ({ className, isMember, proposal: { coun
       <td className='number top'>
         <h1>{formatNumber(id)}</h1>
       </td>
-      <td>
+      <td className='top'>
         <AddressSmall value={proposal.proposer} />
       </td>
       <td className='top'>
@@ -64,6 +64,10 @@ export default function ProposalDisplay ({ className, isMember, proposal: { coun
             />
           </>
         )}
+        <LinkExternal
+          data={id}
+          type='treasury'
+        />
       </td>
     </tr>
   );

+ 152 - 0
packages/react-components/src/LinkExternal.tsx

@@ -0,0 +1,152 @@
+// Copyright 2017-2020 @polkadot/react-components authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import BN from 'bn.js';
+import React, { useMemo } from 'react';
+import styled from 'styled-components';
+import { useApi } from '@polkadot/react-hooks';
+
+import { useTranslation } from './translate';
+import Icon from './Icon';
+import Tooltip from './Tooltip';
+
+export type LinkTypes = 'address' | 'block' | 'council' | 'extrinsic' | 'proposal' | 'referendum' | 'treasury';
+
+interface Props {
+  className?: string;
+  data: BN | number | string;
+  type: LinkTypes;
+  withShort?: boolean;
+}
+
+interface Explorer {
+  isActive: boolean;
+  chains: Record<string, string>;
+  paths: Partial<Record<LinkTypes, string>>;
+  create: (chain: string, path: string, data: BN | number | string) => string;
+}
+
+const EXPLORERS: Record<string, Explorer> = {
+  Commonwealth: {
+    isActive: true,
+    chains: {
+      Edgeware: 'edgeware',
+      Kusama: 'kusama',
+      'Kusama CC3': 'kusama'
+    },
+    paths: {
+      council: 'proposal/councilmotion',
+      proposal: 'proposal/democracyproposal',
+      referendum: 'proposal/referendum',
+      treasury: 'proposal/treasuryproposal'
+    },
+    create: (chain: string, path: string, data: BN | number | string): string =>
+      `https://commonwealth.im/${chain}/${path}/${data.toString()}`
+  },
+  Polkascan: {
+    isActive: true,
+    chains: {
+      Edgeware: 'edgeware',
+      Kulupu: 'kulupu',
+      Kusama: 'kusama',
+      'Kusama CC3': 'kusama',
+      Westend: 'westend'
+    },
+    paths: {
+      address: 'module/account',
+      block: 'system/block',
+      council: 'council/motion',
+      extrinsic: 'system/extrinsic',
+      proposal: 'democracy/proposal',
+      referendum: 'democracy/referendum',
+      treasury: 'treasury/proposal'
+    },
+    create: (chain: string, path: string, data: BN | number | string): string =>
+      `https://polkascan.io/pre/${chain}/${path}/${data.toString()}`
+  },
+  Subscan: {
+    isActive: false,
+    chains: {
+      Kusama: 'kusama',
+      'Kusama CC3': 'kusama'
+    },
+    paths: {
+      address: 'account',
+      block: 'block',
+      extrinsic: 'extrinsic'
+    },
+    create: (chain: string, path: string, data: BN | number | string): string =>
+      `https://${chain}.subscan.io/${path}/${data.toString()}`
+  }
+};
+
+function genLinks (systemChain: string, { data, type, withShort }: Props): React.ReactNode[] {
+  return Object
+    .entries(EXPLORERS)
+    .map(([name, { isActive, chains, paths, create }]): React.ReactNode | null => {
+      const extChain = chains[systemChain];
+      const extPath = paths[type];
+
+      if (!isActive || !extChain || !extPath) {
+        return null;
+      }
+
+      const trigger = `${name}-${type}-${data}`;
+      const link = create(extChain, extPath, data);
+
+      return (
+        <a
+          data-for={trigger}
+          data-tip={true}
+          href={link}
+          key={name}
+          rel='noopener noreferrer'
+          target='_blank'
+        >
+          {withShort
+            ? <Icon name='external' />
+            : name
+          }
+          <Tooltip
+            place='top'
+            text={name}
+            trigger={trigger}
+          />
+        </a>
+      );
+    })
+    .filter((node): node is React.ReactNode => !!node);
+}
+
+function LinkExternal ({ className, data, type, withShort }: Props): React.ReactElement<Props> | null {
+  const { t } = useTranslation();
+  const { systemChain } = useApi();
+  const links = useMemo((): React.ReactNode[] => {
+    return genLinks(systemChain, { data, type, withShort });
+  }, [systemChain, data, type, withShort]);
+
+  if (!links.length) {
+    return null;
+  }
+
+  return (
+    <div className={`${className} ${withShort ? 'withShort' : ''}`}>
+      {!withShort && <div>{t('View this externally')}</div>}<div>{links.map((link, index) => <span key={index}>{link}</span>)}</div>
+    </div>
+  );
+}
+
+export default styled(LinkExternal)`
+  margin-top: 0.75rem;
+  text-align: right;
+
+  > div {
+    display: block;
+    whitespace: nowrap;
+
+    > span+span {
+      margin-left: 0.3rem;
+    }
+  }
+`;

+ 0 - 65
packages/react-components/src/LinkPolkascan.tsx

@@ -1,65 +0,0 @@
-// Copyright 2017-2020 @polkadot/react-components authors & contributors
-// This software may be modified and distributed under the terms
-// of the Apache-2.0 license. See the LICENSE file for details.
-
-import React from 'react';
-import styled from 'styled-components';
-import { useApi } from '@polkadot/react-hooks';
-
-import { useTranslation } from './translate';
-import Icon from './Icon';
-
-export type LinkTypes = 'address' | 'block' | 'extrinsic';
-
-interface Props {
-  className?: string;
-  data: string;
-  type: LinkTypes;
-  withShort?: boolean;
-}
-
-const BASE = 'https://polkascan.io/pre/';
-
-const CHAINS: Record<string, string> = {
-  Kulupu: 'kulupu',
-  Kusama: 'kusama',
-  'Kusama CC3': 'kusama',
-  Westend: 'westend'
-};
-
-const TYPES: Record<string, string> = {
-  address: '/module/account/',
-  block: '/system/block/',
-  extrinsic: '/system/extrinsic/'
-};
-
-function LinkPolkascan ({ className, data, type, withShort }: Props): React.ReactElement<Props> | null {
-  const { t } = useTranslation();
-  const { systemChain } = useApi();
-  const extChain = CHAINS[systemChain];
-  const extType = TYPES[type];
-
-  if (!extChain || !extType) {
-    return null;
-  }
-
-  return (
-    <div className={`${className} ${withShort ? 'withShort' : ''}`}>
-      <a
-        href={`${BASE}${extChain}${extType}${data}`}
-        rel='noopener noreferrer'
-        target='_blank'
-      >
-        {withShort
-          ? <Icon name='external' />
-          : t('View this {{type}} on Polkascan.io', { replace: { type } })
-        }
-      </a>
-    </div>
-  );
-}
-
-export default styled(LinkPolkascan)`
-  margin-top: 1rem;
-  text-align: right;
-`;

+ 1 - 1
packages/react-components/src/index.tsx

@@ -60,7 +60,7 @@ export { default as Inset } from './Inset';
 export { default as Label } from './Label';
 export { default as LabelHelp } from './LabelHelp';
 export { default as Labelled } from './Labelled';
-export { default as LinkPolkascan } from './LinkPolkascan';
+export { default as LinkExternal } from './LinkExternal';
 export { default as LockedVote } from './LockedVote';
 export { default as Menu } from './Menu';
 export { default as Messages } from './Messages';