Example & tutorial (#127)
* Add tutorial step 001 * Update LICENSE header * Query storage via HOC * Tut 003, read proposals * Tut 004 replacing HOC * Pull Intentions & Proposals together * Add final image * Cleanup unused methods * Source links * Remove unused var
This commit is contained in:
parent
8d41e8ada9
commit
3fb85094a6
@ -60,3 +60,7 @@ Demos for the different libraries can be viewed with `yarn run demo:<name>` and
|
||||
- `yarn run demo:identicon`
|
||||
- `yarn run demo:ui`
|
||||
- `yarn run demo:rx`
|
||||
|
||||
## tutorials
|
||||
|
||||
Looking for tutorials to get started? Look at [examples](examples/README.md) for a step-by-step guide to warning the API and Components to display some information.
|
||||
|
||||
17
examples/README.md
Normal file
17
examples/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# examples
|
||||
|
||||
This guide will take you through some basic examples in using the Components and API to build tools to extract information from a Substrate network client. It follows the line of a set of tutorials that will setup basic infrastructure, queies and then string these together into doing some more complex things.
|
||||
|
||||
## tutorials
|
||||
|
||||
- [Basic application scaffolding](tut-001.md) Show the setup of a new app in this framework. (Typically only needed when new top-level features are added)
|
||||
- [Storage and pulling from storage](tut-002.md) Shows how to use the built-in components to show something basic from storage
|
||||
- [Query storage via API](tut-003.md) Shows how to construct an API query to pull information from storage
|
||||
- [Query storage, replace HOC](tut-004.md) Queries storage, replacing the previous use of our HOC
|
||||
- [Combining proposal and intentions](tut-005.md) Using the information collected previously, combine the displays
|
||||
|
||||
## ... and in the end ...
|
||||
|
||||
You will know how to use UI components and the API to render a mapping of intentions, their balanances to proposals
|
||||
|
||||

|
||||
BIN
examples/final.png
Normal file
BIN
examples/final.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
52
examples/tut-001.md
Normal file
52
examples/tut-001.md
Normal file
@ -0,0 +1,52 @@
|
||||
# basic scaffolding
|
||||
|
||||
To add a top-level application, some basic setup is required. This is only applicable for new top-level apps, i.e. like we currently have extrinsics, storage, accounts, etc. When just hacking away on a specific app, this section is not needed.
|
||||
|
||||
# basic setup
|
||||
|
||||
Initially we would need an new application folder to be added under [packages](../packages). For the sake of this tutorial, we have just created [app-example](../packages/app-example), a basic app that will be used in the rest of the tutorials.
|
||||
|
||||
Once created, initialise the app with `package.json`, `README.md`, `LICENSE.md` and the basic folder setup under `src`
|
||||
|
||||
# creating an app entry
|
||||
|
||||
First off, let's create an app entry. Since we will just have a basic sample, let's keep it simple for now. Under [our example src](../packages/app-example/src) we just create an `index.tsx` with a simple entry point.
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
|
||||
export default class App extends React.PureComponent<any> {
|
||||
render () {
|
||||
return (
|
||||
<div>hello world</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(We have kept the original, which has changed as the tutorial progresses as [index-001.tsx](../packages/app-example/src/index-001.tsx))
|
||||
|
||||
# making it visible
|
||||
|
||||
Once the app is created, it needs to be hooked up to the rest, allowing TypeScript, Jest & Webpack to find it and use it.
|
||||
|
||||
- To enable Jest mappings, add the name of the app to [the Jest config](../jest.config.js)
|
||||
- To enable TypeScript mappings, add the app to [the TypeScript config](../tsconfig.json)
|
||||
- To allow the app to be included in the Webpack build, add it to [Webpack config](../packages/apps/webpack.config.js) (Note that this is not high-level, but rather at the entry point of the loader - this allows packages to have their own configs in the case of demos)
|
||||
|
||||
At this point the application will be available and included, even though it does not have a [navigation entry](../packages/apps/routing) yet. Adding the navigation -
|
||||
|
||||
- Add a reference to the application to the [apps package.json](../packages/apps/package.json)
|
||||
- Add a route file the defines the [app, name & icon](../packages/apps/routing/example.ts)
|
||||
- Finally include the route in the [route index](../packages/apps/touting/index.ts)
|
||||
|
||||
# running!
|
||||
|
||||
Basic app added, now let it run and see what it does.
|
||||
|
||||
- Start the webpack server via `WS_URL=wss://poc-1.polkadot.io:9944 yarn run start`
|
||||
- Access the applcation on [localhost:3000/#/example](http://localhost:3000/#/example)
|
||||
|
||||
# that is it
|
||||
|
||||
We have something basic wired-up, now let's [build something](tut-002.md)
|
||||
85
examples/tut-002.md
Normal file
85
examples/tut-002.md
Normal file
@ -0,0 +1,85 @@
|
||||
# storage via HOC
|
||||
|
||||
In the [previous section](tut-001.md) we setup a basic application framework in [app-example](../packages/app-example). Here we will continute adding some useful queries using bare-bones UI components.
|
||||
|
||||
In substrate chains, the storage (state) is exposed via API, allowing any app to perform queries against the current (and previous) states. Here we will use some built-in components to perform a query over the intentions, displaying the results.
|
||||
|
||||
# creating a component
|
||||
|
||||
Here we are creating a component that will attach to a specific storage entry, and display any changes as they happen on-chain. For this example, we are interested in the `intentions` storage area, i.e. those accounts that wish to participate in staking.
|
||||
|
||||
Initially we will just create it, and then walk through what actually happens. In [comp-002.tsx](../packages/app-example/src/comp-002.tsx) we have -
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
|
||||
import storage from '@polkadot/storage';
|
||||
import withStorageDiv from '@polkadot/ui-react-rx/with/storageDiv';
|
||||
import encodeAddress from '@polkadot/util-keyring/address/encode';
|
||||
|
||||
const method = storage.staking.public.intentions;
|
||||
|
||||
const Comp: React.ComponentType<any> = withStorageDiv(method)(
|
||||
(value: Uint8Array[]): string => {
|
||||
if (!value || !value.length) {
|
||||
return 'No intentions found';
|
||||
}
|
||||
|
||||
return value.map(encodeAddress).join(', ');
|
||||
}
|
||||
);
|
||||
|
||||
export default Comp;
|
||||
```
|
||||
|
||||
Additionally, we change our entry-point to import this component.
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
|
||||
import Comp from './comp-002';
|
||||
|
||||
export default class App extends React.PureComponent<any> {
|
||||
render () {
|
||||
return (
|
||||
<Comp />
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now going to the app via [localhost:3000/#/example](http://localhost:3000/#/example) renders a list of accounts.
|
||||
|
||||
# walk-through
|
||||
|
||||
So what have we done. First off, we import all available storage entries and select the method we are interested in, i.e.
|
||||
|
||||
```js
|
||||
import storage from '@polkadot/storage';
|
||||
|
||||
const method = storage.staking.public.intentions;
|
||||
```
|
||||
|
||||
Storage entries can be explored via the storage app and will always be (in code) `storage.<section>.public.<method>`. These are all defined in [@polkadot/storage](https://github.com/polkadot-js/common/tree/master/packages/type-storage/src)
|
||||
|
||||
Next up we use an HOC helper `withStorageDiv` which basically takes a storage method and with the specified renderer wraps it in a Div.
|
||||
|
||||
```js
|
||||
import withStorageDiv from '@polkadot/ui-react-rx/with/storageDiv';
|
||||
|
||||
const Comp: React.ComponentType<any> = withStorageDiv(method)(
|
||||
(value: Uint8Array[]): string => {
|
||||
...
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Here the storage query will return an `Uint8Array[]`, or rather an array of Accounts as used internally. These Accounts however are a list of the actual publicKeys, so for display we convert them with a formatter,
|
||||
|
||||
```js
|
||||
return value.map(encodeAddress).join(', ');
|
||||
```
|
||||
|
||||
# next steps
|
||||
|
||||
At this point we have, with minimal effort, used some HOC helpers to display information from a storage area. [Next up](tut-003.md) we will actually pull this together and do the same query directly via the API.
|
||||
61
examples/tut-003.md
Normal file
61
examples/tut-003.md
Normal file
@ -0,0 +1,61 @@
|
||||
# storage via API
|
||||
|
||||
In the [previous section](tut-002.md) we have used an HOC to query a storage area. Here we will repeat the same, however this time we will actually use the Rx API to accomplish the same goal.
|
||||
|
||||
# creating the component
|
||||
|
||||
As in previous examples, we will first show the [actual component](../packages/app-example/src/comp-003.tsx) we are using and then delve into how it fits together. Focussing on the meat of the API subscription that happens in the `componentMount` cycle -
|
||||
|
||||
```js
|
||||
class Comp extends React.PureComponent<ApiProps, State> {
|
||||
...
|
||||
componentDidMount () {
|
||||
this.subscribeProposals();
|
||||
}
|
||||
|
||||
// TODO We should unsubscribe from subscriptions
|
||||
subscribeProposals () {
|
||||
const { api } = this.props;
|
||||
const key = createStorageKey(method)();
|
||||
const transform = storageTransform(method);
|
||||
|
||||
api.state
|
||||
.getStorage(key)
|
||||
.subscribe((value) => {
|
||||
this.setState({
|
||||
proposals: (transform(value, 0) as any[])
|
||||
.reduce((proposals, [propIdx, proposal, accountId]) => {
|
||||
const address = encodeAddress(accountId);
|
||||
|
||||
if (!proposals[address]) {
|
||||
proposals[address] = [propIdx];
|
||||
} else {
|
||||
proposals[address].push(propIdx);
|
||||
}
|
||||
|
||||
return proposals;
|
||||
}, {})
|
||||
});
|
||||
});
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
export default withApi(Comp);
|
||||
```
|
||||
|
||||
# walk-through
|
||||
|
||||
Since we are working on a slightly lower level now, a couple of things happen.
|
||||
|
||||
Unlike the case where we use an HOC, we need to manually create the actual storage key (as encoded) before calling the API. the `createStorageKey(<method>)(<params>)` function does that for us. In this case the storage entry doesn't require any params (as opposed to e.g. `staking.freeBalanceOf`). This encoding takes the method, params, applys the correct prefixes and hashes the result to form a key that is known to the staorage.
|
||||
|
||||
We use a helper function to decode the values as it comes back - without this we would just have a raw stream of bytes as retrieved from storage. The decding take the raw stream and splits it into a mangeable Array stream (based on trhe number of items), each containing a tuple of data - in this case the `PropIndex`, `Proposal` and `AccountId`.
|
||||
|
||||
We use the API provided on the component props, as injected via `withApi`, to make an API query - subscribing to any updates. As any updates happen to the storage, these will be automatically fed through the API subscriptions, updating the component display.
|
||||
|
||||
Finally we loop through the proposals retrieved, indexing these by the actual account address - this information we finally use in the render to update the display.
|
||||
|
||||
# next up
|
||||
|
||||
In the next round, we will extend our subscriptions to also pull the information we previously got via HOC and then map [these together](tut-004.md).
|
||||
44
examples/tut-004.md
Normal file
44
examples/tut-004.md
Normal file
@ -0,0 +1,44 @@
|
||||
# storage via API (part II)
|
||||
|
||||
[Previously](tut-002.md) we queried intentions via HOC, then [followed up](tut-003.md) with querying via API. Here we will replace our previous HOC component with the same storage queries.
|
||||
|
||||
# the component
|
||||
|
||||
Here we extend the component from the previous section, adding a subscription to intentions to create our [new component](../packages/app-example/src/comp-004.tsx) -
|
||||
|
||||
```js
|
||||
class Comp extends React.PureComponent<ApiProps, State> {
|
||||
...
|
||||
componentDidMount () {
|
||||
this.setState({
|
||||
subscriptions: [
|
||||
this.subscribeIntentions(),
|
||||
this.subscribeProposals()
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
subscribeIntentions () {
|
||||
const { api } = this.props;
|
||||
const key = createStorageKey(intentionsMethod)();
|
||||
const transform = storageTransform(intentionsMethod);
|
||||
|
||||
return api.state
|
||||
.getStorage(key)
|
||||
.subscribe((value) => {
|
||||
this.setState({
|
||||
intentions: (transform(value, 0) as any[]).map(encodeAddress)
|
||||
});
|
||||
});
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
# walk-through
|
||||
|
||||
This subscription is much simpler than what was found before, however it follows the same basic approach - query, transform - in this case the transform is the same as the one applied even for the HOC component.
|
||||
|
||||
# next steps
|
||||
|
||||
[Next up](tut-005.md), we will be using the actual data we have collected to render something useful on-screen.
|
||||
49
examples/tut-005.md
Normal file
49
examples/tut-005.md
Normal file
@ -0,0 +1,49 @@
|
||||
# pulling it together
|
||||
|
||||
Previously we have used the API to query the [actual storage](tut-004.md), here we will build on that base to show something usable on-screen.
|
||||
|
||||
# component
|
||||
|
||||
[This component](../packages/app-example/src/comp-005.tsx) will focus mostly on the `render()` method and display on-screen. As with other examples, we first show the actual code and then provide a short walk-through of the relevant parts -
|
||||
|
||||
```js
|
||||
class Comp extends React.PureComponent<ApiProps, State> {
|
||||
...
|
||||
render () {
|
||||
const { intentions, proposals } = this.state;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>Address</th>
|
||||
<th>Balance</th>
|
||||
<th>Proposals</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{intentions.map((address) => (
|
||||
this.renderAccount(address, proposals[address])
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccount = (address: string, proposals: number[] = []) => {
|
||||
return (
|
||||
<tr key={address}>
|
||||
<td><IdentityIcon size={24} value={address} /></td>
|
||||
<td>{address}</td>
|
||||
<td><Balance params={address} /></td>
|
||||
<td>{proposals.length}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# walk-through
|
||||
|
||||
The render is straight-forward. For each of our intentions, render a row in a table showing the icon & address, the balance of the account and the number of proposals.
|
||||
@ -2,7 +2,7 @@ const config = require('@polkadot/dev-react/config/jest');
|
||||
|
||||
module.exports = Object.assign({}, config, {
|
||||
moduleNameMapper: {
|
||||
'@polkadot/app-(accounts|addresses|explorer|extrinsics|rpc|storage|toolbox|vanitygen)(.*)$': '<rootDir>/packages/ui-$1/src/$2',
|
||||
'@polkadot/app-(accounts|addresses|example|explorer|extrinsics|rpc|storage|toolbox|vanitygen)(.*)$': '<rootDir>/packages/ui-$1/src/$2',
|
||||
'@polkadot/ui-(app|identicon|keyring|react-rx|react|signer)(.*)$': '<rootDir>/packages/ui-$1/src/$2',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
|
||||
'\\.(css|less)$': 'empty/object'
|
||||
|
||||
15
packages/app-example/LICENSE
Normal file
15
packages/app-example/LICENSE
Normal file
@ -0,0 +1,15 @@
|
||||
ISC License (ISC)
|
||||
|
||||
Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
3
packages/app-example/README.md
Normal file
3
packages/app-example/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# @polkadot/app-example
|
||||
|
||||
A set of examples, created [via tutorial](../../examples/index.md).
|
||||
19
packages/app-example/package.json
Normal file
19
packages/app-example/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@polkadot/app-example",
|
||||
"version": "0.18.9",
|
||||
"description": "A basic example app",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
},
|
||||
"author": "Jaco Greeff <jacogr@gmail.com>",
|
||||
"maintainers": [
|
||||
"Jaco Greeff <jacogr@gmail.com>"
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0-beta.51",
|
||||
"@polkadot/storage": "^0.26.9",
|
||||
"@polkadot/ui-react-rx": "^0.18.9",
|
||||
"@polkadot/util-keyring": "^0.26.9"
|
||||
}
|
||||
}
|
||||
23
packages/app-example/src/comp-002.tsx
Normal file
23
packages/app-example/src/comp-002.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import storage from '@polkadot/storage';
|
||||
import withStorageDiv from '@polkadot/ui-react-rx/with/storageDiv';
|
||||
import encodeAddress from '@polkadot/util-keyring/address/encode';
|
||||
|
||||
const method = storage.staking.public.intentions;
|
||||
|
||||
const Comp: React.ComponentType<any> = withStorageDiv(method)(
|
||||
(value: Uint8Array[]): string => {
|
||||
if (!value || !value.length) {
|
||||
return 'No intentions found';
|
||||
}
|
||||
|
||||
return value.map(encodeAddress).join(', ');
|
||||
}
|
||||
);
|
||||
|
||||
export default Comp;
|
||||
71
packages/app-example/src/comp-003.tsx
Normal file
71
packages/app-example/src/comp-003.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ApiProps } from '@polkadot/ui-react-rx/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import storage from '@polkadot/storage';
|
||||
import createStorageKey from '@polkadot/storage/key';
|
||||
import withApi from '@polkadot/ui-react-rx/with/api';
|
||||
import storageTransform from '@polkadot/ui-react-rx/with/transform/storage';
|
||||
import encodeAddress from '@polkadot/util-keyring/address/encode';
|
||||
|
||||
const method = storage.democracy.public.proposals;
|
||||
|
||||
type State = {
|
||||
proposals: {
|
||||
[index: string]: number[]
|
||||
}
|
||||
};
|
||||
|
||||
class Comp extends React.PureComponent<ApiProps, State> {
|
||||
constructor (props: ApiProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
proposals: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.subscribeProposals();
|
||||
}
|
||||
|
||||
// TODO We should unsubscribe from subscriptions
|
||||
subscribeProposals () {
|
||||
const { api } = this.props;
|
||||
const key = createStorageKey(method)();
|
||||
const transform = storageTransform(method);
|
||||
|
||||
api.state
|
||||
.getStorage(key)
|
||||
.subscribe((value) => {
|
||||
this.setState({
|
||||
proposals: (transform(value, 0) as any[])
|
||||
.reduce((proposals, [propIdx, proposal, accountId]) => {
|
||||
const address = encodeAddress(accountId);
|
||||
|
||||
if (!proposals[address]) {
|
||||
proposals[address] = [propIdx];
|
||||
} else {
|
||||
proposals[address].push(propIdx);
|
||||
}
|
||||
|
||||
return proposals;
|
||||
}, {})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { proposals } = this.state;
|
||||
|
||||
return (
|
||||
<div>Found {Object.keys(proposals).length} accounts making proposals.</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withApi(Comp);
|
||||
100
packages/app-example/src/comp-004.tsx
Normal file
100
packages/app-example/src/comp-004.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ApiProps } from '@polkadot/ui-react-rx/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import storage from '@polkadot/storage';
|
||||
import createStorageKey from '@polkadot/storage/key';
|
||||
import withApi from '@polkadot/ui-react-rx/with/api';
|
||||
import storageTransform from '@polkadot/ui-react-rx/with/transform/storage';
|
||||
import encodeAddress from '@polkadot/util-keyring/address/encode';
|
||||
|
||||
const intentionsMethod = storage.staking.public.intentions;
|
||||
const proposalMethod = storage.democracy.public.proposals;
|
||||
|
||||
type State = {
|
||||
intentions: string[],
|
||||
proposals: {
|
||||
[index: string]: number[]
|
||||
},
|
||||
subscriptions: any[]
|
||||
};
|
||||
|
||||
class Comp extends React.PureComponent<ApiProps, State> {
|
||||
constructor (props: ApiProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
intentions: [],
|
||||
proposals: {},
|
||||
subscriptions: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.setState({
|
||||
subscriptions: [
|
||||
this.subscribeIntentions(),
|
||||
this.subscribeProposals()
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
subscribeIntentions () {
|
||||
const { api } = this.props;
|
||||
const key = createStorageKey(intentionsMethod)();
|
||||
const transform = storageTransform(intentionsMethod);
|
||||
|
||||
return api.state
|
||||
.getStorage(key)
|
||||
.subscribe((value) => {
|
||||
this.setState({
|
||||
intentions: (transform(value, 0) as any[]).map(encodeAddress)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
subscribeProposals () {
|
||||
const { api } = this.props;
|
||||
const key = createStorageKey(proposalMethod)();
|
||||
const transform = storageTransform(proposalMethod);
|
||||
|
||||
return api.state
|
||||
.getStorage(key)
|
||||
.subscribe((value) => {
|
||||
this.setState({
|
||||
proposals: (transform(value, 0) as any[])
|
||||
.reduce((proposals, [propIdx, proposal, accountId]) => {
|
||||
const address = encodeAddress(accountId);
|
||||
|
||||
if (!proposals[address]) {
|
||||
proposals[address] = [propIdx];
|
||||
} else {
|
||||
proposals[address].push(propIdx);
|
||||
}
|
||||
|
||||
return proposals;
|
||||
}, {})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { subscriptions } = this.state;
|
||||
|
||||
subscriptions.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intentions, proposals } = this.state;
|
||||
|
||||
return (
|
||||
<div>Found {Object.keys(proposals).length} accounts making proposals and {intentions.length} accounts intending to stake.</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withApi(Comp);
|
||||
127
packages/app-example/src/comp-005.tsx
Normal file
127
packages/app-example/src/comp-005.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ApiProps } from '@polkadot/ui-react-rx/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import storage from '@polkadot/storage';
|
||||
import createStorageKey from '@polkadot/storage/key';
|
||||
import withApi from '@polkadot/ui-react-rx/with/api';
|
||||
import storageTransform from '@polkadot/ui-react-rx/with/transform/storage';
|
||||
import encodeAddress from '@polkadot/util-keyring/address/encode';
|
||||
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
|
||||
import Balance from '@polkadot/ui-react-rx/Balance';
|
||||
|
||||
const intentionsMethod = storage.staking.public.intentions;
|
||||
const proposalMethod = storage.democracy.public.proposals;
|
||||
|
||||
type State = {
|
||||
intentions: string[],
|
||||
proposals: {
|
||||
[index: string]: number[]
|
||||
},
|
||||
subscriptions: any[]
|
||||
};
|
||||
|
||||
class Comp extends React.PureComponent<ApiProps, State> {
|
||||
constructor (props: ApiProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
intentions: [],
|
||||
proposals: {},
|
||||
subscriptions: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.setState({
|
||||
subscriptions: [
|
||||
this.subscribeIntentions(),
|
||||
this.subscribeProposals()
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
subscribeIntentions () {
|
||||
const { api } = this.props;
|
||||
const key = createStorageKey(intentionsMethod)();
|
||||
const transform = storageTransform(intentionsMethod);
|
||||
|
||||
return api.state
|
||||
.getStorage(key)
|
||||
.subscribe((value) => {
|
||||
this.setState({
|
||||
intentions: (transform(value, 0) as any[]).map(encodeAddress)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
subscribeProposals () {
|
||||
const { api } = this.props;
|
||||
const key = createStorageKey(proposalMethod)();
|
||||
const transform = storageTransform(proposalMethod);
|
||||
|
||||
return api.state
|
||||
.getStorage(key)
|
||||
.subscribe((value) => {
|
||||
this.setState({
|
||||
proposals: (transform(value, 0) as any[])
|
||||
.reduce((proposals, [propIdx, proposal, accountId]) => {
|
||||
const address = encodeAddress(accountId);
|
||||
|
||||
if (!proposals[address]) {
|
||||
proposals[address] = [propIdx];
|
||||
} else {
|
||||
proposals[address].push(propIdx);
|
||||
}
|
||||
|
||||
return proposals;
|
||||
}, {})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { subscriptions } = this.state;
|
||||
|
||||
subscriptions.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intentions, proposals } = this.state;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>Address</th>
|
||||
<th>Balance</th>
|
||||
<th>Proposals</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{intentions.map((address) => (
|
||||
this.renderAccount(address, proposals[address])
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccount = (address: string, proposals: number[] = []) => {
|
||||
return (
|
||||
<tr key={address}>
|
||||
<td><IdentityIcon size={24} value={address} /></td>
|
||||
<td>{address}</td>
|
||||
<td><Balance params={address} /></td>
|
||||
<td>{proposals.length}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withApi(Comp);
|
||||
13
packages/app-example/src/index-001.tsx
Normal file
13
packages/app-example/src/index-001.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class App extends React.PureComponent<any> {
|
||||
render () {
|
||||
return (
|
||||
<div className='example--App'>hello world</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
packages/app-example/src/index-002.tsx
Normal file
17
packages/app-example/src/index-002.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Comp from './comp-002';
|
||||
|
||||
export default class App extends React.PureComponent<any> {
|
||||
render () {
|
||||
return (
|
||||
<div className='example--App'>
|
||||
<Comp />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
packages/app-example/src/index-003.tsx
Normal file
17
packages/app-example/src/index-003.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Comp from './comp-003';
|
||||
|
||||
export default class App extends React.PureComponent<any> {
|
||||
render () {
|
||||
return (
|
||||
<div className='example--App'>
|
||||
<Comp />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
packages/app-example/src/index-004.tsx
Normal file
17
packages/app-example/src/index-004.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Comp from './comp-004';
|
||||
|
||||
export default class App extends React.PureComponent<any> {
|
||||
render () {
|
||||
return (
|
||||
<div className='example--App'>
|
||||
<Comp />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
packages/app-example/src/index-005.tsx
Normal file
17
packages/app-example/src/index-005.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Comp from './comp-005';
|
||||
|
||||
export default class App extends React.PureComponent<any> {
|
||||
render () {
|
||||
return (
|
||||
<div className='example--App'>
|
||||
<Comp />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
20
packages/app-example/src/index.css
Normal file
20
packages/app-example/src/index.css
Normal file
@ -0,0 +1,20 @@
|
||||
/* Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
/* This software may be modified and distributed under the terms
|
||||
/* of the ISC license. See the LICENSE file for details. */
|
||||
|
||||
.example--App {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
.example--App .navigation {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.example--App a {
|
||||
cursor: pointer;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.example--App td {
|
||||
padding: 0.125em 0.5em;
|
||||
}
|
||||
51
packages/app-example/src/index.tsx
Normal file
51
packages/app-example/src/index.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2018 @polkadot/app-example authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import './index.css';
|
||||
|
||||
import Tut002 from './comp-002';
|
||||
import Tut003 from './comp-003';
|
||||
import Tut004 from './comp-004';
|
||||
import Tut005 from './comp-005';
|
||||
|
||||
const Components: { [index: string]: React.ComponentType<any> } = {
|
||||
Tut002, Tut003, Tut004, Tut005
|
||||
};
|
||||
|
||||
type State = {
|
||||
Component: React.ComponentType<any>
|
||||
};
|
||||
|
||||
export default class App extends React.PureComponent<any, State> {
|
||||
constructor (props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
Component: Tut002
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { Component } = this.state;
|
||||
|
||||
return (
|
||||
<div className='example--App'>
|
||||
<div className='navigation'>{
|
||||
Object.keys(Components).map((name) => (
|
||||
<a key={name} onClick={() => this.onSelect(name)}>{name}</a>
|
||||
))
|
||||
}</div>
|
||||
<div><Component /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onSelect (name: string) {
|
||||
this.setState({
|
||||
Component: Components[name]
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@
|
||||
"@polkadot/app-accounts": "^0.18.9",
|
||||
"@polkadot/app-addresses": "^0.18.9",
|
||||
"@polkadot/app-explorer": "^0.18.9",
|
||||
"@polkadot/app-example": "^0.18.9",
|
||||
"@polkadot/app-extrinsics": "^0.18.9",
|
||||
"@polkadot/app-rpc": "^0.18.9",
|
||||
"@polkadot/app-storage": "^0.18.9",
|
||||
|
||||
@ -31,22 +31,26 @@ function SideBar ({ children, className, style, t }: Props) {
|
||||
vertical
|
||||
>
|
||||
{
|
||||
routing.routes.map((route, index) => (
|
||||
route
|
||||
? (
|
||||
<Item
|
||||
key={route.name}
|
||||
t={t}
|
||||
route={route}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Menu.Divider
|
||||
hidden
|
||||
key={index}
|
||||
/>
|
||||
)
|
||||
))
|
||||
routing.routes
|
||||
.filter((route) =>
|
||||
!route || !route.isHidden
|
||||
)
|
||||
.map((route, index) => (
|
||||
route
|
||||
? (
|
||||
<Item
|
||||
key={route.name}
|
||||
t={t}
|
||||
route={route}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Menu.Divider
|
||||
hidden
|
||||
key={index}
|
||||
/>
|
||||
)
|
||||
))
|
||||
}
|
||||
<Menu.Divider hidden />
|
||||
<Menu.Item className='apps--SideBar-Item'>
|
||||
|
||||
20
packages/apps/src/routing/example.ts
Normal file
20
packages/apps/src/routing/example.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2017-2018 @polkadot/apps authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { Routes } from '../types';
|
||||
|
||||
import Example from '@polkadot/app-example/index';
|
||||
|
||||
export default ([
|
||||
{
|
||||
Component: Example,
|
||||
i18n: {
|
||||
defaultValue: 'Example'
|
||||
},
|
||||
icon: 'hand spock',
|
||||
isExact: false,
|
||||
isHidden: true,
|
||||
name: 'example'
|
||||
}
|
||||
] as Routes);
|
||||
@ -6,6 +6,7 @@ import { Routing, Routes } from '../types';
|
||||
|
||||
import accounts from './accounts';
|
||||
import addresses from './addresses';
|
||||
import example from './example';
|
||||
import explorer from './explorer';
|
||||
import extrinsics from './extrinsics';
|
||||
// import home from './home';
|
||||
@ -19,7 +20,7 @@ import vanitygen from './vanitygen';
|
||||
export default ({
|
||||
default: 'explorer',
|
||||
routes: ([] as Routes).concat(
|
||||
explorer, extrinsics, storage, null, accounts, addresses, vanitygen, null, rpc, toolbox
|
||||
example, explorer, extrinsics, storage, null, accounts, addresses, vanitygen, null, rpc, toolbox
|
||||
),
|
||||
unknown
|
||||
} as Routing);
|
||||
|
||||
@ -10,7 +10,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
const packages = ['app-accounts', 'app-addresses', 'app-explorer', 'app-extrinsics', 'app-rpc', 'app-storage', 'app-toolbox', 'app-vanitygen', 'ui-app', 'ui-identicon', 'ui-keyring', 'ui-react-rx', 'ui-react', 'ui-signer'];
|
||||
const packages = ['app-accounts', 'app-addresses', 'app-example', 'app-explorer', 'app-extrinsics', 'app-rpc', 'app-storage', 'app-toolbox', 'app-vanitygen', 'ui-app', 'ui-identicon', 'ui-keyring', 'ui-react-rx', 'ui-react', 'ui-signer'];
|
||||
|
||||
function createWebpack ({ alias = {}, context, name = 'index' }) {
|
||||
const pkgJson = require(path.join(context, 'package.json'));
|
||||
|
||||
@ -28,6 +28,7 @@ function arrayToText (type: Param$Type$Array, value: Array<any>, withBound: bool
|
||||
}
|
||||
|
||||
if (type.length === 1) {
|
||||
console.error('value.length', value);
|
||||
return value.map((value) =>
|
||||
valueToText(type[0], value, false)
|
||||
).join(', ');
|
||||
|
||||
@ -10,8 +10,8 @@ import assert from '@polkadot/util/assert';
|
||||
|
||||
import { ApiConsumer } from '../Api/Context';
|
||||
|
||||
export default function withApi <T, P extends object> (Inner: React.ComponentType<P & ApiProps>, defaultProps: DefaultProps<T> = {}): React.ComponentType<P> {
|
||||
return class WithApi extends React.PureComponent<P> {
|
||||
export default function withApi <T, P extends ApiProps> (Inner: React.ComponentType<P>, defaultProps: DefaultProps<T> = {}): React.ComponentType<any> {
|
||||
return class WithApi extends React.PureComponent<any> {
|
||||
constructor (props: any) {
|
||||
super(props);
|
||||
|
||||
|
||||
@ -11,8 +11,8 @@ import decodeParams from '@polkadot/params/decode';
|
||||
export default function storageTransform ({ type }: SectionItem<Storages>): StorageTransform {
|
||||
return (input: Uint8Array = new Uint8Array(0), index: number): Param$Values | null => {
|
||||
try {
|
||||
// FIXME We don't do any conversion checks for the type, currently not an issue, but _could_ turn out to be problematic
|
||||
return decodeParams(type, input, 'latest').value;
|
||||
// FIXME We don't do any conversion checks for the type, currently not an issue, but _could_ turn out to be problematic (since this is storage, apply no transforms)
|
||||
return decodeParams(type, input, 'poc-1').value;
|
||||
} catch (error) {
|
||||
console.error('Decoding', type, '::', error);
|
||||
return null;
|
||||
|
||||
@ -247,7 +247,7 @@ class Signer extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const Component: React.ComponentType<BaseProps> = translate(
|
||||
const Component: React.ComponentType<any> = translate(
|
||||
withApi(Signer)
|
||||
);
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
"@polkadot/app-accounts/*": [ "packages/app-accounts/src/*" ],
|
||||
"@polkadot/app-addresses/*": [ "packages/app-addresses/src/*" ],
|
||||
"@polkadot/app-explorer/*": [ "packages/app-explorer/src/*" ],
|
||||
"@polkadot/app-example/*": [ "packages/app-example/src/*" ],
|
||||
"@polkadot/app-extrinsics/*": [ "packages/app-extrinsics/src/*" ],
|
||||
"@polkadot/app-rpc/*": [ "packages/app-rpc/src/*" ],
|
||||
"@polkadot/app-storage/*": [ "packages/app-storage/src/*" ],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user