Browse Source

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
Jaco Greeff 6 years ago
parent
commit
3fb85094a6

+ 4 - 0
README.md

@@ -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 - 0
examples/README.md

@@ -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
+
+![final](https://raw.githubusercontent.com/polkadot-js/apps/master/examples/final.png)

BIN
examples/final.png


+ 52 - 0
examples/tut-001.md

@@ -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 - 0
examples/tut-002.md

@@ -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 - 0
examples/tut-003.md

@@ -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 - 0
examples/tut-004.md

@@ -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 - 0
examples/tut-005.md

@@ -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.

+ 1 - 1
jest.config.js

@@ -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 - 0
packages/app-example/LICENSE

@@ -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 - 0
packages/app-example/README.md

@@ -0,0 +1,3 @@
+# @polkadot/app-example
+
+A set of examples, created [via tutorial](../../examples/index.md).

+ 19 - 0
packages/app-example/package.json

@@ -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 - 0
packages/app-example/src/comp-002.tsx

@@ -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 - 0
packages/app-example/src/comp-003.tsx

@@ -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 - 0
packages/app-example/src/comp-004.tsx

@@ -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 - 0
packages/app-example/src/comp-005.tsx

@@ -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 - 0
packages/app-example/src/index-001.tsx

@@ -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 - 0
packages/app-example/src/index-002.tsx

@@ -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 - 0
packages/app-example/src/index-003.tsx

@@ -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 - 0
packages/app-example/src/index-004.tsx

@@ -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 - 0
packages/app-example/src/index-005.tsx

@@ -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 - 0
packages/app-example/src/index.css

@@ -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 - 0
packages/app-example/src/index.tsx

@@ -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]
+    });
+  }
+}

+ 1 - 0
packages/apps/package.json

@@ -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",

+ 20 - 16
packages/apps/src/SideBar/index.tsx

@@ -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 - 0
packages/apps/src/routing/example.ts

@@ -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);

+ 2 - 1
packages/apps/src/routing/index.ts

@@ -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);

+ 1 - 1
packages/apps/webpack.config.js

@@ -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'));

+ 1 - 0
packages/ui-app/src/Params/valueToText.ts

@@ -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(', ');

+ 2 - 2
packages/ui-react-rx/src/with/api.tsx

@@ -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);
 

+ 2 - 2
packages/ui-react-rx/src/with/transform/storage.ts

@@ -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;

+ 1 - 1
packages/ui-signer/src/Modal.tsx

@@ -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)
 );
 

+ 1 - 0
tsconfig.json

@@ -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/*" ],