16 |
(this.chartDOM = el)} className={styles.chart} />
17 |
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "commonjs",
5 | "target": "es5",
6 | "lib": ["es6", "dom", "esnext", "es2017"],
7 | "sourceMap": true,
8 | "allowJs": true,
9 | "jsx": "react",
10 | "moduleResolution": "node",
11 | "experimentalDecorators": true,
12 | "noImplicitReturns": true,
13 | "noImplicitThis": false,
14 | // "noImplicitAny": true,
15 | "strictNullChecks": true,
16 | "allowSyntheticDefaultImports": false
17 | },
18 | "exclude": ["node_modules", "dist", "scripts", "webpack"],
19 | "types": ["typePatches"],
20 | "compileOnSave": false,
21 | }
22 |
--------------------------------------------------------------------------------
/src/containers/Dialog/dialog.scss:
--------------------------------------------------------------------------------
1 | .title {
2 | display: flex;
3 | justify-content: space-between;
4 |
5 | h2 {
6 | text-transform: capitalize;
7 | }
8 | }
9 |
10 | .dialogTitle {
11 | text-transform: capitalize; // padding: 0 0 0 17px;
12 |
13 | h2 {
14 | display: flex;
15 | justify-content: center;
16 | align-items: center;
17 | padding-right: 20px;
18 |
19 | button {
20 | position: absolute;
21 | top: 10px;
22 | right: 0;
23 | font-weight: 100;
24 | }
25 |
26 | @media (max-width: 800px) {
27 | justify-content: flex-start;
28 | }
29 | }
30 | }
31 |
32 | .dialogRoot {
33 | border-radius: 4px;
34 | }
35 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 |
13 | # The JSON files contain newlines inconsistently
14 | [*.json]
15 | insert_final_newline = ignore
16 |
17 | # Minified JavaScript files shouldn't be changed
18 | [**.min.js]
19 | indent_style = ignore
20 | insert_final_newline = ignore
21 |
22 | # Makefiles always use tabs for indentation
23 | [Makefile]
24 | indent_style = tab
25 |
26 | # Batch files use tabs for indentation
27 | [*.bat]
28 | indent_style = tab
29 |
30 | [*.md]
31 | trim_trailing_whitespace = false
32 |
33 |
--------------------------------------------------------------------------------
/src/components/Banner/banner.scss:
--------------------------------------------------------------------------------
1 | $height: 151px;
2 | $overlap: 101px;
3 |
4 | .banner {
5 | display: flex;
6 | // flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | width: 100vw;
10 | height: $height;
11 | margin-bottom: -$overlap;
12 | padding-bottom: 38px;
13 | box-sizing: border-box;
14 | color: #fff;
15 | font-size: 30px;
16 | background-repeat: no-repeat;
17 | background-position-x: center;
18 | line-height: 2;
19 | font-size: 24px;
20 | background: linear-gradient(to left, #4f9bea, #4e86ee);
21 |
22 | @media (max-width: 800px) {
23 | height: 117px;
24 | margin-bottom: 0;
25 | padding-bottom: 0;
26 | font-size: 18px;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/BriefStatistics/briefStatistics.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 | @import '../../styles/text.scss';
3 | .briefStatistics {
4 | padding: 0 29px;
5 | padding-top: 0.875rem;
6 | & > div {
7 | display: flex;
8 | align-items: center;
9 | height: 22px;
10 | font-size: 1rem;
11 | margin-bottom: 0.875rem;
12 | color: $plainText;
13 | overflow: hidden;
14 | text-overflow: ellipsis;
15 | white-space: nowrap;
16 | &:first-child {
17 | margin-top: 0.875rem;
18 | }
19 | span {
20 | @extend %hash;
21 | color: $plainText;
22 | font-weight: bold;
23 | padding-left: 0.5rem; // font-size: inherit !important;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10
2 |
3 | # install nginx and so on...
4 | RUN apt-get update -qq && \
5 | apt-get install -y build-essential nodejs git autoconf locales locales-all curl vim openssl libssl-dev libyaml-dev libxslt-dev cmake htop libreadline6-dev nginx && \
6 | apt-get clean && \
7 | rm -rf /var/lib/apt/lists/*
8 |
9 | RUN locale-gen en_US.UTF-8
10 | ENV LANG en_US.UTF-8
11 | ENV LANGUAGE en_US.UTF-8
12 | ENV LC_ALL en_US.UTF-8
13 |
14 | WORKDIR /app
15 |
16 | COPY package*.json .
17 |
18 | COPY yarn.lock .
19 |
20 | COPY config ./config
21 |
22 | RUN npm install && yarn run dll
23 |
24 | COPY . .
25 |
26 | RUN yarn build
27 |
28 | RUN mv dist /etc/nginx/dist
29 |
30 | COPY nginx.conf.example /etc/nginx/conf.d/default.conf
31 |
32 | EXPOSE 80
33 |
34 | CMD [ "nginx", "-g", "daemon off;" ]
35 |
--------------------------------------------------------------------------------
/src/containers/ConfigPage/config.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 | @import '../../styles/layout.scss';
3 | .addServer {
4 | display: flex;
5 | & > div:first-child {
6 | width: 80vw;
7 | }
8 | }
9 |
10 | .panel {
11 | @include boxShadow;
12 | text-transform: capitalize;
13 | }
14 |
15 | .main {
16 | margin: 50px auto 0;
17 | }
18 |
19 | .panelTitle {
20 | text-transform: capitalize;
21 | }
22 |
23 | .switchChecked.switchColorSecondary {
24 | color: #fff !important;
25 | }
26 |
27 | .switchChecked.switchColorSecondary + .switchBar {
28 | background-color: $primary !important;
29 | }
30 |
31 | .switchBar {
32 | width: 38px !important;
33 | height: 22px !important;
34 | border-radius: 11px !important;
35 | margin-top: -11px !important;
36 | margin-left: -19px !important;
37 | }
38 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-07-22 19:56:35
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2018-07-22 19:57:36
6 | */
7 |
8 | import * as React from 'react'
9 | import { HashRouter as Router, } from 'react-router-dom'
10 | import { MuiThemeProvider, } from '@material-ui/core/styles'
11 |
12 | import Routes from './Routes'
13 | import theme from './config/theme'
14 |
15 | import { provideObservabls, startSubjectNewBlock, } from './contexts/observables'
16 | import { provideConfig, } from './contexts/config'
17 |
18 | const App = () => {
19 | startSubjectNewBlock()
20 | return (
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default provideConfig(provideObservabls(App))
30 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-07-22 19:59:17
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2018-11-12 20:58:23
6 | */
7 |
8 | import * as React from 'react'
9 | import { render, } from 'react-dom'
10 | import { I18nextProvider, } from 'react-i18next'
11 | import 'normalize.css'
12 |
13 | import i18n from './config/i18n'
14 | import App from './App'
15 |
16 | import './styles/common'
17 |
18 | const mount = (Comp: any) =>
19 | render(
20 |
21 |
22 | ,
23 | document.getElementById('root') as HTMLElement
24 | )
25 |
26 | mount(App)
27 |
28 | if (module.hot) {
29 | module.hot.accept('./App', () => {
30 | /* eslint-disable global-require */
31 | const NextApp = require('./App').default
32 | /* eslint-enable global-require */
33 | mount(NextApp)
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/config/webpack.config.dll.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const ManifestPlugin = require('webpack-manifest-plugin')
4 |
5 | module.exports = {
6 | entry: {
7 | react: ['react', 'react-dom', 'react-router-dom'],
8 | // styledComponents: ['styled-components'],
9 | },
10 | output: {
11 | path: path.resolve(__dirname, '../lib'),
12 | filename: '[name]_dll.js',
13 | library: '[name]_dll',
14 | },
15 | plugins: [
16 | new webpack.DllPlugin({
17 | context: __dirname,
18 | name: '[name]_dll',
19 | path: path.resolve(__dirname, '../lib/[name]_manifest.json'),
20 | }),
21 | new ManifestPlugin(),
22 | new webpack.DefinePlugin({
23 | 'process.env': {
24 | NODE_ENV: JSON.stringify('production'),
25 | // NODE_ENV: JSON.stringify('development'),
26 | },
27 | }),
28 | new webpack.optimize.UglifyJsPlugin(),
29 | ],
30 | }
31 |
--------------------------------------------------------------------------------
/src/config/localstorage.ts:
--------------------------------------------------------------------------------
1 | export enum LOCAL_STORAGE {
2 | SERVER_LIST = 'server_list',
3 | PRIV_KEY_LIST = 'privkey_list',
4 | PANEL_CONFIGS = 'panel_configs',
5 | LOCAL_DEBUG_ACCOUNTS = 'localDebugAccounts',
6 | }
7 | export interface PanelConfigs {
8 | logo: string;
9 | TPS: boolean;
10 | blockHeight: boolean;
11 | blockHash: boolean;
12 | blockAge: boolean;
13 | blockTransactions: boolean;
14 | blockQuotaUsed: boolean;
15 | blockPageSize: number;
16 | transactionHash: boolean;
17 | transactionFrom: boolean;
18 | transactionTo: boolean;
19 | transactionValue: boolean;
20 | transactionAge: boolean;
21 | transactionQuotaUsed: boolean;
22 | transactionBlockNumber: boolean;
23 | transactionPageSize: number;
24 | graphIPB: true;
25 | graphTPB: true;
26 | graphQuotaUsedBlock: true;
27 | graphQuotaUsedTx: true;
28 | graphProposals: true;
29 | graphMaxCount: number;
30 | }
31 |
32 | export default LOCAL_STORAGE
33 |
--------------------------------------------------------------------------------
/src/containers/Graphs/init.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IBlock,
3 | TransactionFromServer,
4 | ProposalFromServer,
5 | IContainerProps,
6 | BlockNumber,
7 | Timestamp,
8 | Hash,
9 | } from '../../typings/'
10 |
11 | const PRICE = 1
12 |
13 | const initState = {
14 | blocks: [] as IBlock[],
15 | transactions: [] as TransactionFromServer[],
16 | proposals: [] as ProposalFromServer[],
17 | loadBlockHistory: false,
18 | maxCount: 10,
19 | error: {
20 | code: '',
21 | message: '',
22 | },
23 | }
24 |
25 | const GraphsDefault = {
26 | initState,
27 | PRICE,
28 | }
29 |
30 | interface GraphsProps extends IContainerProps {}
31 | type GraphState = typeof initState
32 | type BlockGraphData = [BlockNumber, Timestamp, number, string]
33 | type TxGraphData = [Hash, number]
34 | type ProposalData = [string, number]
35 |
36 | export {
37 | GraphsDefault,
38 | IBlock,
39 | GraphsProps,
40 | GraphState,
41 | BlockGraphData,
42 | TxGraphData,
43 | ProposalData,
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/ErrorNotification/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slide, IconButton, Snackbar, } from '@material-ui/core'
3 | import { Close as CloseIcon, } from '@material-ui/icons'
4 |
5 | const texts = require('../../styles/text.scss')
6 |
7 | const SnackbarTransition = props =>
8 |
9 | export default ({ error, dismissError, }) => (
10 |
{error.message}}
12 | open={!!error.message}
13 | anchorOrigin={{ vertical: 'top', horizontal: 'right', }}
14 | // transition={SnackbarTransition}
15 | action={
16 |
17 |
23 | More
24 |
25 |
26 |
27 |
28 |
29 | }
30 | />
31 | )
32 |
--------------------------------------------------------------------------------
/src/components/Bundle/index.tsx:
--------------------------------------------------------------------------------
1 | // import React, { Component } from 'react'
2 | import * as React from 'react'
3 |
4 | interface Props {
5 | load: any;
6 | children: (Comp: any) => React.ReactNode;
7 | }
8 | interface State {
9 | mod: any;
10 | }
11 |
12 | class Bundle extends React.Component {
13 | state = {
14 | // short for "module" but that's a keyword in js, so "mod"
15 | mod: null,
16 | };
17 |
18 | componentWillMount () {
19 | this.load(this.props)
20 | }
21 |
22 | componentWillReceiveProps (nextProps) {
23 | if (nextProps.load !== this.props.load) {
24 | this.load(nextProps)
25 | }
26 | }
27 |
28 | load (props) {
29 | // this.setState({
30 | // mod: null,
31 | // })
32 | props.load(mod => {
33 | this.setState({
34 | // handle both es imports and cjs
35 | mod: mod.default ? mod.default : mod,
36 | })
37 | })
38 | }
39 |
40 | render () {
41 | return this.state.mod ? this.props.children(this.state.mod) : null
42 | }
43 | }
44 |
45 | export default Bundle
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Cryptape
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/styles/color.scss:
--------------------------------------------------------------------------------
1 | $primary-color: #415dfc;
2 | $light-grey: #637494;
3 | $addr: #3e56ff;
4 | $codeColor: #0d904f;
5 | $highlight: rgba(109, 165, 255, 1) !important;
6 | $text: #4a546c;
7 | $sidebar-text: #4a546c;
8 | $sidebar-bold-text: #384053;
9 | $hash: #4a546c;
10 | $active: #2647fd;
11 | // -------------------------------------------------------
12 | // from pattern
13 | $primary: #2647fd; // button, icon, input, color block
14 | $secondary: #f15a29; // secondary button, status
15 | $bg: #fdfdfd; // background color;
16 | $statusBg: #fff; // status bar background
17 | $divider: rgba(226, 233, 242, 0.6);
18 | $inputBorder: rgba(211, 221, 236, 0.6);
19 | $shaodow: rgba(0, 0, 0, 0.1);
20 | $primaryText: #2647fd; // highlight text
21 | $heavyText: #061027; // important text
22 | $tableText: #4a546c; // table text
23 | $plainText: #637494; // normal text // -----
24 | // form state
25 | $form-default: #d3ddec;
26 | $form-focus: #415dfc;
27 | $form-error: #fc4141;
28 | $form-warn: #f48817;
29 |
30 | // -----------------------------
31 |
32 | // gradient
33 | $gradient1: linear-gradient(to left, #4eadf1, #6a96f0);
34 | $gradient2: linear-gradient(to left, #e2e8ec, #d7d7d7);
35 |
--------------------------------------------------------------------------------
/src/styles/text.scss:
--------------------------------------------------------------------------------
1 | @import './color.scss';
2 |
3 | * {
4 | font-family: Helvetica, Roboto, Arial, Georgia, Consolas; // color: $plainText;
5 | }
6 |
7 | %hash {
8 | font-family: monospace, Rotobo, Helvetica !important;
9 | font-size: inherit !important;
10 | text-decoration: none;
11 | word-break: break-all;
12 | word-wrap: break-word;
13 | }
14 |
15 | .ellipsis {
16 | overflow: hidden;
17 | text-overflow: ellipsis;
18 | white-space: nowrap;
19 | }
20 |
21 | .addr {
22 | @extend %hash; // color: $primaryText;
23 | color: #5b8ee6;
24 | }
25 |
26 | .addrStart {
27 | max-width: 206px;
28 | overflow: hidden;
29 | white-space: nowrap;
30 | }
31 |
32 | .addrEnd::before {
33 | content: '...';
34 | }
35 |
36 | .hash {
37 | @extend %hash;
38 | color: $plainText;
39 | }
40 |
41 | .highlight {
42 | // @extend %hash;
43 | // color: $secondary;
44 | }
45 |
46 | .errMsgLink {
47 | color: $secondary;
48 | }
49 |
50 | @media (max-width: 370px) {
51 | html,
52 | body {
53 | font-size: 14px;
54 | }
55 | }
56 |
57 | .bannerText {
58 | font-size: 24px;
59 |
60 | @media (max-width: 800px) {
61 | font-size: 14px;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Routes/index.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-07-22 19:59:10
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2018-12-17 17:08:48
6 | */
7 |
8 | import * as React from 'react'
9 | import { Route, } from 'react-router-dom'
10 |
11 | import Bundle from '../components/Bundle'
12 | import containers from './containers'
13 |
14 | export const asyncRender = mod => routerProps => {
15 | if (!mod) return null
16 | /* eslint-disable */
17 | const Component = require(`bundle-loader?lazy!../containers/${mod}`);
18 | /* eslint-enable */
19 | return (
20 |
21 | {Comp => (Comp ? : Loading
)}
22 |
23 | )
24 | }
25 | /* eslint-enable import/no-dynamic-require */
26 | /* eslint-enable global-require */
27 |
28 | const Routes = () => (
29 |
30 | {containers.map(container => (
31 |
37 | ))}
38 |
39 | )
40 |
41 | export default Routes
42 |
--------------------------------------------------------------------------------
/src/utils/handleError.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-07-22 21:33:48
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2018-07-26 17:25:09
6 | */
7 |
8 | import { initError, } from './../initValues'
9 |
10 | const createHandleError = () => {
11 | const lastErrorMessageTable = {} as any
12 | let lastErrorMessage = ''
13 |
14 | return ctx => error => {
15 | if (!window.localStorage.getItem('chainIp')) return
16 | // only active when chain ip exsits
17 | if (
18 | error.message === lastErrorMessageTable[ctx.constructor.name] ||
19 | error.message === lastErrorMessage
20 | ) {
21 | ctx.setState(state => ({
22 | loading: state.loading - 1,
23 | }))
24 | return
25 | }
26 | // only active when the last error message is different
27 | lastErrorMessageTable[ctx.constructor.name] = error.message
28 | lastErrorMessage = error.message
29 | ctx.setState(state => ({
30 | loading: state.loading - 1,
31 | error,
32 | }))
33 | // }
34 | }
35 | }
36 |
37 | export const handleError = createHandleError()
38 |
39 | export const dismissError = ctx => () => {
40 | ctx.setState({ error: initError, })
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/HeaderNavs/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Link, } from 'react-router-dom'
3 | import { translate, } from 'react-i18next'
4 | import { Typography, } from '@material-ui/core'
5 |
6 | const styles = require('../../containers/Header/header.scss')
7 |
8 | const HeaderNavs = ({ containers, pathname, logo, t, }) => (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {containers.filter(container => container.nav).map(container => (
17 |
18 |
25 | {t(container.name)}
26 |
27 |
28 | ))}
29 |
30 |
31 | )
32 | export default translate('microscope')(HeaderNavs)
33 |
--------------------------------------------------------------------------------
/src/components/ContractInfoPanel/styles.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 |
3 | .infoblock {
4 | max-width: 650px;
5 | margin: 0 auto 57px;
6 |
7 | &:first-child {
8 | margin-top: 102px;
9 | }
10 |
11 | .header {
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: flex-end;
15 | margin-bottom: 10px;
16 | }
17 |
18 | .title {
19 | font-size: 0.875rem;
20 | font-weight: 500;
21 | text-align: center;
22 | color: #2e313e;
23 | }
24 |
25 | button {
26 | padding: 8px 20px;
27 | border: none;
28 | border-radius: 4px;
29 | background: $gradient1;
30 | color: #fff;
31 | font-size: 0.875rem;
32 | cursor: pointer;
33 | }
34 |
35 | button.copied {
36 | background: $gradient2;
37 | color: #aaa;
38 | }
39 |
40 | button[disabled] {
41 | cursor: default;
42 | }
43 |
44 | .code {
45 | font-size: 0.875rem;
46 | line-height: 1.71;
47 | color: #2e313e;
48 | border-radius: 4px;
49 | border: solid 1px #e7edf5;
50 | background-color: #fafbff;
51 | padding: 14px 77px 16px 18px;
52 | word-break: break-word;
53 | max-height: 280px;
54 | overflow: auto;
55 | word-break: break-all;
56 | white-space: pre-wrap;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/BriefStatistics/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Timestamp, } from '../../typings'
3 |
4 | const styles = require('./briefStatistics.scss')
5 |
6 | /* eslint-disable no-use-before-define */
7 | interface BriefStatistics {
8 | peerCount: number;
9 | number: string;
10 | timestamp: Timestamp;
11 | proposal: string;
12 | tps: number;
13 | tpb: number;
14 | ipb: number;
15 | }
16 | /* eslint-enable no-use-before-define */
17 |
18 | const BriefStatistics: React.SFC = ({
19 | peerCount,
20 | number,
21 | timestamp,
22 | proposal,
23 | tps,
24 | tpb,
25 | ipb,
26 | }) => (
27 |
28 |
29 | peerCount: {peerCount}
30 |
31 |
32 | blockNumber: {number}
33 |
34 |
35 | time: {new Date(timestamp).toLocaleString()}
36 |
37 |
38 | validators:{' '}
39 |
40 | {proposal.slice(0, 20)}
41 | ...
42 |
43 |
44 |
45 | TPS: {tps}
46 |
47 |
48 | TPB: {tpb}
49 |
50 |
51 | IPB: {ipb}
52 |
53 |
54 | )
55 | export default BriefStatistics
56 |
--------------------------------------------------------------------------------
/src/config/graph.ts:
--------------------------------------------------------------------------------
1 | export const BarOption = {
2 | legend: {
3 | show: false,
4 | bottom: '0',
5 | },
6 | tooltip: {},
7 | dataset: {
8 | source: [],
9 | },
10 | xAxis: { type: 'category', },
11 | yAxis: {},
12 | series: [{ type: 'bar', }, ],
13 | }
14 | export const LineOption = {
15 | legend: {
16 | show: false,
17 | bottom: '0',
18 | },
19 | tooltip: {},
20 | dataset: {
21 | source: [],
22 | },
23 | xAxis: { type: 'category', },
24 | yAxis: {},
25 | series: [{ type: 'line', }, ],
26 | backgroundColor: {
27 | type: 'linear',
28 | x: 0,
29 | y: 0,
30 | x2: 0,
31 | y2: 1,
32 | colorStops: [
33 | {
34 | offset: 0,
35 | color: 'red',
36 | },
37 | {
38 | offset: 1,
39 | color: 'blue',
40 | },
41 | ],
42 | globalCoord: false,
43 | },
44 | }
45 | export const PieOption = {
46 | legend: { show: false, bottom: '0', },
47 | tooltip: {},
48 | dataset: { source: [], },
49 | xAxis: { show: false, },
50 | yAxis: { show: false, },
51 | series: [
52 | {
53 | type: 'pie',
54 | radius: ['50%', '70%', ],
55 | emphasis: {
56 | itemStyle: {
57 | shadowColor: 'rgba(0,0,0,0.5)',
58 | shadowBlur: 20,
59 | },
60 | },
61 | },
62 | ],
63 | }
64 |
65 | export default {
66 | BarOption,
67 | PieOption,
68 | LineOption,
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/ERCPanel/styles.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 |
3 | .method {
4 | max-width: 650px;
5 | margin: 0 auto 57px;
6 |
7 | &:first-child {
8 | margin-top: 102px;
9 | }
10 |
11 | input {
12 | border: 1px solid #d3ddec;
13 | width: 120px;
14 | height: 34px;
15 | line-height: 34px;
16 | border-radius: 4px;
17 | padding: 7px;
18 | margin: 8px;
19 | box-sizing: border-box;
20 | }
21 |
22 | button {
23 | margin: 8px;
24 | margin-left: 73px;
25 | width: 80px;
26 | line-height: 34px;
27 | border: none;
28 | border-radius: 4px;
29 | background: $gradient1;
30 | color: #fff;
31 | font-size: 0.875rem;
32 | }
33 | }
34 |
35 | .title {
36 | line-height: 34px;
37 | margin-top: 8px;
38 | text-transform: capitalize;
39 |
40 | &:after {
41 | content: ' Method';
42 | }
43 | }
44 |
45 | .inputs,
46 | .outputs {
47 | position: relative;
48 | line-height: 34px;
49 | min-height: 34px;
50 | margin-left: 65px;
51 |
52 | &:before {
53 | position: absolute;
54 | top: 8px;
55 | right: 100%;
56 | display: inline-block;
57 | width: 65px;
58 | }
59 | }
60 |
61 | .inputs {
62 | &:before {
63 | content: 'Inputs: ';
64 | }
65 | }
66 |
67 | .outputs {
68 | &:before {
69 | content: 'Outputs: ';
70 | }
71 | }
72 |
73 | .noEls {
74 | display: inline-block;
75 | margin: 8px;
76 | color: #ccc;
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/StaticCard/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Card, CardContent, } from '@material-ui/core'
3 | import { TrendingFlat as ArrowIcon, } from '@material-ui/icons'
4 |
5 | const styles = require('./staticCard.scss')
6 |
7 | interface StaticCardProps {
8 | index?: number;
9 | icon: string;
10 | title: string;
11 | page?: string;
12 | className?: string;
13 | children?: React.ReactNode;
14 | }
15 |
16 | export default (props: StaticCardProps) => (
17 |
18 |
19 |
20 | {props.title}
21 | {props.page ? (
22 |
23 | More
24 |
25 |
26 | ) : null}
27 |
28 |
33 | {props.children}
34 |
35 |
36 | )
37 |
38 | interface StaticCardProps {
39 | title: string;
40 | page?: string;
41 | }
42 |
43 | export const StaticCardTitle = props => (
44 |
45 | {props.title}
46 | {props.page ? (
47 |
48 | {'More >'}
49 |
50 | ) : null}
51 |
52 | )
53 |
--------------------------------------------------------------------------------
/src/containers/Account/styles.scss:
--------------------------------------------------------------------------------
1 | .subheader {
2 | word-wrap: break-word;
3 | word-break: break-all;
4 | }
5 |
6 | .accountHeader {
7 | font-size: 1.25rem;
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: space-between;
11 | height: 60px;
12 | }
13 |
14 | .acountIcon {
15 | width: 60px;
16 | height: 60px;
17 | border-radius: 4px;
18 | }
19 |
20 | .basicInfo {
21 | height: 5.75rem;
22 | display: flex;
23 | margin-bottom: 50px;
24 |
25 | & > div {
26 | display: flex;
27 | align-items: center;
28 | justify-content: flex-start;
29 | font-size: 1rem;
30 | font-weight: 100;
31 | background-color: #fff;
32 | border-radius: 6px;
33 | box-shadow: 0 4px 16px 0 rgba(86, 129, 204, 0.1);
34 | flex: 1;
35 |
36 | span {
37 | margin-left: 10px;
38 | color: #6f747a;
39 | }
40 |
41 | &:first-child,
42 | &:last-child {
43 | &:before {
44 | content: '';
45 | display: block;
46 | width: 8px;
47 | height: 8px;
48 | margin-left: 45px;
49 | margin-right: 7px;
50 | border-radius: 50%;
51 | }
52 | }
53 |
54 | &:first-child {
55 | margin-right: 20px;
56 |
57 | &:before {
58 | background-color: #5b8ee6;
59 | }
60 | }
61 |
62 | &:last-child {
63 | margin-left: 20px;
64 |
65 | &:before {
66 | background-color: #3dd895;
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/utils/localstorage.ts:
--------------------------------------------------------------------------------
1 | export const loadedLocalStorage = (key: string) => {
2 | const localChainData = window.localStorage.getItem(key)
3 | if (localChainData === null) return null
4 | const obj = JSON.parse(localChainData)
5 | return obj
6 | }
7 |
8 | export const saveLocalStorage = (key: string, value: any) => {
9 | const json = JSON.stringify(value)
10 | window.localStorage.setItem(key, json)
11 | }
12 |
13 | export const addChainHistory = info => {
14 | const key = 'chainHistory'
15 | let history = loadedLocalStorage(key)
16 | if (!Array.isArray(history)) {
17 | history = [info, ]
18 | saveLocalStorage(key, history)
19 | } else if (!history.map(obj => obj.serverIp).includes(info.serverIp)) {
20 | history.unshift(info)
21 | saveLocalStorage(key, history.slice(0, 2))
22 | }
23 | }
24 |
25 | export const formatedIpInfo = (ip, chainName) => {
26 | let protocol, host
27 | /* eslint-disable prefer-destructuring */
28 | if (ip.startsWith('http')) {
29 | const l = ip.split('//')
30 | protocol = l[0]
31 | host = l[1].split('/')[0]
32 | } else {
33 | protocol = 'http:'
34 | host = ip.split('/')[0]
35 | }
36 | /* eslint-enable prefer-destructuring */
37 | const origin = `${protocol}//${host}`
38 | const info = { serverName: chainName, serverIp: origin, }
39 | return info
40 | }
41 |
42 | export const saveChainHistoryLocal = (ip, chainName) => {
43 | const info = formatedIpInfo(ip, chainName)
44 | addChainHistory(info)
45 | }
46 |
--------------------------------------------------------------------------------
/docs/zh-CN/microscope.md:
--------------------------------------------------------------------------------
1 | # Microscope 简介
2 |
3 | [Microscope](https://github.com/cryptape/Microscope) 是一款功能完备的区块链数据访问平台。
4 |
5 | 通过 Microscope 您可以访问指定 AppChain 上的区块, 交易, 账号(含合约) 数据, 对合约执行 Call 调用, 以及查看 AppChain 实时性能.
6 |
7 | 登录 Microscope, 默认访问我们的体验 AppChain, 如果要切换至您关注的 AppChain, 只需点击 Microscope 右上角状态区的链名(默认为 test-chain), 在呼出的侧边栏中输入指定 AppChain 的地址及端口即可.
8 |
9 | AppChain 的每一个运营者都可以部署一个自己专用的 Microscope 用于更好的展示数据.
10 |
11 | ---
12 |
13 | # AppChain 链切换
14 |
15 | Microscope 支持多条 AppChain 之间的切换, 用户只需要点击右上角 `链名称`, 在呼出的侧边栏中输入指定 AppChain 的地址及端口号即可完成 Microscope 数据源的切换.
16 |
17 | ---
18 |
19 | # 索引及统计功能
20 |
21 | 要实现 AppChain 上索引及统计功能, 需要加入相应链的缓存服务 AgeraONE[https://github.com/Keith-CY/agera_one].
22 |
23 | 基于 AgeraONE, Microscope 可以实现区块 , 交易的索引及 AppChain 性能的评估.
24 |
25 | ---
26 |
27 | # Microscope 二次开发方法:
28 |
29 | - 进入工程目录后执行 `git clone https://github.com/cryptape/Microscope` 获取项目
30 |
31 | - 编辑 `config/webpack.config.base.js` 中 `http://121.196.*.*:1337` 为缓存服务器的地址和端口
32 |
33 | - 执行 `yarn add` 安装依赖
34 |
35 | - 执行 `yarn run dll` 打包动态依赖
36 |
37 | - 执行 `yarn start` 进入开发模式
38 |
39 | - 执行 `yarn run build` 将项目打包到 `dist` 目录下, 用 `nginx` 部署到服务器即可
40 |
41 | ---
42 |
43 | # AgeraONE 二次开发方法:
44 |
45 | - 进入工程目录后执行 `git clone https://github.com/Keith-CY/agera_one` 获取项目
46 |
47 | - 在工程目录下执行 `cp config/dev.secret.exs.example config/dev.secret.exs` 创建秘钥文件
48 |
49 | - 修改 `dev.secret.exs` 中的 `localhost:1337` 为缓存服务器要缓存的链的地址和端口
50 |
51 | - 执行 `mix ecto:migrate` 配置数据库
52 |
53 | - 在工程目录下执行 `mix phx.server` 进入开发模式
54 |
55 | - 在工程目录下执行 `elixir —detached -S mix phx.server` 进入生产模式
56 |
--------------------------------------------------------------------------------
/src/utils/paramsFilter.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-07-22 21:35:08
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2018-07-22 21:35:08
6 | */
7 |
8 | export default params => {
9 | const p = {}
10 | Object.keys(params).forEach(key => {
11 | if (params[key] !== '' && typeof params[key] !== 'undefined') {
12 | p[key] = params[key]
13 | }
14 | })
15 | return p
16 | }
17 |
18 | /*
19 | * @Author: wyy
20 | * @Date: 2019-04-28 20:07:00
21 | * @Last Modified by: wyy
22 | * @Last Modified time: 2019-04-28 20:07:00
23 | */
24 |
25 | // findIndex might not support by some browser
26 | const findIndex = (array:Array, element:string): number => {
27 | let resultIndex = -1 // not found
28 | if (array && array.length > 0 && element) {
29 | for (let i = 0; i < array.length; i++) {
30 | if (element === array[i]) {
31 | resultIndex = i
32 | break
33 | }
34 | }
35 | }
36 | return resultIndex
37 | }
38 |
39 | const oldParams = ['blockFrom', 'blockTo', 'transactionFrom', 'transactionTo', ]
40 | const newParams = ['block_from', 'block_to', 'min_transaction_count', 'max_transaction_count', ]
41 | export const paramsBlocksV2Adapter = params => {
42 | const p = {}
43 | Object.keys(params).forEach(key => {
44 | if (params[key] !== '' && typeof params[key] !== 'undefined') {
45 | const index = findIndex(oldParams, key)
46 | if (index > -1) {
47 | p[newParams[index]] = params[key]
48 | } else {
49 | p[key] = params[key]
50 | }
51 | }
52 | })
53 | return p
54 | }
55 |
--------------------------------------------------------------------------------
/config/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | require('dotenv').config()
5 |
6 | module.exports = {
7 | output: {
8 | path: path.resolve(__dirname, '../dist'),
9 | filename: 'scripts/[name]-[hash:5].js',
10 | chunkFilename: 'scripts/[name]-[hash:5].js',
11 | },
12 | module: {
13 | rules: [{
14 | test: /\.tsx?/,
15 | use: ['react-hot-loader/webpack', {
16 | loader: 'ts-loader',
17 | options: {
18 | configFile: path.resolve(__dirname, './tsconfig.json'),
19 | },
20 | }, ],
21 | include: /src/,
22 | },
23 | {
24 | test: /\.(png|jpg|svg)$/,
25 | loader: 'url-loader',
26 | options: {
27 | limit: 8192,
28 | name: 'images/[name]-[hash].[ext]',
29 | },
30 | include: /src/,
31 | },
32 | {
33 | test: /\.(otf|woff|woff2)$/,
34 | loader: 'file-loader',
35 | options: {
36 | mimetype: 'application/font-woff',
37 | name: 'fonts/[name].[ext]',
38 | },
39 | },
40 | ],
41 | },
42 | plugins: [
43 | new webpack.DefinePlugin({
44 | 'process.env': {
45 | CHAIN_SERVERS: JSON.stringify(process.env.CHAIN_SERVERS),
46 | APP_NAME: JSON.stringify(process.env.APP_NAME),
47 | LNGS: JSON.stringify(process.env.LNGS),
48 | DEBUG_ACCOUNTS: JSON.stringify(process.env.DEBUG_ACCOUNTS),
49 | },
50 | }),
51 | ],
52 | resolve: {
53 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.scss', '.svg', '.png', '.jpg'],
54 | },
55 | }
56 |
--------------------------------------------------------------------------------
/src/images/index.ts:
--------------------------------------------------------------------------------
1 | const Image = {
2 | logo: `${process.env.PUBLIC}/images/microscope_logo_gradient.png`,
3 | extend: `${process.env.PUBLIC}/microscopeIcons/expand.png`,
4 | quota: `${process.env.PUBLIC}/microscopeIcons/petrol_barrel.svg`,
5 | banner: {
6 | block: `${process.env.PUBLIC}/banner/banner-Block.png`,
7 | transaction: `${process.env.PUBLIC}/banner/banner-Transaction.png`,
8 | statistics: `${process.env.PUBLIC}/banner/banner-Statistics.png`,
9 | },
10 | icon: {
11 | block: `${process.env.PUBLIC}/microscopeIcons/mobile_navs/block.svg`,
12 | transaction: `${
13 | process.env.PUBLIC
14 | }/microscopeIcons/mobile_navs/transaction.svg`,
15 | statistics: `${
16 | process.env.PUBLIC
17 | }/microscopeIcons/mobile_navs/statistics.svg`,
18 | config: `${process.env.PUBLIC}/microscopeIcons/mobile_navs/config.svg`,
19 | },
20 | iconActive: {
21 | block: `${process.env.PUBLIC}/microscopeIcons/mobile_navs/block_active.svg`,
22 | transaction: `${
23 | process.env.PUBLIC
24 | }/microscopeIcons/mobile_navs/transaction_active.svg`,
25 | statistics: `${
26 | process.env.PUBLIC
27 | }/microscopeIcons/mobile_navs/statistics_active.svg`,
28 | config: `${
29 | process.env.PUBLIC
30 | }/microscopeIcons/mobile_navs/config_active.svg`,
31 | },
32 | type: {
33 | exchange: 'https://cdn.cryptape.com/microscope/img/icon/typeTransfer.svg',
34 | contractCall: 'https://cdn.cryptape.com/microscope/img/icon/typeCall.svg',
35 | contractCreation:
36 | 'https://cdn.cryptape.com/microscope/img/icon/typeCreate.svg',
37 | },
38 | }
39 |
40 | export default Image
41 |
--------------------------------------------------------------------------------
/src/components/SettingPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { List, ListItem, ListItemText, } from '@material-ui/core'
3 | import { Metadata, } from '../../typings'
4 |
5 | const texts = require('../../styles/text')
6 |
7 | export default ({ metadata, }: { metadata: Metadata }) => (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 | Website:{' '}
23 |
29 | {metadata.website}
30 |
31 |
32 | }
33 | />
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 | (
49 |
50 | {val}
51 |
52 | ))}
53 | />
54 |
55 |
56 | )
57 |
--------------------------------------------------------------------------------
/src/components/StaticCard/staticCard.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 | .title {
3 | display: flex;
4 | justify-content: flex-start;
5 | align-items: center;
6 | padding: 25px 20px 10px;
7 | margin: 0;
8 | font-size: 1.125rem;
9 | color: $heavyText;
10 | border-bottom: 1px solid $divider; // span {
11 | span {
12 | display: flex;
13 | align-items: center;
14 | flex: 1;
15 | }
16 | svg,
17 | img {
18 | font-size: 1.5rem;
19 | margin-right: 10px;
20 | }
21 | img {
22 | height: 24px;
23 | }
24 | @media (max-width: 800px) {
25 | position: relative;
26 | line-height: 2;
27 | margin: 0;
28 | padding: 0 16px;
29 | background: #f7f8fa;
30 | border-bottom: none;
31 | &:before,
32 | &:after {
33 | content: '';
34 | position: absolute;
35 | display: block;
36 | top: 0;
37 | bottom: 0;
38 | width: 32px;
39 | background: #f7f8fa;
40 | }
41 | &:before {
42 | right: 100%;
43 | }
44 | &:after {
45 | left: 100%;
46 | }
47 | }
48 | }
49 |
50 | .onlytitle {
51 | @extend .title;
52 | border-bottom: none;
53 | font-weight: normal;
54 | padding: 0 0 10px 0;
55 | @media (max-width: 800px) {
56 | background: none;
57 | &:before,
58 | &:after {
59 | content: none;
60 | background: none;
61 | }
62 | }
63 | }
64 |
65 | .more {
66 | font-size: 0.875rem;
67 | color: $plainText;
68 | display: flex;
69 | justify-content: center;
70 | align-items: center;
71 | svg {
72 | margin-left: 5px;
73 | margin-right: 0px;
74 | }
75 | }
76 |
77 | .titleRoot {
78 | display: flex !important;
79 | justify-content: space-between;
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/ContractInfoPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withObservables, } from '../../contexts/observables'
3 | import { copyToClipboard, } from '../../utils/copyToClipboard'
4 |
5 | const styles = require('./styles.scss')
6 |
7 | const Infoblock = ({ title, code, copied, copy, }) => (
8 |
9 |
10 | {title}
11 |
18 |
19 |
{code}
20 |
21 | )
22 |
23 | interface PanelProps {
24 | abi: string
25 | code: string
26 | copiedIdx: number
27 | updateCopiedIdx: (idx: number) => void
28 | }
29 |
30 | const Panel = (props: PanelProps) => {
31 | const abi = JSON.stringify(props.abi, null, 2)
32 | const { code, } = props
33 | return (
34 |
35 | {
40 | if (props.copiedIdx === 0) return
41 | copyToClipboard(abi)
42 | props.updateCopiedIdx(0)
43 | }}
44 | />
45 | {
50 | if (props.copiedIdx === 1) return
51 | copyToClipboard(code)
52 | props.updateCopiedIdx(1)
53 | }}
54 | />
55 |
56 | )
57 | }
58 | export default withObservables(Panel)
59 |
--------------------------------------------------------------------------------
/src/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | import CITAObservables from '@appchain/observables'
2 | import { Config, } from '../contexts/config'
3 | import i18n from '../config/i18n'
4 | import './block'
5 |
6 | export * from './block'
7 |
8 | export interface NewCITAObservables extends CITAObservables {
9 | newBlockSubjectStart?: any;
10 | newBlockSubjectAdd?: any;
11 | }
12 | /* eslint-disable no-restricted-globals */
13 | export interface IContainerProps {
14 | config: Config;
15 | CITAObservables: NewCITAObservables;
16 | history: any;
17 | match: {
18 | path: string;
19 | params: {
20 | height?: string;
21 | blockHash?: string;
22 | transaction?: string;
23 | account?: string;
24 | };
25 | };
26 | location: {
27 | hash: string;
28 | pathname: string;
29 | search: string;
30 | };
31 |
32 | i18n: typeof i18n;
33 | t: (key: string) => string;
34 | // i18n: {
35 |
36 | // }
37 | }
38 |
39 | export interface ABIElement {
40 | constant: boolean;
41 | inputs: { name: string; type: string; value?: string }[];
42 | name: string;
43 | outputs: { name: string; type: string; value?: string }[];
44 | payable: boolean;
45 | stateMutability: string;
46 | type: string;
47 | }
48 | /* eslint-enable no-restricted-globals */
49 |
50 | export type ABI = ABIElement[]
51 |
52 | export interface IContainerState {}
53 | export interface UnsignedTransaction {
54 | crypto: number;
55 | signature: string;
56 | sender: {
57 | address: string;
58 | publicKey: string;
59 | };
60 | transaction: {
61 | data: string;
62 | nonce: string;
63 | quota: number;
64 | to: string;
65 | validUntilBlock: number;
66 | value: number;
67 | version: number;
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/src/utils/timeFormatter.ts:
--------------------------------------------------------------------------------
1 | const second = 1
2 | const minute = 60 * second
3 | const hour = 60 * minute
4 | const day = 24 * hour
5 |
6 | const tGen = (n, unit) => {
7 | if (!n) {
8 | return ' '
9 | }
10 |
11 | if (n === 1) {
12 | return ` 1 ${unit}`
13 | }
14 | return ` ${n} ${unit}s`
15 | }
16 |
17 | export const fromNow = time => {
18 | let r = (new Date().getTime() - time) / 1000
19 | if (r < 0) {
20 | return 'Time Error'
21 | }
22 | const d = Math.floor(r / day)
23 | const dText = tGen(d, 'day')
24 | r -= d * day
25 | const h = Math.floor(r / hour)
26 | const hText = tGen(h, 'hr')
27 | r -= h * hour
28 | const m = Math.floor(r / minute)
29 | const mText = tGen(m, 'min')
30 | r -= m * minute
31 | const s = Math.floor(r / second)
32 | const sText = tGen(s, 'sec')
33 | const t = dText + hText + mText + sText
34 | return t.trim().length ? t : '0 sec'
35 | }
36 |
37 | export const parsedNow = time => {
38 | let r = (new Date().getTime() - time) / 1000
39 | if (r < 0) {
40 | return 'Time Error'
41 | }
42 | const d = Math.floor(r / day)
43 | r -= d * day
44 | const h = Math.floor(r / hour)
45 | r -= h * hour
46 | const m = Math.floor(r / minute)
47 | r -= m * minute
48 | const s = Math.floor(r / second)
49 | return {
50 | day: d,
51 | hour: h,
52 | minute: m,
53 | second: s,
54 | }
55 | }
56 |
57 | export const formatedAgeString = time => {
58 | const t = fromNow(time)
59 | if (t === 'Time Error') {
60 | return t
61 | }
62 | return `${t} ago`
63 | }
64 |
65 | export const timeFormatter = (time, withDate = false): string => {
66 | const _time = new Date(+time)
67 | return _time[withDate ? 'toLocaleString' : 'toLocaleTimeString']('zh', {
68 | hour12: false,
69 | })
70 | }
71 |
72 | export default {
73 | fromNow,
74 | timeFormatter,
75 | }
76 |
--------------------------------------------------------------------------------
/docs/script/common-setting.js:
--------------------------------------------------------------------------------
1 | // this file is NOT for customization
2 | // these are the default settings for all the Nervos Documents
3 | // you can overwrite settings in this page by set them again in customization.js
4 |
5 | // add language variable to session storage, if it's not existed
6 | if (sessionStorage.getItem("language")) {
7 | var language = sessionStorage.getItem("language");
8 | } else {
9 | sessionStorage.setItem("language", default_language);
10 | var language = default_language;
11 | }
12 |
13 | // add version variable to session storage, if it's not existed
14 | if (sessionStorage.getItem("version")) {
15 | var version = sessionStorage.getItem("version");
16 | } else {
17 | sessionStorage.setItem("version", "latest");
18 | var version = "latest";
19 | }
20 |
21 | var common = {
22 |
23 | loadSidebar: true,
24 | autoHeader: true,
25 | subMaxLevel: 2,
26 | basePath: versionIsSupported ? `./${language}/${version}/` : `./${language}/`,
27 |
28 |
29 | // configuration for searching plugin
30 | search: {
31 | maxAge: 86400000, // expiration time in milliseconds, one day by default
32 |
33 | // depth of the maximum searching title levels
34 | depth: 6,
35 | },
36 |
37 | plugins: [
38 | function (hook, vm) {
39 | hook.afterEach(function (html, next) {
40 | if (versionIsSupported) {
41 | var url = github_url + language + '/' + version + '/' + vm.route.file
42 | } else {
43 | var url = github_url + language + '/' + vm.route.file
44 | }
45 |
46 | var editHtml = `
If you find any mistakes on this page, feel free to edit this document on GitHub`
47 |
48 | next(html + editHtml)
49 | })
50 | }
51 | ]
52 |
53 |
54 | }
--------------------------------------------------------------------------------
/src/utils/check.ts:
--------------------------------------------------------------------------------
1 | import * as web3utils from 'web3-utils'
2 | import transitions from '@material-ui/core/styles/transitions'
3 |
4 | const startsWith0x = string => /^0x/i.test(string)
5 |
6 | /* eslint-disable */
7 | const checkDigits = number =>
8 | typeof number === 'number' &&
9 | isFinite(number) &&
10 | Math.floor(number) === number
11 | /* eslint-enable */
12 |
13 | const checkDigitsString = number => {
14 | let value = number
15 | if (typeof value === 'string') {
16 | value = Number(value)
17 | }
18 | return checkDigits(value)
19 | }
20 |
21 | const format0x = string => {
22 | let value = string
23 | if (!value) {
24 | return value
25 | }
26 | if (!startsWith0x(value)) {
27 | if (checkDigitsString(value)) {
28 | value = `0x${Number(value).toString(16)}`
29 | } else {
30 | value = `0x${value}`
31 | }
32 | }
33 | return value
34 | }
35 |
36 | const checkDigitsStringDec = number => {
37 | const value = number
38 | if (startsWith0x(value)) {
39 | return false
40 | }
41 | return checkDigitsString(value)
42 | }
43 |
44 | const checkAddress = address => web3utils.isAddress(format0x(address))
45 |
46 | const checkHeight = height => checkDigitsString(format0x(height))
47 |
48 | const checkTransaction = transitionHash => {
49 | const value = format0x(transitionHash).toString()
50 | return value.length === 66 && checkDigitsString(value)
51 | }
52 |
53 | const check = {
54 | address: checkAddress,
55 | height: checkHeight,
56 | digits: checkDigitsString,
57 | digitsDec: checkDigitsStringDec,
58 | transaction: checkTransaction,
59 | format0x,
60 | }
61 |
62 | const errorMessages = {
63 | address: 'Please enter Address here',
64 | // height: 'Please enter only Address',
65 | digits: 'Please enter only digits',
66 | // transaction: 'Please enter only transaction'
67 | }
68 |
69 | export { errorMessages, format0x, }
70 | export default check
71 |
--------------------------------------------------------------------------------
/src/containers/Dialog/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { createPortal, } from 'react-dom'
3 | import {
4 | Dialog,
5 | DialogTitle,
6 | AppBar,
7 | Toolbar,
8 | IconButton,
9 | Typography,
10 | Slide,
11 | } from '@material-ui/core'
12 | import { Close as CloseIcon, } from '@material-ui/icons'
13 |
14 | const styles = require('./dialog.scss')
15 |
16 | function Transition (props) {
17 | return
18 | }
19 |
20 | interface DialogCompProps {
21 | onClose: (e) => void;
22 | on: boolean;
23 | fullScreen?: boolean;
24 | dialogTitle: string;
25 | maxWidth?: 'xs' | 'sm' | 'md';
26 | children?: React.ReactNode;
27 | }
28 | const DialogComp = (props: DialogCompProps) => (
29 |
64 | )
65 | export default class extends React.Component {
66 | render () {
67 | return createPortal(, document.getElementById(
68 | 'dialog'
69 | ) as HTMLElement)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/utils/accessLocalstorage.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-07-22 21:05:01
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2018-10-15 21:17:12
6 | */
7 |
8 | import {
9 | initPanelConfigs,
10 | initServerList,
11 | initPrivateKeyList,
12 | } from '../initValues'
13 | import LOCAL_STORAGE, { PanelConfigs, } from '../config/localstorage'
14 |
15 | export const getServerList = () => {
16 | const storedList = window.localStorage.getItem(LOCAL_STORAGE.SERVER_LIST)
17 | if (storedList) {
18 | try {
19 | const servers = JSON.parse(storedList)
20 | return servers.length ? servers : initServerList
21 | } catch (err) {
22 | console.error(err)
23 | return initServerList
24 | }
25 | }
26 | return initServerList
27 | }
28 |
29 | export const getPrivkeyList = () => {
30 | const storedList = window.localStorage.getItem(LOCAL_STORAGE.PRIV_KEY_LIST)
31 | if (storedList) {
32 | try {
33 | return JSON.parse(storedList)
34 | } catch (err) {
35 | console.error(err)
36 | return initPrivateKeyList
37 | }
38 | }
39 | return initPrivateKeyList
40 | }
41 | export const getPanelConfigs = (
42 | defaultConfig: PanelConfigs = initPanelConfigs
43 | ) => {
44 | const localConfigs = window.localStorage.getItem(LOCAL_STORAGE.PANEL_CONFIGS)
45 | if (localConfigs) {
46 | try {
47 | return JSON.parse(localConfigs)
48 | } catch (err) {
49 | console.error(err)
50 | return defaultConfig
51 | }
52 | }
53 | return defaultConfig
54 | }
55 |
56 | export const setLocalDebugAccounts = (accounts: string[] = []) => {
57 | if (Array.isArray(accounts)) {
58 | window.localStorage.setItem(
59 | LOCAL_STORAGE.LOCAL_DEBUG_ACCOUNTS,
60 | accounts.join(',')
61 | )
62 | }
63 | }
64 |
65 | export const getLocalDebugAccounts = () => {
66 | const accounts = window.localStorage.getItem(
67 | LOCAL_STORAGE.LOCAL_DEBUG_ACCOUNTS
68 | )
69 | if (accounts) {
70 | return Array.from(new Set(accounts.split(',')))
71 | }
72 | return []
73 | }
74 |
--------------------------------------------------------------------------------
/src/containers/Block/block.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/common.scss';
2 |
3 | .subheader {
4 | word-wrap: break-word;
5 | word-break: break-all;
6 | }
7 |
8 | .blockHeader {
9 | font-size: 1.2rem;
10 | }
11 |
12 | .icon {
13 | display: block;
14 | width: 60px;
15 | height: 60px;
16 | margin: 0 auto;
17 | margin-top: 38px;
18 | margin-bottom: 9px;
19 | }
20 |
21 | .hash {
22 | text-align: center;
23 | line-height: 33px;
24 | font-size: 1.5rem;
25 | font-weight: bolder;
26 | word-break: break-all;
27 | }
28 |
29 | .height {
30 | a {
31 | margin: 0 12px;
32 | color: #fff;
33 |
34 | &:last-child {
35 | margin-right: 0;
36 | }
37 |
38 | &:first-child {
39 | margin-left: 0;
40 | }
41 | }
42 | }
43 |
44 | .items {
45 | width: 850px;
46 | color: #4a546c;
47 |
48 | li {
49 | padding: 30px 0 10px;
50 | }
51 |
52 | .itemTitle {
53 | display: flex;
54 | width: 200px;
55 | justify-content: flex-start;
56 | align-items: center;
57 | color: $plainText;
58 |
59 | &:after {
60 | content: ':';
61 | }
62 | }
63 | }
64 |
65 | @media (max-width: 800px) {
66 | .items {
67 | width: 100%;
68 | word-break: break-all;
69 | }
70 | }
71 |
72 | .card {
73 | @include boxShadow;
74 | display: flex;
75 | font-size: 16px;
76 | margin: 0;
77 | padding: 35px 62px;
78 | margin-bottom: 40px;
79 | background: #fff;
80 | border-radius: 4px;
81 |
82 | .numberTitle {
83 | display: inline-block;
84 | font-size: 16px;
85 | min-width: 200px;
86 | }
87 | }
88 |
89 | .blockNavs {
90 | display: flex;
91 |
92 | a {
93 | display: flex;
94 | align-items: center;
95 | justify-content: center;
96 | width: 20px;
97 | height: 20px;
98 | background: $gradient1;
99 | border-radius: 50%;
100 | margin-right: 20px;
101 |
102 | svg {
103 | width: 20px;
104 | height: 20px;
105 | transform-origin: center center;
106 | transform: scale(0.8);
107 | }
108 |
109 | &:last-child {
110 | transform: rotate(180deg);
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Routes/containers.ts:
--------------------------------------------------------------------------------
1 | import Image from '../images'
2 |
3 | export default [
4 | {
5 | path: '/',
6 | name: 'Header',
7 | component: 'Header',
8 | nav: false,
9 | },
10 | {
11 | path: '/',
12 | name: 'Homepage',
13 | component: 'Homepage',
14 | exact: true,
15 | nav: false,
16 | },
17 | {
18 | path: '/block/:blockHash',
19 | name: 'BlockByHash',
20 | component: 'Block',
21 | exact: true,
22 | nav: false,
23 | },
24 | {
25 | path: '/height/:height',
26 | name: 'BlockByHeight',
27 | component: 'Block',
28 | exact: true,
29 | nav: false,
30 | },
31 | {
32 | path: '/blocks',
33 | name: 'Blocks',
34 | component: 'BlockTable',
35 | exact: true,
36 | nav: true,
37 | icon: Image.icon.block,
38 | iconActive: Image.iconActive.block,
39 | },
40 | {
41 | path: '/transactions',
42 | name: 'Transactions',
43 | component: 'TransactionTable',
44 | exact: true,
45 | nav: true,
46 | icon: Image.icon.transaction,
47 | iconActive: Image.iconActive.transaction,
48 | },
49 | {
50 | path: '/transaction/:transaction',
51 | name: 'Transaction',
52 | component: 'Transaction',
53 | exact: true,
54 | nav: false,
55 | },
56 | {
57 | path: '/account/:account',
58 | name: 'Account',
59 | component: 'Account',
60 | exact: true,
61 | nav: false,
62 | },
63 | {
64 | path: '/graphs',
65 | name: 'Statistics',
66 | component: 'Graphs',
67 | exact: true,
68 | nav: true,
69 | icon: Image.icon.statistics,
70 | iconActive: Image.iconActive.statistics,
71 | },
72 | {
73 | path: '/debugger',
74 | name: 'Debugger',
75 | component: 'Debugger',
76 | exact: true,
77 | nav: true,
78 | icon: Image.icon.statistics,
79 | iconActive: Image.iconActive.statistics,
80 | },
81 | {
82 | path: '/config',
83 | name: 'Config',
84 | component: 'ConfigPage',
85 | exact: true,
86 | nav: true,
87 | icon: Image.icon.config,
88 | iconActive: Image.iconActive.config,
89 | },
90 | {
91 | path: '/',
92 | name: 'Footer',
93 | component: 'Footer',
94 | exact: false,
95 | nav: false,
96 | },
97 | ]
98 |
--------------------------------------------------------------------------------
/src/styles/common.scss:
--------------------------------------------------------------------------------
1 | @import './color.scss';
2 |
3 | @mixin boxShadow {
4 | box-shadow: 0 0 20px 0 rgba(170, 170, 170, 0.2) !important;
5 | }
6 |
7 | header {
8 | background: #fff !important; // box-shadow: 0 5px 6px 0 rgba(170, 170, 170, 0.07);
9 | box-shadow: 0 5px 6px 0 rgba(170, 170, 170, 0.07) !important;
10 | }
11 |
12 | body {
13 | display: flex;
14 | flex-direction: column;
15 | min-height: 100vh;
16 | background: #fcfcfc;
17 | overflow-x: hidden;
18 | }
19 |
20 | main {
21 | flex: 1;
22 | }
23 |
24 | a {
25 | text-decoration: none !important;
26 | }
27 |
28 | ul {
29 | margin: 0;
30 | padding: 0;
31 | list-style: none;
32 | }
33 |
34 | table {
35 | color: $tableText;
36 |
37 | thead {
38 | color: $heavyText;
39 | }
40 | }
41 |
42 | input {
43 | padding: 5px 10px;
44 | }
45 |
46 | footer {
47 | background-color: #0d101f;
48 | color: #fff;
49 | font-size: 0.875rem;
50 | padding-top: 34px;
51 | padding-bottom: 34px;
52 | margin-top: 142px;
53 | }
54 |
55 | @media (max-width: 800px) {
56 | header {
57 | // box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12) !important;
58 | box-shadow: none;
59 | }
60 |
61 | footer {
62 | margin-top: 27px;
63 | }
64 | }
65 |
66 | * {
67 | outline: none; // box-sizing: border-box;
68 | }
69 |
70 | :global {
71 | main.root {
72 | margin-top: 89px;
73 | }
74 |
75 | .active {
76 | // background-color: $primary;
77 | // border-color: $primary;
78 |
79 | background: $gradient1;
80 | border-color: transparent !important;
81 | color: #fff !important;
82 | }
83 |
84 | .icon {
85 | width: 1em;
86 | height: 1em;
87 | vertical-align: -0.15em;
88 | fill: currentColor;
89 | overflow: hidden;
90 | }
91 |
92 | .linearProgressRoot {
93 | position: absolute !important;
94 | width: 100vw;
95 | top: 89px;
96 | left: 0;
97 | background: #f15a29 !important;
98 | height: 3px !important;
99 | }
100 |
101 | .fullMask {
102 | z-index: 999;
103 | width: 100vw;
104 | height: 100vh;
105 | position: fixed;
106 | top: 0px;
107 | left: 0px;
108 | }
109 |
110 | @media (max-width: 800px) {
111 | main.root {
112 | margin-top: 56px;
113 | }
114 |
115 | .linearProgressRoot {
116 | top: 56px;
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/components/TransactionList/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Link, } from 'react-router-dom'
3 | import { List, ListItem, ListItemText, } from '@material-ui/core'
4 | import { Transaction, } from '../../typings/'
5 | import valueFormatter from '../../utils/valueFormatter'
6 |
7 | const texts = require('../../styles/text.scss')
8 | const styles = require('./styles.scss')
9 |
10 | export default ({
11 | transactions,
12 | symbol,
13 | }: {
14 | transactions: Transaction[]
15 | symbol: string
16 | }) => (
17 |
18 | {transactions.map(tx => (
19 |
20 |
24 |
28 |
29 | TXID: {tx.hash}
30 |
31 |
32 |
33 | {tx.timestamp && new Date(+tx.timestamp).toLocaleString()}
34 |
35 |
36 | }
37 | secondary={
38 | tx.basicInfo ? (
39 |
40 | From:{' '}
41 |
46 | {tx.basicInfo.from || 'null'}
47 |
48 | {' To: '}
49 | {['Contract Creation', ].includes(tx.basicInfo.to) ? (
50 | tx.basicInfo.to
51 | ) : (
52 |
57 | {tx.basicInfo.to}
58 |
59 | )}
60 | Value
61 | {': '}{' '}
62 |
63 | {valueFormatter(tx.basicInfo.value || '0', symbol)}
64 |
65 |
66 | ) : (
67 | {tx.content}
68 | )
69 | }
70 | />
71 |
72 | ))}
73 |
74 | )
75 |
--------------------------------------------------------------------------------
/src/utils/fetcher.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-07-22 21:11:41
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2018-08-03 14:23:19
6 | */
7 |
8 | import axios, { AxiosResponse, } from 'axios'
9 | import { initServerList, } from '../initValues'
10 | import ErrorTexts from '../typings/errors'
11 |
12 | const baseURL =
13 | window.urlParamChain ||
14 | window.localStorage.getItem('chainIp') ||
15 | initServerList[0]
16 |
17 | const axiosIns = axios.create({
18 | baseURL,
19 | })
20 |
21 | interface Params {
22 | [index: string]: string | number;
23 | }
24 |
25 | export const fetch10Transactions = () =>
26 | axiosIns
27 | .get('api/transactions')
28 | .then((res: AxiosResponse) => res.data)
29 | .catch(() => {
30 | throw new Error(ErrorTexts.CACHE_SERVER_NOT_AVAILABLE)
31 | })
32 |
33 | export const fetchBlocks = (params: Params) =>
34 | axiosIns
35 | .get('api/blocks', { params, })
36 | .then((res: AxiosResponse) => res.data)
37 | .catch(() => {
38 | throw new Error(ErrorTexts.CACHE_SERVER_NOT_AVAILABLE)
39 | })
40 |
41 | export const fetchBlocksV2 = (params: Params) =>
42 | axiosIns
43 | .get('api/v2/blocks', { params, })
44 | .then((res: AxiosResponse) => res.data)
45 | .catch(() => {
46 | throw new Error(ErrorTexts.CACHE_SERVER_NOT_AVAILABLE)
47 | })
48 |
49 | export const fetchTransactions = (params: Params) =>
50 | axiosIns
51 | .get('api/transactions', { params, })
52 | .then((res: AxiosResponse) => res.data)
53 | .catch(() => {
54 | throw new Error(ErrorTexts.CACHE_SERVER_NOT_AVAILABLE)
55 | })
56 |
57 | export const fetchStatistics = params =>
58 | axiosIns
59 | .get('/api/statistics', { params, })
60 | .then((res: AxiosResponse) => res.data)
61 | .catch(() => {
62 | throw new Error(ErrorTexts.CACHE_SERVER_NOT_AVAILABLE)
63 | })
64 |
65 | export const fetchServerList = () =>
66 | axios
67 | .get(`${process.env.PUBLIC}/defaultServerList.json?${new Date().getTime}`)
68 | .then((res: AxiosResponse) => res.data)
69 | .catch(() => {
70 | throw new Error(ErrorTexts.SERVER_LIST_NOT_FOUND)
71 | })
72 |
73 | export const fetchMetadata = ip =>
74 | axios
75 | .post(ip.startsWith('http') ? ip : `https://${ip}`, {
76 | jsonrpc: '2.0',
77 | method: 'getMetaData',
78 | params: ['latest', ],
79 | id: 1,
80 | })
81 | .then((res: AxiosResponse) => res.data)
82 | .catch(() => {
83 | throw new Error(ErrorTexts.CACHE_SERVER_NOT_AVAILABLE)
84 | })
85 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const merge = require('webpack-merge')
4 | const webpack = require('webpack')
5 | const HtmlPlugin = require('html-webpack-plugin')
6 | const DashboardPlugin = require('webpack-dashboard/plugin')
7 |
8 | const baseConfig = require('./webpack.config.base')
9 |
10 | require('dotenv').config()
11 |
12 | const manifest = JSON.parse(
13 | fs.readFileSync(path.resolve(__dirname, '../lib/manifest.json')),
14 | )
15 |
16 | /* eslint-disable import/no-dynamic-require */
17 | const reactManifest = require(path.resolve(__dirname, '../lib/react_manifest'))
18 | /* eslint-enable import/no-dynamic-require */
19 |
20 | const devConfig = {
21 | entry: {
22 | app: [
23 | 'webpack/hot/only-dev-server',
24 | 'webpack-dev-server/client?http://localhost:8080',
25 | 'react-hot-loader/patch',
26 | path.resolve(__dirname, '../src/index.tsx'),
27 | ],
28 | },
29 | module: {
30 | rules: [{
31 | test: /\.s?css$/,
32 | use: [
33 | 'style-loader',
34 | {
35 | loader: 'css-loader',
36 | options: {
37 | modules: true,
38 | importLoaders: 2,
39 | localIdentName: '[local]__[name]--[hash:base64:5]',
40 | },
41 | },
42 | 'sass-loader',
43 | ],
44 | include: [
45 | path.resolve(__dirname, '../src/'),
46 | path.resolve(__dirname, '../node_modules/normalize.css'),
47 | ],
48 | }, ],
49 | },
50 | devtool: 'eval',
51 | plugins: [
52 | new webpack.HotModuleReplacementPlugin(),
53 | new webpack.NamedModulesPlugin(),
54 | new webpack.NamedChunksPlugin(),
55 | new webpack.DefinePlugin({
56 | 'process.env': {
57 | NODE_ENV: JSON.stringify('development'),
58 | OBSERVABLE_INTERVAL: 1000,
59 | PUBLIC: JSON.stringify(process.env.PUBLIC),
60 | },
61 | }),
62 | new DashboardPlugin(),
63 | new webpack.DllReferencePlugin({
64 | context: __dirname,
65 | manifest: reactManifest,
66 | }),
67 | new HtmlPlugin({
68 | title: '开发',
69 | template: path.resolve(__dirname, '../src/templates/index.html'),
70 | react: `../lib/${manifest['react.js']}`,
71 | styledComponents: `../lib/${manifest['styledComponents.js']}`,
72 | apikey: JSON.stringify(process.env.API_KEY),
73 | }),
74 | ],
75 | devServer: {
76 | hot: true,
77 | host: '0.0.0.0',
78 | historyApiFallback: true,
79 | },
80 | }
81 |
82 | module.exports = merge(baseConfig, devConfig)
83 |
--------------------------------------------------------------------------------
/src/containers/NetStatus/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Typography, Toolbar, } from '@material-ui/core'
3 | import { withObservables, } from '../../contexts/observables'
4 | import { withConfig, } from '../../contexts/config'
5 | import { IContainerProps, IContainerState, } from '../../typings/'
6 |
7 | const PlainState = ({ title, value, }: { title: string; value: string }) => (
8 |
9 |
10 | {title}
11 |
12 |
13 | {value}
14 |
15 |
16 | )
17 |
18 | const initialState = {
19 | peerCount: '0',
20 | blockNumber: '',
21 | block: {
22 | body: '',
23 | hash: '',
24 | header: {
25 | quotaUsed: '',
26 | number: '',
27 | prevHash: '',
28 | timestamp: '',
29 | },
30 | version: 0,
31 | },
32 | quotaPrice: 0,
33 | // network: process.env.CHAIN_SERVERS || '',
34 | network: '',
35 | }
36 |
37 | type INetStatusState = Readonly
38 | interface INetStatusProps extends IContainerProps {}
39 | class NetStatus extends React.Component {
40 | static plainStates = [
41 | { title: 'Peer Count', value: 'peerCount', },
42 | { title: 'Current Height', value: 'blockNumber', },
43 | { title: 'Quota Price', value: 'quotaPrice', },
44 | { title: 'Network', value: 'network', },
45 | ];
46 | static updateInterval: any;
47 | readonly state = initialState;
48 | componentWillMount () {
49 | this.fetchStatus()
50 | }
51 |
52 | private fetchStatus = () => {
53 | // fetch peer Count
54 | const { peerCount, newBlockByNumberSubject, } = this.props.CITAObservables
55 | peerCount(60000).subscribe((count: string) =>
56 | this.setState(state => ({ peerCount: count.slice(2), }))
57 | )
58 | // fetch Block Number and Block
59 | newBlockByNumberSubject.subscribe(block => {
60 | this.setState(state => ({ blockNumber: block.header.number, }))
61 | })
62 | newBlockByNumberSubject.connect()
63 | };
64 | render () {
65 | const { block, } = this.state
66 | return (
67 |
68 | {NetStatus.plainStates.map(state => (
69 |
74 | ))}
75 |
76 | )
77 | }
78 | }
79 | export default withConfig(withObservables(NetStatus))
80 |
--------------------------------------------------------------------------------
/src/typings/block.d.ts:
--------------------------------------------------------------------------------
1 | export type Hash = string
2 | export type BlockNumber = string
3 | export type Timestamp = string | number
4 | export interface IBlockHeader {
5 | timestamp: Timestamp;
6 | prevHash: Hash;
7 | number: BlockNumber;
8 | stateRoot: string;
9 | transactionsRoot: string;
10 | receiptsRoot: string;
11 | quotaUsed: string;
12 | proposer: string;
13 | proof: {
14 | Bft: {
15 | proposal: string;
16 | };
17 | };
18 | }
19 |
20 | export interface Transaction {
21 | hash: Hash;
22 | timestamp?: string;
23 | content?: string;
24 | basicInfo?: {
25 | from: string;
26 | to: string;
27 | value: string;
28 | data: string;
29 | };
30 | // content: string
31 | // id: string
32 | // from: Hash
33 | // to: Hash
34 | // tokenValue: string
35 | // timestamp: Timestamp
36 | }
37 |
38 | export interface DetailedTransaction {
39 | hash: Hash;
40 | content?: string;
41 | basicInfo?: {
42 | to: string;
43 | from: string;
44 | data: string;
45 | value: string;
46 | nonce: string;
47 | validUntilBlock: string;
48 | quotaLimit: string;
49 | quotaPrice?: string;
50 | quotaUsed: string;
51 | createdContractAddress: string;
52 | errorMessage: string;
53 | };
54 | blockHash: Hash;
55 | blockNumber: string;
56 | index: string;
57 | }
58 |
59 | export interface IBlock {
60 | body: {
61 | transactions: Transaction[];
62 | };
63 | hash: Hash;
64 | header: IBlockHeader;
65 | version: string | number;
66 | }
67 | export interface TransactionFromServer {
68 | blockNumber: string;
69 | content: string;
70 | from: string;
71 | quotaUsed: string;
72 | hash: string;
73 | timestamp: number;
74 | to: string;
75 | value: number;
76 | }
77 |
78 | export interface BlockFromServer {
79 | version: number;
80 | transactionsCount: number;
81 | header: {
82 | transactionsRoot: string;
83 | timestamp: number;
84 | stateRoot: string;
85 | receiptsRoot: string;
86 | proof: {
87 | Bft: {
88 | proposal: string;
89 | };
90 | };
91 | prevHash: string;
92 | number: string;
93 | quotaUsed: string;
94 | };
95 | hash: string;
96 | }
97 |
98 | export interface ProposalFromServer {
99 | validator: string;
100 | count: number;
101 | }
102 | export interface Metadata {
103 | chainId: number;
104 | chainName: string;
105 | operator: string;
106 | website: string;
107 | genesisTimestamp: string;
108 | validators: string[];
109 | blockInterval: number;
110 | economicalModel: number;
111 | version: number | null;
112 | }
113 |
--------------------------------------------------------------------------------
/src/components/SidebarNavs/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Link, } from 'react-router-dom'
3 | import { translate, } from 'react-i18next'
4 | import {
5 | Typography,
6 | Toolbar,
7 | Drawer,
8 | List,
9 | ListItem,
10 | ListItemText,
11 | IconButton,
12 | AppBar,
13 | } from '@material-ui/core'
14 |
15 | const styles = require('../../containers/Header/header.scss')
16 |
17 | const SidebarNavs = ({
18 | open,
19 | containers,
20 | pathname,
21 | toggleSideNavs,
22 | logo,
23 | t,
24 | }) => (
25 |
26 |
27 |
32 |
38 |
39 |
40 |
41 |
42 |
47 |
51 |
52 |
53 |
54 |
55 | {containers.filter(container => container.nav).map(container => (
56 |
57 |
67 | {pathname === container.path ? (
68 |
73 | ) : (
74 |
79 | )}
80 | {t(container.name)}
81 |
82 | }
83 | />
84 |
85 | ))}
86 |
87 |
88 | )
89 | export default translate('microscope')(SidebarNavs)
90 |
--------------------------------------------------------------------------------
/src/components/SearchPanel/styles.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 | $sidebar-padding: 29px;
3 |
4 | .fields {
5 | & {
6 | display: flex;
7 | padding: 29px;
8 | box-sizing: content-box;
9 | position: relative;
10 | flex-flow: column;
11 | }
12 |
13 | .search {
14 | display: flex;
15 | height: 34px;
16 | margin-bottom: 10px;
17 | }
18 |
19 | input {
20 | border: 1px solid #ccc;
21 | border-right-color: transparent;
22 | border-top-left-radius: 4px;
23 | border-bottom-left-radius: 4px;
24 | flex: 1;
25 |
26 | &:hover,
27 | &:focus {
28 | border-color: $form-focus;
29 | box-shadow: 0 0 4px 0 $form-focus;
30 | border-right-color: transparent;
31 | }
32 | }
33 |
34 | button {
35 | border: none;
36 | border-radius: 0;
37 | border-top-right-radius: 4px;
38 | border-bottom-right-radius: 4px;
39 | background-color: $active;
40 | color: #fff;
41 | padding: 4px 25px;
42 | }
43 | }
44 |
45 | .fields.error {
46 | input {
47 | &:hover,
48 | &:focus {
49 | border-color: $form-error;
50 | box-shadow: 0 0 4px 0 $form-error;
51 | }
52 | }
53 |
54 | .errormessage {
55 | color: $form-error;
56 | width: 416px;
57 | max-width: 100%;
58 | }
59 | }
60 |
61 | @media (min-width: 800px) {
62 | .fields {
63 | input {
64 | min-width: 320px;
65 | }
66 | }
67 | }
68 |
69 | .title {
70 | background-color: #f4f7ff;
71 | font-weight: bold;
72 | padding: 0 $sidebar-padding;
73 | height: 42px;
74 | line-height: 42px;
75 | font-weight: bolder;
76 | }
77 |
78 | .items {
79 | padding: $sidebar-padding;
80 | padding-top: 34px;
81 | padding-bottom: 43px;
82 |
83 | tr {
84 | font-size: 0.875rem;
85 | line-height: 3;
86 | height: 3em;
87 |
88 | td:first-child:after {
89 | content: ':';
90 | }
91 |
92 | td:last-child {
93 | max-width: 30vw;
94 | overflow: hidden;
95 | text-overflow: ellipsis;
96 | white-space: nowrap;
97 | }
98 | }
99 | }
100 |
101 | .more {
102 | display: block;
103 | width: 110px;
104 | height: 34px;
105 | line-height: 34px;
106 | text-align: center;
107 | font-size: 0.875rem;
108 | border: 1px solid $active;
109 | color: $active;
110 | border-radius: 4px;
111 | margin: 0 auto;
112 | margin-top: 31px;
113 | }
114 |
115 | .display {
116 | text-transform: capitalize;
117 | }
118 |
119 | .notFound {
120 | display: flex;
121 | flex-direction: column;
122 | align-items: center;
123 | font-size: 16px;
124 | color: #6c7184;
125 |
126 | img {
127 | width: 150px;
128 | margin-bottom: 20px;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/containers/Header/header.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 |
3 | .headerNavIcon {
4 | color: $heavyText;
5 | margin-right: 31px;
6 | }
7 |
8 | .headerNavs {
9 | display: flex;
10 | flex: 1;
11 | color: $heavyText;
12 |
13 | .navItem {
14 | color: inherit;
15 | padding: 0 25px;
16 | }
17 |
18 | .activeNav::after {
19 | position: absolute;
20 | display: block;
21 | content: '';
22 | width: 100%;
23 | height: 4px;
24 | top: 53px;
25 | left: 0;
26 | background: $active;
27 | }
28 | }
29 |
30 | .navItem {
31 | padding: 0 15px;
32 | color: $text;
33 | text-decoration: none;
34 | display: flex;
35 | align-items: center;
36 | position: relative;
37 | }
38 |
39 | .rightNavs {
40 | display: flex;
41 | justify-content: space-between;
42 | align-items: center !important;
43 | }
44 |
45 | .clickDown {
46 | position: absolute;
47 | bottom: -30px;
48 | right: 0;
49 | width: 29rem;
50 | z-index: 1000;
51 | box-sizing: border-box;
52 | color: black;
53 | background-color: white;
54 | box-shadow: 4px 4px 16px 0 rgba(86, 129, 204, 0.1);
55 | overflow: auto;
56 | text-align: left;
57 | border-radius: 4px;
58 | transform: translate(0, 100%);
59 | text-transform: none;
60 | padding: 29px 20px 0 20px;
61 | }
62 |
63 | .activeNav {
64 | position: relative; // display: block;
65 | color: $primary !important;
66 | font-weight: 600;
67 | }
68 |
69 | .toolbarRoot {
70 | justify-content: space-between;
71 | min-height: 89px !important;
72 | text-transform: capitalize;
73 | padding: 0 !important;
74 | }
75 |
76 | .rightSidebarContent {
77 | max-width: 100vw;
78 |
79 | .toolbarRoot {
80 | padding-left: 25px !important;
81 | }
82 | }
83 |
84 | .toggleIcon {
85 | display: none !important;
86 | }
87 |
88 | @media (max-width: 800px) {
89 | .headerNavs,
90 | .headerNavIcon {
91 | display: none;
92 | }
93 |
94 | .headerLogo {
95 | height: 35px !important;
96 | }
97 |
98 | .toggleIcon {
99 | display: flex !important;
100 | }
101 |
102 | .toolbarRoot {
103 | min-height: 56px !important;
104 | }
105 |
106 | .clickDown {
107 | bottom: -12px;
108 | }
109 | }
110 |
111 | .headerLogo {
112 | height: 49px;
113 | padding-left: 10px;
114 | }
115 |
116 | .sidebarNavs {
117 | padding: 0 15px;
118 |
119 | .toolbarRoot {
120 | &::after {
121 | position: abolute;
122 | content: '';
123 | height: 1px;
124 | width: 100%;
125 | top: 100%;
126 | left: 0;
127 | background: #ccc;
128 | }
129 | }
130 |
131 | & + ul > li {
132 | padding-left: 0px !important;
133 | padding-right: 0px !important;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Microscope Document
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
29 |
30 |
36 |
37 | Loading
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/containers/Footer/styles.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 | $col-span: 92px;
3 |
4 | .footer {
5 | display: flex;
6 |
7 | @media (max-width: 800px) {
8 | flex-direction: column;
9 | align-items: center;
10 |
11 | &>div {
12 | max-width: 90vw;
13 | text-align: center;
14 | padding: 0;
15 | }
16 |
17 | .products {
18 | div {
19 | display: flex;
20 | justify-content: center;
21 | }
22 | }
23 | }
24 | }
25 |
26 | .overview {
27 | display: flex;
28 | flex: 1;
29 | align-items: center;
30 | padding-right: 72px;
31 |
32 | div {
33 | line-height: 2;
34 | }
35 | }
36 |
37 | .products {
38 | width: 584px;
39 | display: flex;
40 | flex-direction: column;
41 | align-items: center;
42 |
43 | @media (max-width: 800px) {
44 | width: 100vw;
45 |
46 | &>div {
47 | display: flex;
48 | flex-wrap: wrap;
49 | margin: 0;
50 | }
51 | }
52 |
53 | padding: 0 $col-span;
54 | box-sizing: border-box;
55 |
56 | &>div {
57 | display: flex;
58 | justify-content: space-between;
59 | flex-wrap: wrap;
60 | margin: 0 -10px;
61 |
62 | &>div {
63 | width: 190px;
64 | margin: 0 10px;
65 | display: flex;
66 | justify-content: center;
67 | img {
68 | height: 74px;
69 | }
70 |
71 | &:last-child {
72 | filter: brightness(100);
73 |
74 | img {
75 | filter: contrast(0);
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
82 | .contacts {
83 | flex: 1;
84 | padding-left: $col-span;
85 |
86 | a {
87 | display: flex;
88 | align-items: center;
89 | font-size: inherit; // line-height: 2.35em;
90 | // height: 2.35em;
91 | color: inherit;
92 |
93 | svg {
94 | margin-right: 0.7rem;
95 | }
96 | }
97 | }
98 |
99 | .footer,
100 | .products,
101 | .contacts {
102 | h1 {
103 | display: inline-block;
104 | padding: 0.25rem 0.5rem;
105 | padding-left: 0;
106 | font-size: 1.125rem;
107 | border-bottom: 2px solid #fff;
108 | height: 1.5rem;
109 | line-height: 1.5rem;
110 | margin-bottom: 0.75rem;
111 | text-transform: capitalize;
112 |
113 | @media (max-width: 800px) {
114 | padding-right: 0;
115 | }
116 | }
117 | }
118 |
119 | .overview,
120 | .products,
121 | .contacts {
122 | &>div {
123 | height: 80px;
124 | display: flex;
125 | flex-direction: column;
126 | justify-content: space-between;
127 | align-items: flex-start;
128 | }
129 |
130 | @media (max-width: 800px) {
131 | p {
132 | display: none;
133 | }
134 | }
135 | }
136 |
137 | .overview {
138 | &>div {
139 | justify-content: center;
140 | }
141 | }
142 | .products{
143 | &>div {
144 | justify-content: center;
145 | &>div:first-child {
146 | margin-left: 0;
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/components/DebugAccounts/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | ExpansionPanel,
4 | ExpansionPanelActions,
5 | ExpansionPanelDetails,
6 | ExpansionPanelSummary,
7 | List,
8 | ListItem,
9 | ListItemText,
10 | TextField,
11 | } from '@material-ui/core'
12 | import { ExpandMore as ExpandMoreIcon, } from '@material-ui/icons'
13 | import { Link, } from 'react-router-dom'
14 | import * as React from 'react'
15 |
16 | const styles = require('./debugAccounts.scss')
17 | const texts = require('../../styles/text.scss')
18 |
19 | export interface DebugAccount {
20 | privateKey: string;
21 | address: string;
22 | balance?: string;
23 | }
24 | const DebugAccounts = ({
25 | accounts,
26 | privateKeysField,
27 | handleAccountsInput,
28 | updateDebugAccounts,
29 | }: {
30 | accounts: DebugAccount[];
31 | privateKeysField: string;
32 | handleAccountsInput: React.EventHandler>;
33 | updateDebugAccounts: React.EventHandler>;
34 | }) => (
35 |
36 | }>
37 | Debug Accounts(
38 | {accounts.length})
39 |
40 |
41 |
42 |
43 |
44 |
50 |
51 | {accounts.map(account => (
52 |
53 |
60 | {account.address || 'null'}
61 |
62 | }
63 | secondary={account.privateKey}
64 | />
65 |
71 |
72 | ))}
73 |
74 |
75 |
76 |
83 |
89 |
92 |
93 |
94 |
95 | )
96 |
97 | export default DebugAccounts
98 |
--------------------------------------------------------------------------------
/src/components/HomepageLists/BlockList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Link, } from 'react-router-dom'
3 | import { translate, } from 'react-i18next'
4 | import { List, ListItem, ListItemText, } from '@material-ui/core'
5 | import { Chain, } from '@appchain/plugin'
6 | import { formatedAgeString, } from '../../utils/timeFormatter'
7 |
8 | const texts = require('../../styles/text.scss')
9 | const styles = require('./homepageList.scss')
10 |
11 | export default translate('microscope')(
12 | ({
13 | blocks,
14 | t,
15 | }: {
16 | blocks: Chain.Block[]
17 | t: (key: string) => string
18 | }) => (
19 |
24 | {blocks.map((block: Chain.Block) => (
25 |
31 |
32 | {t('block')}
33 |
37 | #{+block.header.number}
38 |
39 |
40 |
48 | Hash:
49 |
55 |
56 | {block.hash.slice(0, -4)}
57 |
58 |
59 | {block.hash.slice(-4)}
60 |
61 |
62 |
63 | {formatedAgeString(block.header.timestamp)}
64 |
65 |
66 | }
67 | secondary={
68 |
69 |
70 | {t('including')} {block.body.transactions.length}{' '}
71 | {t('Transactions')}.{' '}
72 |
73 |
74 | {t('proposed by')}{' '}
75 |
76 | {block.header.proposer}
77 |
78 |
79 |
80 | }
81 | />
82 |
83 | ))}
84 |
85 | )
86 | )
87 |
--------------------------------------------------------------------------------
/src/containers/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { createPortal, } from 'react-dom'
3 | import { I18n, translate, } from 'react-i18next'
4 |
5 | const layout = require('../../styles/layout.scss')
6 | const styles = require('./styles.scss')
7 |
8 | interface Product {
9 | logo: string
10 | title: string
11 | url: string
12 | overview: string
13 | }
14 | interface Contact {
15 | icon: any
16 | title: string
17 | url: string
18 | }
19 |
20 | class Footer extends React.Component<{ t: (key: string) => string }, any> {
21 | state = {
22 | overview: {
23 | title: '',
24 | content: 'Microscope provides an easy-to-use user interface to inspect CITA.',
25 | },
26 | products: {
27 | title: 'technologies',
28 | items: [
29 | {
30 | logo: 'https://raw.githubusercontent.com/cryptape/assets/master/CITA-logo.png',
31 | title: '',
32 | url: 'https://github.com/cryptape/cita',
33 | },
34 | ] as Product[],
35 | },
36 | contacts: {
37 | title: 'contact us',
38 | items: [
39 | {
40 | icon: 'github',
41 | title: 'Github',
42 | url: 'https://github.com/cryptape/microscope',
43 | },
44 | {
45 | icon: 'email',
46 | title: 'citahub-team@cryptape.com',
47 | url: 'mailto:citahub-team@cryptape.com',
48 | },
49 | {
50 | icon: 'forum',
51 | title: 'CITAHub Forums',
52 | url: 'https://talk.citahub.com/',
53 | },
54 | ] as Contact[],
55 | },
56 | }
57 | render () {
58 | const { overview, products, contacts, } = this.state
59 | const { t, } = this.props
60 | return (
61 |
62 |
63 |
{overview.content}
64 |
65 |
66 |
{t(products.title)}
67 |
68 | {products.items.map(item => (
69 |
74 | ))}
75 |
76 |
77 |
78 |
{t(contacts.title)}
79 |
89 |
90 |
91 | )
92 | }
93 | }
94 |
95 | const TransFooter = translate('microscope')(Footer)
96 |
97 | export default () => createPortal(, document.getElementById('footer') as HTMLElement)
98 |
--------------------------------------------------------------------------------
/src/components/HomepageLists/homepageList.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 |
3 | .primary {
4 | color: $text;
5 | // display: flex !important;
6 | flex-wrap: wrap;
7 | justify-content: space-between;
8 | align-items: center;
9 |
10 | a {
11 | word-break: break-all;
12 | }
13 |
14 | a.hashlink {
15 | word-break: keep-all;
16 | white-space: nowrap;
17 | display: inline-flex;
18 | flex-wrap: nowrap;
19 | max-width: 80%;
20 | text-transform: none;
21 | }
22 | }
23 |
24 | .secondary {
25 | color: $text;
26 | word-break: break-all;
27 |
28 | & > span {
29 | display: block;
30 | }
31 | }
32 |
33 | .cardContentRoot {
34 | padding-left: 22px;
35 | }
36 |
37 | .listItemContainer {
38 | width: auto !important;
39 | // padding: 16px 0 !important;
40 | // margin-left: 33px;
41 | display: flex;
42 | align-items: stretch !important;
43 | border-bottom: 1px solid $divider;
44 | text-transform: capitalize;
45 | height: 130px;
46 |
47 | // width: 580px;
48 | padding: 37px 0 37px 27px !important;
49 | height: 179px;
50 | border-radius: 4px;
51 | box-shadow: 0 4px 16px 0 rgba(86, 129, 204, 0.1);
52 | background-color: #ffffff;
53 | margin-bottom: 20px;
54 |
55 | a:hover {
56 | text-decoration: underline !important;
57 | }
58 |
59 | svg {
60 | font-size: 1.5rem;
61 | }
62 | }
63 |
64 | .listItemTextRoot {
65 | display: flex;
66 | flex-direction: column;
67 | justify-content: space-between;
68 | padding-left: 14px !important;
69 | padding-right: 100px !important;
70 | }
71 |
72 | .blockIcon {
73 | display: flex;
74 | flex-direction: column;
75 | justify-content: center;
76 | align-items: center;
77 | background-size: cover;
78 | min-width: 100px;
79 | padding: 0 20px;
80 | color: #fff !important;
81 | border-radius: 4px;
82 | box-sizing: border-box;
83 |
84 | font-size: 16px;
85 | line-height: 24px;
86 | width: 96px;
87 | height: 105px;
88 | opacity: 0.8;
89 | border-radius: 6px;
90 | background-color: #75b5ef;
91 |
92 | span {
93 | color: inherit;
94 | }
95 |
96 | a {
97 | color: inherit !important;
98 | }
99 | }
100 |
101 | .time {
102 | font-size: 12px; // color: $plainText;
103 | color: #8b94a0;
104 | position: absolute;
105 | top: 37px;
106 | right: 20px;
107 | text-transform: lowercase !important;
108 | }
109 |
110 | .txInfo {
111 | // line-height: 2;
112 | color: $text;
113 | display: flex;
114 | flex-direction: column;
115 | justify-content: space-between;
116 | }
117 |
118 | .fromTo {
119 | display: flex;
120 |
121 | span {
122 | max-width: 50%;
123 | }
124 |
125 | span:first-child {
126 | margin-right: 30px;
127 | }
128 | }
129 |
130 | .value {
131 | padding: 0px 8px;
132 | border-radius: 4px;
133 | }
134 |
135 | @media (max-width: 800px) {
136 | .listItemContainer {
137 | margin-left: 0;
138 | padding: 16px 0 !important;
139 |
140 | &:last-child {
141 | border-bottom: none;
142 | }
143 | }
144 | }
145 |
146 | .listPadding {
147 | padding: 0;
148 | }
149 |
150 | .txIcon {
151 | align-self: flex-start;
152 | }
153 |
--------------------------------------------------------------------------------
/src/contexts/observables.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | ///
3 | /* eslint-enable */
4 | import * as React from 'react'
5 | import CITAObservables from '@appchain/observables'
6 |
7 | declare global {
8 | interface Window {
9 | urlParamChain: string | null;
10 | }
11 | }
12 |
13 | if (URLSearchParams) {
14 | Object.defineProperty(window, 'urlParamChain', {
15 | value: new URLSearchParams(window.location.search).get('chain'),
16 | })
17 | }
18 |
19 | const initObservables: any = new CITAObservables({
20 | server:
21 | window.urlParamChain ||
22 | window.localStorage.getItem('chainIp') ||
23 | process.env.CHAIN_SERVERS ||
24 | '',
25 | interval:
26 | (process.env.OBSERVABLE_INTERVAL && +process.env.OBSERVABLE_INTERVAL) ||
27 | 1000,
28 | })
29 |
30 | const newBlockCallbackTable = {} as any
31 | let newBlockCallbackInterval = -1 as any
32 |
33 | const handleError = error => {
34 | Object.keys(newBlockCallbackTable).forEach(key => {
35 | const callbackInfo = newBlockCallbackTable[key]
36 | callbackInfo.error(error)
37 | })
38 | }
39 |
40 | const handleCallback = block => {
41 | Object.keys(newBlockCallbackTable).forEach(key => {
42 | const callbackInfo = newBlockCallbackTable[key]
43 | callbackInfo.callback(block)
44 | })
45 | }
46 |
47 | const fetchBlockByNumber = (number, time = 3) => {
48 | initObservables
49 | .blockByNumber(number, false)
50 | .subscribe(handleCallback, error => {
51 | const t = time - 1
52 | if (t > -1) {
53 | fetchBlockByNumber(number, t)
54 | }
55 | handleError(error)
56 | })
57 | }
58 |
59 | export const stopSubjectNewBlock = () =>
60 | clearInterval(newBlockCallbackInterval)
61 |
62 | export const startSubjectNewBlock = () => {
63 | stopSubjectNewBlock()
64 | let current = 0
65 | newBlockCallbackInterval = setInterval(() => {
66 | initObservables.newBlockNumber(0, false).subscribe(blockNumber => {
67 | const latest = +blockNumber
68 | if (current === 0) {
69 | current = latest - 1
70 | }
71 | if (latest <= +current) return
72 | if (latest === current + 1) {
73 | current = latest
74 | fetchBlockByNumber(latest)
75 | } else {
76 | while (current < latest) {
77 | current++
78 | fetchBlockByNumber(current)
79 | }
80 | }
81 | }, handleError)
82 | }, 1000)
83 | }
84 |
85 | initObservables.newBlockSubjectAdd = (key, callback, error, { ...params }) => {
86 | newBlockCallbackTable[key] = {
87 | callback,
88 | error,
89 | ...params,
90 | }
91 | }
92 |
93 | const ObservableContext = React.createContext(initObservables)
94 |
95 | export const withObservables = Comp => props => (
96 |
97 | {(observables: CITAObservables) => (
98 |
99 | )}
100 |
101 | )
102 | export const provideObservabls = Comp => props => (
103 |
104 |
105 |
106 | )
107 |
108 | export default ObservableContext
109 |
--------------------------------------------------------------------------------
/src/components/ERCPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { List, Divider, } from '@material-ui/core'
3 | import { ABI, ABIElement, } from '../../typings'
4 |
5 | const styles = require('./styles.scss')
6 |
7 | const Item = ({ label, fields, }) => (
8 |
9 |
{label}
10 |
11 |
{fields}
12 |
13 | )
14 | /* eslint-disable no-restricted-globals */
15 | interface ErcPanel {
16 | abi: ABI
17 | handleAbiValueChange: (
18 | index: number
19 | ) => (inputIndex: number) => (e: any) => void
20 | handleEthCall: (index: number) => (e: any) => void
21 | }
22 | /* eslint-enable no-restricted-globals */
23 |
24 | const ContractMethod = ({
25 | abiEl,
26 | handleAbiValueChange,
27 | handleEthCall,
28 | }: {
29 | abiEl: ABIElement;
30 | index: number;
31 | handleAbiValueChange: (inputIndex: number) => (e: any) => void;
32 | handleEthCall: (e: any) => void;
33 | }) => (
34 | -
38 |
39 | {abiEl.inputs && abiEl.inputs.length ? (
40 | abiEl.inputs.map((input, inputIndex) => (
41 |
48 | ))
49 | ) : (
50 | No Inputs
51 | )}
52 |
53 |
54 | {abiEl.outputs && abiEl.outputs.length ? (
55 | abiEl.outputs.map(output => {
56 | if (output.value && output.value.match(/jpg|png|gif/)) {
57 | return (
58 |

64 | )
65 | }
66 | return (
67 |
{}}
74 | />
75 | )
76 | })
77 | ) : (
78 |
No Outputs
79 | )}
80 |
81 |
82 |
83 | }
84 | />
85 | )
86 |
87 | const ERCPanel: React.SFC = props => (
88 |
89 | {props.abi.map((abiEl, index) => (
90 |
97 | ))}
98 |
99 | )
100 |
101 | export default ERCPanel
102 |
--------------------------------------------------------------------------------
/src/containers/Homepage/homepage.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 |
3 | .card {
4 | border-radius: 2px;
5 | box-shadow: 0 0 10px 0 rgba(170, 170, 170, 0.2) !important;
6 | }
7 |
8 | .listCards {
9 | display: flex;
10 | flex-direction: column;
11 | }
12 |
13 | .blockListHeader {
14 | display: flex;
15 | flex-wrap: wrap;
16 | justify-content: space-between;
17 | align-items: center;
18 | }
19 |
20 | .metadataTable {
21 | width: 100%;
22 | max-width: 1200px;
23 | display: flex;
24 | flex-flow: wrap;
25 | margin-bottom: 60px;
26 | box-sizing: border-box;
27 | }
28 |
29 | .mainInfoBlock {
30 | display: flex;
31 | width: 100%;
32 | justify-content: space-between;
33 | }
34 |
35 | .mainInfoCell {
36 | width: 380px;
37 | max-width: 32%;
38 | box-sizing: border-box;
39 | max-height: 150px;
40 | padding: 45px 0 35px 40px;
41 | margin-bottom: 30px;
42 | border-radius: 6px;
43 | background: $gradient1;
44 | color: #fff;
45 | display: flex;
46 | align-items: flex-end;
47 | }
48 |
49 | .alertContiner {
50 | position: relative;
51 | }
52 |
53 | .alert {
54 | z-index: 1001;
55 | position: absolute;
56 | overflow: auto;
57 | bottom: 0;
58 | right: 0;
59 | transform: translate(0, 100%);
60 | background: white;
61 | color: black;
62 | box-shadow: 0 4px 16px 0 rgba(86, 129, 204, 0.1);
63 | border-radius: 5px;
64 | box-sizing: border-box;
65 |
66 | div {
67 | padding: 20px;
68 | }
69 | }
70 |
71 | .mainInfoIcon {
72 | width: 70px;
73 | height: 70px;
74 | background-color: #699be5;
75 | margin-right: 20px;
76 | border-radius: 50%;
77 |
78 | svg {
79 | margin: (70 - 34px) / 2;
80 | width: 34px;
81 | height: 34px;
82 | color: white;
83 | }
84 | }
85 |
86 | .mainInfoContent {
87 | font-size: 32px;
88 | line-height: 40px;
89 | margin-bottom: 4px;
90 | }
91 |
92 | .mainInfoName {
93 | font-size: 16px;
94 | line-height: 22px;
95 | }
96 |
97 | .subInfoBlock {
98 | display: flex;
99 | width: 100%;
100 | flex-flow: wrap;
101 | border-radius: 6px;
102 | box-shadow: 0 4px 16px 0 rgba(86, 129, 204, 0.1);
103 | background-color: #ffffff;
104 | padding: 14px 0 20px 0;
105 | }
106 |
107 | .subInfoCell {
108 | width: 33%;
109 | padding: 12px 0 0 58px;
110 | box-sizing: border-box;
111 | display: flex;
112 | }
113 |
114 | .subInfoIcon {
115 | width: 22px;
116 | height: 22px;
117 | margin-right: 18px;
118 |
119 | svg {
120 | width: 22px;
121 | height: 22px;
122 | color: #5486db;
123 | }
124 | }
125 |
126 | .subInfoContent {
127 | font-size: 18px;
128 | margin-bottom: 5px;
129 | color: #4a5563;
130 | }
131 |
132 | .subInfoName {
133 | font-size: 14px;
134 | line-height: 20px;
135 | color: #979a9e;
136 | }
137 |
138 | .firstChart,
139 | .secondChart {
140 | }
141 |
142 | @media (max-width: 960px) {
143 | .mainInfoBlock {
144 | flex-flow: column;
145 | align-items: center;
146 | }
147 |
148 | .mainInfoCell {
149 | max-width: 100%;
150 | }
151 |
152 | .subInfoCell {
153 | width: 50%;
154 | padding: 0;
155 | }
156 | }
157 |
158 | @media (max-width: 600px) {
159 | .subInfoCell {
160 | width: 100%;
161 | margin-bottom: 20px;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/components/LocalAccounts/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Link, } from 'react-router-dom'
3 |
4 | import {
5 | ExpansionPanel,
6 | ExpansionPanelSummary,
7 | ExpansionPanelDetails,
8 | ExpansionPanelActions,
9 | Typography,
10 | List,
11 | ListItem,
12 | ListItemText,
13 | ListItemSecondaryAction,
14 | IconButton,
15 | TextField,
16 | Button,
17 | } from '@material-ui/core'
18 | import {
19 | ExpandMore as ExpandMoreIcon,
20 | Add as AddIcon,
21 | Delete as DeleteIcon,
22 | } from '@material-ui/icons'
23 |
24 | const texts = require('../../styles/text.scss')
25 | const styles = require('./styles.scss')
26 | /* eslint-disable no-restricted-globals */
27 | export interface LocalAccount {
28 | name: string;
29 | abi?: string;
30 | addr: string;
31 | }
32 | /* eslint-enable no-restricted-globals */
33 |
34 | export default props =>
35 | props.addrGroups.map(group => (
36 |
37 | }>
38 |
39 | Including: {props[group.key].length} {group.label}
40 | (s)
41 |
42 |
43 |
44 |
45 | {props[group.key].length ? (
46 | props[group.key].map((item: LocalAccount, index) => (
47 |
48 |
59 | {item.addr}
60 |
61 | }
62 | />
63 |
64 |
67 |
68 |
69 |
70 |
71 | ))
72 | ) : (
73 |
74 |
75 |
76 | )}
77 |
78 |
79 |
80 |
85 |
90 |
98 |
99 |
100 | ))
101 |
--------------------------------------------------------------------------------
/src/components/HomepageLists/TransactionList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Link, } from 'react-router-dom'
3 | import { translate, } from 'react-i18next'
4 | import { List, ListItem, ListItemText, } from '@material-ui/core'
5 | import { TransactionFromServer, } from '../../typings/'
6 | import { ContractCreation, } from '../../initValues'
7 | import { formatedAgeString, } from '../../utils/timeFormatter'
8 | import valueFormatter from '../../utils/valueFormatter'
9 | import { TX_TYPE, } from '../../containers/Transaction'
10 | import Image from '../../images'
11 |
12 | const texts = require('../../styles/text.scss')
13 | const styles = require('./homepageList.scss')
14 |
15 | const TransactionTypeInfo = {
16 | [TX_TYPE.EXCHANGE]: {
17 | icon: Image.type.exchange,
18 | },
19 | [TX_TYPE.CONTRACT_CREATION]: {
20 | icon: Image.type.contractCreation,
21 | },
22 | [TX_TYPE.CONTRACT_CALL]: {
23 | icon: Image.type.contractCall,
24 | },
25 | }
26 |
27 | const Primary = ({ tx, t, symbol, }) => (
28 |
29 | TX#:
30 |
31 | {tx.hash.slice(0, 23)}
32 | {tx.hash.slice(-4)}
33 |
34 | {formatedAgeString(tx.timestamp)}
35 |
36 | )
37 |
38 | const Secondary = ({ tx, t, symbol, }) => (
39 |
40 |
41 |
42 | From
43 |
44 | {tx.from}
45 |
46 |
47 | {tx.type === TX_TYPE.CONTRACT_CREATION || tx.to === '0x' ? null : (
48 |
49 | To
50 |
51 | {tx.to}
52 |
53 |
54 | )}
55 |
56 |
57 | {t('value')}:{' '}
58 |
59 | {valueFormatter(+tx.value, symbol)}
60 |
61 |
62 |
63 | )
64 |
65 | const TransactionCell = ({ tx, t, symbol, }) => (
66 |
67 |
68 |

69 |
70 | }
73 | secondary={}
74 | />
75 |
76 | )
77 |
78 | export default translate('microscope')(
79 | ({
80 | transactions,
81 | t,
82 | symbol,
83 | }: {
84 | transactions: TransactionFromServer[]
85 | t: (key: string) => string
86 | symbol?: string
87 | }) => (
88 |
93 | {transactions.map(tx => (
94 |
95 | ))}
96 |
97 | )
98 | )
99 |
--------------------------------------------------------------------------------
/config/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const webpack = require('webpack')
4 | const merge = require('webpack-merge')
5 | const HtmlPlugin = require('html-webpack-plugin')
6 | const ExtractPlugin = require('extract-text-webpack-plugin')
7 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
8 | const SWPrecachePlugin = require('sw-precache-webpack-plugin')
9 | const CopyPlugin = require('copy-webpack-plugin')
10 | const AutoprefixerPlugin = require('autoprefixer')
11 |
12 | require('dotenv').config()
13 |
14 | /* eslint-disable import/no-dynamic-require */
15 | const baseConfig = require(path.resolve(__dirname, './webpack.config.base'))
16 | const reactManifest = require(path.resolve(__dirname, '../lib/react_manifest'))
17 | /* eslint-enable import/no-dynamic-require */
18 |
19 |
20 | const manifest = JSON.parse(
21 | fs.readFileSync(path.resolve(__dirname, '../lib/manifest.json')),
22 | )
23 | const prodConfig = {
24 | entry: {
25 | app: path.resolve(__dirname, '../src/index.tsx'),
26 | },
27 | module: {
28 | rules: [{
29 | test: /\.s?css$/,
30 | use: ExtractPlugin.extract({
31 | fallback: 'style-loader',
32 | publicPath: '../',
33 | use: [{
34 | loader: 'css-loader',
35 | options: {
36 | modules: true,
37 | importLoaders: 3,
38 | localIdentName: '[local]__[name]--[hash:base64:5]',
39 | minimize: true,
40 | },
41 | },
42 | {
43 | loader: 'postcss-loader',
44 | options: {
45 | ident: 'postcss',
46 | sourceMap: false,
47 | plugins: () => [AutoprefixerPlugin],
48 | },
49 | },
50 | 'resolve-url-loader',
51 | 'sass-loader',
52 | ],
53 | }),
54 | include: [
55 | path.resolve(__dirname, '../src/'),
56 | path.resolve(__dirname, '../node_modules/normalize.css'),
57 | ],
58 | }, ],
59 | },
60 | plugins: [
61 | new ExtractPlugin('styles/style.[contenthash:base64:5].css'),
62 | new SWPrecachePlugin(),
63 | new webpack.DefinePlugin({
64 | 'process.env': {
65 | NODE_ENV: JSON.stringify('production'),
66 | OBSERVABLE_INTERVAL: 1000,
67 | PUBLIC: JSON.stringify(process.env.PUBLIC),
68 | },
69 | }),
70 | new UglifyJSPlugin(),
71 | new webpack.DllReferencePlugin({
72 | context: __dirname,
73 | manifest: reactManifest,
74 | }),
75 | new CopyPlugin([{
76 | from: path.resolve(__dirname, '../lib'),
77 | to: path.resolve(__dirname, '../dist/lib'),
78 | }, ]),
79 | new HtmlPlugin({
80 | title: 'Microscope',
81 | template: path.resolve(__dirname, '../src/templates/index.html'),
82 | react: `./lib/${manifest['react.js']}`,
83 | minify: {
84 | removeComments: true,
85 | collapseWhitespace: true,
86 | removeRedundantAttributes: true,
87 | useShortDoctype: true,
88 | removeEmptyAttributes: true,
89 | removeStyleLinkTypeAttributes: true,
90 | keepClosingSlash: true,
91 | minifyJS: true,
92 | minifyCSS: true,
93 | minifyURLs: true,
94 | },
95 | }),
96 | ],
97 | }
98 |
99 | module.exports = merge(baseConfig, prodConfig)
100 |
--------------------------------------------------------------------------------
/src/components/MetadataPanel/metadata.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 | $sidebar-padding: 29px;
3 |
4 | .display {
5 | padding: 0 $sidebar-padding;
6 | padding-top: 6px;
7 | color: $sidebar-text;
8 | text-transform: capitalize;
9 |
10 | .item {
11 | font-size: 1rem;
12 | line-height: 2.2rem;
13 | height: 2.2rem;
14 |
15 | span {
16 | text-transform: none;
17 | }
18 | }
19 |
20 | .itemValue {
21 | color: $sidebar-bold-text;
22 | }
23 |
24 | .validators {
25 | .box {
26 | margin-top: 10px;
27 | padding: 10px 17px;
28 | border: 1px solid #ccc;
29 | border-radius: 4px;
30 | margin-bottom: 41px;
31 |
32 | & > div {
33 | height: 0.875rem;
34 | line-height: 1.7rem;
35 | height: 1.7rem;
36 | max-width: 30vw;
37 |
38 | @media (max-width: 800px) {
39 | max-width: 80vw;
40 | }
41 |
42 | @media (max-width: 500px) {
43 | max-width: 50vw;
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | .title {
51 | background-color: #f4f7ff;
52 | font-weight: bold;
53 | padding: 0 $sidebar-padding;
54 | height: 36px;
55 | line-height: 36px;
56 | font-weight: bolder;
57 | text-transform: capitalize;
58 | }
59 |
60 | .fields {
61 | display: flex;
62 | height: 34px;
63 | box-sizing: content-box;
64 | position: relative;
65 |
66 | input {
67 | border: 1px solid #ccc;
68 | border-radius: 4px;
69 | flex: 1;
70 | }
71 |
72 | input:focus {
73 | border-color: $form-focus;
74 | box-shadow: 0 0 4px 0 $form-focus;
75 | }
76 |
77 | border-color: $form-error;
78 |
79 | button {
80 | border: none;
81 | border-radius: 4px;
82 | background-color: $active;
83 | color: #fff;
84 | padding: 6px 24px;
85 | margin-left: 10px;
86 | text-transform: capitalize;
87 | cursor: pointer;
88 |
89 | &:disabled {
90 | background: #eee;
91 | color: #aaa;
92 | cursor: default;
93 | }
94 | }
95 |
96 | .chainerror {
97 | color: $form-error;
98 | position: absolute;
99 | bottom: 0;
100 | }
101 |
102 | .chainerror {
103 | color: $form-error;
104 | position: absolute;
105 | bottom: 0;
106 | }
107 | }
108 |
109 | .fields:focus-within {
110 | .alert {
111 | display: block;
112 | }
113 | }
114 |
115 | .alert {
116 | display: none;
117 | position: absolute;
118 | top: 120%;
119 | padding: 10px 20px;
120 | box-shadow: 0 4px 16px 0 rgba(86, 129, 204, 0.1);
121 | color: #fff;
122 | background: $gradient1;
123 | border-radius: 4px;
124 | line-height: 1.2;
125 | opacity: 0.8;
126 | z-index: 1;
127 |
128 | a:visited {
129 | color: #fff;
130 | }
131 | }
132 |
133 | .chainerror {
134 | @extend .alert;
135 | color: $form-error;
136 | background: #333;
137 | }
138 |
139 | .listItem:hover {
140 | cursor: pointer;
141 |
142 | &:hover {
143 | background: rgba(0, 0, 0, 0.08);
144 | }
145 | }
146 |
147 | .serverGutters {
148 | padding-left: 20px !important;
149 | padding-right: 0px !important;
150 | position: relative;
151 |
152 | &::before {
153 | content: '';
154 | position: absolute;
155 | top: 21px;
156 | left: 0;
157 | width: 5px;
158 | height: 5px;
159 | border-radius: 50%;
160 | background: #3dd895;
161 | }
162 | }
163 |
164 | .serverPrimary {
165 | color: $heavyText !important;
166 | }
167 |
168 | .serverSecondary {
169 | color: $plainText !important;
170 | }
171 |
--------------------------------------------------------------------------------
/src/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | <%= htmlWebpackPlugin.options.title %>
15 |
16 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | m
112 | i
113 | c
114 | r
115 | o
116 | s
117 | c
118 | o
119 | p
120 | e
121 |
122 |
123 |
124 |
132 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Microscope",
3 | "version": "0.1.0",
4 | "description": "Nervos Web",
5 | "main": "index.js",
6 | "repository": "https://github.com/cryptape/microscope",
7 | "author": "Keith ",
8 | "license": "MIT",
9 | "scripts": {
10 | "start": "clear && webpack-dev-server --config config/webpack.config.dev.js",
11 | "start:d": "clear && webpack-dashboard -- webpack-dev-server --config config/webpack.config.dev.js",
12 | "build": "yarn run build:local",
13 | "build:local": "rimraf dist && webpack --config config/webpack.config.prod.js",
14 | "build:test": "rimraf dist && yarn install && webpack --config config/webpack.config.prod.js",
15 | "build:prod": "rimraf dist && yarn install && webpack --config config/webpack.config.prod.js",
16 | "dll": "rimraf lib && webpack --config ./config/webpack.config.dll.js",
17 | "precommit": "lint-staged",
18 | "predeploy": "yarn run build",
19 | "deploy": "gh-pages -d dist",
20 | "docker:init": "docker-compose up",
21 | "docker:start": "docker-compose down && docker-compose up -d"
22 | },
23 | "lint-staged": {
24 | "src/**/*.{tsx,ts}": [
25 | "eslint --fix",
26 | "git add"
27 | ]
28 | },
29 | "dependencies": {
30 | "@appchain/observables": "0.20.2",
31 | "@cryptape/cita-signer": "2.4.0",
32 | "@material-ui/core": "^1.2.0",
33 | "@material-ui/icons": "^1.1.0",
34 | "@reactivex/rxjs": "^5.5.6",
35 | "axios": "^0.18.0",
36 | "echarts": "^4.0.4",
37 | "i18next": "^10.3.0",
38 | "i18next-browser-languagedetector": "^2.1.0",
39 | "normalize.css": "^7.0.0",
40 | "react": "16.3",
41 | "react-dom": "16.3",
42 | "react-i18next": "^7.3.4",
43 | "react-loadable": "^5.3.1",
44 | "react-pager": "^1.3.3",
45 | "react-router-dom": "^4.2.2",
46 | "web3": "1.0.0-beta.33",
47 | "web3-eth-abi": "1.0.0-beta.36",
48 | "web3-eth-accounts": "1.0.0-beta.36",
49 | "web3-eth-contract": "1.0.0-beta.36",
50 | "web3-utils": "1.0.0-beta.36"
51 | },
52 | "devDependencies": {
53 | "@appchain/plugin": "^0.20.1",
54 | "@types/echarts": "^0.0.12",
55 | "@types/node": "^9.4.0",
56 | "@types/react": "^16.0.35",
57 | "@types/react-dom": "^16.0.3",
58 | "@types/react-i18next": "^7.6.1",
59 | "@types/react-loadable": "^5.3.3",
60 | "@types/react-router-dom": "^4.2.3",
61 | "@types/webpack": "^3.8.4",
62 | "@types/webpack-dev-server": "^2.9.2",
63 | "@types/webpack-env": "^1.13.4",
64 | "autoprefixer": "^7.2.5",
65 | "babel-loader": "^7.1.2",
66 | "bundle-loader": "^0.5.5",
67 | "copy-webpack-plugin": "^4.3.1",
68 | "css-loader": "^0.28.9",
69 | "dotenv": "^6.0.0",
70 | "eslint": "^4.16.0",
71 | "eslint-config-airbnb": "^16.1.0",
72 | "eslint-config-prettier": "^2.9.0",
73 | "eslint-plugin-import": "^2.8.0",
74 | "eslint-plugin-jsx-a11y": "^6.0.3",
75 | "eslint-plugin-react": "^7.6.1",
76 | "eslint-plugin-typescript": "^0.8.1",
77 | "extract-text-webpack-plugin": "^3.0.2",
78 | "file-loader": "^1.1.6",
79 | "gh-pages": "^1.1.0",
80 | "html-webpack-plugin": "^2.30.1",
81 | "husky": "^0.14.3",
82 | "lint-staged": "^6.1.0",
83 | "node-babel": "^0.1.2",
84 | "node-sass": "^4.7.2",
85 | "postcss": "^6.0.16",
86 | "postcss-loader": "^2.0.10",
87 | "prettier": "^1.10.2",
88 | "prop-types": "^15.6.0",
89 | "react-hot-loader": "^3.1.3",
90 | "resolve-url-loader": "^2.2.1",
91 | "rimraf": "^2.6.2",
92 | "sass-loader": "^6.0.6",
93 | "style-loader": "^0.20.1",
94 | "sw-precache-webpack-plugin": "^0.11.4",
95 | "ts-loader": "^3.3.1",
96 | "typescript": "^2.6.2",
97 | "typescript-eslint-parser": "^12.0.0",
98 | "uglifyjs-webpack-plugin": "^1.1.8",
99 | "url-loader": "^0.6.2",
100 | "web3-typescript-typings": "^0.10.2",
101 | "webpack": "^3.10.0",
102 | "webpack-dashboard": "^1.1.1",
103 | "webpack-dev-server": "^2.11.1",
104 | "webpack-manifest-plugin": "^1.3.2",
105 | "webpack-merge": "^4.1.1"
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/contexts/config.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | ///
3 | /* eslint-enable */
4 | import * as React from 'react'
5 | import { initConfigContext, } from '../initValues'
6 | import LOCAL_STORAGE from '../config/localstorage'
7 |
8 | interface ConfigProviderActions {
9 | addServer: (server: string) => boolean;
10 | deleteServer: (idx: number) => boolean;
11 | addPrivkey: (privkey: string) => boolean;
12 | deletePrivkey: (idx: number) => boolean;
13 | changePanelConfig: (configs: any) => boolean;
14 | }
15 |
16 | export type Config = typeof initConfigContext & ConfigProviderActions
17 |
18 | const ConfigContext = React.createContext({
19 | ...initConfigContext,
20 | })
21 |
22 | class ConfigProvider extends React.Component {
23 | readonly state = initConfigContext;
24 | protected setSymbol = (symbol: string) => {
25 | this.setState({ symbol, })
26 | return true
27 | };
28 |
29 | protected addServer = (server: string): boolean => {
30 | const serverList = [...this.state.serverList, ]
31 | if (!serverList.includes(server)) {
32 | const newServerList = [...serverList, server, ]
33 | this.setState({
34 | serverList: newServerList,
35 | })
36 | // side effect
37 | window.localStorage.setItem(
38 | LOCAL_STORAGE.SERVER_LIST,
39 | JSON.stringify(newServerList)
40 | )
41 | return true
42 | }
43 | return false
44 | };
45 |
46 | protected deleteServer = (idx: number): boolean => {
47 | if (!this.state.serverList.length) {
48 | return false
49 | }
50 | const serverList = [...this.state.serverList, ].splice(idx, 1)
51 | this.setState({
52 | serverList,
53 | })
54 | // side effect
55 | window.localStorage.setItem(
56 | LOCAL_STORAGE.SERVER_LIST,
57 | JSON.stringify(serverList)
58 | )
59 | return true
60 | };
61 |
62 | protected addPrivkey = (privkey: string): boolean => {
63 | const privkeyList = [...this.state.privkeyList, ]
64 | if (!privkeyList.includes(privkey)) {
65 | const newPrivkeyList = [...privkeyList, privkey, ]
66 | this.setState({ privkeyList: newPrivkeyList, })
67 | // side effect
68 | window.localStorage.setItem(
69 | LOCAL_STORAGE.PRIV_KEY_LIST,
70 | JSON.stringify(newPrivkeyList)
71 | )
72 | return true
73 | }
74 | return false
75 | };
76 |
77 | protected deletePrivkey = (idx: number): boolean => {
78 | if (!this.state.privkeyList.length) {
79 | return false
80 | }
81 | const privkeyList = [...this.state.privkeyList, ].splice(idx, 1)
82 | this.setState({ privkeyList, })
83 | // side effect
84 | window.localStorage.setItem(
85 | LOCAL_STORAGE.PRIV_KEY_LIST,
86 | JSON.stringify(privkeyList)
87 | )
88 | return true
89 | };
90 |
91 | protected changePanelConfig = newPanelConfigs => {
92 | this.setState({
93 | panelConfigs: newPanelConfigs,
94 | })
95 | // side effect
96 | window.localStorage.setItem(
97 | LOCAL_STORAGE.PANEL_CONFIGS,
98 | JSON.stringify(newPanelConfigs)
99 | )
100 | return true
101 | };
102 |
103 | public render () {
104 | return (
105 |
116 | {this.props.children}
117 |
118 | )
119 | }
120 | }
121 |
122 | export const provideConfig = Comp => props => (
123 |
124 |
125 |
126 | )
127 |
128 | export const withConfig = Comp => props => (
129 |
130 | {config => }
131 |
132 | )
133 |
134 | export default ConfigContext
135 |
--------------------------------------------------------------------------------
/src/components/MetadataPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { List, ListItem, ListItemText, } from '@material-ui/core'
3 | import { translate, } from 'react-i18next'
4 | import { Metadata, } from '../../typings'
5 | import { Loading, } from '../../components/Icons'
6 |
7 | const styles = require('./metadata.scss')
8 | const text = require('../../styles/text.scss')
9 |
10 | const list = [
11 | { name: 'Name', value: 'chainName', },
12 | { name: 'Id', value: 'chainId', },
13 | { name: 'Operator', value: 'operator', },
14 | { name: 'Website', value: 'website', },
15 | { name: 'Genesis Time', value: 'genesisTimestamp', },
16 | { name: 'Version', value: 'version', },
17 | { name: 'Block Interval', value: 'blockInterval', unitName: 'ms', },
18 | { name: 'Token Name', value: 'tokenName', },
19 | { name: 'Token Symbol', value: 'tokenSymbol', },
20 | { name: 'Economical Model', value: 'economicalModel', },
21 | ]
22 |
23 | const MetadataRender = translate('microscope')(
24 | ({ metadata, t, }: { metadata: Metadata; t: (key: string) => string }) => (
25 |
26 | {list.map(item => (
27 |
28 | {t(item.name)}:{' '}
29 |
30 | {metadata[item.value]} {item.unitName || ''}
31 |
32 |
33 | ))}
34 |
35 |
{t('Validators')}:
36 | {metadata.validators && metadata.validators.length ? (
37 |
38 | {metadata.validators.map((validator, index) => (
39 |
40 | {index + 1}: {validator}
41 |
42 | ))}
43 |
44 | ) : null}
45 |
46 |
47 | )
48 | )
49 |
50 | export type ServerList = { serverName: string; serverIp: string }[]
51 |
52 | interface MetadataPanelProps {
53 | metadata: Metadata;
54 | searchIp: string;
55 | searchResult: Metadata;
56 | waitingMetadata: boolean;
57 | inputChainError: boolean;
58 | handleInput: (key: string) => (e: any) => void;
59 | switchChain: (ip?: string, immediate?: boolean) => (e) => void;
60 | handleKeyUp: (e: React.KeyboardEvent) => void;
61 | t: (key: string) => string;
62 | serverList: ServerList;
63 | }
64 |
65 | export const ChainSwitchPanel = ({
66 | metadata,
67 | searchIp,
68 | searchResult,
69 | inputChainError,
70 | waitingMetadata,
71 | handleInput,
72 | handleKeyUp,
73 | switchChain,
74 | t,
75 | serverList,
76 | }) => (
77 |
78 |
79 |
87 |
90 | {inputChainError ? (
91 |
92 | Please enter a URL to AppChain node or ReBirth server
93 |
94 | ) : (
95 |
96 | If you connect to an AppChain node instead of a{' '}
97 |
ReBirth server,
98 | Microscope will NOT be fully functional.
99 |
100 | )}
101 |
102 |
103 | {serverList.map(({ serverName, serverIp, }) => (
104 |
112 |
120 |
121 | ))}
122 |
123 |
124 | )
125 |
--------------------------------------------------------------------------------
/src/containers/Transaction/transaction.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 | @import '../../styles/common.scss';
3 | @import '../../styles/text.scss';
4 |
5 | .infoValue {
6 | word-wrap: break-word;
7 | word-break: break-all;
8 | margin-bottom: 18px;
9 | font-size: 1.125rem !important;
10 | color: #000 !important;
11 | }
12 |
13 | .infoTitle {
14 | font-size: 1.125rem !important;
15 | color: $plainText !important;
16 | padding-bottom: 8px;
17 | }
18 |
19 | .hash {
20 | height: 29px;
21 | margin-top: 17px;
22 | margin-bottom: 12px;
23 | font-size: 1.5rem; // font-weight: bold;
24 | color: $plainText;
25 | word-break: break-all;
26 | }
27 |
28 | .lists {
29 | padding: 23px 28px 0;
30 | }
31 |
32 | .listHeaderRoot {
33 | font-size: 1.25rem !important;
34 | color: $tableText !important;
35 | }
36 |
37 | @media (max-width: 800px) {
38 | .lists {
39 | flex-direction: column;
40 | margin-top: 15px;
41 | }
42 |
43 | .attrs {
44 | display: flex;
45 | flex-direction: column;
46 |
47 | span:last-child {
48 | margin-top: 10px;
49 | }
50 | }
51 | }
52 |
53 | .hashCardRoot {
54 | @include boxShadow;
55 | margin-bottom: 20px;
56 | padding-top: 8px;
57 |
58 | @media (max-width: 800px) {
59 | padding-top: 0;
60 | }
61 | }
62 |
63 | .hashTitle {
64 | font-size: 20px;
65 | margin-right: 5px; // font-size: 24px;
66 |
67 | // @media (max-width: 800px) {
68 | // font-size: 16px;
69 | // }
70 | }
71 |
72 | .hashText {
73 | @extend %hash;
74 | font-size: 20px;
75 | word-wrap: break-word;
76 | box-sizing: border-box;
77 |
78 | @media (max-width: 800px) {
79 | width: 100%;
80 | padding: 0 50px;
81 | line-height: 1.4;
82 | padding-bottom: 5px;
83 | text-align: center;
84 | }
85 | }
86 |
87 | .attrs {
88 | display: flex;
89 | font-size: 18px;
90 |
91 | @media (max-width: 800px) {
92 | font-size: 16px;
93 |
94 | svg,
95 | img {
96 | margin-left: 0 !important;
97 | }
98 | }
99 |
100 | & > span {
101 | flex: 1;
102 | display: flex;
103 | align-items: center;
104 | }
105 |
106 | .attrTitle {
107 | font-size: 1.125rem;
108 | color: $plainText !important;
109 | margin: 0 20px 0 10px;
110 |
111 | @media (max-width: 800px) {
112 | margin-right: 8px;
113 | }
114 | }
115 |
116 | svg {
117 | font-size: 24px;
118 | color: $primary;
119 | margin-left: 24px;
120 | }
121 | }
122 |
123 | .quotaIcon {
124 | height: 24px;
125 | margin-left: 24px;
126 | }
127 |
128 | .listRoot {
129 | margin-right: 31px !important;
130 | }
131 |
132 | .cardContentRoot {
133 | @media (max-width: 800px) {
134 | padding: 0 !important;
135 |
136 | & > div {
137 | margin: 0 !important;
138 | }
139 |
140 | .listHeaderRoot {
141 | line-height: 2;
142 | display: flex;
143 | align-items: center;
144 |
145 | &::before {
146 | display: block;
147 | width: 6px;
148 | height: 6px;
149 | border-radius: 50%;
150 | background-color: $primary;
151 | content: '';
152 | margin-right: 5px;
153 | }
154 | }
155 | }
156 | }
157 |
158 | .detailItem {
159 | & > span:first-child {
160 | display: inline-block;
161 | min-width: 200px;
162 | color: #6f747a;
163 |
164 | &:after {
165 | content: ':';
166 | display: inline;
167 | color: currentColor;
168 | }
169 | }
170 |
171 | & > span:last-child {
172 | color: #47484a;
173 | }
174 |
175 | margin-bottom: 36px;
176 | }
177 |
178 | .dataTypeSwitch {
179 | & > span {
180 | font-size: 12px;
181 | padding: 6px 7px;
182 | cursor: pointer;
183 | color: #979a9e;
184 | border: 1px solid #979a9e;
185 |
186 | &:first-child {
187 | border-top-left-radius: 4px;
188 | border-bottom-left-radius: 4px;
189 | }
190 |
191 | &:last-child {
192 | border-top-right-radius: 4px;
193 | border-bottom-right-radius: 4px;
194 | }
195 | }
196 |
197 | .active {
198 | background: #979a9e !important;
199 | color: #fff;
200 | }
201 | }
202 |
203 | .hexData,
204 | .parameters {
205 | display: block;
206 | width: 482px;
207 | color: #000;
208 | margin-left: 200px;
209 | min-height: 200px;
210 | max-height: 500px;
211 | resize: none;
212 | padding: 15px;
213 | }
214 |
215 | .hexData {
216 | border: 1px solid #e7edf5;
217 | border-radius: 8px;
218 | line-height: 1.8;
219 | }
220 |
221 | .success,
222 | .failure {
223 | svg {
224 | margin-right: 4px;
225 | }
226 | }
227 |
228 | .success {
229 | color: #3dd895;
230 | }
231 |
232 | .failure {
233 | color: #ff8181;
234 | }
235 |
--------------------------------------------------------------------------------
/src/containers/ConfigPage/init.ts:
--------------------------------------------------------------------------------
1 | import { PanelConfigs, } from '../../config/localstorage'
2 | import { withConfig, Config, } from '../../contexts/config'
3 |
4 | enum ConfigType {
5 | DISPLAY,
6 | COUNT,
7 | ITEMS,
8 | VALUE,
9 | }
10 |
11 | enum ConfigPanel {
12 | GENERAL = 'general',
13 | HEADER = 'header',
14 | BLOCK = 'block',
15 | TRANSACTION = 'transaction',
16 | GRAPH = 'graph',
17 | DEBUGGER = 'debugger',
18 | }
19 |
20 | /* eslint-disable no-use-before-define */
21 | interface ConfigDetailType {
22 | panel: ConfigPanel;
23 | type: ConfigType;
24 | key: string;
25 | title: string;
26 | }
27 | /* eslint-enable no-use-before-define */
28 |
29 | const panels = [
30 | // ConfigPanel.GENERAL,
31 | // ConfigPanel.HEADER,
32 | ConfigPanel.BLOCK,
33 | ConfigPanel.TRANSACTION,
34 | ConfigPanel.GRAPH,
35 | ConfigPanel.DEBUGGER,
36 | ]
37 |
38 | const configs = [
39 | {
40 | panel: ConfigPanel.GENERAL,
41 | type: ConfigType.VALUE,
42 | key: 'logo',
43 | title: 'logo',
44 | },
45 | {
46 | panel: ConfigPanel.HEADER,
47 | type: ConfigType.DISPLAY,
48 | key: 'TPS',
49 | title: 'TPS',
50 | },
51 | {
52 | panel: ConfigPanel.BLOCK,
53 | type: ConfigType.DISPLAY,
54 | key: 'blockHeight',
55 | title: 'height',
56 | },
57 | {
58 | panel: ConfigPanel.BLOCK,
59 | type: ConfigType.DISPLAY,
60 | key: 'blockHash',
61 | title: 'hash',
62 | },
63 | {
64 | panel: ConfigPanel.BLOCK,
65 | type: ConfigType.DISPLAY,
66 | key: 'blockAge',
67 | title: 'age',
68 | },
69 | {
70 | panel: ConfigPanel.BLOCK,
71 | type: ConfigType.DISPLAY,
72 | key: 'blockTransactions',
73 | title: 'transactions',
74 | },
75 | {
76 | panel: ConfigPanel.BLOCK,
77 | type: ConfigType.DISPLAY,
78 | key: 'blockQuotaUsed',
79 | title: 'quota used',
80 | },
81 | {
82 | panel: ConfigPanel.BLOCK,
83 | type: ConfigType.VALUE,
84 | key: 'blockPageSize',
85 | title: 'page size',
86 | },
87 | {
88 | panel: ConfigPanel.TRANSACTION,
89 | type: ConfigType.DISPLAY,
90 | key: 'transactionHash',
91 | title: 'hash',
92 | },
93 | {
94 | panel: ConfigPanel.TRANSACTION,
95 | type: ConfigType.DISPLAY,
96 | key: 'transactionFrom',
97 | title: 'from',
98 | },
99 | {
100 | panel: ConfigPanel.TRANSACTION,
101 | type: ConfigType.DISPLAY,
102 | key: 'transactionTo',
103 | title: 'to',
104 | },
105 | {
106 | panel: ConfigPanel.TRANSACTION,
107 | type: ConfigType.DISPLAY,
108 | key: 'transactionValue',
109 | title: 'value',
110 | },
111 | {
112 | panel: ConfigPanel.TRANSACTION,
113 | type: ConfigType.DISPLAY,
114 | key: 'transactionAge',
115 | title: 'age',
116 | },
117 | {
118 | panel: ConfigPanel.TRANSACTION,
119 | type: ConfigType.DISPLAY,
120 | key: 'transactionBlockNumber',
121 | title: 'block height',
122 | },
123 | {
124 | panel: ConfigPanel.TRANSACTION,
125 | type: ConfigType.DISPLAY,
126 | key: 'transactionQuotaUsed',
127 | title: 'quota used',
128 | },
129 | {
130 | panel: ConfigPanel.TRANSACTION,
131 | type: ConfigType.VALUE,
132 | key: 'transactionPageSize',
133 | title: 'page size',
134 | },
135 | {
136 | panel: ConfigPanel.GRAPH,
137 | type: ConfigType.DISPLAY,
138 | key: 'graphIPB',
139 | title: 'Interval/Block',
140 | },
141 | {
142 | panel: ConfigPanel.GRAPH,
143 | type: ConfigType.DISPLAY,
144 | key: 'graphTPB',
145 | title: 'Transactions/Block',
146 | },
147 | {
148 | panel: ConfigPanel.GRAPH,
149 | type: ConfigType.DISPLAY,
150 | key: 'graphQuotaUsedBlock',
151 | title: 'Quota Used/Block',
152 | },
153 | {
154 | panel: ConfigPanel.GRAPH,
155 | type: ConfigType.DISPLAY,
156 | key: 'graphQuotaUsedTx',
157 | title: 'Quota Used/Transaction',
158 | },
159 | {
160 | panel: ConfigPanel.GRAPH,
161 | type: ConfigType.DISPLAY,
162 | key: 'graphProposals',
163 | title: 'Proposals/Validator',
164 | },
165 | {
166 | panel: ConfigPanel.GRAPH,
167 | type: ConfigType.VALUE,
168 | key: 'graphMaxCount',
169 | title: 'MaxCount',
170 | },
171 | {
172 | panel: ConfigPanel.DEBUGGER,
173 | type: ConfigType.DISPLAY,
174 | key: 'debugger',
175 | title: 'Debugger',
176 | },
177 | ] as ConfigDetailType[]
178 |
179 | const ConfigPageDefault = {
180 | configs,
181 | panels,
182 | }
183 |
184 | interface ConfigPageProps {
185 | config: Config;
186 | t: (key: string) => string;
187 | }
188 |
189 | interface ConfigPageState {
190 | configs: PanelConfigs;
191 | inputTimeout?: any;
192 | saving?: boolean;
193 | }
194 |
195 | export {
196 | ConfigPageProps,
197 | ConfigPageState,
198 | ConfigDetailType,
199 | ConfigType,
200 | ConfigPanel,
201 | ConfigPageDefault,
202 | }
203 |
--------------------------------------------------------------------------------
/src/components/TableWithSelector/tableWithSelector.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/color.scss';
2 | @import '../../styles/common.scss';
3 |
4 | // @mixin boxShadow {
5 | // box-shadow: 0 5px 6px 0 rgba(170, 170, 170, 0.07);
6 | // }
7 | @mixin button {
8 | color: #fff;
9 | background: $primary;
10 | border: none;
11 | border-radius: 4px;
12 | padding: 9px 20px;
13 | cursor: pointer;
14 | }
15 |
16 | .container {
17 | @include boxShadow;
18 | margin-top: 30px;
19 | padding: 38px 20px 138px;
20 | }
21 |
22 | .insetContainer {
23 | margin-top: 30px;
24 | }
25 |
26 | .options {
27 | display: flex;
28 | justify-content: space-between;
29 | align-items: flex-end;
30 | border-bottom: 1px solid #e2e9f2;
31 | color: $plainText;
32 | font-size: 0.875rem;
33 | text-transform: capitalize;
34 |
35 | .selector {
36 | color: #fff;
37 | }
38 |
39 | padding-bottom: 5px;
40 | margin-bottom: 28px;
41 |
42 | button {
43 | @include button;
44 | text-transform: inherit;
45 | background: $gradient1;
46 | border-radius: 6px;
47 | height: 2.25rem;
48 | }
49 | }
50 |
51 | .table {
52 | width: 100%;
53 | border-collapse: collapse;
54 | text-align: center;
55 | table-layout: fixed;
56 | margin-bottom: 58px;
57 |
58 | thead {
59 | height: 55px;
60 | line-height: 55px;
61 | font-weight: bolder;
62 | background-color: rgba(38, 71, 253, 0.06);
63 |
64 | th {
65 | text-transform: capitalize; // max-width: 200px;
66 | }
67 | }
68 |
69 | tbody {
70 | tr {
71 | height: 55px;
72 |
73 | &:nth-child(even) {
74 | background-color: rgba(38, 71, 253, 0.03);
75 | }
76 | }
77 |
78 | td {
79 | // max-width: 200px;
80 | padding: 0 15px;
81 | }
82 | }
83 | }
84 |
85 | .pager {
86 | display: flex;
87 | justify-content: flex-end;
88 |
89 | li {
90 | box-sizing: border-box;
91 | padding: 0 10px;
92 | height: 28px;
93 | line-height: 28px;
94 | box-sizing: border-box;
95 | display: flex;
96 | justify-content: center;
97 | align-content: center;
98 | color: $plainText;
99 | border: 1px solid #d9d9d9;
100 | border-radius: 4px;
101 | margin: 0 4px;
102 | cursor: pointer;
103 | }
104 | }
105 |
106 | :global {
107 | .primary {
108 | color: #fff !important;
109 | background-color: $primary !important;
110 | border-color: $primary !important;
111 | }
112 |
113 | .btn-first-page,
114 | .btn-prev-page,
115 | .btn-next-page,
116 | .btn-last-page {
117 | padding: 0 4px !important;
118 | }
119 | }
120 |
121 | .dialog {
122 | padding: 24px 30px;
123 | text-align: right;
124 |
125 | @media (max-width: 800px) {
126 | padding: 24px 30px;
127 | }
128 |
129 | input {
130 | border: 1px solid #d3ddec;
131 | border-radius: 4px;
132 | min-width: 125px;
133 | height: 34px;
134 | line-height: 34px;
135 | border-color: #e7edf5;
136 | background: #fafbff;
137 | flex: 1;
138 | margin-bottom: 23px;
139 | }
140 |
141 | .rangeSelector {
142 | display: flex;
143 | align-items: center;
144 | flex-wrap: wrap;
145 | margin-right: -40px;
146 |
147 | input {
148 | margin-right: 65px;
149 |
150 | @media (max-width: 800px) {
151 | margin-right: 40px;
152 | }
153 |
154 | &::after {
155 | content: '-';
156 | display: block;
157 | position: absolute;
158 | left: 100%;
159 | top: 0;
160 | }
161 | }
162 | }
163 |
164 | button {
165 | @include button;
166 | align-self: flex-end;
167 | background: $gradient1;
168 | }
169 | }
170 |
171 | .dash {
172 | padding: 1rem;
173 | }
174 |
175 | .titles {
176 | display: flex;
177 | flex-direction: column;
178 | justify-content: flex-start;
179 | }
180 |
181 | .title {
182 | flex: 1;
183 | text-transform: capitalize;
184 | display: inline-block;
185 | text-align: right;
186 | margin-right: 0.5rem;
187 | margin-bottom: 23px;
188 | line-height: 46px;
189 |
190 | &:after {
191 | content: ':';
192 | }
193 | }
194 |
195 | .fields {
196 | display: flex;
197 | }
198 |
199 | .inputs {
200 | flex: 1;
201 |
202 | & > div {
203 | display: flex;
204 | flex-wrap: nowrap;
205 | }
206 | }
207 |
208 | // @media (max-width: 800px) {
209 | // .title {
210 | // min-width: 40px;
211 | // }
212 | // }
213 | .buttonContainer {
214 | width: 100%;
215 | }
216 |
217 | .singleSelector {
218 | @media (min-width: 800px) {
219 | input {
220 | min-width: 360px;
221 | margin-right: 50px;
222 | }
223 | }
224 | }
225 |
226 | .inputouter {
227 | & {
228 | position: relative;
229 | }
230 |
231 | input:focus {
232 | border-color: $form-focus;
233 | box-shadow: 0 0 4px 0 $form-focus;
234 | }
235 | }
236 |
237 | .inputouter.error {
238 | // input {
239 | // border: $form-error;
240 | // }
241 | input:focus {
242 | border-color: $form-error;
243 | box-shadow: 0 0 4px 0 $form-error;
244 | }
245 |
246 | .errormessage {
247 | color: $form-error;
248 | position: absolute;
249 | bottom: 0;
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/README-CN.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/cryptape/microscope)
2 | [](https://www.citahub.com/)
3 |
4 | [English](./README.md) | 简体中文
5 |
6 | # 概述
7 |
8 | Microscope提供了一个易于使用的用户界面来查询所有CITA链上信息,可通过在元数据面板中切换目标链。
9 |
10 | # 关于 Microscope
11 |
12 | Microscope一个区块链浏览器,用[React](https://reactjs.org/)构建,用于查询CITA链。 它支持搜索区块,交易,帐户信息和调用智能合约方法。 它还可以与[ReBirth](https://github.com/cryptape/re-birth)一起使用,实现指定组合条件的区块交易列表查询,分析CITA的工作状态等高级功能。
13 |
14 |
15 | ## 功能特性
16 |
17 | - [x] **开源开发**: Welcome广大开发者PR及贡献.
18 | - [x] **多链切换**: 支持在线CITA链间切换
19 | - [x] **智能合约支持**: 提供友好界面调用智能合约方法.
20 | - [x] **用户自定义**: 支持页面展示信息的开关配置.
21 | - [x] **渐进增强**: 可以独立运行, 也可以跟ReBirth项目协作实现更强大的功能[ReBirth](https://github.com/cryptape/re-birth).
22 | - [x] **国际化**: 支持i18n, 默认带中文/Englisgh.
23 |
24 | ## 开始
25 |
26 | - [开发](#开发)
27 |
28 | - [用户手册](#用户手册)
29 |
30 | # 开发
31 |
32 | 1. 下载仓库
33 |
34 | ```shell
35 | git clone https://github.com/cryptape/microscope/
36 | ```
37 |
38 | 2. 安装依赖
39 |
40 | ```bash
41 | yarn install
42 | ```
43 |
44 | 3. 构建dll
45 |
46 | ```shell
47 | yarn run dll
48 | ```
49 |
50 | 4. 添加配置
51 |
52 | ```shell
53 | cp ./.env.example ./.env
54 | ```
55 |
56 | set env variables in `./.env`
57 |
58 | ```
59 | PUBLIC= # public content server address
60 | CHAIN_SERVERS= # default appchain addresses
61 | APP_NAME= # explorer name
62 | DEBUG_ACCOUNTS= # built-in debug account's private key, e.g. 0xaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee,0xaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeea
63 | ```
64 |
65 | > 注意: 我们静态资源所在CDN 在 `https://cdn.cryptape.com/`, 图标图片添加 `PUBLIC=https://cdn.cryptape.com/` 到 `.env`.
66 |
67 | 5. 开发调试
68 |
69 | ```shell
70 | yarn start
71 | ```
72 |
73 | 6. 构建
74 |
75 | ```shell
76 | yarn run build:prod
77 | ```
78 |
79 | ## 使用Docker
80 |
81 | 首先,需要安装docker并且知道如何使用
82 |
83 | 1. 下载
84 |
85 | ```shell
86 | git clone https://github.com/cryptape/microscope/
87 | ```
88 |
89 | 2. 添加配置
90 |
91 | ```shell
92 | cp ./.env.example ./.env
93 | ```
94 |
95 | 设置环境变量 `./.env`
96 |
97 | ```
98 | PUBLIC= # public content server address
99 | CHAIN_SERVERS= # default appchain addresses
100 | APP_NAME= # explorer name
101 | DEBUG_ACCOUNTS= # built-in debug account's private key, e.g. 0xaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee,0xaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeea
102 | ```
103 |
104 | > 注意: 我们静态资源所在CDN 在 `https://cdn.cryptape.com/`, 图标图片添加 `PUBLIC=https://cdn.cryptape.com/` 到 `.env`.
105 |
106 | 修改 nginx 配置 `./nginx.conf.example`
107 |
108 | 4. 启用 Docker Compose. 这一步可能很耗时.
109 |
110 | ```shell
111 | yarn docker:init
112 | ```
113 |
114 | 如果成功,你会在如下地址看到Microscope [0.0.0.0:8089](0.0.0.0:8089).
115 |
116 | 下一次运行, 使用:
117 |
118 | ```shell
119 | yarn docker:start
120 | ```
121 |
122 | 来重新加载.
123 |
124 | # 用户手册
125 |
126 | ## 链设置
127 |
128 | 如果您是第一次访问,则会弹出侧面板,要求您设置要侦听查询的CITA地址
129 |
130 | ## Microscope上的数据信息
131 |
132 | 主要功能包括 **首页信息**, **区块信息**, **交易信息**, **账户信息**, **账户信息**, **配置页面**, 基本都可以通过导航栏访问
133 |
134 | ### 首页信息
135 |
136 | 首页展现 `最新的 10 个区块` 及 `最新的 10 笔交易`.
137 |
138 | ### 区块信息
139 |
140 | > 注意: 该功能只能配合 [ReBirth](https://github.com/cryptape/re-birth)使用, 针对CITA的缓存服务器.
141 |
142 | **区块页面** 展示区块列表, 表格项可在 **配置页面** 配置是否显示
143 |
144 | 过滤功能可通过 **高级选择器**, 可用参数包括 `numberFrom`, `numberTo`, `transactionFrom`, `transactionTo`.
145 |
146 | `numberFrom` 和 `numberTo` 设定块高范围.
147 |
148 | `transactionFrom` 和 `transactionTo` 设定区块所收录的交易数目.
149 |
150 | 区块详情可以点击表格链接查看
151 |
152 |
153 | ### 交易信息
154 |
155 | > 注意: 该功能只能配合 [ReBirth](https://github.com/cryptape/re-birth)使用, 针对CITA的缓存服务器.
156 |
157 | **交易页面** 展现交易列表, 表格项可在 **配置页面** 配置是否显示
158 |
159 | 过滤功能可通过 **高级选择器**, 可用参数包括 `from`, `to`.
160 |
161 | `from` 和 `to` 设定 `transaction.from` and `transaction.to`.
162 |
163 | 交易详情可以点击 `hash` 链接查看
164 |
165 | 区块详情可以点击 `height` 链接查看
166 |
167 | 账户详情可以点击 `from` 和 `to` 链接查看
168 |
169 | data 详情可在 `Hex` 查看,并且若合约上链同时上传 ABI 文件,还可根据 ABI 文件解析成可读文本,从而可在 `Parameters` 查看一些简要信息。
170 |
171 | ### 统计
172 |
173 | > 注意: 该功能只能配合 [ReBirth](https://github.com/cryptape/re-birth)使用, 针对CITA的缓存服务器.
174 |
175 | **统计页面** 展现各类图表, 可在 **配置页面** 配置是否显示
176 |
177 | 当前, **统计页面** 包含 `出块间隔`, `区块交易数`, `区块Quota使用`, `交易Quota使用`, `提案/验证节点` 图表统计.
178 |
179 | ### 账户信息
180 |
181 | **账户页面** 展示 **余额** and **交易记录**, 如果是合约账户并且上传 ABI 文件, 则可以看到 abi 面板.
182 |
183 | ## 其他Widgets
184 |
185 | ### Header面板
186 |
187 | 重要功能在标题右侧显示,它们是 **链名**, **TPS**,**搜索**,**语言**,所有这些都有自己的面板。
188 |
189 |
190 | ### 元数据面板
191 |
192 | 点击 **链名** 可以调出 **元数据面板**
193 |
194 | **元数据面板** 用于查询当前链的元数据信息,或者通过数据新链的IP信息切换到新链.
195 |
196 | ### 统计面板
197 |
198 |
199 | **统计面板** 用于统计当前链的活跃状态.
200 |
201 | ### 搜索面板
202 |
203 | On click of **Search** the **Search Panel** will be called out.
204 |
205 | **搜索面板** 用于检索区块, 交易, 账户信息详情 通过输入hash或者数值.
206 |
207 | ### 语言
208 |
209 | 当前, 更多语言('zh', 'en', 'jp', 'ko', 'de', 'it', 'fr') 可以在语言菜单设置
210 |
211 | ## 其他
212 |
213 | > 通知: 区块详情可以通过访问 `localhost/#/block/:blockHash` 和 `localhost/#/height/:blockNumber`
214 |
215 | > 交易详情可以访问 `localhost/#/transaction/:transactionHash`
216 |
217 | > 账户详情可访问 `localhost/#/account/:accountAddress`
218 |
--------------------------------------------------------------------------------
/src/containers/Debugger/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Grid, } from '@material-ui/core'
3 | import { Chain, } from '@appchain/plugin'
4 | import { unsigner, } from '@cryptape/cita-signer'
5 | import * as EthAccount from 'web3-eth-accounts'
6 |
7 | import StaticCard from '../../components/StaticCard'
8 | import BlockList from '../../components/HomepageLists/BlockList'
9 | import TransactionList from '../../components/HomepageLists/TransactionList'
10 | import DebugAccounts, { DebugAccount, } from '../../components/DebugAccounts'
11 | import ErrorNotification from '../../components/ErrorNotification'
12 |
13 | import { withObservables, } from '../../contexts/observables'
14 |
15 | import hideLoader from '../../utils/hideLoader'
16 | import { IContainerProps, TransactionFromServer, } from '../../typings'
17 | import { handleError, dismissError, } from '../../utils/handleError'
18 | import { getLocalDebugAccounts, setLocalDebugAccounts, } from '../../utils/accessLocalstorage'
19 |
20 | const layout = require('../../styles/layout.scss')
21 | const styles = require('./debugger.scss')
22 |
23 | const ethAccounts = new EthAccount()
24 |
25 | const privateKeysToAccounts = (privateKeys: string[]) =>
26 | privateKeys.map(privateKey => {
27 | const { address, } = ethAccounts.privateKeyToAccount(privateKey)
28 | return {
29 | privateKey,
30 | address,
31 | balance: 'unloaded',
32 | }
33 | })
34 |
35 | const initState = {
36 | accounts: [] as DebugAccount[],
37 | privateKeysField: '',
38 | blocks: [] as Chain.Block[],
39 | transactions: [] as TransactionFromServer[],
40 | error: {
41 | code: '',
42 | message: '',
43 | },
44 | }
45 |
46 | interface DebuggerProps extends IContainerProps {}
47 |
48 | type DebuggerState = typeof initState
49 |
50 | class Debugger extends React.Component {
51 | public readonly state = initState
52 | public componentDidMount () {
53 | hideLoader()
54 | this.loadDebugAccounts()
55 | this.props.CITAObservables.newBlockByNumberSubject.subscribe((block: Chain.Block) => {
56 | if (block.body.transactions.length) {
57 | this.setState((state: DebuggerState) => {
58 | const blocks = [...state.blocks, block, ]
59 | const newTransactions = block.body.transactions.map(tx => {
60 | const unsignedTx = unsigner(tx.content)
61 | return {
62 | blockNumber: block.number,
63 | content: tx.content,
64 | from: unsignedTx.sender.address,
65 | quotaUsed: '',
66 | hash: tx.hash,
67 | timestamp: block.header.timestamp,
68 | to: unsignedTx.transaction.to,
69 | value: +unsignedTx.transaction.value,
70 | }
71 | })
72 | const transactions = [...state.transactions, ...newTransactions, ]
73 | return {
74 | blocks,
75 | transactions,
76 | }
77 | })
78 | }
79 | })
80 | this.props.CITAObservables.newBlockByNumberSubject.subscribe(block => {
81 | // new block comes
82 | this.fetchAndUpdateAccounts(this.state.accounts)
83 | })
84 | }
85 | public loadDebugAccounts = () => {
86 | let privateKeys = getLocalDebugAccounts()
87 | if (!privateKeys.length) {
88 | privateKeys = process.env.DEBUG_ACCOUNTS ? process.env.DEBUG_ACCOUNTS.split(',') : []
89 | }
90 | const accounts = privateKeysToAccounts(privateKeys)
91 | this.setState({ accounts, privateKeysField: privateKeys.join(','), })
92 | this.fetchAndUpdateAccounts(accounts)
93 | }
94 | public updateDebugAccounts = () => {
95 | const { privateKeysField, } = this.state
96 | try {
97 | const privateKeys = Array.from(new Set(privateKeysField.replace(/(\s|\n|\r)+/gi, '').split(',')))
98 | const accounts = privateKeysToAccounts(privateKeys)
99 | setLocalDebugAccounts(privateKeys)
100 | this.fetchAndUpdateAccounts(accounts)
101 | } catch (err) {
102 | window.alert(err)
103 | }
104 | }
105 | public fetchAndUpdateAccounts = (accounts: DebugAccount[]) => {
106 | accounts.forEach((account, idx) => {
107 | this.props.CITAObservables.getBalance({
108 | addr: account.address,
109 | blockNumber: 'latest',
110 | }).subscribe(balance => {
111 | this.setState(state => {
112 | const _accounts = [...accounts, ]
113 | _accounts[idx].balance = `${+balance}`
114 | return { ...state, accounts: _accounts, }
115 | })
116 | })
117 | })
118 | }
119 | private handleInput = key => (e: React.ChangeEvent) => {
120 | const { value, } = e.currentTarget
121 | this.setState(state => ({
122 | ...state,
123 | [key]: value,
124 | }))
125 | }
126 | private handleError = handleError(this)
127 | private dismissError = dismissError(this)
128 | public render () {
129 | return (
130 |
131 |
132 | {window.location.hostname === 'localhost' ? (
133 |
139 | ) : null}
140 | 800 ? 24 : 0}>
141 |
142 |
143 |
144 |
145 |
146 |
147 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | )
161 | }
162 | }
163 | export default withObservables(Debugger)
164 |
--------------------------------------------------------------------------------
/src/containers/BlockTable/index.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-08-02 12:05:46
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2018-08-03 18:28:02
6 | */
7 |
8 | import * as React from 'react'
9 |
10 | import { LinearProgress, } from '@material-ui/core'
11 |
12 | import { IContainerProps, } from '../../typings'
13 | import { BlockFromServer, } from '../../typings/block'
14 | import { withConfig, } from '../../contexts/config'
15 |
16 | import TableWithSelector, {
17 | TableWithSelectorProps,
18 | } from '../../components/TableWithSelector'
19 | import ErrorNotification from '../../components/ErrorNotification'
20 | import Banner from '../../components/Banner'
21 |
22 | import { fetchBlocksV2, } from '../../utils/fetcher'
23 | import { paramsBlocksV2Adapter, } from '../../utils/paramsFilter'
24 | import hideLoader from '../../utils/hideLoader'
25 | import { handleError, dismissError, } from '../../utils/handleError'
26 | import { rangeSelectorText, } from '../../utils/searchTextGen'
27 |
28 | import { initBlockTableState, } from '../../initValues'
29 | import { formatedAgeString, } from '../../utils/timeFormatter'
30 |
31 | interface BlockSelectors {
32 | selectorsValue: {
33 | [index: string]: number | string;
34 | };
35 | }
36 | type BlockTableState = TableWithSelectorProps &
37 | BlockSelectors & {
38 | loading: number;
39 | error: { code: string; message: string };
40 | }
41 | interface BlockTableProps extends IContainerProps {}
42 |
43 | const initialState: BlockTableState = initBlockTableState
44 |
45 | class BlockTable extends React.Component {
46 | readonly state = initialState;
47 | public componentWillMount () {
48 | this.setParamsFromUrl()
49 | this.setVisibleHeaders()
50 | this.setPageSize()
51 | }
52 |
53 | public componentDidMount () {
54 | hideLoader()
55 | this.fetchBlock({
56 | ...this.state.selectorsValue,
57 | offset: this.state.pageNo * this.state.pageSize,
58 | limit: this.state.pageSize,
59 | })
60 | }
61 |
62 | public componentDidCatch (err) {
63 | this.handleError(err)
64 | }
65 |
66 | private onSearch = params => {
67 | this.setState(state =>
68 | Object.assign({}, state, { selectorsValue: params, pageNo: 0, })
69 | )
70 | this.fetchBlock(params)
71 | };
72 |
73 | private setPageSize = () => {
74 | const { blockPageSize: pageSize, } = this.props.config.panelConfigs
75 | this.setState({ pageSize, })
76 | };
77 |
78 | private setVisibleHeaders = () => {
79 | // hide invisible header
80 | this.setState(state => {
81 | const { headers, } = state
82 | const visibleHeaders = headers.filter(
83 | header =>
84 | this.props.config.panelConfigs[
85 | `block${header.key[0].toUpperCase()}${header.key.slice(1)}`
86 | ] !== false
87 | )
88 | return { headers: visibleHeaders, }
89 | })
90 | };
91 |
92 | private setParamsFromUrl = () => {
93 | const actParams = new URLSearchParams(this.props.location.search)
94 | const params = {
95 | numberFrom: '',
96 | numberTo: '',
97 | transactionFrom: '',
98 | transactionTo: '',
99 | pageNo: '',
100 | }
101 | Object.keys(params).forEach(key => {
102 | const value = actParams.get(key)
103 | params[key] = value
104 | })
105 |
106 | const selectorsValue = {}
107 | Object.keys(this.state.selectorsValue).forEach(key => {
108 | selectorsValue[key] = params[key] || this.state.selectorsValue[key]
109 | })
110 |
111 | const pageNo = +params.pageNo >= 1 ? +params.pageNo - 1 : this.state.pageNo
112 |
113 | this.setState({
114 | selectorsValue,
115 | pageNo,
116 | })
117 | };
118 |
119 | private handlePageChanged = newPage => {
120 | const offset = newPage * this.state.pageSize
121 | const limit = this.state.pageSize
122 | this.fetchBlock({
123 | offset,
124 | limit,
125 | ...this.state.selectorsValue,
126 | })
127 | .then(() => {
128 | this.setState({ pageNo: newPage, })
129 | })
130 | .catch(this.handleError)
131 | };
132 |
133 | private fetchBlock = (params: { [index: string]: string | number } = {}) => {
134 | this.setState(state => ({ loading: state.loading + 1, }))
135 | return fetchBlocksV2(paramsBlocksV2Adapter(params))
136 | .then(
137 | ({
138 | result,
139 | }: {
140 | result: { blocks: BlockFromServer[]; }
141 | }) => {
142 | this.setState(state => ({
143 | loading: state.loading - 1,
144 | // count: result.count,
145 | items: result.blocks.map(block => ({
146 | key: block.hash,
147 | height: `${+block.header.number}`,
148 | hash: block.hash,
149 | age: formatedAgeString(block.header.timestamp),
150 | transactions: `${block.transactionsCount}`,
151 | quotaUsed: `${+block.header.quotaUsed}`,
152 | })),
153 | }))
154 | }
155 | )
156 | .catch(err => {
157 | this.handleError(err)
158 | })
159 | };
160 | private handleError = handleError(this);
161 | private dismissError = dismissError(this);
162 |
163 | public render () {
164 | const {
165 | headers,
166 | items,
167 | selectors,
168 | selectorsValue,
169 | pageSize,
170 | pageNo,
171 | loading,
172 | error,
173 | } = this.state
174 | const activeParams = paramsBlocksV2Adapter(selectorsValue) as any
175 | const blockSearchText = rangeSelectorText(
176 | 'Number',
177 | activeParams.numberFrom,
178 | activeParams.numberTo
179 | )
180 | const transactionSearchText = rangeSelectorText(
181 | 'Transaction',
182 | activeParams.transactionFrom,
183 | activeParams.transactionTo
184 | )
185 | const searchText =
186 | blockSearchText && transactionSearchText
187 | ? `${blockSearchText}, ${transactionSearchText}`
188 | : blockSearchText || transactionSearchText
189 |
190 | return (
191 |
192 | {loading ? (
193 |
198 | ) : null}
199 |
200 | {searchText ? `Current Search: ${searchText}` : 'Blocks'}
201 |
202 |
213 |
214 |
215 | )
216 | }
217 | }
218 |
219 | export default withConfig(BlockTable)
220 |
--------------------------------------------------------------------------------
/src/containers/ConfigPage/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { translate, } from 'react-i18next'
3 |
4 | import {
5 | List,
6 | ListItem,
7 | ListItemText,
8 | ListItemSecondaryAction,
9 | TextField,
10 | Typography,
11 | ExpansionPanel,
12 | ExpansionPanelSummary,
13 | ExpansionPanelDetails,
14 | Divider,
15 | Switch,
16 | } from '@material-ui/core'
17 | import { ExpandMore as ExpandMoreIcon, } from '@material-ui/icons'
18 |
19 | import Banner from '../../components/Banner'
20 | import Icon, { Loading, } from '../../components/Icons'
21 |
22 | import { withConfig, } from '../../contexts/config'
23 | import hideLoader from '../../utils/hideLoader'
24 | import {
25 | ConfigPageProps,
26 | ConfigPageState,
27 | ConfigDetailType,
28 | ConfigType,
29 | ConfigPageDefault,
30 | } from './init'
31 |
32 | const layout = require('../../styles/layout.scss')
33 | const styles = require('./config.scss')
34 |
35 | const ConfigDetail = translate('microscope')(
36 | ({
37 | config,
38 | value,
39 | handleSwitch,
40 | handleInput,
41 | controlInputScope,
42 | saving,
43 | t,
44 | }: {
45 | config: ConfigDetailType
46 | value: number | string | boolean | undefined
47 | handleSwitch: (key: string) => (e: any) => void
48 | handleInput: (
49 | key: string
50 | ) => (e: React.ChangeEvent) => void
51 | controlInputScope: any
52 | saving: any
53 | t: (key: string) => string
54 | }) => (
55 |
56 |
59 | {t(config.type === ConfigType.DISPLAY ? 'display' : 'set')}{' '}
60 | {t(config.title)}
61 |
62 | }
63 | />
64 |
65 | {config.type === ConfigType.DISPLAY ? (
66 |
77 | ) : (
78 |
79 |
83 | {saving ? : }
84 |
85 | )}
86 |
87 |
88 | )
89 | )
90 |
91 | const ConfigItem = translate('microscope')(
92 | ({
93 | title,
94 | configs,
95 | values,
96 | handleSwitch,
97 | handleInput,
98 | controlInputScope,
99 | saving,
100 | t,
101 | }: {
102 | title: any;
103 | configs: any;
104 | values: any;
105 | handleSwitch: any;
106 | handleInput: any;
107 | controlInputScope: any;
108 | saving: any;
109 | t: any;
110 | }) => (
111 |
116 | }>
117 |
118 | {t(title)} {t('config')}
119 |
120 |
121 |
122 |
123 |
124 | {configs.map((config, idx) => (
125 |
134 | ))}
135 |
136 |
137 |
138 |
139 | )
140 | )
141 |
142 | class ConfigPage extends React.Component {
143 | static panels = ConfigPageDefault.panels
144 | static configs = ConfigPageDefault.configs
145 |
146 | public constructor (props) {
147 | super(props)
148 | this.state = {
149 | configs: props.config.panelConfigs,
150 | inputTimeout: null,
151 | saving: false,
152 | }
153 | }
154 |
155 | public componentDidMount () {
156 | hideLoader()
157 | }
158 |
159 | private handleSwitch = key => (e?: any) => {
160 | this.setState(state => {
161 | const { configs, } = this.state
162 | const newConfig = { ...configs, [key]: !configs[key], }
163 | if (this.props.config.changePanelConfig(newConfig)) {
164 | return { configs: newConfig, }
165 | }
166 | return state
167 | })
168 | };
169 |
170 | private handleInput = key => (e: React.ChangeEvent) => {
171 | const { value, } = e.currentTarget
172 | this.setState(state => {
173 | const { configs, } = state
174 | const newConfig = { ...configs, [key]: value, }
175 | if (this.props.config.changePanelConfig(newConfig)) {
176 | return { configs: newConfig, }
177 | }
178 | return state
179 | })
180 | }
181 |
182 | private controlInputScope = key => e => {
183 | const { configs, inputTimeout, } = this.state
184 | const { value, } = e.currentTarget
185 | let v = Number(value)
186 | this.setState({ configs: { ...configs, [key]: v, }, saving: true, })
187 |
188 | clearTimeout(inputTimeout)
189 | if (Math.round(v) === v && v >= 10 && v <= 100) {
190 | this.props.config.changePanelConfig({ ...configs, [key]: v, })
191 | this.setState({ saving: false, })
192 | } else {
193 | const t = setTimeout(() => {
194 | if (v < 10) {
195 | v = 10
196 | } else {
197 | v = 100
198 | }
199 | this.props.config.changePanelConfig({ ...configs, [key]: v, })
200 | this.setState({ configs: { ...configs, [key]: v, }, saving: false, })
201 | }, 1000)
202 | this.setState({ inputTimeout: t, saving: true, })
203 | }
204 | };
205 |
206 | public render () {
207 | return (
208 |
209 | Config
210 |
211 | {ConfigPage.panels.map(panel => (
212 | config.panel === panel
217 | )}
218 | values={this.state.configs}
219 | handleSwitch={this.handleSwitch}
220 | handleInput={this.handleInput}
221 | controlInputScope={this.controlInputScope}
222 | saving={this.state.saving}
223 | />
224 | ))}
225 |
226 |
227 | )
228 | }
229 | }
230 |
231 | export default withConfig(ConfigPage)
232 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | module.exports = {
3 | parser: 'typescript-eslint-parser',
4 | parserOptions: {
5 | ecmaVersion: 8,
6 | sourceType: 'module',
7 | },
8 |
9 | env: {
10 | es6: true,
11 | node: true,
12 | browser: true,
13 | },
14 |
15 | extends: ['airbnb', 'prettier'],
16 | plugins: ['react', 'import', 'typescript'],
17 |
18 | globals: {
19 | document: true,
20 | navigator: true,
21 | window: true,
22 | node: true,
23 | },
24 |
25 | rules: {
26 | 'accessor-pairs': 2,
27 | 'arrow-spacing': [2, {
28 | before: true,
29 | after: true
30 | }],
31 | 'block-spacing': [2, 'always'],
32 | 'brace-style': [2, '1tbs', {
33 | allowSingleLine: true
34 | }],
35 | camelcase: [2, {
36 | properties: 'never'
37 | }],
38 | 'comma-spacing': [2, {
39 | before: false,
40 | after: true
41 | }],
42 | 'comma-style': [2, 'last'],
43 | 'comma-dangle': [2, 'always'],
44 | 'constructor-super': 2,
45 | curly: [2, 'multi-line'],
46 | 'dot-location': [2, 'property'],
47 | 'eol-last': 2,
48 | eqeqeq: [2, 'allow-null'],
49 | 'func-call-spacing': [2, 'never'],
50 | 'handle-callback-err': [2, '^(err|error)$'],
51 | indent: [2, 2, {
52 | SwitchCase: 1
53 | }],
54 | 'key-spacing': [2, {
55 | beforeColon: false,
56 | afterColon: true
57 | }],
58 | 'keyword-spacing': [2, {
59 | before: true,
60 | after: true
61 | }],
62 | 'new-cap': [2, {
63 | newIsCap: true,
64 | capIsNew: false
65 | }],
66 | 'new-parens': 2,
67 | 'no-console': 0,
68 | 'no-array-constructor': 2,
69 | 'no-caller': 2,
70 | 'no-class-assign': 2,
71 | 'no-cond-assign': 2,
72 | 'no-const-assign': 2,
73 | 'no-constant-condition': [2, {
74 | checkLoops: false
75 | }],
76 | 'no-control-regex': 2,
77 | 'no-debugger': 2,
78 | 'no-delete-var': 2,
79 | 'no-dupe-args': 2,
80 | 'no-dupe-class-members': 2,
81 | 'no-dupe-keys': 2,
82 | 'no-duplicate-case': 2,
83 | 'no-duplicate-imports': 2,
84 | 'no-empty-character-class': 2,
85 | 'no-empty-pattern': 2,
86 | 'no-eval': 2,
87 | 'no-ex-assign': 2,
88 | 'no-extend-native': 2,
89 | 'no-extra-bind': 2,
90 | 'no-extra-boolean-cast': 2,
91 | 'no-extra-parens': [2, 'functions'],
92 | 'no-fallthrough': 2,
93 | 'no-floating-decimal': 2,
94 | 'no-func-assign': 2,
95 | 'no-global-assign': 2,
96 | 'no-implied-eval': 2,
97 | 'no-inner-declarations': [2, 'functions'],
98 | 'no-invalid-regexp': 2,
99 | 'no-irregular-whitespace': 2,
100 | 'no-iterator': 2,
101 | 'no-label-var': 2,
102 | 'no-labels': [2, {
103 | allowLoop: false,
104 | allowSwitch: false
105 | }],
106 | 'no-lone-blocks': 2,
107 | 'no-mixed-spaces-and-tabs': 2,
108 | 'no-multi-spaces': 2,
109 | 'no-multi-str': 2,
110 | 'no-multiple-empty-lines': [2, {
111 | max: 1
112 | }],
113 | 'no-native-reassign': 2,
114 | 'no-negated-in-lhs': 2,
115 | 'no-new': 2,
116 | 'no-new-func': 2,
117 | 'no-new-object': 2,
118 | 'no-new-require': 2,
119 | 'no-new-symbol': 2,
120 | 'no-new-wrappers': 2,
121 | 'no-obj-calls': 2,
122 | 'no-octal': 2,
123 | 'no-octal-escape': 2,
124 | 'no-path-concat': 2,
125 | 'no-plusplus': 0,
126 | 'no-proto': 2,
127 | 'no-redeclare': 2,
128 | 'no-regex-spaces': 2,
129 | 'no-return-assign': [2, 'except-parens'],
130 | 'no-self-assign': 2,
131 | 'no-self-compare': 2,
132 | 'no-sequences': 2,
133 | 'no-shadow-restricted-names': 2,
134 | 'no-sparse-arrays': 2,
135 | 'no-tabs': 2,
136 | 'no-template-curly-in-string': 2,
137 | 'no-this-before-super': 2,
138 | 'no-throw-literal': 2,
139 | 'no-trailing-spaces': 2,
140 | 'no-undef': 0,
141 | 'no-undef-init': 2,
142 | 'no-unexpected-multiline': 2,
143 | 'no-unmodified-loop-condition': 2,
144 | 'no-unneeded-ternary': [2, {
145 | defaultAssignment: false
146 | }],
147 | 'no-unreachable': 2,
148 | 'no-unsafe-finally': 2,
149 | 'no-unsafe-negation': 2,
150 | 'no-unused-vars': [0, {
151 | vars: 'all',
152 | args: 'none'
153 | }],
154 | 'no-useless-call': 2,
155 | 'no-useless-computed-key': 2,
156 | 'no-useless-constructor': 2,
157 | 'no-useless-escape': 2,
158 | 'no-useless-rename': 2,
159 | 'no-whitespace-before-property': 2,
160 | 'no-with': 2,
161 | 'object-property-newline': [2, {
162 | allowMultiplePropertiesPerLine: true
163 | }],
164 | 'one-var': [2, {
165 | initialized: 'never'
166 | }],
167 | 'operator-linebreak': [
168 | 2,
169 | 'after',
170 | {
171 | overrides: {
172 | '?': 'before',
173 | ':': 'before'
174 | }
175 | },
176 | ],
177 | 'padded-blocks': [2, 'never'],
178 | quotes: [2, 'single', {
179 | avoidEscape: true,
180 | allowTemplateLiterals: true
181 | }],
182 | 'rest-spread-spacing': [2, 'never'],
183 | semi: [2, 'never'],
184 | 'semi-spacing': [2, {
185 | before: false,
186 | after: true
187 | }],
188 | 'space-before-blocks': [2, 'always'],
189 | 'space-before-function-paren': [2, 'always'],
190 | 'space-in-parens': [2, 'never'],
191 | 'space-infix-ops': 2,
192 | 'space-unary-ops': [2, {
193 | words: true,
194 | nonwords: false
195 | }],
196 | 'spaced-comment': [
197 | 2,
198 | 'always',
199 | {
200 | line: {
201 | markers: ['*package', '!', ',']
202 | },
203 | block: {
204 | balanced: true,
205 | markers: ['*package', '!', ','],
206 | exceptions: ['*'],
207 | },
208 | },
209 | ],
210 | 'template-curly-spacing': [2, 'never'],
211 | 'unicode-bom': [2, 'never'],
212 | 'use-isnan': 2,
213 | 'valid-typeof': 2,
214 | 'wrap-iife': [2, 'any'],
215 | 'yield-star-spacing': [2, 'both'],
216 | yoda: [2, 'never'],
217 | 'react/forbid-prop-types': [0],
218 | 'react/prop-types': [0],
219 | 'import/no-extraneous-dependencies': [0],
220 | 'import/no-unresolved': [2, {
221 | commonjs: true,
222 | amd: true
223 | }],
224 | 'import/extensions': [2, 'never'],
225 | // 'react/display-name': 1,
226 | 'react/jsx-filename-extension': [
227 | 1,
228 | {
229 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts']
230 | },
231 | ],
232 | 'jsx-a11y/label-has-for': 0,
233 | 'jsx-a11y/click-events-have-key-events': [0],
234 | 'jsx-a11y/no-static-element-interactions': [0],
235 | 'no-nested-ternary': [0],
236 | 'no-underscore-dangle': [0]
237 | },
238 | settings: {
239 | 'import/resolver': {
240 | node: {
241 | paths: [path.resolve(__dirname, 'src')],
242 | extensions: [
243 | '.ts',
244 | '.tsx',
245 | '.js',
246 | '.jsx',
247 | '.d.ts',
248 | '.scss',
249 | '.svg',
250 | '.png',
251 | '.jpg',
252 | ],
253 | },
254 | },
255 | },
256 | }
257 |
--------------------------------------------------------------------------------
/src/initValues.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: Keith-CY
3 | * @Date: 2018-07-22 19:59:22
4 | * @Last Modified by: Keith-CY
5 | * @Last Modified time: 2019-01-04 11:22:15
6 | */
7 |
8 | import { Chain, } from '@appchain/plugin'
9 | import {
10 | IBlock,
11 | IBlockHeader,
12 | Transaction,
13 | Metadata,
14 | ABI,
15 | UnsignedTransaction,
16 | TransactionFromServer,
17 | } from './typings'
18 | import widerThan from './utils/widerThan'
19 | import { Contract, AccountType, } from './typings/account'
20 | import { LocalAccount, } from './components/LocalAccounts'
21 | import { ServerList, } from './components/MetadataPanel'
22 | import { SelectorType, } from './components/TableWithSelector'
23 | import LOCAL_STORAGE, { PanelConfigs, } from './config/localstorage'
24 | import {
25 | getServerList,
26 | getPrivkeyList,
27 | getPanelConfigs,
28 | } from './utils/accessLocalstorage'
29 | import check, { errorMessages, } from './utils/check'
30 |
31 | const isDesktop = widerThan(800)
32 | export const initHeader: IBlockHeader = {
33 | timestamp: '',
34 | prevHash: '',
35 | number: '',
36 | stateRoot: '',
37 | transactionsRoot: '',
38 | receiptsRoot: '',
39 | quotaUsed: '',
40 | proposer: '',
41 | proof: {
42 | Bft: {
43 | proposal: '',
44 | },
45 | },
46 | }
47 | export const initBlock: IBlock = {
48 | body: {
49 | transactions: [],
50 | },
51 | hash: '',
52 | header: initHeader,
53 | version: 0,
54 | }
55 |
56 | export const initTransaction: Transaction = {
57 | hash: '',
58 | timestamp: '',
59 | content: '',
60 | basicInfo: {
61 | from: '',
62 | to: '',
63 | value: '',
64 | data: '',
65 | },
66 | }
67 | export const initUnsignedTransaction: UnsignedTransaction = {
68 | crypto: 0,
69 | signature: '',
70 | sender: {
71 | address: '',
72 | publicKey: '',
73 | },
74 | transaction: {
75 | data: '',
76 | nonce: '',
77 | quota: 0,
78 | to: '',
79 | validUntilBlock: 0,
80 | value: 0,
81 | version: 0,
82 | },
83 | }
84 | export const initMetadata: Metadata = {
85 | chainId: -1,
86 | chainName: '',
87 | operator: '',
88 | website: '',
89 | genesisTimestamp: '',
90 | validators: [],
91 | blockInterval: 0,
92 | economicalModel: 0,
93 | version: null,
94 | }
95 |
96 | // init config values
97 | export const initPanelConfigs: PanelConfigs = {
98 | logo: 'www.demo.com',
99 | TPS: true,
100 | blockHeight: true,
101 | blockHash: true,
102 | blockAge: isDesktop,
103 | blockTransactions: true,
104 | blockQuotaUsed: isDesktop,
105 | blockPageSize: 10,
106 | transactionHash: true,
107 | transactionFrom: isDesktop,
108 | transactionTo: isDesktop,
109 | transactionValue: isDesktop,
110 | transactionAge: isDesktop,
111 | transactionQuotaUsed: isDesktop,
112 | transactionBlockNumber: true,
113 | transactionPageSize: 10,
114 | graphIPB: true,
115 | graphTPB: true,
116 | graphQuotaUsedBlock: true,
117 | graphQuotaUsedTx: true,
118 | graphProposals: true,
119 | graphMaxCount: 10,
120 | }
121 |
122 | export const initServerList = (process.env.CHAIN_SERVERS || '').split(',')
123 | export const initPrivateKeyList = []
124 | export const initError = { message: '', code: '', }
125 |
126 | export const initHeaderState = {
127 | keyword: '',
128 | metadata: initMetadata,
129 | showMetadata: false,
130 | sidebarNavs: false,
131 | activePanel: window.urlParamChain || '',
132 | searchIp: '',
133 | otherMetadata: initMetadata,
134 | tps: 0,
135 | tpb: 0,
136 | ipb: 0,
137 | peerCount: 0,
138 | block: initBlock,
139 | anchorEl: undefined,
140 | lngOpen: false,
141 | lng: window.localStorage.getItem('i18nextLng'),
142 | inputChainError: false,
143 | waitingMetadata: false,
144 | error: {
145 | code: '',
146 | message: '',
147 | },
148 | overtime: 0,
149 | serverList: [] as ServerList,
150 | }
151 |
152 | export const initAccountState = {
153 | loading: 0,
154 | type: AccountType.NORMAL,
155 | addr: '',
156 | abi: [] as ABI,
157 | code: '0x',
158 | contract: { _jsonInterface: [], methods: [], } as Contract,
159 | balance: '',
160 | txCount: 0,
161 | creator: '',
162 | transactions: [] as Transaction[],
163 | customToken: {
164 | name: '',
165 | },
166 |
167 | normals: [] as LocalAccount[],
168 | erc20s: [] as LocalAccount[],
169 | erc721s: [] as LocalAccount[],
170 | panelOn: 'tx',
171 | addrsOn: false,
172 | normalsAdd: {
173 | name: '',
174 | addr: '',
175 | },
176 | erc20sAdd: {
177 | name: '',
178 | addr: '',
179 | },
180 | erc721sAdd: {
181 | name: '',
182 | addr: '',
183 | },
184 | error: {
185 | code: '',
186 | message: '',
187 | },
188 | copiedIdx: -1,
189 | }
190 |
191 | export const initBlockState = {
192 | loading: 0,
193 | hash: '',
194 | header: {
195 | timestamp: '',
196 | prevHash: '',
197 | number: '',
198 | stateRoot: '',
199 | transactionsRoot: '',
200 | receiptsRoot: '',
201 | quotaUsed: '',
202 | proposer: '',
203 | proof: {
204 | Bft: {
205 | proposal: '',
206 | },
207 | },
208 | },
209 | body: {
210 | transactions: [],
211 | },
212 | version: 0,
213 | transactionsOn: false,
214 | error: initError,
215 | quotaPrice: '',
216 | fee: '',
217 | }
218 |
219 | export const initBlockTableState = {
220 | headers: [
221 | { key: 'height', text: 'height', href: '/height/', },
222 | { key: 'hash', text: 'hash', href: '/block/', },
223 | { key: 'age', text: 'age', },
224 | { key: 'transactions', text: 'transactions', },
225 | { key: 'quotaUsed', text: 'quota used', },
226 | ],
227 | items: [] as any[],
228 | count: 0,
229 | pageSize: 10,
230 | pageNo: 0,
231 | selectors: [
232 | {
233 | type: SelectorType.RANGE,
234 | key: 'number',
235 | text: 'height selector',
236 | items: [
237 | {
238 | key: 'numberFrom',
239 | text: 'Height From',
240 | },
241 | {
242 | key: 'numberTo',
243 | text: 'Height To',
244 | },
245 | ],
246 | check: check.digitsDec,
247 | errorMessage: errorMessages.digits,
248 | },
249 | {
250 | type: SelectorType.RANGE,
251 | key: 'transaction',
252 | text: 'transactions counts',
253 | items: [
254 | {
255 | key: 'transactionFrom',
256 | text: 'From',
257 | },
258 | {
259 | key: 'transactionTo',
260 | text: 'To',
261 | },
262 | ],
263 | check: check.digitsDec,
264 | errorMessage: errorMessages.digits,
265 | },
266 | ],
267 | selectorsValue: {
268 | numberFrom: '',
269 | numberTo: '',
270 | transactionFrom: '',
271 | transactionTo: '',
272 | },
273 | loading: 0,
274 | error: {
275 | code: '',
276 | message: '',
277 | },
278 | }
279 | export const initConfigContext = {
280 | localStorage: LOCAL_STORAGE,
281 | serverList: getServerList(),
282 | privkeyList: getPrivkeyList(),
283 | panelConfigs: getPanelConfigs(),
284 | symbol: '',
285 | setSymbol: (symbol: string) => false,
286 | addServer: server => false,
287 | deleteServer: server => false,
288 | addPrivkey: privkey => false,
289 | deletePrivkey: privkey => false,
290 | changePanelConfig: (config: any) => false,
291 | }
292 |
293 | export const ContractCreation = 'Contract Creation'
294 |
295 | export const initHomePageState = {
296 | loading: 0,
297 | metadata: initMetadata,
298 | overtime: 0,
299 | blocks: [] as Chain.Block[],
300 | transactions: [] as TransactionFromServer[],
301 | showValidators: false,
302 | healthy: {
303 | count: '',
304 | },
305 | error: {
306 | code: '',
307 | message: '',
308 | },
309 | }
310 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/cryptape/microscope)
2 | [](https://www.citahub.com/)
3 |
4 | English | [简体中文](./README-CN.md)
5 |
6 | # Overview
7 |
8 | Microscope provides an easy-to-use user interface to inspect CITA, you can switch target chain in Metadata Panel of the Microscope.
9 |
10 | # About Microscope
11 |
12 | Microscope is a blockchain explorer built with [React](https://reactjs.org/) for inspecting CITA. It supports searching block, transaction, account and invoking call method of smart contract. It also can work with [ReBirth](https://github.com/cryptape/re-birth) to display a list of blocks and transactions on specified conditions, or even analyzes CITA‘s working status.
13 |
14 | ## Features
15 |
16 | - [x] **Open Source Development**: This project is welcome anyone to use and PR.
17 | - [x] **Multi-Chain Switch**: This project supports switch between CITA chains.
18 | - [x] **Smart Contract Support**: This project provides a user friendly interface to call methods of smart contracts.
19 | - [x] **User Customized**: This project supports a config page to specify which value should be displayed.
20 | - [x] **Progressive**: This project is under progressive development, which means it can work independently, and is able to work with [ReBirth](https://github.com/cryptape/re-birth), another project for CITA.
21 | - [x] **Internationalized**: This project supports i18n, default to 中文 and Englisgh.
22 |
23 | ## Getting Started
24 |
25 | - [Development](#development)
26 |
27 | - [Usage](#usage)
28 |
29 | # Development
30 |
31 | 1. clone the repo
32 |
33 | ```shell
34 | git clone https://github.com/cryptape/microscope/ && cd microscope
35 | ```
36 |
37 | 2. Install Dependencies
38 |
39 | ```bash
40 | yarn install
41 | ```
42 |
43 | 3. Build DLL Packages
44 |
45 | ```shell
46 | yarn run dll
47 | ```
48 |
49 | 4. Add Config
50 |
51 | ```shell
52 | cp ./.env.example ./.env
53 | ```
54 |
55 | set env variables in `./.env`
56 |
57 | ```
58 | PUBLIC= # public content server address
59 | CHAIN_SERVERS= # default appchain addresses
60 | APP_NAME= # explorer name
61 | DEBUG_ACCOUNTS= # built-in debug account's private key, e.g. 0xaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee,0xaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeea
62 | ```
63 |
64 | > NOTICE: Our CDN for static assets is available at `https://cdn.cryptape.com/`, namely icons and images can be added by setting `PUBLIC=https://cdn.cryptape.com/` on `.env`.
65 |
66 | 5. Developing
67 |
68 | ```shell
69 | yarn start
70 | ```
71 |
72 | 6. Building
73 |
74 | ```shell
75 | yarn run build:prod
76 | ```
77 |
78 | ## Use Docker
79 |
80 | At first, you should install docker and learn how to use it.
81 |
82 | 1. Clone the repo
83 |
84 | ```shell
85 | git clone https://github.com/cryptape/microscope/
86 | ```
87 |
88 | 2. Add Config
89 |
90 | ```shell
91 | cp ./.env.example ./.env
92 | ```
93 |
94 | set env variables in `./.env`
95 |
96 | ```
97 | PUBLIC= # public content server address
98 | CHAIN_SERVERS= # default appchain addresses
99 | APP_NAME= # explorer name
100 | DEBUG_ACCOUNTS= # built-in debug account's private key, e.g. 0xaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee,0xaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeea
101 | ```
102 |
103 | > NOTICE: Our CDN for static assets is available at `https://cdn.cryptape.com/`, namely icons and images can be added by setting `PUBLIC=https://cdn.cryptape.com/` on `.env`.
104 |
105 | Change nginx config in `./nginx.conf.example`
106 |
107 | 4. Start Docker Compose. This step may take a long time.
108 |
109 | ```shell
110 | yarn docker:init
111 | ```
112 |
113 | If success, you can use Microscope at [0.0.0.0:8089](0.0.0.0:8089).
114 |
115 | Next time, use:
116 |
117 | ```shell
118 | yarn docker:start
119 | ```
120 |
121 | to reload it.
122 |
123 | # Usage
124 |
125 | ## Set CITA
126 |
127 | If you visit the explorer for the first time, the side panel will pop up asking to set CITA address you want to listen to.
128 |
129 | ## Data in Microscope
130 |
131 | The main sections consists of **homepage**, **block**, **transaction**, **account**, **statistics**, **config**, and most of them can be accessed via navigation bar.
132 |
133 | ### Homepage
134 |
135 | On homeage it displays `Latest 10 Blocks` and `Latest 10 Transactions`.
136 |
137 | ### Block
138 |
139 | > NOTICE: This page only works with [ReBirth](https://github.com/cryptape/re-birth), the cache server for CITA.
140 |
141 | **Block Page** show list of blocks, the table items can be specified in **Config Page**
142 |
143 | Filters can be set in **Advanced Selector**, available params are `numberFrom`, `numberTo`, `transactionFrom`, `transactionTo`.
144 |
145 | `numberFrom` and `numberTo` limit the range of block number.
146 |
147 | `transactionFrom` and `transactionTo` limit the range of transaction count in one block.
148 |
149 | Block Detail can be inspected via table link.
150 |
151 |
152 | ### Transaction
153 |
154 | > NOTICE: This page only works with [ReBirth](https://github.com/cryptape/re-birth), the cache server for CITA.
155 |
156 | **Transaction Page** show list of transaction, the table items can be specified in **Config Page**
157 |
158 | Filters can be set in **Advanced Selector**, available params are `from`, `to`.
159 |
160 | `from` and `to` limit `transaction.from` and `transaction.to`.
161 |
162 | Transaction Detail can be inspected via `hash`.
163 |
164 | Block Detail can be inspected via `height`.
165 |
166 | Account Detail can be inspected via `from` and `to`.
167 |
168 | Data Detail can be inspected via `Hex` , if ABI files are uploaded, then according to the ABI files the data can also be parsed into readable text and can be inspected via `Parameters`.
169 |
170 | ### Statisitcs
171 |
172 | > NOTICE: Partial diagrams works with [ReBirth](https://github.com/cryptape/ReBirth), the cache server for CITA.
173 |
174 | **Statistics Page** show list of diagrams, the displaying items can be specified in **Config Page**
175 |
176 | For now, **Statistics Page** includes `Interval/Block`, `Transactions/Block`, `Quota Used/Block`, `Quota Used/Transaction`, `Proposals/Validator` diagrams.
177 |
178 | ### Account
179 |
180 | **Account Page** displays its **balance** and **transaction records**, and if the account is an contract and upload ABI files, the abi panel will also be available.
181 |
182 | ## Other Widgets
183 |
184 | ### Header
185 |
186 | Important Functionalities are shown as badges in the right of header, they are **Chain Name**, **TPS**, **Search**, **Languages**, all of them has their own panel.
187 |
188 | ### Metadata Panel
189 |
190 | On click of **Chain Name** the **Metapata Panel** will be called out.
191 |
192 | The **Metadata Panel** is used to check metadata of active chain, or inspect and switch to other chain by entering IP in the search field.
193 |
194 | ### Statistics Panel
195 |
196 | > NOTICE: this panel is hidden by default, corresponding code is at `src/containers/Header/index.tsx`
197 |
198 | On click of **TPS** the **Statistics Panel** will be called out.
199 |
200 | The **Statistics Panel** is used to inspect current status of active chain.
201 |
202 | ### Search Panel
203 |
204 | On click of **Search** the **Search Panel** will be called out.
205 |
206 | The **Search Panel** is used to inspect block, transaction and account's detail by searching hash or number.
207 |
208 | ### Languages
209 |
210 | For now, some languages('zh', 'en', 'jp', 'ko', 'de', 'it', 'fr') can be set by language menu.
211 |
212 | ## Others
213 |
214 | > NOTICE: Block Detail can be visited `localhost/#/block/:blockHash` and `localhost/#/height/:blockNumber`
215 |
216 | > Transaction Detail can be visited `localhost/#/transaction/:transactionHash`
217 |
218 | > Account Detail can be visited `localhost/#/account/:accountAddress`
219 |
--------------------------------------------------------------------------------