├── .travis.yml ├── README.md ├── client ├── .editorconfig ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── doc │ ├── Makefile │ ├── make.bat │ └── source │ │ ├── conf.py │ │ └── index.rst ├── rust-toolchain ├── rustfmt.toml └── src │ ├── client.rs │ ├── client.yml │ ├── handler.rs │ ├── hardware_usage.rs │ ├── logger │ ├── logger.rs │ ├── macros.rs │ └── mod.rs │ ├── main.rs │ ├── process │ ├── binary_update.rs │ ├── codechain_process.rs │ ├── fs_util.rs │ ├── git_update.rs │ ├── git_util.rs │ ├── mod.rs │ ├── rpc.rs │ └── update.rs │ ├── rpc │ ├── api.rs │ ├── mod.rs │ ├── router.rs │ └── types.rs │ └── types.rs ├── screenshot └── codechain-dashboard.png ├── server ├── .editorconfig ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── create_user_and_db.sql ├── rust-toolchain ├── rustfmt.toml └── src │ ├── bin │ ├── delete-all-table.rs │ ├── generate-schema.rs │ └── refresh-materialized-view.rs │ ├── client │ ├── client.rs │ ├── codechain_rpc.rs │ ├── handler.rs │ ├── mod.rs │ ├── service.rs │ └── types.rs │ ├── common_rpc_types.rs │ ├── cron │ ├── mod.rs │ └── remove_network_usage.rs │ ├── daily_reporter.rs │ ├── db │ ├── event.rs │ ├── mod.rs │ ├── queries │ │ ├── client_extra.rs │ │ ├── config.rs │ │ ├── logs.rs │ │ ├── mod.rs │ │ ├── network_usage.rs │ │ ├── network_usage_graph.rs │ │ └── peer_count.rs │ ├── service.rs │ └── types.rs │ ├── event_propagator.rs │ ├── frontend │ ├── api.rs │ ├── handler.rs │ ├── mod.rs │ ├── service.rs │ └── types.rs │ ├── jsonrpc.rs │ ├── lib.rs │ ├── logger │ ├── logger.rs │ ├── macros.rs │ └── mod.rs │ ├── main.rs │ ├── noti │ ├── mod.rs │ ├── sendgrid.rs │ └── slack.rs │ ├── router.rs │ ├── rpc.rs │ └── util.rs └── ui ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── RequestAgent.ts ├── actions │ ├── chainNetworks.ts │ ├── graph.ts │ ├── log.ts │ └── nodeInfo.ts ├── components │ ├── App │ │ ├── App.scss │ │ ├── App.test.tsx │ │ └── App.tsx │ ├── Dashboard │ │ ├── ConnectGraphContainer │ │ │ ├── ConnectionGraphContainer.scss │ │ │ └── ConnectionGraphContainer.tsx │ │ ├── ConnectionGraph │ │ │ └── ConnectionGraph.tsx │ │ ├── Dashboard.scss │ │ └── Dashboard.tsx │ ├── GlobalNavigationBar │ │ ├── GlobalNavigationBar.scss │ │ ├── GlobalNavigationBar.tsx │ │ └── img │ │ │ └── arrow.svg │ ├── Graph │ │ ├── Graph.tsx │ │ ├── GraphNode │ │ │ ├── GraphNode.tsx │ │ │ ├── NetworkOutNodeExtensionGraph │ │ │ │ ├── NetworkOutNodeExtensionGraph.scss │ │ │ │ └── NetworkOutNodeExtensionGraph.tsx │ │ │ └── NetworkOutNodePeerGraph │ │ │ │ ├── NetworkOutNodePeerGraph.scss │ │ │ │ └── NetworkOutNodePeerGraph.tsx │ │ ├── NetworkOutAllAVGGraph │ │ │ ├── NetworkOutAllAVGGraph.scss │ │ │ └── NetworkOutAllAVGGraph.tsx │ │ └── NetworkOutAllGraph │ │ │ ├── NetworkOutAllGraph.scss │ │ │ └── NetworkOutAllGraph.tsx │ ├── Header │ │ ├── Header.scss │ │ ├── Header.tsx │ │ └── img │ │ │ └── logo.png │ ├── Log │ │ ├── LeftFilter │ │ │ ├── ColorPicker │ │ │ │ ├── ColorPicker.scss │ │ │ │ └── ColorPicker.tsx │ │ │ ├── LeftFilter.scss │ │ │ └── LeftFilter.tsx │ │ ├── Log.scss │ │ ├── Log.tsx │ │ ├── LogViewer │ │ │ ├── LogItem │ │ │ │ ├── LogItem.scss │ │ │ │ └── LogItem.tsx │ │ │ ├── LogViewer.scss │ │ │ └── LogViewer.tsx │ │ └── TopFilter │ │ │ ├── TopFilter.scss │ │ │ └── TopFilter.tsx │ ├── NodeList │ │ ├── NodeDetailContainer │ │ │ ├── NodeDetail │ │ │ │ ├── NodeDetail.scss │ │ │ │ ├── NodeDetail.tsx │ │ │ │ └── StartNodeModal │ │ │ │ │ ├── StartNodeModal.scss │ │ │ │ │ └── StartNodeModal.tsx │ │ │ ├── NodeDetailContainer.scss │ │ │ └── NodeDetailContainer.tsx │ │ ├── NodeList.tsx │ │ ├── NodeListContainer │ │ │ ├── NodeItem │ │ │ │ ├── NodeItem.scss │ │ │ │ └── NodeItem.tsx │ │ │ ├── NodeListContainer.scss │ │ │ ├── NodeListContainer.test.tsx │ │ │ ├── NodeListContainer.tsx │ │ │ └── SelectNodesModal │ │ │ │ ├── SelectNodeCheckbox.tsx │ │ │ │ ├── SelectNodesModal.scss │ │ │ │ └── SelectNodesModal.tsx │ │ └── UpgradeNodeModal │ │ │ ├── UpgradeNodeModal.scss │ │ │ └── UpgradeNodeModal.tsx │ └── RPC │ │ ├── RPC.scss │ │ ├── RPC.tsx │ │ ├── RPCLeftPanel │ │ ├── RPCLeftPanel.scss │ │ └── RPCLeftPanel.tsx │ │ └── RPCRightPanel │ │ ├── RPCRightPanel.scss │ │ └── RPCRightPanel.tsx ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── reducers │ ├── chainNetworks.ts │ ├── graph.ts │ ├── index.ts │ ├── log.ts │ └── nodeInfo.ts ├── registerServiceWorker.ts ├── requests │ ├── index.ts │ └── types.ts ├── setupTests.ts ├── styles │ ├── _variables.scss │ └── index.scss └── utils │ ├── getStatusClass.ts │ └── storage.ts ├── tsconfig.json ├── tsconfig.test.json ├── tslint.json └── yarn.lock /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - 1.42.0 4 | jobs: 5 | include: 6 | - name: server 7 | before_install: 8 | - cd $TRAVIS_JOB_NAME 9 | install: 10 | - rustup set profile minimal 11 | - rustup toolchain install nightly-2020-02-23 12 | - rustup component add rustfmt-preview --toolchain nightly-2020-02-23 13 | - rustup component add clippy-preview --toolchain nightly-2020-02-23 14 | before_script: 15 | - cargo fetch --verbose 16 | script: 17 | - cargo +nightly-2020-02-23 fmt -- --check 18 | && RUST_BACKTRACE=1 cargo test --verbose --all 19 | && cargo +nightly-2020-02-23 clippy --all --all-targets -- -D warnings 20 | - name: client 21 | before_install: 22 | - cd $TRAVIS_JOB_NAME 23 | install: 24 | - rustup set profile minimal 25 | - rustup toolchain install nightly-2020-02-23 26 | - rustup component add rustfmt-preview --toolchain nightly-2020-02-23 27 | - rustup component add clippy-preview --toolchain nightly-2020-02-23 28 | before_script: 29 | - cargo fetch --verbose 30 | script: 31 | - cargo +nightly-2020-02-23 fmt -- --check 32 | && RUST_BACKTRACE=1 cargo test --verbose --all 33 | && cargo +nightly-2020-02-23 clippy --all --all-targets -- -D warnings 34 | - name: ui 35 | before_install: 36 | - cd $TRAVIS_JOB_NAME 37 | - nvm install 10 38 | - nvm use 10 39 | - npm install -g yarn 40 | install: 41 | - yarn install 42 | script: 43 | - yarn lint 44 | - yarn build 45 | cache: 46 | cargo: true 47 | directories: 48 | - "$HOME/.rustup" 49 | before_cache: 50 | - rm -rf $HOME/.cargo/registry 51 | - rm -rf $TRAVIS_BUILD_DIR/target 52 | git: 53 | depth: 1 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeChain Dashboard [![Build Status](https://travis-ci.com/CodeChain-io/codechain-dashboard.svg?branch=master)](https://travis-ci.com/CodeChain-io/codechain-dashboard) 2 | 3 | ![Screen Shot](./screenshot/codechain-dashboard.png?raw=true "Screen Shot") -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*.rs] 3 | indent_style=space 4 | indent_size=4 5 | tab_width=8 6 | end_of_line=lf 7 | charset=utf-8 8 | trim_trailing_whitespace=true 9 | max_line_length=120 10 | insert_final_newline=true 11 | 12 | [*.yml] 13 | indent_style=space 14 | indent_size=4 15 | tab_width=8 16 | end_of_line=lf 17 | charset=utf-8 18 | trim_trailing_whitespace=true 19 | insert_final_newline=true 20 | 21 | [.travis.yml] 22 | indent_style=space 23 | indent_size=2 24 | tab_width=8 25 | end_of_line=lf 26 | charset=utf-8 27 | 28 | [*.json] 29 | indent_style=space 30 | indent_size=2 31 | tab_width=4 32 | end_of_line=lf 33 | charset=utf-8 34 | trim_trailing_whitespace=true 35 | insert_final_newline=true 36 | 37 | 38 | [*.toml] 39 | indent_style=space 40 | indent_size=4 41 | tab_width=8 42 | end_of_line=lf 43 | charset=utf-8 44 | trim_trailing_whitespace=true 45 | insert_final_newline=true 46 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | doc/build/ 2 | 3 | # Generated by Cargo 4 | **/target/ 5 | 6 | **/*.rs.bk 7 | **/*.iml 8 | .idea/ 9 | /db/ 10 | /snapshot/ 11 | /log/ 12 | /keys/ 13 | 14 | # macOS 15 | .DS_store 16 | .vscode/ 17 | /build/ 18 | -------------------------------------------------------------------------------- /client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "codechain-dashboard-client" 3 | version = "0.1.0" 4 | authors = ["CodeChain Team "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | atty = "0.2" 9 | clap = { version = "*", features = ["yaml"] } 10 | colored = "1.6" 11 | crossbeam = "0.4" 12 | env_logger = "0.5.7" 13 | jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.11" } 14 | libc = "0.2" 15 | log = "0.4.1" 16 | parking_lot = "0.7.1" 17 | reopen = "0.2.2" 18 | reqwest = "0.9.0" 19 | serde = "1.0" 20 | serde_derive = "1.0" 21 | serde_json = "1.0" 22 | subprocess = "0.1.18" 23 | sysinfo = "0.6.1" 24 | systemstat = "0.1.3" 25 | time = "0.1" 26 | tokio = "0.1.18" 27 | tokio-codec = "0.1.1" 28 | tokio-uds = "0.2.5" 29 | toml = "0.5.5" 30 | ws = "*" 31 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # CodeChain Dashboard Client 2 | 3 | ## Requirements 4 | 5 | The following are the software dependencies required to install and run CodeChain-Dashboard-Client: 6 | 7 | - [CodeChain](https://github.com/CodeChain-io/codechain) 8 | - [CodeChain-agent-server](../server) 9 | 10 | ### Install dependencies (Ubuntu) 11 | 12 | ``` 13 | sudo apt install pkg-config libssl-dev 14 | ``` 15 | 16 | ## Run 17 | 18 | To run CodeChain-Dashboard-Client, just run 19 | 20 | ``` 21 | cargo run -- --agent-hub-url --codechain-dir --codechain-p2p-address --name 22 | ``` 23 | 24 | ## Formatting 25 | 26 | Make sure you run `rustfmt` before creating a PR to the repo. You need to install the nightly-2018-12-06 version of `rustfmt`. 27 | 28 | ``` 29 | rustup toolchain install nightly-2019-10-13 30 | rustup component add rustfmt-preview --toolchain nightly-2019-10-13 31 | ``` 32 | 33 | To run `rustfmt`, 34 | 35 | ``` 36 | cargo +nightly-2019-10-13 fmt 37 | ``` 38 | 39 | ## User Manual 40 | 41 | Under `docs` folder, run following command. 42 | 43 | ``` 44 | make html 45 | ``` 46 | 47 | User manual will be generated at `docs/_build/html`. 48 | -------------------------------------------------------------------------------- /client/doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = CodeChainAgentProtocol 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /client/doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=CodeChainAgentProtocol 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /client/doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = u'CodeChainAgentProtocol' 23 | copyright = u'2018, Park Juhyung' 24 | author = u'Park Juhyung' 25 | 26 | # The short X.Y version 27 | version = u'' 28 | # The full version, including alpha/beta/rc tags 29 | release = u'' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | # 59 | # This is also used if you do content translation via gettext catalogs. 60 | # Usually you set "language" from the command line for these cases. 61 | language = None 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | # This pattern also affects html_static_path and html_extra_path . 66 | exclude_patterns = [] 67 | 68 | # The name of the Pygments (syntax highlighting) style to use. 69 | pygments_style = 'sphinx' 70 | 71 | 72 | # -- Options for HTML output ------------------------------------------------- 73 | 74 | # The theme to use for HTML and HTML Help pages. See the documentation for 75 | # a list of builtin themes. 76 | # 77 | html_theme = 'alabaster' 78 | 79 | # Theme options are theme-specific and customize the look and feel of a theme 80 | # further. For a list of options available for each theme, see the 81 | # documentation. 82 | # 83 | # html_theme_options = {} 84 | 85 | # Add any paths that contain custom static files (such as style sheets) here, 86 | # relative to this directory. They are copied after the builtin static files, 87 | # so a file named "default.css" will overwrite the builtin "default.css". 88 | html_static_path = ['_static'] 89 | 90 | # Custom sidebar templates, must be a dictionary that maps document names 91 | # to template names. 92 | # 93 | # The default sidebars (for documents that don't match any pattern) are 94 | # defined by theme itself. Builtin themes are using these templates by 95 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 96 | # 'searchbox.html']``. 97 | # 98 | # html_sidebars = {} 99 | 100 | 101 | # -- Options for HTMLHelp output --------------------------------------------- 102 | 103 | # Output file base name for HTML help builder. 104 | htmlhelp_basename = 'CodeChainAgentProtocoldoc' 105 | 106 | 107 | # -- Options for LaTeX output ------------------------------------------------ 108 | 109 | latex_elements = { 110 | # The paper size ('letterpaper' or 'a4paper'). 111 | # 112 | # 'papersize': 'letterpaper', 113 | 114 | # The font size ('10pt', '11pt' or '12pt'). 115 | # 116 | # 'pointsize': '10pt', 117 | 118 | # Additional stuff for the LaTeX preamble. 119 | # 120 | # 'preamble': '', 121 | 122 | # Latex figure (float) alignment 123 | # 124 | # 'figure_align': 'htbp', 125 | } 126 | 127 | # Grouping the document tree into LaTeX files. List of tuples 128 | # (source start file, target name, title, 129 | # author, documentclass [howto, manual, or own class]). 130 | latex_documents = [ 131 | (master_doc, 'CodeChainAgentProtocol.tex', u'CodeChainAgentProtocol Documentation', 132 | u'Park Juhyung', 'manual'), 133 | ] 134 | 135 | 136 | # -- Options for manual page output ------------------------------------------ 137 | 138 | # One entry per manual page. List of tuples 139 | # (source start file, name, description, authors, manual section). 140 | man_pages = [ 141 | (master_doc, 'codechainagentprotocol', u'CodeChainAgentProtocol Documentation', 142 | [author], 1) 143 | ] 144 | 145 | 146 | # -- Options for Texinfo output ---------------------------------------------- 147 | 148 | # Grouping the document tree into Texinfo files. List of tuples 149 | # (source start file, target name, title, author, 150 | # dir menu entry, description, category) 151 | texinfo_documents = [ 152 | (master_doc, 'CodeChainAgentProtocol', u'CodeChainAgentProtocol Documentation', 153 | author, 'CodeChainAgentProtocol', 'One line description of project.', 154 | 'Miscellaneous'), 155 | ] -------------------------------------------------------------------------------- /client/rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.42.0 2 | -------------------------------------------------------------------------------- /client/rustfmt.toml: -------------------------------------------------------------------------------- 1 | indent_style = "Block" 2 | use_small_heuristics = "Off" # "Default" 3 | binop_separator = "Front" 4 | # combine_control_expr = true 5 | comment_width = 120 # 80 6 | condense_wildcard_suffixes = true # false 7 | control_brace_style = "AlwaysSameLine" 8 | # disable_all_formatting = false 9 | error_on_line_overflow = false # true 10 | # error_on_unformatted = false 11 | fn_args_layout = "Tall" 12 | brace_style = "PreferSameLine" # "SameLineWhere" 13 | empty_item_single_line = true 14 | enum_discrim_align_threshold = 0 15 | fn_single_line = false 16 | # where_single_line = false 17 | force_explicit_abi = true 18 | format_strings = false 19 | format_macro_matchers = false 20 | format_macro_bodies = true 21 | hard_tabs = false 22 | imports_indent = "Block" # "Visual" 23 | imports_layout = "Mixed" 24 | merge_imports = false 25 | match_block_trailing_comma = false 26 | max_width = 120 # 100 27 | merge_derives = true 28 | # force_multiline_blocks = false 29 | newline_style = "Unix" 30 | normalize_comments = false 31 | remove_nested_parens = true 32 | reorder_imports = true 33 | reorder_modules = true 34 | # reorder_impl_items = false 35 | # report_todo = "Never" 36 | # report_fixme = "Never" 37 | # skip_children = false 38 | space_after_colon = true 39 | space_before_colon = false 40 | struct_field_align_threshold = 0 41 | spaces_around_ranges = false 42 | ## struct_lit_single_line = true 43 | tab_spaces = 4 44 | trailing_comma = "Vertical" 45 | trailing_semicolon = false # true 46 | # type_punctuation_density = "Wide" 47 | use_field_init_shorthand = true # false 48 | use_try_shorthand = true # false 49 | # format_code_in_doc_comments = false 50 | wrap_comments = false 51 | match_arm_blocks = true 52 | overflow_delimited_expr = true 53 | blank_lines_upper_bound = 2 # 1 54 | blank_lines_lower_bound = 0 55 | # required_version 56 | hide_parse_errors = false 57 | color = "Always" # "Auto" 58 | unstable_features = false 59 | # license_template_path 60 | # ignore 61 | edition = "2018" 62 | # version 63 | normalize_doc_attributes = true # false 64 | inline_attribute_width = 0 65 | -------------------------------------------------------------------------------- /client/src/client.rs: -------------------------------------------------------------------------------- 1 | use super::handler::WebSocketHandler; 2 | use super::hardware_usage::HardwareService; 3 | use super::logger::init as logger_init; 4 | use super::process::{self, ProcessOption}; 5 | use super::rpc::api::add_routing; 6 | use super::rpc::router::Router; 7 | use super::types::{ClientArgs, HandlerContext}; 8 | use std::cell::Cell; 9 | use std::rc::Rc; 10 | use std::sync::Arc; 11 | use std::thread; 12 | use std::time::Duration; 13 | use ws::connect; 14 | 15 | pub fn run(args: &ClientArgs<'_>) { 16 | logger_init().expect("Logger should be initialized"); 17 | 18 | let count = Rc::new(Cell::new(0)); 19 | 20 | let mut router = Arc::new(Router::new()); 21 | add_routing(Arc::get_mut(&mut router).unwrap()); 22 | 23 | let process = process::spawn(ProcessOption { 24 | codechain_dir: args.codechain_dir.to_string(), 25 | log_file_path: args.log_file_path.to_string(), 26 | }); 27 | 28 | let hardware_service = HardwareService::run_thread(); 29 | 30 | let context = Arc::new(HandlerContext { 31 | codechain_address: args.codechain_address, 32 | name: args.name.to_string(), 33 | process, 34 | hardware_service, 35 | }); 36 | 37 | loop { 38 | let count = count.clone(); 39 | let router = router.clone(); 40 | let context = context.clone(); 41 | cinfo!(MAIN, "Connect to {}", args.hub_url); 42 | if let Err(err) = connect(args.hub_url, move |out| WebSocketHandler { 43 | out, 44 | count: count.clone(), 45 | router: router.clone(), 46 | context: context.clone(), 47 | }) { 48 | cerror!(MAIN, "Error from websocket {}", err); 49 | } 50 | cinfo!(MAIN, "Unconnected from Hub"); 51 | thread::sleep(Duration::new(1, 0)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/src/client.yml: -------------------------------------------------------------------------------- 1 | name: codechain-dashboard-client 2 | version: "0.1.0" 3 | version_short: "v" 4 | author: CodeChain Team 5 | about: CodeChain Dashboard Client 6 | args: 7 | - codechain-dir: 8 | long: codechain-dir 9 | help: CodeChain's repository 10 | required: true 11 | takes_value: true 12 | - log-file: 13 | long: log-file 14 | help: log file will be saved to the path 15 | required: false 16 | takes_value: true 17 | - agent-hub-url: 18 | long: agent-hub-url 19 | help: URL of Agent Hub. ex) "ws://127.0.0.1:4012" 20 | required: true 21 | takes_value: true 22 | - codechain-p2p-address: 23 | long: codechain-p2p-address 24 | short: a 25 | help: Codechain node's p2p address. ex) "127.0.0.1" 26 | required: true 27 | takes_value: true 28 | - name: 29 | long: name 30 | short: n 31 | help: Dashboard Client's name. This will be present in the Dashboard. 32 | required: true 33 | takes_value: true 34 | -------------------------------------------------------------------------------- /client/src/logger/logger.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use env_logger::filter::{Builder as FilterBuilder, Filter}; 3 | use log::{LevelFilter, Log, Metadata, Record}; 4 | use std::{env, thread}; 5 | 6 | pub struct Logger { 7 | filter: Filter, 8 | } 9 | 10 | impl Logger { 11 | pub fn new() -> Self { 12 | let mut builder = FilterBuilder::new(); 13 | builder.filter(None, LevelFilter::Info); 14 | 15 | if let Ok(rust_log) = env::var("RUST_LOG") { 16 | builder.parse(&rust_log); 17 | } 18 | 19 | Self { 20 | filter: builder.build(), 21 | } 22 | } 23 | 24 | pub fn filter(&self) -> LevelFilter { 25 | self.filter.filter() 26 | } 27 | } 28 | 29 | impl Log for Logger { 30 | fn enabled(&self, metadata: &Metadata<'_>) -> bool { 31 | self.filter.enabled(metadata) 32 | } 33 | 34 | fn log(&self, record: &Record<'_>) { 35 | if self.filter.matches(record) { 36 | let thread_name = thread::current().name().unwrap_or_default().to_string(); 37 | let timestamp = time::strftime("%Y-%m-%d %H:%M:%S %Z", &time::now()).unwrap(); 38 | 39 | let stderr_isatty = atty::is(atty::Stream::Stderr); 40 | let timestamp = if stderr_isatty { 41 | timestamp.bold() 42 | } else { 43 | timestamp.normal() 44 | }; 45 | let thread_name = if stderr_isatty { 46 | thread_name.blue().bold() 47 | } else { 48 | thread_name.normal() 49 | }; 50 | let log_level = record.level(); 51 | let log_target = record.target(); 52 | let log_message = record.args(); 53 | eprintln!("#{} {} {} {} {}", timestamp, thread_name, log_level, log_target, log_message); 54 | } 55 | } 56 | 57 | fn flush(&self) {} 58 | } 59 | -------------------------------------------------------------------------------- /client/src/logger/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! log_target { 3 | (PROCESS) => { 4 | "client-process" 5 | }; 6 | (MAIN) => { 7 | "client-main" 8 | }; 9 | (WEB) => { 10 | "client-web" 11 | }; 12 | (HARDWARE) => { 13 | "client-hardware" 14 | }; 15 | } 16 | 17 | #[macro_export] 18 | macro_rules! clog { 19 | ($target:ident, $lvl:expr, $($arg:tt)+) => ({ 20 | log::log!(target: log_target!($target), $lvl, $($arg)*); 21 | }); 22 | } 23 | 24 | #[macro_export] 25 | macro_rules! cerror { 26 | ($target:ident, $($arg:tt)*) => ( 27 | clog!($target, $crate::logger::Level::Error, $($arg)*) 28 | ); 29 | } 30 | 31 | #[macro_export] 32 | macro_rules! cwarn { 33 | ($target:ident, $($arg:tt)*) => ( 34 | clog!($target, $crate::logger::Level::Warn, $($arg)*) 35 | ); 36 | } 37 | 38 | #[macro_export] 39 | macro_rules! cinfo { 40 | ($target:ident, $($arg:tt)*) => ( 41 | clog!($target, $crate::logger::Level::Info, $($arg)*) 42 | ); 43 | } 44 | 45 | #[macro_export] 46 | macro_rules! cdebug { 47 | ($target:ident, $($arg:tt)*) => ( 48 | clog!($target, $crate::logger::Level::Debug, $($arg)*) 49 | ); 50 | } 51 | 52 | #[macro_export] 53 | macro_rules! ctrace { 54 | ($target:ident, $($arg:tt)*) => ( 55 | clog!($target, $crate::logger::Level::Trace, $($arg)*) 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /client/src/logger/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(feature = "cargo-clippy", allow(clippy::module_inception))] 2 | mod logger; 3 | #[macro_use] 4 | pub mod macros; 5 | 6 | use self::logger::Logger; 7 | pub use log::Level; 8 | use log::SetLoggerError; 9 | 10 | pub fn init() -> Result<(), SetLoggerError> { 11 | let logger = Logger::new(); 12 | log::set_max_level(logger.filter()); 13 | log::set_boxed_logger(Box::new(logger)) 14 | } 15 | -------------------------------------------------------------------------------- /client/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod logger; 3 | mod client; 4 | mod handler; 5 | mod hardware_usage; 6 | mod process; 7 | mod rpc; 8 | mod types; 9 | 10 | use self::client::run; 11 | use clap::load_yaml; 12 | use types::ClientArgs; 13 | 14 | fn main() { 15 | let yaml = load_yaml!("client.yml"); 16 | let matches = clap::App::from_yaml(yaml).get_matches(); 17 | 18 | let codechain_dir = matches.value_of("codechain-dir").expect("codechain-dir is required option"); 19 | let log_file_path = matches.value_of("log-file").unwrap_or("codechain.log"); 20 | let hub_url = matches.value_of("agent-hub-url").expect("agent-hub-url is required option"); 21 | let codechain_address = 22 | matches.value_of("codechain-p2p-address").expect("codechain-p2p-address is required option"); 23 | let codechain_address = codechain_address.parse().expect("codechain-p2p-address field's format is invalid"); 24 | let name = matches.value_of("name").expect("name is required option"); 25 | 26 | let args = ClientArgs { 27 | codechain_dir, 28 | log_file_path, 29 | hub_url, 30 | codechain_address, 31 | name, 32 | }; 33 | run(&args); 34 | } 35 | -------------------------------------------------------------------------------- /client/src/process/binary_update.rs: -------------------------------------------------------------------------------- 1 | use super::update::{CallbackResult, Sender}; 2 | use super::{fs_util, Error}; 3 | use std::thread; 4 | 5 | pub struct Job {} 6 | 7 | impl Job { 8 | pub fn run( 9 | codechain_dir: String, 10 | binary_url: String, 11 | binary_checksum: String, 12 | callback: crossbeam::Sender, 13 | ) -> Sender { 14 | thread::Builder::new() 15 | .name("binary update job".to_string()) 16 | .spawn(move || { 17 | let result = Self::update(codechain_dir, &binary_url, &binary_checksum); 18 | callback.send(result); 19 | }) 20 | .expect("Should success running update job thread") 21 | } 22 | 23 | fn update(codechain_dir: String, binary_url: &str, binary_checksum: &str) -> Result<(), Error> { 24 | if let Err(err) = fs_util::move_file(&codechain_dir, "codechain", "codechain.backup") { 25 | cwarn!(PROCESS, "Cannot move file codechain to codechain.backup: {:?}", err); 26 | } 27 | match Self::update_inner(&codechain_dir, binary_url, binary_checksum) { 28 | Ok(()) => Ok(()), 29 | Err(err) => { 30 | if let Err(move_err) = fs_util::move_file(&codechain_dir, "codechain.backup", "codechain") { 31 | cwarn!(PROCESS, "Cannot move file codechain.backup to codechain: {:?}", move_err); 32 | } 33 | Err(err) 34 | } 35 | } 36 | } 37 | 38 | fn update_inner(codechain_dir: &str, binary_url: &str, binary_checksum: &str) -> Result<(), Error> { 39 | fs_util::download_codechain(&codechain_dir, binary_url)?; 40 | fs_util::check_checksum(&codechain_dir, binary_checksum)?; 41 | fs_util::make_executable(&codechain_dir)?; 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/src/process/codechain_process.rs: -------------------------------------------------------------------------------- 1 | use super::ProcessOption; 2 | use parking_lot::Mutex; 3 | use reopen::Reopen; 4 | use std::fs::OpenOptions; 5 | use std::io::{self, Read, Write}; 6 | use std::path::Path; 7 | use std::sync::Arc; 8 | use std::thread; 9 | use std::time::Duration; 10 | use subprocess::{Exec, ExitStatus, Popen, PopenError, Redirection}; 11 | 12 | #[derive(Clone)] 13 | pub struct CodeChainProcess { 14 | process: Arc>, 15 | } 16 | 17 | impl CodeChainProcess { 18 | pub fn new(envs: Vec<(&str, &str)>, args: Vec, option: &ProcessOption) -> Result { 19 | let log_file_path = option.log_file_path.clone(); 20 | let mut file = 21 | Reopen::new(Box::new(move || OpenOptions::new().append(true).create(true).open(log_file_path.clone()))) 22 | .map_err(|err| err.to_string())?; 23 | file.handle().register_signal(libc::SIGHUP).unwrap(); 24 | 25 | let mut exec = if Path::new(&option.codechain_dir).join("codechain").exists() { 26 | Exec::cmd("./codechain") 27 | .cwd(option.codechain_dir.clone()) 28 | .stdout(Redirection::Pipe) 29 | .stderr(Redirection::Merge) 30 | .args(&args) 31 | } else { 32 | Exec::cmd("cargo") 33 | .arg("run") 34 | .arg("--") 35 | .cwd(option.codechain_dir.clone()) 36 | .stdout(Redirection::Pipe) 37 | .stderr(Redirection::Merge) 38 | .args(&args) 39 | }; 40 | 41 | for (k, v) in envs { 42 | exec = exec.env(k, v); 43 | } 44 | 45 | let child = exec.popen().map_err(|err| err.to_string())?; 46 | 47 | let process = CodeChainProcess { 48 | process: Arc::new(Mutex::new(child)), 49 | }; 50 | 51 | let process_in_thread = process.clone(); 52 | 53 | thread::Builder::new() 54 | .name("codechain_log_writer".to_string()) 55 | .spawn(move || { 56 | let mut buf: [u8; 1024] = [0; 1024]; 57 | loop { 58 | let length = match process_in_thread.read(&mut buf) { 59 | Ok(length) => length, 60 | Err(err) => { 61 | cerror!(PROCESS, "Fail to read stdout of CodeChain : {}", err); 62 | return 63 | } 64 | }; 65 | 66 | if let Err(err) = file.write_all(&buf[0..length]) { 67 | cerror!(PROCESS, "Fail to write stdout of CodeChain : {}", err); 68 | return 69 | } 70 | } 71 | }) 72 | .expect("Should success running process thread"); 73 | 74 | Ok(process) 75 | } 76 | 77 | pub fn read(&self, buf: &mut [u8]) -> Result { 78 | let mut process = self.process.lock(); 79 | process.stdout.as_mut().expect("Process opened with pipe").read(buf) 80 | } 81 | 82 | pub fn is_running(&self) -> bool { 83 | let mut process = self.process.lock(); 84 | process.poll().is_none() 85 | } 86 | 87 | pub fn terminate(&self) -> Result<(), io::Error> { 88 | let mut process = self.process.lock(); 89 | process.terminate() 90 | } 91 | 92 | pub fn wait_timeout(&self, duration: Duration) -> Result, PopenError> { 93 | let mut process = self.process.lock(); 94 | process.wait_timeout(duration) 95 | } 96 | 97 | pub fn kill(&self) -> Result<(), io::Error> { 98 | let mut process = self.process.lock(); 99 | process.kill() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/src/process/fs_util.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use std::path::Path; 3 | use subprocess::Exec; 4 | 5 | pub fn move_file(dir: &str, from: &str, to: &str) -> Result<(), Error> { 6 | cdebug!(PROCESS, "Move {} to {}", from, to); 7 | let exec = Exec::cmd("mv").arg(from).arg(to).cwd(dir).capture()?; 8 | if exec.exit_status.success() { 9 | Ok(()) 10 | } else { 11 | Err(Error::ShellError { 12 | exit_code: exec.exit_status, 13 | stdout: exec.stdout_str(), 14 | stderr: exec.stderr_str(), 15 | }) 16 | } 17 | } 18 | 19 | pub fn download_codechain(codechain_dir: &str, codechain_url: &str) -> Result<(), Error> { 20 | cdebug!(PROCESS, "RUN wget {}", codechain_url); 21 | let exec = Exec::cmd("wget").arg(codechain_url).cwd(codechain_dir).capture()?; 22 | if exec.exit_status.success() { 23 | Ok(()) 24 | } else { 25 | Err(Error::ShellError { 26 | exit_code: exec.exit_status, 27 | stdout: exec.stdout_str(), 28 | stderr: exec.stderr_str(), 29 | }) 30 | } 31 | } 32 | 33 | pub fn make_executable(codechain_dir: &str) -> Result<(), Error> { 34 | cdebug!(PROCESS, "Run cmod +x codechain"); 35 | let exec = Exec::cmd("chmod").arg("+x").arg("codechain").cwd(codechain_dir).capture()?; 36 | if exec.exit_status.success() { 37 | Ok(()) 38 | } else { 39 | Err(Error::ShellError { 40 | exit_code: exec.exit_status, 41 | stdout: exec.stdout_str(), 42 | stderr: exec.stderr_str(), 43 | }) 44 | } 45 | } 46 | 47 | pub fn check_checksum(codechain_dir: &str, binary_checksum: &str) -> Result<(), Error> { 48 | cdebug!(PROCESS, "Run shasum codechain | awk '{{ print $1 }}'"); 49 | let shasum = Exec::cmd("shasum").arg("codechain").cwd(codechain_dir); 50 | let get_1_column = Exec::cmd("awk").arg("{ print $1 }").cwd(codechain_dir); 51 | let calculated_checksum = { shasum | get_1_column }.capture()?; 52 | 53 | if !calculated_checksum.exit_status.success() { 54 | return Err(Error::ShellError { 55 | exit_code: calculated_checksum.exit_status, 56 | stdout: calculated_checksum.stdout_str(), 57 | stderr: "".to_string(), 58 | }) 59 | } 60 | 61 | if calculated_checksum.stdout_str().trim() != binary_checksum.trim() { 62 | return Err(Error::BinaryChecksumMismatch { 63 | expected: binary_checksum.trim().to_string(), 64 | actual: calculated_checksum.stdout_str().trim().to_string(), 65 | }) 66 | } 67 | 68 | Ok(()) 69 | } 70 | 71 | pub fn get_checksum_or_default(dir: &str, file: &str) -> Result { 72 | let path = Path::new(dir).join(file); 73 | if !path.exists() { 74 | return Ok("".to_string()) 75 | } 76 | 77 | cdebug!(PROCESS, "Run shasum {:?}", path); 78 | let exec = Exec::cmd("shasum").arg("-a").arg("256").arg(file).cwd(dir).capture()?; 79 | 80 | if exec.exit_status.success() { 81 | Ok(exec.stdout_str().trim().to_string()) 82 | } else { 83 | Err(Error::ShellError { 84 | exit_code: exec.exit_status, 85 | stdout: exec.stdout_str(), 86 | stderr: exec.stderr_str(), 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /client/src/process/git_update.rs: -------------------------------------------------------------------------------- 1 | use super::super::types::CommitHash; 2 | use super::update::{CallbackResult, Sender}; 3 | use super::{git_util, Error}; 4 | use std::thread; 5 | 6 | pub struct Job {} 7 | 8 | impl Job { 9 | pub fn run(codechain_dir: String, commit_hash: CommitHash, callback: crossbeam::Sender) -> Sender { 10 | thread::Builder::new() 11 | .name("git update job".to_string()) 12 | .spawn(move || { 13 | let result = Self::update(codechain_dir, &commit_hash); 14 | callback.send(result); 15 | }) 16 | .expect("Should success running update job thread") 17 | } 18 | 19 | fn update(codechain_dir: String, commit_hash: &str) -> Result<(), Error> { 20 | git_util::remote_update(codechain_dir.clone())?; 21 | git_util::reset_hard(codechain_dir.clone(), commit_hash.to_string())?; 22 | let current_hash = git_util::current_hash(codechain_dir)?; 23 | if commit_hash != current_hash { 24 | cwarn!(PROCESS, "Updated commit hash not matched expected {} found {}", commit_hash, current_hash); 25 | Err(Error::Unknown(format!("Cannot update to {}", commit_hash))) 26 | } else { 27 | Ok(()) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/src/process/git_util.rs: -------------------------------------------------------------------------------- 1 | use super::super::types::CommitHash; 2 | use super::{Error, Exec}; 3 | 4 | pub fn current_hash(codechain_dir: String) -> Result { 5 | cdebug!(PROCESS, "Run git rev-parse HEAD at {}", codechain_dir); 6 | let result = match Exec::cmd("git").arg("rev-parse").arg("HEAD").cwd(codechain_dir).capture() { 7 | Ok(exec) => exec.stdout_str().trim().to_string(), 8 | Err(err) => { 9 | cwarn!(PROCESS, "Cannot get git hash {}", err); 10 | "NONE".to_string() 11 | } 12 | }; 13 | Ok(result) 14 | } 15 | 16 | pub fn remote_update(codechain_dir: String) -> Result<(), Error> { 17 | cinfo!(PROCESS, "Run git remote update"); 18 | let exec = Exec::cmd("git").arg("remote").arg("update").cwd(codechain_dir).capture()?; 19 | if exec.exit_status.success() { 20 | ctrace!(PROCESS, "git remote update\n stdout: {}\n stderr: {}\n", exec.stdout_str(), exec.stderr_str()); 21 | Ok(()) 22 | } else { 23 | Err(Error::ShellError { 24 | exit_code: exec.exit_status, 25 | stdout: exec.stdout_str(), 26 | stderr: exec.stderr_str(), 27 | }) 28 | } 29 | } 30 | 31 | pub fn reset_hard(codechain_dir: String, target_commit_hash: CommitHash) -> Result<(), Error> { 32 | cinfo!(PROCESS, "Run git reset --hard"); 33 | let exec = Exec::cmd("git").arg("reset").arg("--hard").arg(target_commit_hash).cwd(codechain_dir).capture()?; 34 | if exec.exit_status.success() { 35 | ctrace!(PROCESS, "git remote update\n stdout: {}\n stderr: {}\n", exec.stdout_str(), exec.stderr_str()); 36 | Ok(()) 37 | } else { 38 | Err(Error::ShellError { 39 | exit_code: exec.exit_status, 40 | stdout: exec.stdout_str(), 41 | stderr: exec.stderr_str(), 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/src/process/rpc.rs: -------------------------------------------------------------------------------- 1 | use jsonrpc_core::types::Version; 2 | use parking_lot::Mutex; 3 | use serde_json::{Error as SerdeError, Value}; 4 | use std::io::Error as IoError; 5 | use std::path::Path; 6 | use std::sync::Arc; 7 | use tokio::io::{write_all, AsyncRead}; 8 | use tokio::prelude::future::Future; 9 | use tokio::prelude::stream::Stream; 10 | use tokio::runtime::Runtime; 11 | use tokio_codec::{FramedRead, LinesCodec}; 12 | use tokio_uds::UnixStream; 13 | 14 | #[derive(Debug)] 15 | pub enum CallRPCError { 16 | Serde(SerdeError), 17 | Io(IoError), 18 | NoResponse, 19 | } 20 | 21 | impl ::std::fmt::Display for CallRPCError { 22 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 23 | match self { 24 | CallRPCError::Serde(err) => err.fmt(f), 25 | CallRPCError::Io(err) => err.fmt(f), 26 | CallRPCError::NoResponse => write!(f, "CodeChain doesn't respond"), 27 | } 28 | } 29 | } 30 | 31 | pub struct RPCClient { 32 | path: String, 33 | } 34 | 35 | impl RPCClient { 36 | pub fn new(path: String) -> Self { 37 | Self { 38 | path, 39 | } 40 | } 41 | 42 | /// Return JSONRPC response object 43 | /// Example: {"jsonrpc": "2.0", "result": 19, "id": 1} 44 | pub fn call_rpc(&self, method: String, arguments: Vec) -> Result { 45 | let jsonrpc_request = jsonrpc_core::MethodCall { 46 | jsonrpc: Some(Version::V2), 47 | method, 48 | params: Some(jsonrpc_core::Params::Array(arguments)), 49 | id: jsonrpc_core::Id::Num(1), 50 | }; 51 | 52 | ctrace!(PROCESS, "Send JSONRPC to CodeChain {:#?}", jsonrpc_request); 53 | 54 | if !Path::new(&self.path).exists() { 55 | cerror!(PROCESS, "IPC file does not exist, please check CodeChain's config whether ipc is disabled or not"); 56 | } 57 | 58 | let mut rt = Runtime::new()?; 59 | 60 | let response = Arc::new(Mutex::new(None)); 61 | let last_error = Arc::new(Mutex::new(None)); 62 | 63 | let body = serde_json::to_vec(&jsonrpc_request)?; 64 | let cloned_response = Arc::clone(&response); 65 | let cloned_last_error = Arc::clone(&last_error); 66 | let stream = UnixStream::connect(&self.path).map_err(CallRPCError::from).and_then(|stream| { 67 | let (read, write) = stream.split(); 68 | let framed_read = FramedRead::new(read, LinesCodec::new()); 69 | 70 | write_all(write, body).map_err(CallRPCError::from).and_then(move |_| { 71 | framed_read 72 | .map_err(CallRPCError::from) 73 | .filter_map(move |s| match serde_json::from_str(&s) { 74 | Ok(json) => Some(json), 75 | Err(err) => { 76 | *cloned_last_error.lock() = Some(err); 77 | None 78 | } 79 | }) 80 | .take(1) 81 | .for_each(move |response| { 82 | *cloned_response.lock() = Some(response); 83 | Ok(()) 84 | }) 85 | }) 86 | }); 87 | 88 | // TODO: Remove the below thread blocking code. 89 | rt.block_on(stream)?; 90 | 91 | let mut response = response.lock(); 92 | let mut last_error = last_error.lock(); 93 | 94 | if let Some(result) = response.take() { 95 | ctrace!(PROCESS, "Receive JSONRPC response from CodeChain {:#?}", result); 96 | Ok(result) 97 | } else if let Some(err) = last_error.take() { 98 | Err(err.into()) 99 | } else { 100 | Err(CallRPCError::NoResponse) 101 | } 102 | } 103 | } 104 | 105 | impl From for CallRPCError { 106 | fn from(err: SerdeError) -> Self { 107 | CallRPCError::Serde(err) 108 | } 109 | } 110 | 111 | impl From for CallRPCError { 112 | fn from(err: IoError) -> Self { 113 | CallRPCError::Io(err) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /client/src/process/update.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use std::thread::JoinHandle; 3 | 4 | pub type Sender = JoinHandle<()>; 5 | pub type CallbackResult = Result<(), Error>; 6 | -------------------------------------------------------------------------------- /client/src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod router; 3 | pub mod types; 4 | -------------------------------------------------------------------------------- /client/src/rpc/router.rs: -------------------------------------------------------------------------------- 1 | use super::super::types::HandlerContext; 2 | use super::types::{RPCError, RPCResult}; 3 | use serde::de::Deserialize; 4 | use serde::Serialize; 5 | use serde_json::{self, Value}; 6 | use std::collections::HashMap; 7 | 8 | pub trait Route { 9 | fn run(&self, context: &HandlerContext, value: Value) -> RPCResult; 10 | } 11 | 12 | pub struct Router { 13 | table: HashMap<&'static str, Box>, 14 | } 15 | 16 | impl Route for fn(&HandlerContext, Arg) -> RPCResult 17 | where 18 | Res: Serialize, 19 | for<'de> Arg: Deserialize<'de>, 20 | { 21 | fn run(&self, context: &HandlerContext, value: Value) -> RPCResult { 22 | let arg = serde_json::from_value(value)?; 23 | let result = self(context, arg)?; 24 | if let Some(result) = result { 25 | Ok(Some(serde_json::to_value(result)?)) 26 | } else { 27 | Ok(None) 28 | } 29 | } 30 | } 31 | 32 | impl Route for fn(&HandlerContext) -> RPCResult 33 | where 34 | Res: Serialize, 35 | { 36 | fn run(&self, context: &HandlerContext, _value: Value) -> RPCResult { 37 | let result = self(context)?; 38 | if let Some(result) = result { 39 | let value_result = serde_json::to_value(result)?; 40 | Ok(Some(value_result)) 41 | } else { 42 | Ok(None) 43 | } 44 | } 45 | } 46 | 47 | pub enum Error { 48 | MethodNotFound, 49 | RPC(RPCError), 50 | } 51 | 52 | impl Router { 53 | pub fn new() -> Self { 54 | let table: HashMap<&'static str, Box> = HashMap::new(); 55 | Self { 56 | table, 57 | } 58 | } 59 | 60 | pub fn add_route(&mut self, method: &'static str, route: Box) { 61 | self.table.insert(method, route); 62 | } 63 | 64 | pub fn run(&self, context: &HandlerContext, method: &str, arg: Value) -> Result, Error> { 65 | let route = self.table.get(method); 66 | match route { 67 | None => Err(Error::MethodNotFound), 68 | Some(route) => match route.run(context, arg) { 69 | Ok(value) => Ok(value), 70 | Err(err) => Err(Error::RPC(err)), 71 | }, 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /client/src/types.rs: -------------------------------------------------------------------------------- 1 | use super::hardware_usage::HardwareService; 2 | use super::process::Message as ProcessMessage; 3 | use crossbeam::Sender; 4 | use std::net::IpAddr; 5 | 6 | pub type CommitHash = String; 7 | 8 | pub struct ClientArgs<'a> { 9 | pub codechain_dir: &'a str, 10 | pub log_file_path: &'a str, 11 | pub hub_url: &'a str, 12 | pub codechain_address: IpAddr, 13 | pub name: &'a str, 14 | } 15 | 16 | pub struct HandlerContext { 17 | pub process: Sender, 18 | pub codechain_address: IpAddr, 19 | pub name: String, 20 | pub hardware_service: HardwareService, 21 | } 22 | -------------------------------------------------------------------------------- /screenshot/codechain-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeChain-io/codechain-dashboard/882bf546584eb8da0524412d20c1e0b2f0cc019a/screenshot/codechain-dashboard.png -------------------------------------------------------------------------------- /server/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*.rs] 3 | indent_style=space 4 | indent_size=4 5 | tab_width=8 6 | end_of_line=lf 7 | charset=utf-8 8 | trim_trailing_whitespace=true 9 | max_line_length=120 10 | insert_final_newline=true 11 | 12 | [*.yml] 13 | indent_style=space 14 | indent_size=4 15 | tab_width=8 16 | end_of_line=lf 17 | charset=utf-8 18 | trim_trailing_whitespace=true 19 | insert_final_newline=true 20 | 21 | [.travis.yml] 22 | indent_style=space 23 | indent_size=2 24 | tab_width=8 25 | end_of_line=lf 26 | charset=utf-8 27 | 28 | [*.json] 29 | indent_style=space 30 | indent_size=2 31 | tab_width=4 32 | end_of_line=lf 33 | charset=utf-8 34 | trim_trailing_whitespace=true 35 | insert_final_newline=true 36 | 37 | 38 | [*.toml] 39 | indent_style=space 40 | indent_size=4 41 | tab_width=8 42 | end_of_line=lf 43 | charset=utf-8 44 | trim_trailing_whitespace=true 45 | insert_final_newline=true 46 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo lock in subs 2 | **/Cargo.lock 3 | 4 | # Generated by Cargo 5 | **/target/ 6 | 7 | **/*.rs.bk 8 | **/*.iml 9 | .idea/ 10 | /db/ 11 | /snapshot/ 12 | /log/ 13 | /keys/ 14 | 15 | # macOS 16 | .DS_store 17 | -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "codechain-dashboard-server" 3 | version = "0.1.0" 4 | authors = ["CodeChain Team "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | atty = "0.2" 9 | chrono = { version = "0.4", features = ["serde"] } 10 | colored = "1.6" 11 | env_logger = "0.5.7" 12 | iron = "*" 13 | jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.11" } 14 | lazy_static = "1.3.0" 15 | log = "0.4.1" 16 | parking_lot = "0.7.1" 17 | postgres = { version = "0.15", features = ["with-chrono"] } 18 | primitives = { git = "https://github.com/CodeChain-io/rust-codechain-primitives.git", version = "0.4.0" } 19 | rand = "0.5.5" 20 | regex = "1" 21 | sendgrid = "0.8.1" 22 | serde = "1.0" 23 | serde_derive = "1.0" 24 | serde_json = "1.0" 25 | slack-hook = "0.8.0" 26 | time = "0.1" 27 | ws = "*" 28 | r2d2_postgres = "0.14.0" 29 | r2d2 = "0.8.6" 30 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | CodeChain Dashboard Server 2 | ========================== 3 | 4 | [![Gitter](https://badges.gitter.im/CodeChain-io.svg)](https://gitter.im/CodeChain-io/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 5 | 6 | CodeChain Dashboard Server is a server which collects many CodeChain node's information(best block, pending transactions, log, ...). Also, CodeChain Dashboard Server serves collected data to CodeChain Dashboard. 7 | 8 | Install 9 | -------- 10 | 11 | You should set up a rust toolchain. 12 | 13 | You can install codechain-dashboard-server by running `cargo install` 14 | 15 | Install Postgres and create schema 16 | ----------------- 17 | 18 | Ubuntu 19 | ``` 20 | sudo apt install postgresql postgresql-contrib 21 | sudo -u postgres psql -f create_user_and_db.sql 22 | generate-schema 23 | ``` 24 | 25 | Mac (brew) 26 | ``` 27 | brew install postgresql 28 | brew services start postgresql 29 | psql postgres -f create_user_and_db.sql 30 | generate-schema 31 | ``` 32 | 33 | Run 34 | ---- 35 | 36 | Just run `codechain-dashboard-server` in your shell. 37 | To safely communicate with the Dashboard, please set the `PASSPHRASE` environment variable. The Dashboard program should use the same passphrase. 38 | Also, you should set `NETWORK_ID` environment variable to print the network id in log messages. 39 | 40 | When you are using the `PASSPHRASE` you should use SSL over the connection. If you don't use the SSL, the `PASSPHRASE` is open to the internet. 41 | 42 | CodeChain Dashboard Server will listen 3012 port to communicate with the Dashboard using JSON-RPC. 43 | 44 | CodeChain Dashboard Server will listen 4012 port to communicate with the Dashboard Client using JSON-RPC. 45 | 46 | Alerts 47 | ------- 48 | 49 | The server sends an alert via Slack and Email in situations where there likely is a problem. 50 | 51 | ## Email alerts 52 | To use email alerts, the server needs the [Sendgird](https://sendgrid.com/) api key. 53 | ``` 54 | SENDGRID_API_KEY={api key} SENDGRID_TO={email address} codechain-dashboard-server 55 | ``` 56 | 57 | ## Slack alerts 58 | The server uses [webhooks](https://api.slack.com/incoming-webhooks) 59 | ``` 60 | SLACK_HOOK_URL={web hook url} codechain-dashboard-server 61 | ``` 62 | 63 | Environmental Variables 64 | ------------------------ 65 | 66 | | NAME | DESCRIPTION | 67 | | ------------------- | ------------------------------------------------------------------------------------------------------------------ | 68 | | START_AT_CONNECT | If this variable is set, a CodeChain instance is started once a dashboard client connects to the dashboard server. | 69 | | NETWORK_ID | Network ID information that is used in error messages or logs. | 70 | | SLACK_WEBHOOK_URL | Used to send alarms to Slack. | 71 | | SENDGRID_TO | An email address to receive alarm emails. | 72 | | SENDGRID_API_KEY | An API Key that is used to send alarms. | 73 | | PASSPHRASE | A passphrase that is used to communicate with the Dashboard safely. | 74 | | ENABLE_MEMORY_ALARM | When this variable is set, the Dashboard Server sends memory alarms. | 75 | -------------------------------------------------------------------------------- /server/create_user_and_db.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE "codechain-dashboard-server"; 2 | CREATE USER "codechain-dashboard-server" WITH ENCRYPTED PASSWORD 'preempt-entreat-bell-chanson'; 3 | GRANT ALL PRIVILEGES ON DATABASE "codechain-dashboard-server" TO "codechain-dashboard-server"; -------------------------------------------------------------------------------- /server/rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.42.0 2 | -------------------------------------------------------------------------------- /server/rustfmt.toml: -------------------------------------------------------------------------------- 1 | indent_style = "Block" 2 | use_small_heuristics = "Off" # "Default" 3 | binop_separator = "Front" 4 | # combine_control_expr = true 5 | comment_width = 120 # 80 6 | condense_wildcard_suffixes = true # false 7 | control_brace_style = "AlwaysSameLine" 8 | # disable_all_formatting = false 9 | error_on_line_overflow = false # true 10 | # error_on_unformatted = false 11 | fn_args_layout = "Tall" 12 | brace_style = "PreferSameLine" # "SameLineWhere" 13 | empty_item_single_line = true 14 | enum_discrim_align_threshold = 0 15 | fn_single_line = false 16 | # where_single_line = false 17 | force_explicit_abi = true 18 | format_strings = false 19 | format_macro_matchers = false 20 | format_macro_bodies = true 21 | hard_tabs = false 22 | imports_indent = "Block" # "Visual" 23 | imports_layout = "Mixed" 24 | merge_imports = false 25 | match_block_trailing_comma = false 26 | max_width = 120 # 100 27 | merge_derives = true 28 | # force_multiline_blocks = false 29 | newline_style = "Unix" 30 | normalize_comments = false 31 | remove_nested_parens = true 32 | reorder_imports = true 33 | reorder_modules = true 34 | # reorder_impl_items = false 35 | # report_todo = "Never" 36 | # report_fixme = "Never" 37 | # skip_children = false 38 | space_after_colon = true 39 | space_before_colon = false 40 | struct_field_align_threshold = 0 41 | spaces_around_ranges = false 42 | ## struct_lit_single_line = true 43 | tab_spaces = 4 44 | trailing_comma = "Vertical" 45 | trailing_semicolon = false # true 46 | # type_punctuation_density = "Wide" 47 | use_field_init_shorthand = true # false 48 | use_try_shorthand = true # false 49 | # format_code_in_doc_comments = false 50 | wrap_comments = false 51 | match_arm_blocks = true 52 | overflow_delimited_expr = true 53 | blank_lines_upper_bound = 2 # 1 54 | blank_lines_lower_bound = 0 55 | # required_version 56 | hide_parse_errors = false 57 | color = "Always" # "Auto" 58 | unstable_features = false 59 | # license_template_path 60 | # ignore 61 | edition = "2018" 62 | # version 63 | normalize_doc_attributes = true # false 64 | inline_attribute_width = 0 65 | -------------------------------------------------------------------------------- /server/src/bin/delete-all-table.rs: -------------------------------------------------------------------------------- 1 | use codechain_dashboard_server::{cinfo, logger_init}; 2 | use postgres::{Connection, TlsMode}; 3 | 4 | fn main() { 5 | logger_init().expect("Logger should be initialized"); 6 | 7 | // FIXME: move to configuration file 8 | let user = "codechain-dashboard-server"; 9 | let password = "preempt-entreat-bell-chanson"; 10 | let conn_uri = format!("postgres://{}:{}@localhost", user, password); 11 | let conn = Connection::connect(conn_uri, TlsMode::None).unwrap(); 12 | 13 | let table_names = get_all_table_names(&conn); 14 | cinfo!("Table names to delete are {:?}", table_names); 15 | 16 | for table_name in table_names { 17 | cinfo!("Drop table {}", &table_name); 18 | drop_table(&conn, &table_name); 19 | } 20 | } 21 | 22 | fn get_all_table_names(conn: &Connection) -> Vec { 23 | let rows = conn 24 | .query( 25 | "SELECT table_name \ 26 | FROM information_schema.tables \ 27 | WHERE table_schema='public' \ 28 | AND table_type='BASE TABLE'", 29 | &[], 30 | ) 31 | .unwrap(); 32 | rows.iter().map(|row| row.get(0)).collect() 33 | } 34 | 35 | fn drop_table(conn: &Connection, table_name: &str) { 36 | conn.execute(&format!("DROP TABLE {} CASCADE", table_name), &[]).unwrap(); 37 | } 38 | -------------------------------------------------------------------------------- /server/src/bin/refresh-materialized-view.rs: -------------------------------------------------------------------------------- 1 | use codechain_dashboard_server::logger_init; 2 | use postgres::{Connection, TlsMode}; 3 | 4 | fn main() { 5 | logger_init().expect("Logger should be initialized"); 6 | 7 | // FIXME: move to configuration file 8 | let user = "codechain-dashboard-server"; 9 | let password = "preempt-entreat-bell-chanson"; 10 | let conn_uri = format!("postgres://{}:{}@localhost", user, password); 11 | let conn = Connection::connect(conn_uri, TlsMode::None).unwrap(); 12 | 13 | conn.execute("REFRESH MATERIALIZED VIEW time_5min_report_view_materialized", &[]).unwrap(); 14 | conn.execute("REFRESH MATERIALIZED VIEW time_5min_avg_report_view_materialized", &[]).unwrap(); 15 | conn.execute("REFRESH MATERIALIZED VIEW time_5min_extension_report_view_materialized", &[]).unwrap(); 16 | conn.execute("REFRESH MATERIALIZED VIEW time_5min_peer_report_view_materialized", &[]).unwrap(); 17 | } 18 | -------------------------------------------------------------------------------- /server/src/client/codechain_rpc.rs: -------------------------------------------------------------------------------- 1 | use super::super::common_rpc_types::{ 2 | BlackList, BlockId, NetworkUsage, NodeStatus, PendingTransaction, StructuredLog, WhiteList, 3 | }; 4 | use super::client::{ClientSender, SendClientRPC}; 5 | use super::types::ChainGetBestBlockIdResponse; 6 | use jsonrpc_core::types::{Failure, Output, Success}; 7 | use serde::de::DeserializeOwned; 8 | use serde_json::{self, Value}; 9 | use std::net::SocketAddr; 10 | 11 | pub struct CodeChainRPC { 12 | sender: ClientSender, 13 | } 14 | 15 | impl CodeChainRPC { 16 | pub fn new(sender: ClientSender) -> Self { 17 | Self { 18 | sender, 19 | } 20 | } 21 | 22 | pub fn get_peers(&self, status: NodeStatus) -> Result, String> { 23 | self.call_rpc(status, "net_getEstablishedPeers", Vec::new()) 24 | } 25 | 26 | pub fn get_network_id(&self, status: NodeStatus) -> Result, String> { 27 | self.call_rpc(status, "chain_getNetworkId", Vec::new()) 28 | } 29 | 30 | pub fn get_best_block_id(&self, status: NodeStatus) -> Result, String> { 31 | let response: Option = 32 | self.call_rpc(status, "chain_getBestBlockId", Vec::new())?; 33 | 34 | Ok(response.map(|response| BlockId { 35 | block_number: response.number, 36 | hash: response.hash, 37 | })) 38 | } 39 | 40 | pub fn version(&self, status: NodeStatus) -> Result, String> { 41 | self.call_rpc(status, "version", Vec::new()) 42 | } 43 | 44 | pub fn commit_hash(&self, status: NodeStatus) -> Result, String> { 45 | self.call_rpc(status, "commitHash", Vec::new()) 46 | } 47 | 48 | pub fn get_pending_transactions(&self, _status: NodeStatus) -> Result, String> { 49 | // self.call_rpc(status, "mempool_getPendingTransactions") 50 | Ok(Vec::new()) 51 | } 52 | 53 | pub fn get_whitelist(&self, status: NodeStatus) -> Result, String> { 54 | self.call_rpc(status, "net_getWhitelist", Vec::new()) 55 | } 56 | 57 | pub fn get_blacklist(&self, status: NodeStatus) -> Result, String> { 58 | self.call_rpc(status, "net_getBlacklist", Vec::new()) 59 | } 60 | 61 | pub fn get_network_usage(&self, status: NodeStatus) -> Result, String> { 62 | self.call_rpc(status, "net_recentNetworkUsage", Vec::new()) 63 | } 64 | 65 | pub fn get_logs(&self, status: NodeStatus) -> Result, String> { 66 | if status != NodeStatus::Run { 67 | return Ok(Default::default()) 68 | } 69 | let response = self.sender.shell_get_codechain_log().map_err(|err| format!("{}", err))?; 70 | 71 | Ok(response) 72 | } 73 | 74 | fn call_rpc(&self, status: NodeStatus, method: &str, params: Vec) -> Result 75 | where 76 | T: Default + DeserializeOwned, { 77 | if status != NodeStatus::Run { 78 | return Ok(Default::default()) 79 | } 80 | 81 | let response = 82 | self.sender.codechain_call_rpc((method.to_string(), params)).map_err(|err| format!("{}", err))?; 83 | 84 | let response: T = match response { 85 | Output::Success(Success { 86 | result, 87 | .. 88 | }) => serde_json::from_value(result).map_err(|err| format!("{}", err))?, 89 | Output::Failure(Failure { 90 | error, 91 | .. 92 | }) => return Err(format!("{} error {:#?}", method, error)), 93 | }; 94 | 95 | Ok(response) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /server/src/client/handler.rs: -------------------------------------------------------------------------------- 1 | use super::super::{client, jsonrpc}; 2 | use ws::{self, CloseCode, Error as WSError, Handler, Handshake, Result, Sender as WSSender}; 3 | 4 | pub struct WebSocketHandler { 5 | pub out: WSSender, 6 | pub count: usize, 7 | pub client_service: client::ServiceSender, 8 | pub jsonrpc_context: jsonrpc::Context, 9 | } 10 | 11 | impl WebSocketHandler { 12 | pub fn new(out: WSSender, client_service: client::ServiceSender) -> Self { 13 | let jsonrpc_context = jsonrpc::Context::new(out.clone()); 14 | client_service 15 | .send(client::Message::InitializeClient(jsonrpc_context.clone())) 16 | .expect("Should success send InitializeClient to service"); 17 | Self { 18 | out, 19 | count: 0, 20 | client_service, 21 | jsonrpc_context, 22 | } 23 | } 24 | } 25 | 26 | impl Handler for WebSocketHandler { 27 | fn on_open(&mut self, _: Handshake) -> Result<()> { 28 | // We have a new connection, so we increment the connection counter 29 | self.count += 1; 30 | Ok(()) 31 | } 32 | 33 | fn on_message(&mut self, msg: ws::Message) -> Result<()> { 34 | // Tell the user the current count 35 | ctrace!("The number of live connections is {}", self.count); 36 | 37 | match msg { 38 | ws::Message::Text(text) => jsonrpc::on_receive(self.jsonrpc_context.clone(), text), 39 | _ => { 40 | cwarn!("Byte data received from client"); 41 | } 42 | }; 43 | Ok(()) 44 | } 45 | 46 | fn on_close(&mut self, code: CloseCode, reason: &str) { 47 | match code { 48 | CloseCode::Normal => cinfo!("The client is done with the connection."), 49 | CloseCode::Away => cinfo!("The client is leaving the site."), 50 | CloseCode::Abnormal => cinfo!("Closing handshake failed! Unable to obtain closing status from client."), 51 | _ => cinfo!("The client encountered an error: {}", reason), 52 | } 53 | 54 | // The connection is going down, so we need to decrement the count 55 | self.count -= 1; 56 | } 57 | 58 | fn on_error(&mut self, err: WSError) { 59 | cerror!("The server encountered an error: {:?}", err); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(feature = "cargo-clippy", allow(clippy::module_inception))] 2 | pub mod client; 3 | mod codechain_rpc; 4 | pub mod handler; 5 | pub mod service; 6 | mod types; 7 | 8 | pub use self::client::{SendClientRPC, State}; 9 | pub use self::handler::WebSocketHandler; 10 | pub use self::service::{Message, Service, ServiceSender}; 11 | -------------------------------------------------------------------------------- /server/src/client/service.rs: -------------------------------------------------------------------------------- 1 | use super::super::{db, jsonrpc}; 2 | use super::client::{Client, ClientSender, State as ClientState}; 3 | use crate::noti::Noti; 4 | use parking_lot::RwLock; 5 | use std::sync::mpsc::{channel, SendError, Sender}; 6 | use std::sync::Arc; 7 | use std::thread; 8 | use std::vec::Vec; 9 | 10 | #[derive(Default)] 11 | pub struct State { 12 | clients: Vec<(i32, ClientSender)>, 13 | } 14 | 15 | #[derive(Clone)] 16 | pub struct ServiceSender { 17 | sender: Sender, 18 | state: Arc>, 19 | } 20 | 21 | impl ServiceSender { 22 | pub fn send(&self, message: Message) -> Result<(), SendError> { 23 | self.sender.send(message) 24 | } 25 | 26 | pub fn get_client(&self, name: &str) -> Option { 27 | let state = self.state.read(); 28 | let find_result = state.clients.iter().find(|(_, client)| { 29 | let client_state = client.read_state(); 30 | match client_state.name() { 31 | None => false, 32 | Some(client_name) => client_name == name, 33 | } 34 | }); 35 | 36 | find_result.map(|(_, client)| client.clone()) 37 | } 38 | 39 | pub fn get_clients_states(&self) -> Vec { 40 | let state = self.state.read(); 41 | let mut result = Vec::new(); 42 | for (_, client) in state.clients.iter() { 43 | let state = client.read_state().clone(); 44 | result.push(state); 45 | } 46 | 47 | result 48 | } 49 | 50 | pub fn reset_maximum_memory_usages(&self) { 51 | let state = self.state.write(); 52 | for (_, client) in state.clients.iter() { 53 | client.reset_maximum_memory_usage(); 54 | } 55 | } 56 | } 57 | 58 | pub struct Service { 59 | state: Arc>, 60 | next_id: i32, 61 | sender: ServiceSender, 62 | db_service: db::ServiceSender, 63 | } 64 | 65 | pub enum Message { 66 | InitializeClient(jsonrpc::Context), 67 | AddClient(i32, ClientSender), 68 | RemoveClient(i32), 69 | } 70 | 71 | impl Service { 72 | pub fn run_thread(db_service: db::ServiceSender, noti: Arc) -> ServiceSender { 73 | let (sender, rx) = channel(); 74 | let state = Default::default(); 75 | let service_sender = ServiceSender { 76 | sender, 77 | state: Arc::clone(&state), 78 | }; 79 | 80 | let mut service = Service::new(service_sender.clone(), state, db_service); 81 | 82 | thread::Builder::new() 83 | .name("client service".to_string()) 84 | .spawn(move || { 85 | for message in rx { 86 | match message { 87 | Message::InitializeClient(jsonrpc_context) => { 88 | service.create_client(jsonrpc_context, Arc::clone(¬i)); 89 | } 90 | Message::AddClient(id, client_sender) => { 91 | service.add_client(id, client_sender); 92 | } 93 | Message::RemoveClient(id) => { 94 | service.remove_client(id); 95 | } 96 | } 97 | } 98 | }) 99 | .expect("Should success running client service thread"); 100 | 101 | service_sender 102 | } 103 | 104 | fn new(sender: ServiceSender, state: Arc>, db_service: db::ServiceSender) -> Self { 105 | Service { 106 | state, 107 | next_id: 0_i32, 108 | sender, 109 | db_service, 110 | } 111 | } 112 | 113 | fn create_client(&mut self, jsonrpc_context: jsonrpc::Context, noti: Arc) { 114 | let id = self.next_id; 115 | self.next_id += 1; 116 | Client::run_thread(id, jsonrpc_context, self.sender.clone(), self.db_service.clone(), noti); 117 | cdebug!("Client {} initialization starts", id); 118 | } 119 | 120 | fn add_client(&mut self, id: i32, client_sender: ClientSender) { 121 | let mut state = self.state.write(); 122 | state.clients.push((id, client_sender)); 123 | cdebug!("Client {} is added to ClientService", id); 124 | } 125 | 126 | fn remove_client(&mut self, id: i32) { 127 | let mut state = self.state.write(); 128 | 129 | let client_index = state.clients.iter().position(|(iter_id, _)| *iter_id == id); 130 | if client_index.is_none() { 131 | cerror!("Cannot find client {} to delete", id); 132 | return 133 | } 134 | state.clients.remove(client_index.unwrap()); 135 | cdebug!("Client {} is removed from ClientService", id); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /server/src/client/types.rs: -------------------------------------------------------------------------------- 1 | use super::super::common_rpc_types::{NodeName, NodeStatus}; 2 | use primitives::H256; 3 | use serde_derive::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::net::SocketAddr; 6 | 7 | #[derive(Debug, Serialize, Deserialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct ClientGetInfoResponse { 10 | pub status: NodeStatus, 11 | pub name: NodeName, 12 | pub address: Option, 13 | pub codechain_commit_hash: String, 14 | pub codechain_binary_checksum: String, 15 | } 16 | 17 | #[derive(Debug, Serialize, Deserialize)] 18 | #[serde(rename_all = "camelCase")] 19 | pub struct CodeChainCallRPCResponse { 20 | pub inner_response: Value, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct CodeChainCallRPCResponseHelper { 26 | pub result: Option, 27 | pub error: Option, 28 | } 29 | 30 | #[derive(Debug, Serialize, Deserialize)] 31 | #[serde(rename_all = "camelCase")] 32 | pub struct ChainGetBestBlockIdResponse { 33 | pub hash: H256, 34 | pub number: i64, 35 | } 36 | -------------------------------------------------------------------------------- /server/src/common_rpc_types.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use primitives::H256; 3 | use serde_derive::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | use std::net::IpAddr; 6 | 7 | pub type NodeName = String; 8 | 9 | #[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq)] 10 | pub enum NodeStatus { 11 | Starting, 12 | Run, 13 | Stop, 14 | Updating, 15 | Error, 16 | UFO, 17 | } 18 | 19 | impl Default for NodeStatus { 20 | fn default() -> NodeStatus { 21 | NodeStatus::Stop 22 | } 23 | } 24 | 25 | #[derive(Debug, Serialize, Deserialize, Clone)] 26 | #[serde(rename_all = "camelCase")] 27 | pub struct ShellStartCodeChainRequest { 28 | pub env: String, 29 | pub args: String, 30 | } 31 | 32 | pub type ShellUpdateCodeChainRequest = (ShellStartCodeChainRequest, UpdateCodeChainRequest); 33 | 34 | pub type Connection = (NodeName, NodeName); 35 | 36 | #[derive(Debug, Serialize, Deserialize, PartialEq, Copy, Clone)] 37 | #[serde(rename_all = "camelCase")] 38 | pub struct BlockId { 39 | pub block_number: i64, 40 | pub hash: H256, 41 | } 42 | 43 | #[derive(Debug, Serialize, PartialEq, Clone)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct NodeVersion { 46 | pub version: String, 47 | pub hash: String, 48 | pub binary_checksum: String, 49 | } 50 | 51 | pub type PendingTransaction = serde_json::Value; 52 | 53 | pub type Tag = String; 54 | 55 | #[derive(Debug, Serialize, PartialEq, Clone, Deserialize)] 56 | #[serde(rename_all = "camelCase")] 57 | pub struct WhiteList { 58 | pub list: Vec<(IpAddr, Tag)>, 59 | pub enabled: bool, 60 | } 61 | 62 | pub type BlackList = WhiteList; 63 | 64 | pub type NetworkUsage = HashMap; 65 | 66 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] 67 | #[serde(rename_all = "camelCase")] 68 | pub struct HardwareUsage { 69 | pub total: i64, 70 | pub available: i64, 71 | pub percentage_used: f64, 72 | } 73 | 74 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] 75 | #[serde(rename_all = "camelCase")] 76 | pub struct HardwareInfo { 77 | pub cpu_usage: Vec, 78 | #[serde(default)] 79 | pub disk_usage: Option, 80 | #[serde(default)] 81 | pub disk_usages: Option>, 82 | pub memory_usage: HardwareUsage, 83 | } 84 | 85 | #[derive(Debug, Deserialize, Clone)] 86 | #[serde(rename_all = "camelCase")] 87 | pub struct StructuredLog { 88 | pub level: String, 89 | pub target: String, 90 | pub message: String, 91 | pub timestamp: String, 92 | pub thread_name: String, 93 | } 94 | 95 | #[derive(Debug, Deserialize, Serialize)] 96 | #[serde(rename_all = "camelCase")] 97 | #[serde(tag = "type")] 98 | pub enum UpdateCodeChainRequest { 99 | #[serde(rename_all = "camelCase")] 100 | Git { 101 | commit_hash: String, 102 | }, 103 | #[serde(rename_all = "camelCase")] 104 | Binary { 105 | #[serde(rename = "binaryURL")] 106 | binary_url: String, 107 | binary_checksum: String, 108 | }, 109 | } 110 | 111 | 112 | #[derive(Debug, Clone, Serialize, Deserialize)] 113 | #[serde(rename_all = "camelCase")] 114 | pub enum GraphPeriod { 115 | Minutes5, 116 | Hour, 117 | Day, 118 | } 119 | 120 | #[derive(Debug, Serialize, Deserialize, Clone)] 121 | #[serde(rename_all = "camelCase")] 122 | pub struct GraphCommonArgs { 123 | pub from: DateTime, 124 | pub to: DateTime, 125 | pub period: GraphPeriod, 126 | } 127 | 128 | #[derive(Debug, Serialize, Deserialize)] 129 | #[serde(rename_all = "camelCase")] 130 | pub struct GraphNetworkOutAllRow { 131 | pub node_name: String, 132 | pub time: DateTime, 133 | pub value: f32, 134 | } 135 | 136 | pub type GraphNetworkOutAllAVGRow = GraphNetworkOutAllRow; 137 | 138 | #[derive(Debug, Serialize, Deserialize)] 139 | #[serde(rename_all = "camelCase")] 140 | pub struct GraphNetworkOutNodeExtensionRow { 141 | pub extension: String, 142 | pub time: DateTime, 143 | pub value: f32, 144 | } 145 | 146 | #[derive(Debug, Serialize, Deserialize)] 147 | #[serde(rename_all = "camelCase")] 148 | pub struct GraphNetworkOutNodePeerRow { 149 | pub peer: String, 150 | pub time: DateTime, 151 | pub value: f32, 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use super::*; 157 | 158 | #[test] 159 | fn serialize_day() { 160 | let period = GraphPeriod::Day; 161 | assert_eq!("\"day\"", &serde_json::to_string(&period).unwrap()); 162 | } 163 | #[test] 164 | fn serialize_minutes5() { 165 | let period = GraphPeriod::Minutes5; 166 | assert_eq!("\"minutes5\"", &serde_json::to_string(&period).unwrap()); 167 | } 168 | #[test] 169 | fn serialize_hour() { 170 | let period = GraphPeriod::Hour; 171 | assert_eq!("\"hour\"", &serde_json::to_string(&period).unwrap()); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /server/src/cron/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod remove_network_usage; 2 | -------------------------------------------------------------------------------- /server/src/cron/remove_network_usage.rs: -------------------------------------------------------------------------------- 1 | use crate::db::queries::network_usage; 2 | use crate::db::queries::peer_count; 3 | use r2d2_postgres::PostgresConnectionManager; 4 | use std::{format, thread}; 5 | 6 | pub fn run(db_user: &str, db_password: &str) { 7 | let manager = PostgresConnectionManager::new( 8 | format!("postgres://{}:{}@localhost", db_user, db_password), 9 | r2d2_postgres::TlsMode::None, 10 | ) 11 | .expect("Create connection manager"); 12 | let pool = r2d2::Pool::new(manager).expect("Create connection pool"); 13 | 14 | let five_minutes = std::time::Duration::from_secs(5 * 60); 15 | 16 | thread::Builder::new() 17 | .name("cron-remove-network-usage".to_string()) 18 | .spawn(move || loop { 19 | let current_date = chrono::Utc::now(); 20 | let one_week_ago = current_date - time::Duration::days(7); 21 | 22 | match pool.get() { 23 | Ok(connection) => { 24 | if let Err(err) = network_usage::remove_older_logs(&connection, one_week_ago) { 25 | cwarn!("Fail remove_older_logs: {:?}", err) 26 | } 27 | if let Err(err) = peer_count::remove_older_logs(&connection, one_week_ago) { 28 | cwarn!("Fail remove_older_logs: {:?}", err) 29 | } 30 | } 31 | Err(err) => cwarn!("remove_older_logs: {:?}", err), 32 | } 33 | 34 | thread::sleep(five_minutes); 35 | }) 36 | .expect("Should success listening frontend"); 37 | } 38 | -------------------------------------------------------------------------------- /server/src/db/event.rs: -------------------------------------------------------------------------------- 1 | use super::super::common_rpc_types::NodeName; 2 | use super::types::{ClientExtra, ClientQueryResult}; 3 | 4 | pub enum Event { 5 | ClientUpdated { 6 | before: Box>, 7 | after: Box, 8 | }, 9 | ConnectionChanged { 10 | added: Vec<(NodeName, NodeName)>, 11 | removed: Vec<(NodeName, NodeName)>, 12 | }, 13 | ClientExtraUpdated { 14 | name: NodeName, 15 | before: Option, 16 | after: ClientExtra, 17 | }, 18 | } 19 | 20 | pub trait EventSubscriber: Send { 21 | fn on_event(&self, event: Event); 22 | } 23 | -------------------------------------------------------------------------------- /server/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod event; 2 | pub mod queries; 3 | mod service; 4 | mod types; 5 | 6 | pub use self::event::{Event, EventSubscriber}; 7 | pub use self::service::{Service, ServiceNewArg, ServiceSender}; 8 | pub use self::types::{ClientExtra, ClientQueryResult, Error, Log, LogQueryParams}; 9 | -------------------------------------------------------------------------------- /server/src/db/queries/client_extra.rs: -------------------------------------------------------------------------------- 1 | use super::super::types::DBConnection; 2 | use super::super::ClientExtra; 3 | 4 | pub fn get(conn: &DBConnection, node_name: &str) -> postgres::Result> { 5 | ctrace!("Query client extra by name {}", node_name); 6 | 7 | let rows = conn.query("SELECT * FROM client_extra WHERE name=$1;", &[&node_name])?; 8 | if rows.is_empty() { 9 | return Ok(None) 10 | } 11 | let row = rows.get(0); 12 | Ok(Some(ClientExtra { 13 | prev_env: row.get("prev_env"), 14 | prev_args: row.get("prev_args"), 15 | })) 16 | } 17 | 18 | pub fn upsert(conn: &DBConnection, node_name: &str, client_extra: &ClientExtra) -> postgres::Result<()> { 19 | ctrace!("Upsert client extra {:?}", client_extra); 20 | let result = conn.execute( 21 | "INSERT INTO client_extra (name, prev_env, prev_args) VALUES ($1, $2, $3) \ 22 | ON CONFLICT (name) DO UPDATE \ 23 | SET prev_env=excluded.prev_env, \ 24 | prev_args=excluded.prev_args", 25 | &[&node_name, &client_extra.prev_env, &client_extra.prev_args], 26 | )?; 27 | ctrace!("Upsert result {}", result); 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /server/src/db/queries/config.rs: -------------------------------------------------------------------------------- 1 | use super::super::types::DBConnection; 2 | 3 | pub fn set_query_timeout(conn: &DBConnection) -> postgres::Result<()> { 4 | conn.execute("SET SESSION statement_timeout TO 2000", &[])?; 5 | Ok(()) 6 | } 7 | -------------------------------------------------------------------------------- /server/src/db/queries/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client_extra; 2 | pub mod config; 3 | pub mod logs; 4 | pub mod network_usage; 5 | pub mod network_usage_graph; 6 | pub mod peer_count; 7 | -------------------------------------------------------------------------------- /server/src/db/queries/network_usage.rs: -------------------------------------------------------------------------------- 1 | use super::super::types::DBConnection; 2 | use crate::common_rpc_types::NetworkUsage; 3 | use crate::util::{floor_to_5min, start_of_day, start_of_hour}; 4 | use lazy_static::lazy_static; 5 | use regex::{Captures, Regex}; 6 | 7 | pub fn insert( 8 | conn: &DBConnection, 9 | node_name: &str, 10 | network_usage: NetworkUsage, 11 | time: chrono::DateTime, 12 | ) -> postgres::Result<()> { 13 | ctrace!("Add network usage of {}", node_name); 14 | 15 | if network_usage.is_empty() { 16 | return Ok(()) 17 | } 18 | 19 | let stmt = conn.prepare( 20 | "INSERT INTO network_usage (time, name, extension, target_ip, bytes, time_5min, time_hour, time_day) \ 21 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", 22 | )?; 23 | for key in network_usage.keys() { 24 | let parse_result = parse_network_usage_key(key); 25 | let (extension, ip) = match parse_result { 26 | Ok((extension, ip)) => (extension, ip), 27 | Err(err) => { 28 | cerror!("Network Usage Parse Failed {:?}", err); 29 | // FIXME: propagate the error 30 | return Ok(()) 31 | } 32 | }; 33 | let bytes = network_usage[key]; 34 | 35 | stmt.execute(&[ 36 | &time, 37 | &node_name, 38 | &extension, 39 | &ip, 40 | &bytes, 41 | &floor_to_5min(&time), 42 | &start_of_hour(&time), 43 | &start_of_day(&time), 44 | ])?; 45 | } 46 | 47 | Ok(()) 48 | } 49 | 50 | fn parse_network_usage_key(key: &str) -> Result<(String, String), String> { 51 | // Ex) ::block-propagation@54.180.74.243:3485 52 | lazy_static! { 53 | static ref KEY_REGEX: Regex = Regex::new(r"::(?P[a-zA-Z\-]*)@(?P[0-9\.]*)").unwrap(); 54 | } 55 | 56 | let reg_result: Captures = KEY_REGEX.captures(key).ok_or_else(|| "Parse Error".to_string())?; 57 | 58 | Ok((reg_result["extension"].to_string(), reg_result["ip"].to_string())) 59 | } 60 | 61 | pub fn remove_older_logs(conn: &DBConnection, time: chrono::DateTime) -> postgres::Result<()> { 62 | ctrace!("Remove network usage older than {}", time); 63 | 64 | let result = conn.execute("DELETE FROM network_usage WHERE time<$1", &[&time])?; 65 | ctrace!("Delete result {}", result); 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /server/src/db/queries/network_usage_graph.rs: -------------------------------------------------------------------------------- 1 | use super::super::types::DBConnection; 2 | use crate::common_rpc_types::{ 3 | GraphCommonArgs, GraphNetworkOutAllRow, GraphNetworkOutNodeExtensionRow, GraphNetworkOutNodePeerRow, GraphPeriod, 4 | NodeName, 5 | }; 6 | 7 | pub fn query_network_out_all( 8 | conn: &DBConnection, 9 | graph_args: GraphCommonArgs, 10 | ) -> postgres::Result> { 11 | let time_column_name = get_sql_column_name_by_period(graph_args.period); 12 | let query_stmt = "\ 13 | SELECT \ 14 | name, \ 15 | time_5min, \ 16 | value \ 17 | FROM time_5min_report_view_materialized \ 18 | WHERE time_5min<$1 and time_5min>$2"; 19 | 20 | let rows = conn.query(&query_stmt, &[&graph_args.to, &graph_args.from])?; 21 | 22 | Ok(rows 23 | .into_iter() 24 | .map(|row| GraphNetworkOutAllRow { 25 | node_name: row.get("name"), 26 | time: row.get(time_column_name), 27 | value: row.get("value"), 28 | }) 29 | .collect()) 30 | } 31 | 32 | fn get_sql_column_name_by_period(period: GraphPeriod) -> &'static str { 33 | match period { 34 | GraphPeriod::Minutes5 => "time_5min", 35 | GraphPeriod::Hour => "time_hour", 36 | GraphPeriod::Day => "time_day", 37 | } 38 | } 39 | 40 | pub fn query_network_out_all_avg( 41 | conn: &DBConnection, 42 | graph_args: GraphCommonArgs, 43 | ) -> postgres::Result> { 44 | let time_column_name = get_sql_column_name_by_period(graph_args.period); 45 | let query_stmt = "\ 46 | SELECT \ 47 | name, \ 48 | time_5min, \ 49 | value \ 50 | FROM time_5min_avg_report_view_materialized \ 51 | WHERE time_5min<$1 and time_5min>$2"; 52 | 53 | let rows = conn.query(&query_stmt, &[&graph_args.to, &graph_args.from])?; 54 | 55 | Ok(rows 56 | .into_iter() 57 | .map(|row| GraphNetworkOutAllRow { 58 | node_name: row.get("name"), 59 | time: row.get(time_column_name), 60 | value: row.get("value"), 61 | }) 62 | .collect()) 63 | } 64 | 65 | pub fn query_network_out_node_extension( 66 | conn: &DBConnection, 67 | node_name: NodeName, 68 | graph_args: GraphCommonArgs, 69 | ) -> postgres::Result> { 70 | let time_column_name = get_sql_column_name_by_period(graph_args.period); 71 | let query_stmt = "\ 72 | SELECT \ 73 | extension, \ 74 | time_5min, \ 75 | value \ 76 | FROM time_5min_extension_report_view_materialized \ 77 | WHERE time_5min<$1 AND time_5min>$2 \ 78 | AND name=$3"; 79 | 80 | let rows = conn.query(&query_stmt, &[&graph_args.to, &graph_args.from, &node_name])?; 81 | 82 | Ok(rows 83 | .into_iter() 84 | .map(|row| GraphNetworkOutNodeExtensionRow { 85 | extension: row.get("extension"), 86 | time: row.get(time_column_name), 87 | value: row.get("value"), 88 | }) 89 | .collect()) 90 | } 91 | 92 | pub fn query_network_out_node_peer( 93 | conn: &DBConnection, 94 | node_name: NodeName, 95 | graph_args: GraphCommonArgs, 96 | ) -> postgres::Result> { 97 | let time_column_name = get_sql_column_name_by_period(graph_args.period); 98 | let query_stmt = "\ 99 | SELECT \ 100 | target_ip, \ 101 | time_5min, \ 102 | value \ 103 | FROM time_5min_peer_report_view_materialized \ 104 | WHERE time_5min<$1 AND time_5min>$2 \ 105 | AND name=$3"; 106 | 107 | let rows = conn.query(&query_stmt, &[&graph_args.to, &graph_args.from, &node_name])?; 108 | 109 | Ok(rows 110 | .into_iter() 111 | .map(|row| GraphNetworkOutNodePeerRow { 112 | peer: row.get("target_ip"), 113 | time: row.get(time_column_name), 114 | value: row.get("value"), 115 | }) 116 | .collect()) 117 | } 118 | -------------------------------------------------------------------------------- /server/src/db/queries/peer_count.rs: -------------------------------------------------------------------------------- 1 | use super::super::types::DBConnection; 2 | 3 | pub fn insert( 4 | conn: &DBConnection, 5 | node_name: &str, 6 | peer_count: i32, 7 | time: chrono::DateTime, 8 | ) -> postgres::Result<()> { 9 | ctrace!("Add peer count of {}", node_name); 10 | 11 | conn.execute("INSERT INTO peer_count (time, name, peer_count) VALUES ($1, $2, $3)", &[ 12 | &time, 13 | &node_name, 14 | &peer_count, 15 | ])?; 16 | Ok(()) 17 | } 18 | 19 | pub fn remove_older_logs(conn: &DBConnection, time: chrono::DateTime) -> postgres::Result<()> { 20 | ctrace!("Remove peer count older than {}", time); 21 | 22 | let result = conn.execute("DELETE FROM peer_count WHERE time<$1", &[&time])?; 23 | ctrace!("Delete result {}", result); 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /server/src/frontend/handler.rs: -------------------------------------------------------------------------------- 1 | use super::super::jsonrpc; 2 | use super::super::router::Router; 3 | use super::types::Context; 4 | use std::error::Error; 5 | use std::fmt; 6 | use std::sync::Arc; 7 | use ws::{self, CloseCode, Error as WSError, ErrorKind, Handler, Handshake, Result, Sender}; 8 | 9 | #[derive(Debug)] 10 | struct CustomError {} 11 | 12 | impl Error for CustomError {} 13 | 14 | impl fmt::Display for CustomError { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | write!(f, "Custom error") 17 | } 18 | } 19 | 20 | pub struct WebSocketHandler { 21 | pub out: Sender, 22 | pub context: Context, 23 | pub router: Arc>, 24 | pub frontend_service: super::ServiceSender, 25 | } 26 | 27 | impl Handler for WebSocketHandler { 28 | fn on_open(&mut self, handshake: Handshake) -> Result<()> { 29 | if format!("/{}", self.context.passphrase) != handshake.request.resource() { 30 | return Err(WSError::new(ErrorKind::Custom(Box::new(CustomError {})), "Authorization Error")) 31 | } 32 | 33 | self.frontend_service 34 | .send(super::Message::AddWS(self.out.clone())) 35 | .expect("Should success adding ws to frontend_service"); 36 | Ok(()) 37 | } 38 | 39 | fn on_message(&mut self, msg: ws::Message) -> Result<()> { 40 | let response: Option = match msg { 41 | ws::Message::Text(text) => { 42 | cinfo!("Receive {}", text); 43 | jsonrpc::handle(|method, arg| self.router.run(self.context.clone(), &method, arg), text) 44 | } 45 | _ => Some(jsonrpc::invalid_format()), 46 | }; 47 | 48 | cinfo!("Response {:?}", response); 49 | if let Some(response) = response { 50 | self.out.send(ws::Message::Text(response)) 51 | } else { 52 | Ok(()) 53 | } 54 | } 55 | 56 | fn on_close(&mut self, code: CloseCode, reason: &str) { 57 | match code { 58 | CloseCode::Normal => cinfo!("The client is done with the connection."), 59 | CloseCode::Away => cinfo!("The client is leaving the site."), 60 | CloseCode::Abnormal => cinfo!("Closing handshake failed! Unable to obtain closing status from client."), 61 | _ => cinfo!("The client encountered an error: {}", reason), 62 | } 63 | self.frontend_service 64 | .send(super::Message::RemoveWS(self.out.clone())) 65 | .expect("Should success remove ws from frontend_service"); 66 | } 67 | 68 | fn on_error(&mut self, err: WSError) { 69 | if let Err(error) = self.out.close_with_reason(CloseCode::Error, "Error") { 70 | cerror!("Fail to close connection {}", error); 71 | } 72 | cerror!("The server encountered an error: {:?}", err); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/src/frontend/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod handler; 3 | pub mod service; 4 | pub mod types; 5 | 6 | pub use self::api::add_routing; 7 | pub use self::handler::WebSocketHandler; 8 | pub use self::service::{Message, Service, ServiceSender}; 9 | pub use self::types::*; 10 | -------------------------------------------------------------------------------- /server/src/frontend/service.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{channel, Sender}; 2 | use std::thread; 3 | use std::vec::Vec; 4 | 5 | #[derive(Default)] 6 | pub struct Service { 7 | web_sockets: Vec, 8 | } 9 | 10 | pub type ServiceSender = Sender; 11 | 12 | pub enum Message { 13 | AddWS(ws::Sender), 14 | RemoveWS(ws::Sender), 15 | SendEvent(String), 16 | } 17 | 18 | impl Service { 19 | pub fn run_thread() -> ServiceSender { 20 | let (service_sender, rx) = channel(); 21 | 22 | let mut service = Service::default(); 23 | 24 | thread::Builder::new() 25 | .name("frontend service".to_string()) 26 | .spawn(move || { 27 | for message in rx { 28 | match message { 29 | Message::SendEvent(jsonrpc_data) => { 30 | service.send_event(jsonrpc_data); 31 | } 32 | Message::AddWS(web_socket) => { 33 | service.add_ws(web_socket); 34 | } 35 | Message::RemoveWS(web_socket) => { 36 | service.remove_ws(web_socket); 37 | } 38 | } 39 | } 40 | }) 41 | .expect("Should success running client service thread"); 42 | 43 | service_sender 44 | } 45 | 46 | pub fn send_event(&mut self, data: String) { 47 | for web_socket in &self.web_sockets { 48 | if let Err(err) = web_socket.send(data.clone()) { 49 | cwarn!("Error when sending event to frontend {}", err); 50 | } 51 | } 52 | } 53 | 54 | pub fn add_ws(&mut self, web_socket: ws::Sender) { 55 | debug_assert_eq!(false, self.web_sockets.contains(&web_socket)); 56 | self.web_sockets.push(web_socket); 57 | } 58 | 59 | pub fn remove_ws(&mut self, web_socket: ws::Sender) { 60 | debug_assert_eq!(true, self.web_sockets.contains(&web_socket)); 61 | let index = self.web_sockets.iter().position(|web_socket_iter| *web_socket_iter == web_socket); 62 | match index { 63 | None => cerror!("Cannot find websocket to delete, {:?}", web_socket.token()), 64 | Some(index) => { 65 | self.web_sockets.remove(index); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /server/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod logger; 3 | 4 | pub use logger::init as logger_init; 5 | -------------------------------------------------------------------------------- /server/src/logger/logger.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use env_logger::filter::{Builder as FilterBuilder, Filter}; 3 | use log::{LevelFilter, Log, Metadata, Record}; 4 | use std::{env, thread}; 5 | 6 | pub struct Logger { 7 | filter: Filter, 8 | } 9 | 10 | impl Logger { 11 | pub fn new() -> Self { 12 | let mut builder = FilterBuilder::new(); 13 | builder.filter(None, LevelFilter::Info); 14 | 15 | if let Ok(rust_log) = env::var("RUST_LOG") { 16 | builder.parse(&rust_log); 17 | } 18 | 19 | Self { 20 | filter: builder.build(), 21 | } 22 | } 23 | 24 | pub fn filter(&self) -> LevelFilter { 25 | self.filter.filter() 26 | } 27 | } 28 | 29 | impl Log for Logger { 30 | fn enabled(&self, metadata: &Metadata<'_>) -> bool { 31 | self.filter.enabled(metadata) 32 | } 33 | 34 | fn log(&self, record: &Record<'_>) { 35 | if self.filter.matches(record) { 36 | let thread_name = thread::current().name().unwrap_or_default().to_string(); 37 | let timestamp = time::strftime("%Y-%m-%d %H:%M:%S %Z", &time::now()).unwrap(); 38 | 39 | let stderr_isatty = atty::is(atty::Stream::Stderr); 40 | let timestamp = if stderr_isatty { 41 | timestamp.bold() 42 | } else { 43 | timestamp.normal() 44 | }; 45 | let thread_name = if stderr_isatty { 46 | thread_name.blue().bold() 47 | } else { 48 | thread_name.normal() 49 | }; 50 | let log_level = record.level(); 51 | let log_target = record.target(); 52 | let log_message = record.args(); 53 | eprintln!("#{} {} {} {} {}", timestamp, thread_name, log_level, log_target, log_message); 54 | } 55 | } 56 | 57 | fn flush(&self) {} 58 | } 59 | -------------------------------------------------------------------------------- /server/src/logger/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! clog { 3 | ($lvl:expr, $($arg:tt)+) => ({ 4 | log::log!(target: "dashboard-server", $lvl, $($arg)*); 5 | }); 6 | } 7 | 8 | #[macro_export] 9 | macro_rules! cerror { 10 | ($($arg:tt)*) => ( 11 | $crate::clog!($crate::logger::Level::Error, $($arg)*) 12 | ); 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! cwarn { 17 | ($($arg:tt)*) => ( 18 | $crate::clog!($crate::logger::Level::Warn, $($arg)*) 19 | ); 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! cinfo { 24 | ($($arg:tt)*) => ( 25 | $crate::clog!($crate::logger::Level::Info, $($arg)*) 26 | ); 27 | } 28 | 29 | #[macro_export] 30 | macro_rules! cdebug { 31 | ($($arg:tt)*) => ( 32 | $crate::clog!($crate::logger::Level::Debug, $($arg)*) 33 | ); 34 | } 35 | 36 | #[macro_export] 37 | macro_rules! ctrace { 38 | ($($arg:tt)*) => ( 39 | $crate::clog!($crate::logger::Level::Trace, $($arg)*) 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /server/src/logger/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(feature = "cargo-clippy", allow(clippy::module_inception))] 2 | mod logger; 3 | #[macro_use] 4 | pub mod macros; 5 | 6 | use self::logger::Logger; 7 | pub use log::Level; 8 | use log::SetLoggerError; 9 | 10 | pub fn init() -> Result<(), SetLoggerError> { 11 | let logger = Logger::new(); 12 | log::set_max_level(logger.filter()); 13 | log::set_boxed_logger(Box::new(logger)) 14 | } 15 | -------------------------------------------------------------------------------- /server/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod logger; 3 | mod client; 4 | mod common_rpc_types; 5 | mod cron; 6 | mod daily_reporter; 7 | mod db; 8 | mod event_propagator; 9 | mod frontend; 10 | mod jsonrpc; 11 | mod noti; 12 | mod router; 13 | mod rpc; 14 | mod util; 15 | 16 | use self::event_propagator::EventPropagator; 17 | use self::logger::init as logger_init; 18 | use self::noti::NotiBuilder; 19 | use self::router::Router; 20 | use std::sync::Arc; 21 | use std::thread; 22 | use ws::listen; 23 | 24 | fn main() { 25 | logger_init().expect("Logger should be initialized"); 26 | 27 | let mut noti_builder = NotiBuilder::default(); 28 | if let Ok(slack_hook_url) = std::env::var("SLACK_WEBHOOK_URL") { 29 | cinfo!("Set slack"); 30 | noti_builder.slack(slack_hook_url); 31 | } 32 | match (std::env::var("SENDGRID_API_KEY"), std::env::var("SENDGRID_TO")) { 33 | (Ok(api_key), Ok(to)) => { 34 | cinfo!("Set email to {}", to); 35 | noti_builder.sendgrid(api_key, to); 36 | } 37 | (Ok(_), _) => { 38 | panic!("You set a sendgrid api key, but not a destination"); 39 | } 40 | (_, Ok(_)) => { 41 | panic!("You set a sendgrid destination, but not an api key"); 42 | } 43 | _ => {} 44 | } 45 | let noti = noti_builder.build(); 46 | 47 | // FIXME: move to config 48 | let db_user = "codechain-dashboard-server"; 49 | let db_password = "preempt-entreat-bell-chanson"; 50 | 51 | let frontend_service_sender = frontend::Service::run_thread(); 52 | let event_propagator = Box::new(EventPropagator::new(frontend_service_sender.clone())); 53 | let db_service_sender = db::Service::run_thread(db::ServiceNewArg { 54 | event_subscriber: event_propagator, 55 | db_user: db_user.to_string(), 56 | db_password: db_password.to_string(), 57 | }); 58 | let client_service_sender = client::Service::run_thread(db_service_sender.clone(), Arc::clone(¬i)); 59 | let client_service_for_frontend = client_service_sender.clone(); 60 | 61 | let db_service_sender_for_frontend = db_service_sender.clone(); 62 | let frontend_join = thread::Builder::new() 63 | .name("frontend listen".to_string()) 64 | .spawn(move || { 65 | let mut frontend_router = Arc::new(Router::new()); 66 | frontend::add_routing(Arc::get_mut(&mut frontend_router).unwrap()); 67 | let frontend_context = frontend::Context { 68 | client_service: client_service_for_frontend, 69 | db_service: db_service_sender_for_frontend.clone(), 70 | passphrase: std::env::var("PASSPHRASE").unwrap_or_else(|_| "passphrase".to_string()), 71 | }; 72 | listen("0.0.0.0:3012", move |out| frontend::WebSocketHandler { 73 | out, 74 | context: frontend_context.clone(), 75 | router: Arc::clone(&frontend_router), 76 | frontend_service: frontend_service_sender.clone(), 77 | }) 78 | .unwrap(); 79 | }) 80 | .expect("Should success listening frontend"); 81 | 82 | let client_service_for_client = client_service_sender.clone(); 83 | let client_join = thread::Builder::new() 84 | .name("client listen".to_string()) 85 | .spawn(move || { 86 | listen("0.0.0.0:4012", |out| client::WebSocketHandler::new(out, client_service_for_client.clone())) 87 | .unwrap(); 88 | }) 89 | .expect("Should success listening client"); 90 | 91 | cron::remove_network_usage::run(db_user, db_password); 92 | 93 | let daily_reporter_join = daily_reporter::start(noti, db_service_sender, client_service_sender); 94 | 95 | frontend_join.join().expect("Join frontend listener"); 96 | client_join.join().expect("Join client listener"); 97 | daily_reporter_join.join().expect("Join daily reporter"); 98 | } 99 | -------------------------------------------------------------------------------- /server/src/noti/mod.rs: -------------------------------------------------------------------------------- 1 | mod sendgrid; 2 | mod slack; 3 | 4 | use self::sendgrid::Sendgrid; 5 | use chrono::Utc; 6 | use slack::Slack; 7 | use std::sync::Arc; 8 | 9 | #[derive(Default)] 10 | pub struct NotiBuilder { 11 | slack: Option, 12 | sendgrid: Option<(String, String)>, 13 | } 14 | 15 | impl NotiBuilder { 16 | pub fn slack(&mut self, url: String) -> &Self { 17 | self.slack = Some(url); 18 | self 19 | } 20 | pub fn sendgrid(&mut self, api_key: String, to: String) -> &Self { 21 | self.sendgrid = Some((api_key, to)); 22 | self 23 | } 24 | 25 | pub fn build(self) -> Arc { 26 | let slack = self.slack.map(|url| Slack::try_new(url).unwrap()); 27 | let sendgrid = self.sendgrid.map(|(api_key, to)| Sendgrid::new(api_key, to)); 28 | Arc::new(Noti { 29 | slack, 30 | sendgrid, 31 | }) 32 | } 33 | } 34 | 35 | pub struct Noti { 36 | slack: Option, 37 | sendgrid: Option, 38 | } 39 | 40 | impl Noti { 41 | pub fn error(&self, network_id: &str, message: &str) { 42 | let targets = self.targets(); 43 | if targets.is_empty() { 44 | cerror!("No targets to send error: {}", message); 45 | return 46 | } 47 | cinfo!("Send an error to {}: {}", targets.join(", "), message); 48 | 49 | if let Some(slack) = self.slack.as_ref() { 50 | if let Err(err) = slack.send(format!("{}: {}", network_id, message)) { 51 | cerror!("Cannot send a slack message({}): {}", message, err); 52 | } 53 | } 54 | if let Some(sendgrid) = self.sendgrid.as_ref() { 55 | if let Err(err) = sendgrid.send( 56 | format!("[error][{}][dashboard-server] Error at {}", network_id, Utc::now().to_rfc3339()), 57 | message, 58 | ) { 59 | cerror!("Cannot send an email({}): {}", message, err); 60 | } 61 | } 62 | } 63 | 64 | pub fn warn(&self, network_id: &str, message: &str) { 65 | let targets = self.targets(); 66 | if targets.is_empty() { 67 | cinfo!("No targets to send warning: {}", message); 68 | return 69 | } 70 | cinfo!("Send a warning to {}: {}", targets.join(", "), message); 71 | 72 | if let Some(slack) = self.slack.as_ref() { 73 | if let Err(err) = slack.send(format!("{}: {}", network_id, message)) { 74 | cwarn!("Cannot send a slack message({}): {}", message, err); 75 | } 76 | } 77 | } 78 | 79 | pub fn info(&self, network_id: &str, title: &str, message: &str) { 80 | let targets = self.targets(); 81 | if targets.is_empty() { 82 | cinfo!("No targets to send info: {}", message); 83 | return 84 | } 85 | cinfo!("Send a info to {}: {}", targets.join(", "), message); 86 | 87 | if let Some(slack) = self.slack.as_ref() { 88 | if let Err(err) = slack.send(format!("{}-{}: {}", network_id, title, message)) { 89 | cwarn!("Cannot send a slack message({}): {}", message, err); 90 | } 91 | } 92 | } 93 | 94 | fn targets(&self) -> Vec<&str> { 95 | let mut targets = Vec::with_capacity(2); 96 | if self.slack.is_some() { 97 | targets.push("slack"); 98 | } 99 | if self.sendgrid.is_some() { 100 | targets.push("sendgrid"); 101 | } 102 | targets 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /server/src/noti/sendgrid.rs: -------------------------------------------------------------------------------- 1 | use sendgrid::errors::SendgridResult; 2 | use sendgrid::{Destination, Mail, SGClient}; 3 | 4 | pub struct Sendgrid { 5 | client: SGClient, 6 | to: String, 7 | } 8 | 9 | impl Sendgrid { 10 | pub fn new(api_key: String, to: String) -> Self { 11 | Self { 12 | client: SGClient::new(api_key), 13 | to, 14 | } 15 | } 16 | 17 | pub fn send(&self, subject: impl AsRef, text: impl AsRef) -> SendgridResult<()> { 18 | let mail = Mail::new() 19 | .add_to(Destination { 20 | address: self.to.as_str(), 21 | name: self.to.as_str(), 22 | }) 23 | .add_from("no-reply+dashboard-server@devop.codechan.io") 24 | .add_subject(subject.as_ref()) 25 | .add_text(text.as_ref()); 26 | let result = self.client.send(mail)?; 27 | cinfo!("Send email to {}: {}", self.to, result); 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/noti/slack.rs: -------------------------------------------------------------------------------- 1 | use slack_hook::{PayloadBuilder, Result, Slack as Hook, SlackText}; 2 | 3 | pub struct Slack(Hook); 4 | 5 | impl Slack { 6 | pub fn try_new(url: impl AsRef) -> Result { 7 | Ok(Self(Hook::new(url.as_ref())?)) 8 | } 9 | 10 | pub fn send(&self, message: impl Into) -> Result<()> { 11 | let p = PayloadBuilder::new().text(message).build()?; 12 | 13 | self.0.send(&p) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/router.rs: -------------------------------------------------------------------------------- 1 | use super::rpc::{RPCError, RPCResponse}; 2 | use serde::de::Deserialize; 3 | use serde::Serialize; 4 | use serde_json::{self, Value}; 5 | use std::collections::HashMap; 6 | 7 | pub trait Route { 8 | type Context; 9 | fn run(&self, context: Self::Context, value: Value) -> RPCResponse; 10 | } 11 | 12 | pub struct Router { 13 | table: HashMap<&'static str, Box>>, 14 | } 15 | 16 | impl Route for fn(context: C, Arg) -> RPCResponse 17 | where 18 | Result: Serialize, 19 | for<'de> Arg: Deserialize<'de>, 20 | { 21 | type Context = C; 22 | fn run(&self, context: Self::Context, value: Value) -> RPCResponse { 23 | let arg = serde_json::from_value(value)?; 24 | let result = self(context, arg)?; 25 | if let Some(result) = result { 26 | Ok(Some(serde_json::to_value(result)?)) 27 | } else { 28 | Ok(None) 29 | } 30 | } 31 | } 32 | 33 | impl Route for fn(context: C) -> RPCResponse 34 | where 35 | Result: Serialize, 36 | { 37 | type Context = C; 38 | fn run(&self, context: Self::Context, _value: Value) -> RPCResponse { 39 | let result = self(context)?; 40 | if let Some(result) = result { 41 | let value_result = serde_json::to_value(result)?; 42 | Ok(Some(value_result)) 43 | } else { 44 | Ok(None) 45 | } 46 | } 47 | } 48 | 49 | pub enum Error { 50 | MethodNotFound, 51 | RPC(RPCError), 52 | } 53 | 54 | impl Router { 55 | pub fn new() -> Self { 56 | let table: HashMap<&'static str, Box>> = HashMap::new(); 57 | Self { 58 | table, 59 | } 60 | } 61 | 62 | pub fn add_route(&mut self, method: &'static str, route: Box>) { 63 | self.table.insert(method, route); 64 | } 65 | 66 | pub fn run(&self, context: C, method: &str, arg: Value) -> Result, Error> { 67 | let route = self.table.get(method); 68 | match route { 69 | None => Err(Error::MethodNotFound), 70 | Some(route) => match route.run(context, arg) { 71 | Ok(value) => Ok(value), 72 | Err(err) => Err(Error::RPC(err)), 73 | }, 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server/src/rpc.rs: -------------------------------------------------------------------------------- 1 | use super::db::Error as DBError; 2 | use super::jsonrpc; 3 | use jsonrpc_core::types::{Error as JSONRPCError, ErrorCode}; 4 | use serde_json::{json, Error as SerdeError, Value}; 5 | use std::fmt; 6 | use std::result::Result; 7 | 8 | pub type RPCResponse = Result, RPCError>; 9 | 10 | pub type RPCResult = Result; 11 | 12 | #[derive(Debug)] 13 | pub enum RPCError { 14 | Internal(String), 15 | FromClient(JSONRPCError), 16 | FromDB(DBError), 17 | 18 | ClientNotFound, 19 | } 20 | 21 | impl fmt::Display for RPCError { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | match self { 24 | RPCError::Internal(err) => write!(f, "RPCError {}", err), 25 | RPCError::FromClient(err) => write!(f, "JSONRPCError from Client {:?}", err), 26 | RPCError::FromDB(err) => write!(f, "JSONRPCError from DB {:?}", err), 27 | RPCError::ClientNotFound => write!(f, "Client not found"), 28 | } 29 | } 30 | } 31 | 32 | pub fn response(value: T) -> RPCResponse { 33 | Ok(Some(value)) 34 | } 35 | 36 | const ERR_AGENT_NOT_FOUND: i64 = -1; 37 | 38 | impl From for JSONRPCError { 39 | fn from(err: RPCError) -> Self { 40 | match err { 41 | RPCError::Internal(str) => RPCError::create_internal_rpc_error(str), 42 | RPCError::FromClient(mut error) => { 43 | error.data = match error.data { 44 | None => Some(json!("Error from client")), 45 | Some(inner_data) => Some(json!({ 46 | "message": "This error is from the client", 47 | "inner": inner_data, 48 | })), 49 | }; 50 | error 51 | } 52 | RPCError::FromDB(_) => RPCError::create_internal_rpc_error(err.to_string()), 53 | RPCError::ClientNotFound => RPCError::create_rpc_error(ERR_AGENT_NOT_FOUND, err.to_string()), 54 | } 55 | } 56 | } 57 | 58 | impl RPCError { 59 | fn create_internal_rpc_error(msg: String) -> JSONRPCError { 60 | let mut ret = JSONRPCError::new(ErrorCode::InternalError); 61 | ret.data = Some(Value::String(msg)); 62 | ret 63 | } 64 | 65 | fn create_rpc_error(code: i64, msg: String) -> JSONRPCError { 66 | let mut ret = JSONRPCError::new(ErrorCode::ServerError(code)); 67 | ret.message = msg; 68 | ret 69 | } 70 | } 71 | 72 | impl From for RPCError { 73 | fn from(err: SerdeError) -> Self { 74 | RPCError::Internal(format!("Internal error about JSON serialize/deserialize : {:?}", err)) 75 | } 76 | } 77 | 78 | impl From for RPCError { 79 | fn from(err: jsonrpc::CallError) -> Self { 80 | match err { 81 | jsonrpc::CallError::Response(jsonrpc_error) => RPCError::FromClient(jsonrpc_error), 82 | _ => RPCError::Internal(format!("Internal error about jsonrpc call : {:?}", err)), 83 | } 84 | } 85 | } 86 | 87 | impl From for RPCError { 88 | fn from(err: DBError) -> Self { 89 | RPCError::FromDB(err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /server/src/util.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Timelike, Utc}; 2 | use std::error; 3 | use std::fmt::Debug; 4 | use std::result::Result; 5 | 6 | pub fn log_error(context: T, result: Result<(), Box>) 7 | where 8 | T: Debug, { 9 | if let Err(err) = result { 10 | cerror!("Error at {:?} : {}", context, err); 11 | } 12 | } 13 | 14 | pub fn floor_to_5min(time: &DateTime) -> DateTime { 15 | time.with_minute(time.minute() - (time.minute() % 5)).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap() 16 | } 17 | 18 | pub fn start_of_hour(time: &DateTime) -> DateTime { 19 | time.with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap() 20 | } 21 | 22 | pub fn start_of_day(time: &DateTime) -> DateTime { 23 | time.with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap() 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | 30 | #[test] 31 | fn to_5min() { 32 | fn utc_from_string(str_time: &'static str) -> DateTime { 33 | DateTime::parse_from_rfc3339(str_time).unwrap().with_timezone(&Utc) 34 | } 35 | 36 | let time_a = utc_from_string("1996-12-19T16:39:57Z"); 37 | assert_eq!(floor_to_5min(&time_a), utc_from_string("1996-12-19T16:35:00Z")); 38 | 39 | let time_b = utc_from_string("2018-05-22T16:00:00Z"); 40 | assert_eq!(floor_to_5min(&time_b), utc_from_string("2018-05-22T16:00:00Z")); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | src/**/*.css -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # CodeChain Dashboard UI 2 | 3 | ## Requirements 4 | 5 | The software dependencies required to install and run CodeChain-Dashboard are: 6 | 7 | - Latest version of the [CodeChain-Dashboard-Server](https://github.com/CodeChain-io/codechain-dashboard/tree/master/server) 8 | 9 | ## Run 10 | 11 | Run codechain-dashboard-ui in the development mode. 12 | 13 | ``` 14 | yarn install 15 | yarn run start 16 | ``` 17 | 18 | ## Production build 19 | 20 | ``` 21 | yarn install 22 | yarn run build 23 | ``` 24 | 25 | ## Configuration 26 | 27 | | | Default | 28 | | ------------------------------ | --------------------- | 29 | | REACT_APP_AGENT_HUB_HOST | ws://localhost:3012 | 30 | | REACT_APP_LOG_SERVER_HOST | http://localhost:5012 | 31 | | REACT_APP_TITLE | | 32 | | REACT_APP_AGENT_HUB_PASSPHRASE | passphrase | 33 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codechain-dashboard", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.4", 7 | "@fortawesome/free-solid-svg-icons": "^5.3.1", 8 | "@fortawesome/react-fontawesome": "^0.1.3", 9 | "@types/react-widgets": "^4.3.3", 10 | "@types/reactstrap": "^6.0.3", 11 | "animate.css": "^3.7.0", 12 | "axios": "^0.18.1", 13 | "bootstrap": "^4.1.3", 14 | "chart.js": "^2.7.2", 15 | "codechain-sdk": "^0.4.0-rc3", 16 | "deepmerge": "^2.1.1", 17 | "lodash": "^4.17.19", 18 | "moment": "^2.22.2", 19 | "node-sass-chokidar": "^1.3.3", 20 | "npm-run-all": "^4.1.5", 21 | "plotly.js": "^1.47.4", 22 | "react": "^16.8.6", 23 | "react-chartjs-2": "^2.7.4", 24 | "react-color": "^2.14.1", 25 | "react-confirm-alert": "^2.0.5", 26 | "react-copy-to-clipboard": "^5.0.1", 27 | "react-datepicker": "^1.8.0", 28 | "react-dom": "^16.5.0", 29 | "react-modal": "^3.5.1", 30 | "react-plotly.js": "^2.3.0", 31 | "react-redux": "^5.0.7", 32 | "react-router-dom": "^4.3.1", 33 | "react-scripts": "^3.0.1", 34 | "react-toastify": "^4.3.2", 35 | "react-vis-force": "^0.3.1", 36 | "react-widgets": "^4.4.10", 37 | "reactstrap": "^6.4.0", 38 | "redux-devtools-extension": "^2.13.5", 39 | "redux-logger": "^3.0.6", 40 | "redux-thunk": "2.2.0", 41 | "rpc-websockets": "^4.3.2", 42 | "uuid": "^3.3.2" 43 | }, 44 | "scripts": { 45 | "build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", 46 | "watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", 47 | "start-js": "react-scripts --max_old_space_size=4096 start", 48 | "start": "npm-run-all -p watch-css start-js", 49 | "build-js": "react-scripts --max_old_space_size=4096 build", 50 | "build": "npm-run-all build-css build-js", 51 | "test": "react-scripts test", 52 | "eject": "react-scripts eject", 53 | "lint": "tslint -p tsconfig.json && prettier 'src/**/*.{ts,tsx,scss,json,html,js,jsx}' -l", 54 | "fmt": "tslint -p tsconfig.json --fix && prettier 'src/**/*.{ts,tsx,scss,json,html,js,jsx}' --write" 55 | }, 56 | "devDependencies": { 57 | "@types/chart.js": "^2.7.34", 58 | "@types/jest": "^23.3.2", 59 | "@types/lodash": "^4.14.116", 60 | "@types/node": "^10.9.4", 61 | "@types/react": "^16.4.14", 62 | "@types/react-color": "^2.13.6", 63 | "@types/react-copy-to-clipboard": "^4.2.6", 64 | "@types/react-datepicker": "^1.1.7", 65 | "@types/react-dom": "^16.0.7", 66 | "@types/react-modal": "^3.8.2", 67 | "@types/react-plotly.js": "^2.2.2", 68 | "@types/react-redux": "^6.0.7", 69 | "@types/react-router-dom": "^4.3.0", 70 | "@types/redux-logger": "^3.0.6", 71 | "jest-canvas-mock": "^2.1.0", 72 | "prettier": "^1.14.2", 73 | "tslint": "^5.11.0", 74 | "tslint-config-prettier": "^1.15.0", 75 | "tslint-react": "^3.6.0", 76 | "typescript": "^3.4.5" 77 | }, 78 | "browserslist": { 79 | "production": [ 80 | ">0.2%", 81 | "not dead", 82 | "not op_mini all" 83 | ], 84 | "development": [ 85 | "last 1 chrome version", 86 | "last 1 firefox version", 87 | "last 1 safari version" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeChain-io/codechain-dashboard/882bf546584eb8da0524412d20c1e0b2f0cc019a/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | CodeChain Dashboard 23 | 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/RequestAgent.ts: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | import { updateChainNetworks } from "./actions/chainNetworks"; 3 | import { updateNodeInfo } from "./actions/nodeInfo"; 4 | import { 5 | ChainNetworksUpdate, 6 | CommonError, 7 | NodeUpdateInfo 8 | } from "./requests/types"; 9 | const WebSocket = require("rpc-websockets").Client; 10 | 11 | export interface JsonRPCError { 12 | code: number; 13 | message: string; 14 | } 15 | 16 | export default class RequestAgent { 17 | public static getInstance = () => { 18 | return RequestAgent.instance; 19 | }; 20 | private static instance: RequestAgent = new RequestAgent(); 21 | private ws: any; 22 | private dispatch: any; 23 | private agentHubHost = process.env.REACT_APP_AGENT_HUB_HOST 24 | ? process.env.REACT_APP_AGENT_HUB_HOST 25 | : "ws://localhost:3012"; 26 | private passphrase = process.env.REACT_APP_AGENT_HUB_PASSPHRASE 27 | ? process.env.REACT_APP_AGENT_HUB_PASSPHRASE 28 | : "passphrase"; 29 | private isConnected: boolean = false; 30 | constructor() { 31 | console.log("Create websocket"); 32 | this.ws = new WebSocket(this.agentHubHost + `/${this.passphrase}`); 33 | this.ws.on("open", () => { 34 | console.log("connected"); 35 | this.isConnected = true; 36 | this.ws 37 | .subscribe(["dashboard_updated", "node_updated"]) 38 | .catch((e: any) => { 39 | console.log(e); 40 | }); 41 | 42 | this.ws.on("dashboard_updated", (e: ChainNetworksUpdate) => { 43 | this.dispatch(updateChainNetworks(e)); 44 | }); 45 | 46 | this.ws.on("node_updated", (e: NodeUpdateInfo) => { 47 | this.dispatch(updateNodeInfo(e.name, e)); 48 | }); 49 | }); 50 | this.ws.on("error", (e: any) => { 51 | console.log("error", e); 52 | }); 53 | this.ws.on("close", () => { 54 | toast.error("Agent hub is closed."); 55 | console.log("closed"); 56 | }); 57 | } 58 | public setDispatch = (dispatch: any) => { 59 | this.dispatch = dispatch; 60 | }; 61 | public call = async ( 62 | method: string, 63 | params: object | Array 64 | ): Promise => { 65 | try { 66 | await this.ensureConnection(); 67 | } catch (e) { 68 | toast.error("Agent hub is not responding."); 69 | throw e; 70 | } 71 | let response; 72 | try { 73 | response = await this.ws.call(method, params); 74 | } catch (e) { 75 | if (!this.handleCommonError(e)) { 76 | throw e; 77 | } 78 | } 79 | return response; 80 | }; 81 | public close = () => { 82 | this.ws.close(); 83 | }; 84 | private handleCommonError = (e: JsonRPCError) => { 85 | switch (e.code) { 86 | case CommonError.AgentNotFound: 87 | toast.error("Agent not found"); 88 | return true; 89 | case CommonError.CodeChainIsNotRunning: 90 | toast.error("CodeChain is not running."); 91 | return true; 92 | case CommonError.InternalError: 93 | toast.error("Internal error"); 94 | return true; 95 | } 96 | return false; 97 | }; 98 | // Set timeout to 5 sec 99 | private ensureConnection = () => { 100 | let requestCount = 0; 101 | return new Promise((resolve, reject) => { 102 | (function waitForConnection() { 103 | if (RequestAgent.getInstance().isConnected) { 104 | return resolve(); 105 | } 106 | if (requestCount < 100) { 107 | requestCount++; 108 | setTimeout(waitForConnection, 50); 109 | } else { 110 | return reject(); 111 | } 112 | })(); 113 | }); 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /ui/src/actions/chainNetworks.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { ReducerConfigure } from "../reducers"; 3 | import { ChainNetworksState } from "../reducers/chainNetworks"; 4 | import RequestAgent from "../RequestAgent"; 5 | import { ChainNetworks, ChainNetworksUpdate } from "../requests/types"; 6 | import { changeFilters } from "./log"; 7 | export type ChainNetworksAction = 8 | | SetChainNetworks 9 | | UpdateChainNetworks 10 | | RequestChainNetworks; 11 | 12 | export interface SetChainNetworks { 13 | type: "SetChainNetworks"; 14 | data: ChainNetworks; 15 | receivedAt: number; 16 | } 17 | 18 | export interface UpdateChainNetworks { 19 | type: "UpdateChainNetworks"; 20 | data: ChainNetworksUpdate; 21 | } 22 | 23 | export interface RequestChainNetworks { 24 | type: "RequestChainNetworks"; 25 | } 26 | 27 | export const setChainNetworks = (data: ChainNetworks) => ({ 28 | type: "SetChainNetworks", 29 | data, 30 | receivedAt: Date.now() 31 | }); 32 | 33 | export const updateChainNetworks = (data: ChainNetworksUpdate) => ({ 34 | type: "UpdateChainNetworks", 35 | data 36 | }); 37 | 38 | export const requestChainNetworks = () => ({ 39 | type: "RequestChainNetworks" 40 | }); 41 | 42 | const shouldFetchChainNetworks = (state: ChainNetworksState) => { 43 | if (!state.chainNetworks) { 44 | return true; 45 | } else if (state.isFetching) { 46 | return false; 47 | } 48 | return true; 49 | }; 50 | 51 | export const fetchChainNetworksIfNeeded = () => { 52 | return async (dispatch: any, getState: () => ReducerConfigure) => { 53 | if (shouldFetchChainNetworks(getState().chainNetworksReducer)) { 54 | dispatch(requestChainNetworks()); 55 | const chainNetworks = await RequestAgent.getInstance().call< 56 | ChainNetworks 57 | >("dashboard_getNetwork", []); 58 | dispatch(setChainNetworks(chainNetworks)); 59 | dispatch( 60 | changeFilters({ 61 | filter: { nodeNames: _.map(chainNetworks.nodes, node => node.name) } 62 | }) 63 | ); 64 | } 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /ui/src/actions/nodeInfo.ts: -------------------------------------------------------------------------------- 1 | import { ReducerConfigure } from "../reducers"; 2 | import { NodeState } from "../reducers/nodeInfo"; 3 | import RequestAgent from "../RequestAgent"; 4 | import { NodeInfo, NodeUpdateInfo } from "../requests/types"; 5 | export type NodeInfoAction = SetNodeInfo | UpdateNodeInfo | RequestNodeInfo; 6 | 7 | export interface SetNodeInfo { 8 | type: "SetNodeInfo"; 9 | name: string; 10 | data: NodeInfo; 11 | receivedAt: number; 12 | } 13 | 14 | export interface UpdateNodeInfo { 15 | type: "UpdateNodeInfo"; 16 | name: string; 17 | data: NodeInfo; 18 | } 19 | 20 | export interface RequestNodeInfo { 21 | type: "RequestNodeInfo"; 22 | name: string; 23 | } 24 | 25 | export const setNodeInfo = (name: string, data: NodeInfo) => ({ 26 | type: "SetNodeInfo", 27 | name, 28 | data, 29 | receivedAt: Date.now() 30 | }); 31 | 32 | export const requestNodeInfo = (name: string) => ({ 33 | type: "RequestNodeInfo", 34 | name 35 | }); 36 | 37 | export const updateNodeInfo = (name: string, data: NodeUpdateInfo) => ({ 38 | type: "UpdateNodeInfo", 39 | name, 40 | data 41 | }); 42 | 43 | const shouldFetchNodeInfo = (state: NodeState, nodeName: string) => { 44 | const nodeInfo = state.nodeInfos[nodeName]; 45 | if (!nodeInfo) { 46 | return true; 47 | } else if (nodeInfo.isFetching) { 48 | return false; 49 | } 50 | return true; 51 | }; 52 | 53 | export const fetchNodeInfoIfNeeded = (nodeName: string) => { 54 | return async (dispatch: any, getState: () => ReducerConfigure) => { 55 | if (shouldFetchNodeInfo(getState().nodeInfoReducer, nodeName)) { 56 | dispatch(requestNodeInfo(nodeName)); 57 | const nodeInfo = await RequestAgent.getInstance().call( 58 | "node_getInfo", 59 | [nodeName] 60 | ); 61 | dispatch(setNodeInfo(nodeName, nodeInfo)); 62 | } 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /ui/src/components/App/App.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | 3 | .app { 4 | .content-container { 5 | margin-top: $header-height; 6 | margin-left: $gnb-width; 7 | overflow: scroll; 8 | height: calc(100vh - #{$header-height}); 9 | width: calc(100vw - #{$gnb-width}); 10 | padding: 18px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/components/App/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import { applyMiddleware, createStore } from "redux"; 5 | import { composeWithDevTools } from "redux-devtools-extension"; 6 | import thunkMiddleware from "redux-thunk"; 7 | import appReducer from "../../reducers"; 8 | import App from "./App"; 9 | 10 | it("renders without crashing", () => { 11 | const div = document.createElement("div"); 12 | const composeEnhancers = composeWithDevTools({}); 13 | const store = createStore( 14 | appReducer, 15 | composeEnhancers(applyMiddleware(thunkMiddleware)) 16 | ); 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | div 22 | ); 23 | ReactDOM.unmountComponentAtNode(div); 24 | }); 25 | -------------------------------------------------------------------------------- /ui/src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactModal from "react-modal"; 3 | import { connect, DispatchProp } from "react-redux"; 4 | import { BrowserRouter as Router, Route } from "react-router-dom"; 5 | import { ToastContainer } from "react-toastify"; 6 | import "react-toastify/dist/ReactToastify.css"; 7 | import RequestAgent from "../../RequestAgent"; 8 | import Dashboard from "../Dashboard/Dashboard"; 9 | import { GlobalNavigationBar } from "../GlobalNavigationBar/GlobalNavigationBar"; 10 | import Graph from "../Graph/Graph"; 11 | import { Header } from "../Header/Header"; 12 | import Log from "../Log/Log"; 13 | import NodeList from "../NodeList/NodeList"; 14 | import RPC from "../RPC/RPC"; 15 | import "./App.css"; 16 | 17 | class App extends React.Component { 18 | public componentDidMount() { 19 | if (process.env.NODE_ENV !== "test") { 20 | ReactModal.setAppElement("#app"); 21 | } 22 | } 23 | public componentWillMount() { 24 | RequestAgent.getInstance().setDispatch(this.props.dispatch); 25 | } 26 | public componentWillUnmount() { 27 | RequestAgent.getInstance().close(); 28 | } 29 | public render() { 30 | return ( 31 | 32 |
33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 |
45 | ); 46 | } 47 | } 48 | export default connect()(App); 49 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/ConnectGraphContainer/ConnectionGraphContainer.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | 3 | .connection-graph-container { 4 | border: 1px solid $container-border-color; 5 | border-radius: 3px; 6 | overflow: hidden; 7 | .connection-graph-header { 8 | color: $dark-text-color; 9 | background-color: $dark-background-color; 10 | padding: 9px; 11 | } 12 | 13 | .connection-graph-body { 14 | background-color: white; 15 | padding: 9px; 16 | 17 | .connection-graph { 18 | height: 809px; 19 | } 20 | } 21 | .rv-force__node { 22 | cursor: pointer; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/ConnectGraphContainer/ConnectionGraphContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./ConnectionGraphContainer.css"; 3 | 4 | import { faCodeBranch } from "@fortawesome/free-solid-svg-icons"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { ChainNetworks } from "../../../requests/types"; 7 | import { ConnectionGraph } from "../ConnectionGraph/ConnectionGraph"; 8 | 9 | interface Props { 10 | className?: string; 11 | chainNetworks: ChainNetworks; 12 | onSelectNode: (node: { id: string; label: string }) => void; 13 | onDeselect: () => void; 14 | } 15 | export class ConnectionGraphContainer extends React.Component { 16 | public render() { 17 | const { className, chainNetworks, onSelectNode, onDeselect } = this.props; 18 | return ( 19 |
20 |
21 |
22 | 23 | Node Connection Graph 24 |
25 |
26 |
27 | 33 |
34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/ConnectionGraph/ConnectionGraph.tsx: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as React from "react"; 3 | import { ChainNetworks, NodeStatus } from "../../../requests/types"; 4 | const { 5 | InteractiveForceGraph, 6 | ForceGraphLink, 7 | ForceGraphNode 8 | } = require("react-vis-force"); 9 | 10 | interface Props { 11 | className?: string; 12 | chainNetworks: ChainNetworks; 13 | onSelectNode: (node: { id: string; label: string }) => void; 14 | onDeselect: () => void; 15 | } 16 | interface States { 17 | width: number; 18 | height: number; 19 | drawNodeList: boolean; 20 | isUpdatingGraph: boolean; 21 | } 22 | export class ConnectionGraph extends React.Component { 23 | private containerRef: React.RefObject; 24 | constructor(props: Props) { 25 | super(props); 26 | this.state = { 27 | width: 0, 28 | height: 0, 29 | drawNodeList: false, 30 | isUpdatingGraph: false 31 | }; 32 | this.containerRef = React.createRef(); 33 | } 34 | 35 | public componentDidMount() { 36 | this.setWindowDimensions(); 37 | window.addEventListener("resize", this.updateWindowDimensions); 38 | } 39 | 40 | public componentWillUnmount() { 41 | window.removeEventListener("resize", this.updateWindowDimensions); 42 | } 43 | 44 | public render() { 45 | const { className, chainNetworks, onSelectNode, onDeselect } = this.props; 46 | const { width, height, drawNodeList } = this.state; 47 | return ( 48 |
49 | {drawNodeList ? ( 50 | onSelectNode(node)} 60 | // tslint:disable-next-line:jsx-no-lambda 61 | onDeselectNode={(event: any, node: any) => onDeselect()} 62 | highlightDependencies={true} 63 | > 64 | {_.map(chainNetworks.nodes, node => ( 65 | 75 | ))} 76 | {_.map(chainNetworks.connections, connection => ( 77 | 85 | ))} 86 | 87 | ) : null} 88 |
89 | ); 90 | } 91 | private getNodeColor = (nodeStatus: NodeStatus) => { 92 | switch (nodeStatus) { 93 | case "Run": 94 | return "#28a745"; 95 | case "Error": 96 | return "#dc3545"; 97 | case "Stop": 98 | return "#868e96"; 99 | case "Starting": 100 | case "Updating": 101 | return "#ffc107"; 102 | case "UFO": 103 | return "#17a2b8"; 104 | } 105 | return "#868e96"; 106 | }; 107 | private setWindowDimensions = () => { 108 | this.setState({ 109 | width: this.containerRef.current 110 | ? this.containerRef.current.offsetWidth 111 | : 500, 112 | height: this.containerRef.current 113 | ? this.containerRef.current.offsetHeight 114 | : 500, 115 | drawNodeList: true 116 | }); 117 | }; 118 | private updateWindowDimensions = () => { 119 | if (this.state.isUpdatingGraph) { 120 | return; 121 | } 122 | this.setState({ 123 | drawNodeList: false, 124 | isUpdatingGraph: true 125 | }); 126 | 127 | setTimeout(() => { 128 | this.setState({ 129 | width: this.containerRef.current 130 | ? this.containerRef.current.offsetWidth 131 | : 500, 132 | height: this.containerRef.current 133 | ? this.containerRef.current.offsetHeight 134 | : 500, 135 | drawNodeList: true, 136 | isUpdatingGraph: false 137 | }); 138 | }, 500); 139 | }; 140 | } 141 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/Dashboard.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | 3 | .dashboard { 4 | min-width: 800px; 5 | .left-panel { 6 | flex-grow: 1; 7 | } 8 | .right-panel { 9 | width: 300px; 10 | .dashboard-item { 11 | border: 1px solid $container-border-color; 12 | border-radius: 3px; 13 | overflow: hidden; 14 | } 15 | .dashboard-item-header { 16 | background-color: $dark-background-color; 17 | color: $dark-text-color; 18 | padding: 9px; 19 | } 20 | .node-item-info-panel { 21 | height: 180px; 22 | } 23 | .dashboard-item-body { 24 | background-color: white; 25 | padding: 9px; 26 | } 27 | } 28 | .node-info-element { 29 | position: relative; 30 | height: 100%; 31 | 32 | .bottom-container { 33 | position: absolute; 34 | bottom: 0px; 35 | } 36 | } 37 | .view-details { 38 | text-decoration: underline !important; 39 | color: black; 40 | font-style: italic; 41 | &:hover { 42 | color: rgb(97, 97, 97); 43 | } 44 | } 45 | .selected-network-node-list-container { 46 | margin-top: 18px; 47 | height: 190px; 48 | overflow: auto; 49 | white-space: nowrap; 50 | 51 | .network-node-info { 52 | position: relative; 53 | height: 190px; 54 | width: 300px; 55 | display: inline-block; 56 | background: white; 57 | padding: 18px; 58 | margin-right: 18px; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ui/src/components/GlobalNavigationBar/GlobalNavigationBar.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | 3 | .global-navigation-bar { 4 | width: $gnb-width; 5 | height: calc(100vh - #{$header-height}); 6 | position: fixed; 7 | top: $header-height; 8 | left: 0; 9 | background-color: $header-background-color; 10 | .gnb-list { 11 | margin-top: 60px; 12 | .gnb-list-item { 13 | position: relative; 14 | &:hover { 15 | color: white; 16 | } 17 | transition: color 0.5s ease; 18 | color: rgba(255, 255, 255, 0.5); 19 | width: $gnb-width; 20 | margin-bottom: 30px; 21 | 22 | .gnb-list-item-icon { 23 | font-size: 1.1rem; 24 | } 25 | 26 | .gnb-list-item-title { 27 | font-size: 0.5rem; 28 | } 29 | 30 | &.active { 31 | color: white; 32 | } 33 | .gnb-list-item-selected-arrow { 34 | position: absolute; 35 | top: 5px; 36 | right: 0; 37 | width: 10px; 38 | color: white !important; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/components/GlobalNavigationBar/GlobalNavigationBar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | faChartLine, 3 | faCoins, 4 | faHistory, 5 | faRetweet, 6 | faTachometerAlt, 7 | IconDefinition 8 | } from "@fortawesome/free-solid-svg-icons"; 9 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 10 | import * as React from "react"; 11 | import { Link, withRouter } from "react-router-dom"; 12 | import "./GlobalNavigationBar.css"; 13 | import { ReactComponent as ArrowImg } from "./img/arrow.svg"; 14 | const getGnbMenu = ( 15 | url: string, 16 | title: string, 17 | icon: IconDefinition, 18 | isSelected: boolean 19 | ) => { 20 | return ( 21 |
  • 22 | 23 |
    24 | {isSelected ? ( 25 | 26 | ) : null} 27 |
    28 | 29 |
    30 |
    31 | {title} 32 |
    33 |
    34 | 35 |
  • 36 | ); 37 | }; 38 | export const GlobalNavigationBar = withRouter(props => { 39 | const pathname = props.location.pathname; 40 | return ( 41 |
    42 |
      43 | {getGnbMenu("", "Dashboard", faTachometerAlt, pathname === "/")} 44 | {getGnbMenu( 45 | "nodelist", 46 | "Node List", 47 | faCoins, 48 | /^\/nodelist/.test(pathname) 49 | )} 50 | {getGnbMenu("rpc", "RPC", faRetweet, pathname === "/rpc")} 51 | {getGnbMenu("log", "Log", faHistory, pathname === "/log")} 52 | {getGnbMenu("graph", "Graph", faChartLine, pathname === "/graph")} 53 |
    54 |
    55 | ); 56 | }); 57 | -------------------------------------------------------------------------------- /ui/src/components/GlobalNavigationBar/img/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Untitled 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/components/Graph/Graph.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import * as React from "react"; 3 | import { Route } from "react-router"; 4 | import GraphNode from "./GraphNode/GraphNode"; 5 | import NetworkOutAllAVGGraph from "./NetworkOutAllAVGGraph/NetworkOutAllAVGGraph"; 6 | import NetworkOutAllGraph from "./NetworkOutAllGraph/NetworkOutAllGraph"; 7 | 8 | interface Props { 9 | match: any; 10 | history: any; 11 | } 12 | 13 | export default class Graph extends Component { 14 | public render() { 15 | const { match } = this.props; 16 | return ( 17 |
    18 | 19 | 20 |
    21 | ); 22 | } 23 | 24 | private renderAllNodeGraph = () => { 25 | return ( 26 |
    27 | 28 | 29 |
    30 | ); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/components/Graph/GraphNode/GraphNode.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import NetworkOutNodeExtensionGraph from "./NetworkOutNodeExtensionGraph/NetworkOutNodeExtensionGraph"; 3 | import NetworkOutNodePeerGraph from "./NetworkOutNodePeerGraph/NetworkOutNodePeerGraph"; 4 | 5 | interface OwnProps { 6 | match: { 7 | params: { 8 | nodeId: string; 9 | }; 10 | }; 11 | } 12 | 13 | export default class GraphNode extends React.Component { 14 | public render() { 15 | const { match } = this.props; 16 | return ( 17 |
    18 |

    Node {match.params.nodeId}

    19 | 20 | 21 | 22 |
    23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/components/Graph/GraphNode/NetworkOutNodeExtensionGraph/NetworkOutNodeExtensionGraph.scss: -------------------------------------------------------------------------------- 1 | .network-out-node-extension-graph { 2 | background-color: white; 3 | width: 1100px; 4 | padding: 10px; 5 | margin: 10px; 6 | 7 | .plot { 8 | display: block; 9 | } 10 | } 11 | 12 | .to-time { 13 | display: inline-block; 14 | margin: 5px; 15 | 16 | label { 17 | margin-right: 5px; 18 | } 19 | } 20 | .from-time { 21 | display: inline-block; 22 | margin: 5px; 23 | 24 | label { 25 | margin-right: 5px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/components/Graph/GraphNode/NetworkOutNodePeerGraph/NetworkOutNodePeerGraph.scss: -------------------------------------------------------------------------------- 1 | .network-out-node-peer-graph { 2 | background-color: white; 3 | width: 1100px; 4 | padding: 10px; 5 | margin: 10px; 6 | 7 | .plot { 8 | display: block; 9 | } 10 | } 11 | 12 | .to-time { 13 | display: inline-block; 14 | margin: 5px; 15 | 16 | label { 17 | margin-right: 5px; 18 | } 19 | } 20 | .from-time { 21 | display: inline-block; 22 | margin: 5px; 23 | 24 | label { 25 | margin-right: 5px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/components/Graph/NetworkOutAllAVGGraph/NetworkOutAllAVGGraph.scss: -------------------------------------------------------------------------------- 1 | .network-out-all-avg-graph { 2 | background-color: white; 3 | width: 1100px; 4 | padding: 10px; 5 | margin: 10px; 6 | 7 | .plot { 8 | display: block; 9 | } 10 | } 11 | 12 | .to-time { 13 | display: inline-block; 14 | margin: 5px; 15 | 16 | label { 17 | margin-right: 5px; 18 | } 19 | } 20 | .from-time { 21 | display: inline-block; 22 | margin: 5px; 23 | 24 | label { 25 | margin-right: 5px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/components/Graph/NetworkOutAllGraph/NetworkOutAllGraph.scss: -------------------------------------------------------------------------------- 1 | .network-out-all-graph { 2 | background-color: white; 3 | width: 1100px; 4 | padding: 10px; 5 | margin: 10px; 6 | 7 | plot { 8 | display: block; 9 | } 10 | } 11 | 12 | .to-time { 13 | display: inline-block; 14 | margin: 5px; 15 | 16 | label { 17 | margin-right: 5px; 18 | } 19 | } 20 | .from-time { 21 | display: inline-block; 22 | margin: 5px; 23 | 24 | label { 25 | margin-right: 5px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | 3 | .header { 4 | height: $header-height; 5 | width: 100%; 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | background: $header-background-color; 10 | color: white; 11 | 12 | .logo-container { 13 | width: $gnb-width; 14 | .logo { 15 | width: 30px; 16 | height: 30px; 17 | } 18 | } 19 | .title-container { 20 | flex: 1; 21 | } 22 | .option-container { 23 | width: $gnb-width; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import "./Header.css"; 4 | import Logo from "./img/logo.png"; 5 | 6 | const getTitle = (pathName: string) => { 7 | if (pathName === "/") { 8 | return "CodeChain Dashboard"; 9 | } else if (/^\/nodelist/.test(pathName)) { 10 | if (pathName === "/nodelist") { 11 | return "CodeChain Node List"; 12 | } else { 13 | return "CodeChain Node Details"; 14 | } 15 | } else if (pathName === "/rpc") { 16 | return "CodeChain RPC"; 17 | } else if (pathName === "/log") { 18 | return "CodeChain Log"; 19 | } else { 20 | return "CodeChain"; 21 | } 22 | }; 23 | 24 | export const Header = withRouter(props => { 25 | return ( 26 |
    27 |
    28 | 29 |
    30 |
    31 |

    32 | {getTitle(props.location.pathname)} 33 |

    34 |
    35 |
    36 | {process.env.REACT_APP_TITLE} 37 |
    38 |
    39 |
    40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /ui/src/components/Header/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeChain-io/codechain-dashboard/882bf546584eb8da0524412d20c1e0b2f0cc019a/ui/src/components/Header/img/logo.png -------------------------------------------------------------------------------- /ui/src/components/Log/LeftFilter/ColorPicker/ColorPicker.scss: -------------------------------------------------------------------------------- 1 | .color-picker { 2 | position: relative; 3 | .color-picker-button { 4 | width: 20px; 5 | height: 20px; 6 | border: 1px gray solid; 7 | cursor: pointer; 8 | } 9 | .block-picker-container { 10 | position: absolute; 11 | z-index: 2; 12 | left: -7px; 13 | top: 29px; 14 | } 15 | .cover { 16 | position: fixed; 17 | top: 0; 18 | right: 0; 19 | bottom: 0; 20 | left: 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/components/Log/LeftFilter/ColorPicker/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ColorState, GithubPicker } from "react-color"; 3 | 4 | import "./ColorPicker.css"; 5 | 6 | interface Props { 7 | onColorSelected: (hex: string) => void; 8 | className?: string; 9 | color: string; 10 | } 11 | 12 | interface State { 13 | isColorPickerOpen: boolean; 14 | } 15 | 16 | export default class ColorPicker extends React.Component { 17 | public constructor(props: Props) { 18 | super(props); 19 | this.state = { 20 | isColorPickerOpen: false 21 | }; 22 | } 23 | public render() { 24 | const { isColorPickerOpen } = this.state; 25 | const { className, color } = this.props; 26 | return ( 27 |
    28 |
    33 | 34 | {isColorPickerOpen && [ 35 |
    36 | 37 |
    , 38 |
    39 | ]} 40 |
    41 | ); 42 | } 43 | public togglePicker = () => { 44 | this.setState({ isColorPickerOpen: !this.state.isColorPickerOpen }); 45 | }; 46 | public handleClose = () => { 47 | this.setState({ isColorPickerOpen: false }); 48 | }; 49 | public handleOnChangeColor = (color: ColorState) => { 50 | this.setState({ 51 | isColorPickerOpen: false 52 | }); 53 | this.props.onColorSelected(color.hex); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /ui/src/components/Log/LeftFilter/LeftFilter.scss: -------------------------------------------------------------------------------- 1 | .left-filter { 2 | background-color: white; 3 | padding: 18px; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/components/Log/Log.scss: -------------------------------------------------------------------------------- 1 | .log { 2 | .left { 3 | width: 250px; 4 | } 5 | .right { 6 | flex: 1; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/components/Log/Log.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import LeftFilter from "./LeftFilter/LeftFilter"; 3 | import "./Log.css"; 4 | import LogViewer from "./LogViewer/LogViewer"; 5 | import TopFilter from "./TopFilter/TopFilter"; 6 | 7 | export default class Log extends React.Component { 8 | public render() { 9 | return ( 10 |
    11 |
    12 | 13 |
    14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/components/Log/LogViewer/LogItem/LogItem.scss: -------------------------------------------------------------------------------- 1 | .log-item { 2 | cursor: pointer; 3 | td { 4 | text-overflow: ellipsis; 5 | overflow: hidden; 6 | white-space: nowrap; 7 | font-size: 0.8rem; 8 | padding: 7px; 9 | } 10 | } 11 | 12 | .expended-item { 13 | background-color: #f1f1f1; 14 | td { 15 | font-size: 0.8rem; 16 | padding: 18px; 17 | 18 | .expend-text { 19 | white-space: pre-wrap; 20 | background-color: white; 21 | padding: 9px; 22 | border: 1px solid #dfdfdf; 23 | } 24 | 25 | .btn { 26 | font-size: 0.8rem; 27 | padding-left: 15px; 28 | padding-right: 15px; 29 | padding-top: 3px; 30 | padding-bottom: 3px; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/components/Log/LogViewer/LogItem/LogItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | faAngleDown, 3 | faAngleRight, 4 | faCopy 5 | } from "@fortawesome/free-solid-svg-icons"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import moment from "moment"; 8 | import * as React from "react"; 9 | import CopyToClipboard from "react-copy-to-clipboard"; 10 | import { connect } from "react-redux"; 11 | import { ReducerConfigure } from "../../../../reducers"; 12 | import { Log } from "../../../../requests/types"; 13 | import "./LogItem.css"; 14 | 15 | interface OwnProps { 16 | log: Log; 17 | } 18 | 19 | interface StateProps { 20 | nodeColors: { 21 | [nodeName: string]: string; 22 | }; 23 | } 24 | 25 | interface State { 26 | isExpended: boolean; 27 | isCopied: boolean; 28 | } 29 | 30 | type Props = OwnProps & StateProps; 31 | 32 | class LogItem extends React.Component { 33 | public constructor(props: Props) { 34 | super(props); 35 | this.state = { 36 | isExpended: false, 37 | isCopied: false 38 | }; 39 | } 40 | public render() { 41 | const { log, nodeColors } = this.props; 42 | const { isExpended, isCopied } = this.state; 43 | 44 | const getItem = () => { 45 | return ( 46 | 62 | 63 | 64 | {isExpended ? ( 65 | 66 | ) : ( 67 | 68 | )} 69 | 70 | {moment(log.timestamp).format("YYYY-MM-DD HH:mm:ss")} 71 | 72 | {log.nodeName} 73 | {log.level} 74 | {log.target} 75 | {log.message} 76 | 77 | ); 78 | }; 79 | const getExpendText = () => { 80 | return `${log.nodeName}, ${log.level}, ${log.target}, ${moment( 81 | log.timestamp 82 | ).format("YYYY-MM-DD HH:mm:ssZ")} content is\r\n\r\n${log.message}`; 83 | }; 84 | if (isExpended) { 85 | return [ 86 | getItem(), 87 | 88 | 89 |
    {getExpendText()}
    90 |
    91 | 95 | 99 | 100 | {isCopied && Copied!} 101 |
    102 | 103 | 104 | ]; 105 | } else { 106 | return getItem(); 107 | } 108 | } 109 | private handleOnCopy = () => { 110 | this.setState({ isCopied: true }); 111 | }; 112 | private getColorByBackground = (hexBackground: string) => { 113 | const brightness = this.getBrightness(this.hexToRgb(hexBackground)); 114 | return brightness > 0.5 ? "#000000" : "#ffffff"; 115 | }; 116 | private getBrightness = (rgb: number[]) => { 117 | const R = rgb[0] / 255; 118 | const G = rgb[1] / 255; 119 | const B = rgb[2] / 255; 120 | return Math.sqrt( 121 | 0.299 * Math.pow(R, 2.2) + 122 | 0.587 * Math.pow(G, 2.2) + 123 | 0.114 * Math.pow(B, 2.2) 124 | ); 125 | }; 126 | private hexToRgb = (hex: string) => { 127 | return hex 128 | .replace( 129 | /^#?([a-f\d])([a-f\d])([a-f\d])$/i, 130 | (m, r, g, b) => "#" + r + r + g + g + b + b 131 | ) 132 | .substring(1) 133 | .match(/.{2}/g)! 134 | .map(x => parseInt(x, 16)); 135 | }; 136 | 137 | private toggleExpend = () => { 138 | this.setState({ isExpended: !this.state.isExpended }); 139 | }; 140 | } 141 | 142 | const mapStateToProps = (state: ReducerConfigure) => ({ 143 | nodeColors: state.logReducer.nodeColor 144 | }); 145 | export default connect(mapStateToProps)(LogItem); 146 | -------------------------------------------------------------------------------- /ui/src/components/Log/LogViewer/LogViewer.scss: -------------------------------------------------------------------------------- 1 | .log-viewer { 2 | background-color: white; 3 | table { 4 | table-layout: fixed; 5 | th { 6 | padding: 7px; 7 | } 8 | } 9 | .date-table-header { 10 | cursor: pointer; 11 | } 12 | .load-more { 13 | cursor: pointer; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/components/Log/LogViewer/LogViewer.tsx: -------------------------------------------------------------------------------- 1 | import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import * as _ from "lodash"; 4 | import * as React from "react"; 5 | import { connect } from "react-redux"; 6 | import Table from "reactstrap/lib/Table"; 7 | import { 8 | changeFilters, 9 | loadMoreLog, 10 | setAutoRefresh 11 | } from "../../../actions/log"; 12 | import { ReducerConfigure } from "../../../reducers"; 13 | import { Log } from "../../../requests/types"; 14 | import LogItem from "./LogItem/LogItem"; 15 | import "./LogViewer.css"; 16 | 17 | interface StateProps { 18 | orderBy: "DESC" | "ASC"; 19 | logs?: Log[] | null; 20 | isFetchingLog: boolean; 21 | noMoreData: boolean; 22 | itemPerPage: number; 23 | } 24 | 25 | interface DispatchProps { 26 | dispatch: any; 27 | } 28 | 29 | type Props = StateProps & DispatchProps; 30 | class LogViewer extends React.Component { 31 | public constructor(props: any) { 32 | super(props); 33 | } 34 | public render() { 35 | const { 36 | orderBy, 37 | isFetchingLog, 38 | logs, 39 | noMoreData, 40 | itemPerPage 41 | } = this.props; 42 | return ( 43 |
    44 | 45 | 46 | 47 | 59 | 62 | 65 | 68 | 85 | 86 | 87 | 88 | {logs && _.map(logs, log => )} 89 | {isFetchingLog ? ( 90 | 91 | 94 | 95 | ) : noMoreData ? ( 96 | 97 | 100 | 101 | ) : ( 102 | 103 | 106 | 107 | )} 108 | 109 |
    52 | Date{" "} 53 | {orderBy === "DESC" ? ( 54 | 55 | ) : ( 56 | 57 | )} 58 | 60 | Node 61 | 63 | Level 64 | 66 | Target 67 | 69 | Message{" "} 70 | 71 | Show{" "} 72 | {" "} 82 | Items 83 | 84 |
    92 | Loading... 93 |
    98 | No more log 99 |
    104 | Load More {itemPerPage} items 105 |
    110 |
    111 | ); 112 | } 113 | private handleChangeItemPerpage = (event: any) => { 114 | this.props.dispatch( 115 | changeFilters({ itemPerPage: parseInt(event.target.value, 10) }) 116 | ); 117 | }; 118 | private toggleOrder = () => { 119 | this.props.dispatch( 120 | changeFilters({ orderBy: this.props.orderBy === "DESC" ? "ASC" : "DESC" }) 121 | ); 122 | }; 123 | private handleLoadMore = () => { 124 | this.props.dispatch(setAutoRefresh(false)); 125 | this.props.dispatch(loadMoreLog()); 126 | }; 127 | } 128 | const mapStateToProps = (state: ReducerConfigure) => ({ 129 | logs: state.logReducer.logs as any[], 130 | orderBy: state.logReducer.orderBy, 131 | isFetchingLog: state.logReducer.isFetchingLog, 132 | noMoreData: state.logReducer.noMoreData, 133 | itemPerPage: state.logReducer.itemPerPage 134 | }); 135 | export default connect(mapStateToProps)(LogViewer); 136 | -------------------------------------------------------------------------------- /ui/src/components/Log/TopFilter/TopFilter.scss: -------------------------------------------------------------------------------- 1 | .top-filter { 2 | background-color: white; 3 | padding: 18px; 4 | 5 | .label-time { 6 | display: inline-block; 7 | width: 70px; 8 | } 9 | 10 | .date-picker { 11 | width: 250px; 12 | height: 30px; 13 | text-align: center; 14 | } 15 | 16 | .search-container { 17 | input { 18 | height: 40px; 19 | width: 300px; 20 | padding-left: 8px; 21 | padding-right: 8px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeDetailContainer/NodeDetail/NodeDetail.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | .node-detail { 3 | background-color: white; 4 | border: $container-border-color; 5 | border-radius: 3px; 6 | overflow: hidden; 7 | 8 | .left-panel { 9 | width: 50%; 10 | padding: 16px; 11 | border-right: 1px solid $container-border-color; 12 | } 13 | .status-btn { 14 | font-size: 0.8rem; 15 | } 16 | .link-text { 17 | cursor: pointer; 18 | text-decoration: underline !important; 19 | color: black; 20 | font-style: italic; 21 | &:hover { 22 | color: rgb(97, 97, 97); 23 | } 24 | } 25 | .spin { 26 | animation-name: spin; 27 | animation-duration: 2000ms; 28 | animation-iteration-count: infinite; 29 | animation-timing-function: linear; 30 | /* transform: rotate(3deg); */ 31 | /* transform: rotate(0.3rad);/ */ 32 | /* transform: rotate(3grad); */ 33 | /* transform: rotate(.03turn); */ 34 | } 35 | @keyframes spin { 36 | from { 37 | transform: rotate(0deg); 38 | } 39 | to { 40 | transform: rotate(360deg); 41 | } 42 | } 43 | .right-panel { 44 | flex: 1; 45 | padding: 16px; 46 | } 47 | .chart-container { 48 | width: 50%; 49 | .doughnut-chart { 50 | width: 300px; 51 | } 52 | } 53 | .chart-data-container { 54 | width: 50%; 55 | .chart-data { 56 | width: 200px; 57 | } 58 | } 59 | .data-container { 60 | height: 55px; 61 | padding: 8px; 62 | background-color: $dark-background-color; 63 | overflow: scroll; 64 | border: 1px solid $container-border-color; 65 | } 66 | .data-row { 67 | display: flex; 68 | justify-content: space-between !important; 69 | align-items: center !important; 70 | div:nth-child(1) { 71 | width: 50%; 72 | } 73 | div:nth-child(2) { 74 | width: 50%; 75 | text-align: right; 76 | text-overflow: ellipsis; 77 | /* Required for text-overflow to do anything */ 78 | white-space: nowrap; 79 | overflow: hidden; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeDetailContainer/NodeDetail/StartNodeModal/StartNodeModal.scss: -------------------------------------------------------------------------------- 1 | .start-node-modal-form { 2 | width: 500px; 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeDetailContainer/NodeDetail/StartNodeModal/StartNodeModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Modal from "react-modal"; 3 | import { Form, Label } from "reactstrap"; 4 | import "./StartNodeModal.css"; 5 | const customStyles = { 6 | content: { 7 | top: "50%", 8 | left: "50%", 9 | right: "auto", 10 | bottom: "auto", 11 | marginRight: "-50%", 12 | transform: "translate(-50%, -50%)" 13 | } 14 | }; 15 | interface Props { 16 | onClose: () => void; 17 | onStartNode: (env: string, args: string) => void; 18 | onAfterOpen: () => void; 19 | startOption?: { 20 | env: string; 21 | args: string; 22 | }; 23 | isOpen: boolean; 24 | } 25 | interface State { 26 | env: string; 27 | args: string; 28 | } 29 | 30 | export default class StartNodeModal extends React.Component { 31 | public constructor(props: Props) { 32 | super(props); 33 | const startOption = props.startOption; 34 | this.state = { 35 | env: startOption ? startOption.env : "", 36 | args: startOption ? startOption.args : "" 37 | }; 38 | } 39 | public render() { 40 | const { isOpen, onAfterOpen, onClose, startOption } = this.props; 41 | const { env, args } = this.state; 42 | return ( 43 |
    44 | 51 |
    52 |
    53 | 56 | 66 | 67 | {startOption ? startOption.env : ""} 68 | 69 |
    70 |
    71 | 72 | 82 | 83 | {startOption ? startOption.args : ""} 84 | 85 |
    86 |
    87 | 94 | 101 |
    102 |
    103 |
    104 |
    105 | ); 106 | } 107 | 108 | private onCloseClick = (e: any) => { 109 | e.preventDefault(); 110 | this.props.onClose(); 111 | }; 112 | 113 | private onSubmit = (e: any) => { 114 | e.preventDefault(); 115 | const { args, env } = this.state; 116 | this.props.onStartNode(env, args); 117 | }; 118 | 119 | private handleArgsChange = (event: any) => { 120 | this.setState({ args: event.target.value }); 121 | }; 122 | 123 | private handleEnvChange = (event: any) => { 124 | this.setState({ env: event.target.value }); 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeDetailContainer/NodeDetailContainer.scss: -------------------------------------------------------------------------------- 1 | .node-detail-container { 2 | min-width: 1000px; 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeDetailContainer/NodeDetailContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { connect } from "react-redux"; 3 | import { fetchNodeInfoIfNeeded } from "../../../actions/nodeInfo"; 4 | import { ReducerConfigure } from "../../../reducers"; 5 | import { NodeInfo } from "../../../requests/types"; 6 | import NodeDetail from "./NodeDetail/NodeDetail"; 7 | import "./NodeDetailContainer.css"; 8 | 9 | interface OwnProps { 10 | match: { 11 | params: { 12 | nodeId: string; 13 | }; 14 | }; 15 | } 16 | 17 | interface StateProps { 18 | nodeInfo?: NodeInfo | null; 19 | } 20 | 21 | interface DispatchProps { 22 | getNodeInfo: () => void; 23 | } 24 | 25 | type Props = DispatchProps & OwnProps & StateProps; 26 | class NodeDetailContainer extends React.Component { 27 | public componentDidMount() { 28 | if (!this.props.nodeInfo) { 29 | this.props.getNodeInfo(); 30 | } 31 | } 32 | public render() { 33 | const { nodeInfo } = this.props; 34 | if (!nodeInfo) { 35 | return
    Loading...
    ; 36 | } 37 | return ( 38 |
    39 | 40 |
    41 | ); 42 | } 43 | } 44 | const mapStateToProps = (state: ReducerConfigure, ownProps: OwnProps) => ({ 45 | nodeInfo: 46 | state.nodeInfoReducer.nodeInfos[decodeURI(ownProps.match.params.nodeId)] && 47 | state.nodeInfoReducer.nodeInfos[decodeURI(ownProps.match.params.nodeId)] 48 | .info 49 | }); 50 | const mapDispatchToProps = (dispatch: any, ownProps: OwnProps) => ({ 51 | getNodeInfo: async () => { 52 | const nodeId = decodeURI(ownProps.match.params.nodeId); 53 | dispatch(fetchNodeInfoIfNeeded(nodeId)); 54 | } 55 | }); 56 | export default connect( 57 | mapStateToProps, 58 | mapDispatchToProps 59 | )(NodeDetailContainer); 60 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeList.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import * as React from "react"; 3 | import { Route } from "react-router-dom"; 4 | import NodeDetailContainer from "./NodeDetailContainer/NodeDetailContainer"; 5 | import NodeListContainer from "./NodeListContainer/NodeListContainer"; 6 | 7 | interface Props { 8 | match: any; 9 | } 10 | export default class NodeList extends Component { 11 | public render() { 12 | const { match } = this.props; 13 | return ( 14 |
    15 | 16 | 17 |
    18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeListContainer/NodeItem/NodeItem.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | 3 | .node-item { 4 | border: 1px solid $container-border-color; 5 | border-radius: 3px; 6 | overflow: hidden; 7 | 8 | .node-item-info-container { 9 | background-color: white; 10 | flex: 1; 11 | padding: 8px; 12 | height: 66px; 13 | a { 14 | color: $dark-text-color; 15 | } 16 | .node-status { 17 | width: 50px; 18 | } 19 | .node-name { 20 | flex: 1; 21 | } 22 | &.active:hover { 23 | background-color: $dark-background-color; 24 | } 25 | } 26 | 27 | .setting-btn-container { 28 | background-color: $dark-background-color; 29 | color: $gray-text-color; 30 | width: 80px; 31 | border-left: 1px solid $container-border-color; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeListContainer/NodeItem/NodeItem.tsx: -------------------------------------------------------------------------------- 1 | import { faCircle, faCog } from "@fortawesome/free-solid-svg-icons"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import * as React from "react"; 4 | import { Link } from "react-router-dom"; 5 | import { NetworkNodeInfo } from "../../../../requests/types"; 6 | import { getStatusClass } from "../../../../utils/getStatusClass"; 7 | import "./NodeItem.css"; 8 | interface Props { 9 | className?: string; 10 | nodeInfo: NetworkNodeInfo; 11 | } 12 | 13 | const NodeItem = (props: Props) => { 14 | const { className, nodeInfo } = props; 15 | if (nodeInfo.status === "UFO") { 16 | return ( 17 |
    18 |
    19 |
    20 |
    21 | 25 |
    26 |
    27 | {nodeInfo.address 28 | ? `${nodeInfo.name} (${nodeInfo.address})` 29 | : nodeInfo.name} 30 |
    31 | 42 |
    43 |
    44 |
    45 | 46 |
    47 |
    48 | ); 49 | } else { 50 | return ( 51 |
    52 |
    53 | 54 |
    55 |
    56 | 60 |
    61 |
    62 | {nodeInfo.address 63 | ? `${nodeInfo.name} (${nodeInfo.address})` 64 | : nodeInfo.name} 65 |
    66 |
    67 |
    68 | Block:{" "} 69 | {nodeInfo.bestBlockId 70 | ? nodeInfo.bestBlockId.blockNumber 71 | : "Unknown"}{" "} 72 | ( 73 | {nodeInfo.bestBlockId 74 | ? nodeInfo.bestBlockId.hash.substr(0, 6) 75 | : "Unknown"} 76 | ) 77 |
    78 |
    79 | Version:{" "} 80 | {nodeInfo.version ? nodeInfo.version.version : "Unknown"} ( 81 | {nodeInfo.version 82 | ? nodeInfo.version.hash.substr(0, 6) 83 | : "Unknown"} 84 | ) 85 |
    86 |
    87 |
    88 | 89 |
    90 |
    91 | 92 |
    93 |
    94 | ); 95 | } 96 | }; 97 | 98 | export default NodeItem; 99 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeListContainer/NodeListContainer.scss: -------------------------------------------------------------------------------- 1 | .node-list-container { 2 | min-width: 800px; 3 | label { 4 | margin-left: 3px; 5 | margin-right: 10px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeListContainer/NodeListContainer.test.tsx: -------------------------------------------------------------------------------- 1 | import { NetworkNodeInfo } from "../../../requests/types"; 2 | import * as NLC from "./NodeListContainer"; 3 | 4 | describe("Test node ordering", () => { 5 | const alice: NetworkNodeInfo = { 6 | status: "Run", 7 | address: "119.202.81.99:8000", 8 | version: { 9 | version: "1.0.2", 10 | hash: "" 11 | }, 12 | bestBlockId: { 13 | blockNumber: 100, 14 | hash: "" 15 | }, 16 | name: "alice" 17 | }; 18 | const bob: NetworkNodeInfo = { 19 | status: "Starting", 20 | address: "141.223.175.99:8001", 21 | version: { 22 | version: "1.0.1", 23 | hash: "" 24 | }, 25 | bestBlockId: { 26 | blockNumber: 300, 27 | hash: "" 28 | }, 29 | name: "bob" 30 | }; 31 | const charlie: NetworkNodeInfo = { 32 | status: "Stop", 33 | address: "119.202.81.99:8002", 34 | version: { 35 | version: "1.1.4", 36 | hash: "" 37 | }, 38 | bestBlockId: { 39 | blockNumber: 200, 40 | hash: "" 41 | }, 42 | name: "charlie" 43 | }; 44 | const david: NetworkNodeInfo = { 45 | status: "Updating", 46 | address: "110.202.81.27:9090", 47 | version: { 48 | version: "2.1.1", 49 | hash: "" 50 | }, 51 | bestBlockId: { 52 | blockNumber: 320, 53 | hash: "" 54 | }, 55 | name: "david" 56 | }; 57 | const eve: NetworkNodeInfo = { 58 | status: "Error", 59 | address: "172.88.192.91:1010", 60 | version: { 61 | version: "1.2.7", 62 | hash: "" 63 | }, 64 | bestBlockId: { 65 | blockNumber: 270, 66 | hash: "" 67 | }, 68 | name: "eve" 69 | }; 70 | const nodeArray = [eve, david, charlie, bob, alice]; 71 | it("Test order by name", () => { 72 | const oracle = [alice, bob, charlie, david, eve]; 73 | nodeArray.sort(NLC.nodeNameComparator); 74 | expect(nodeArray).toEqual(oracle); 75 | }); 76 | it("Test order by socketAddress", () => { 77 | const oracle = [david, alice, charlie, bob, eve]; 78 | nodeArray.sort(NLC.nodeSocketAddressComapartor); 79 | expect(nodeArray).toEqual(oracle); 80 | }); 81 | it("Test order by blockNumber", () => { 82 | const oracle = [alice, charlie, eve, bob, david]; 83 | nodeArray.sort(NLC.nodeBlockNumberComparator); 84 | expect(nodeArray).toEqual(oracle); 85 | }); 86 | it("Test order by version", () => { 87 | const oracle = [bob, alice, charlie, eve, david]; 88 | nodeArray.sort(NLC.nodeVersionComparator); 89 | expect(nodeArray).toEqual(oracle); 90 | }); 91 | it("Test order by status", () => { 92 | const oracle = [eve, charlie, bob, david, alice]; 93 | nodeArray.sort(NLC.nodeStatusComparator); 94 | expect(nodeArray).toEqual(oracle); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeListContainer/SelectNodesModal/SelectNodeCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props { 4 | name: string; 5 | checked: boolean; 6 | onChange: (name: string) => void; 7 | } 8 | 9 | export default class SelectNodeCheckbox extends React.Component { 10 | public render() { 11 | const { name, checked } = this.props; 12 | return ( 13 |
    14 | 15 | 20 |
    21 | ); 22 | } 23 | 24 | private handleOnChange = () => { 25 | this.props.onChange(this.props.name); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeListContainer/SelectNodesModal/SelectNodesModal.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeChain-io/codechain-dashboard/882bf546584eb8da0524412d20c1e0b2f0cc019a/ui/src/components/NodeList/NodeListContainer/SelectNodesModal/SelectNodesModal.scss -------------------------------------------------------------------------------- /ui/src/components/NodeList/NodeListContainer/SelectNodesModal/SelectNodesModal.tsx: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as React from "react"; 3 | import Modal from "react-modal"; 4 | import { ChainNetworks, NetworkNodeInfo } from "../../../../requests/types"; 5 | import SelectNodeCheckbox from "./SelectNodeCheckbox"; 6 | 7 | const customStyles = { 8 | content: { 9 | top: "50%", 10 | left: "50%", 11 | right: "auto", 12 | bottom: "auto", 13 | marginRight: "-50%", 14 | transform: "translate(-50%, -50%)" 15 | } 16 | }; 17 | 18 | interface Props { 19 | onClose: () => void; 20 | isOpen: boolean; 21 | onSelectNodes: (selectedNodes: string[]) => void; 22 | chainNetworks: ChainNetworks; 23 | } 24 | 25 | interface State { 26 | selectedNodes: { [index: string]: boolean }; 27 | } 28 | 29 | export default class SelectNodesModal extends React.Component { 30 | public constructor(props: Props) { 31 | super(props); 32 | this.state = { 33 | selectedNodes: _.chain(props.chainNetworks.nodes) 34 | .map((nodeInfo: NetworkNodeInfo) => { 35 | return [nodeInfo.name, false]; 36 | }) 37 | .fromPairs() 38 | .value() 39 | }; 40 | } 41 | 42 | public render() { 43 | const { isOpen, onClose, chainNetworks } = this.props; 44 | const { selectedNodes } = this.state; 45 | return ( 46 |
    47 | 53 |
    54 |
    55 | {_.map(chainNetworks.nodes, (nodeInfo: NetworkNodeInfo) => { 56 | return ( 57 | 62 | ); 63 | })} 64 |
    65 | 68 | 71 | 74 |
    75 |
    76 |
    77 | ); 78 | } 79 | 80 | private handleNodeCheckboxChange = (name: string) => { 81 | const prevState = this.state.selectedNodes[name]; 82 | this.setState({ 83 | selectedNodes: { 84 | ...this.state.selectedNodes, 85 | [name]: !prevState 86 | } 87 | }); 88 | }; 89 | 90 | private handleSelectAllClick = () => { 91 | const prevSelection = this.state.selectedNodes; 92 | this.setState({ 93 | selectedNodes: _.mapValues(prevSelection, () => true) 94 | }); 95 | }; 96 | 97 | private handleDeselectAllClick = () => { 98 | const prevSelection = this.state.selectedNodes; 99 | this.setState({ 100 | selectedNodes: _.mapValues(prevSelection, () => false) 101 | }); 102 | }; 103 | 104 | private handleConfirmClick = () => { 105 | const selectedNodeNames = _.chain(this.state.selectedNodes) 106 | .pickBy(selected => selected) 107 | .keys() 108 | .value(); 109 | _.pickBy(this.state.selectedNodes); 110 | this.props.onSelectNodes(selectedNodeNames); 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /ui/src/components/NodeList/UpgradeNodeModal/UpgradeNodeModal.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | .upgrade-node-modal { 3 | width: 800px; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/components/RPC/RPC.scss: -------------------------------------------------------------------------------- 1 | .rpc-container { 2 | min-width: 900px; 3 | .left-panel { 4 | width: 300px; 5 | background-color: white; 6 | margin-right: 18px; 7 | height: 800px; 8 | } 9 | 10 | .right-panel { 11 | flex: 1; 12 | background-color: white; 13 | height: 800px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/components/RPC/RPC.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./RPC.css"; 3 | import { RPCLeftPanel } from "./RPCLeftPanel/RPCLeftPanel"; 4 | import RPCRightPanel from "./RPCRightPanel/RPCRightPanel"; 5 | 6 | interface State { 7 | selectedItem?: { 8 | method: string; 9 | params: object[] | object; 10 | }; 11 | } 12 | export default class RPC extends React.Component<{}, State> { 13 | public constructor(props: {}) { 14 | super(props); 15 | this.state = { 16 | selectedItem: undefined 17 | }; 18 | } 19 | public render() { 20 | const { selectedItem } = this.state; 21 | return ( 22 |
    23 | 27 | 28 |
    29 | ); 30 | } 31 | 32 | private handleItemSelect = (rpc: { 33 | method: string; 34 | params: object[] | object; 35 | }) => { 36 | this.setState({ selectedItem: rpc }); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/components/RPC/RPCLeftPanel/RPCLeftPanel.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | .rpc-left-panel { 3 | height: 100%; 4 | overflow: hidden; 5 | flex-direction: column; 6 | border: $container-border-color; 7 | border-radius: 3px; 8 | 9 | .button-container { 10 | padding-top: 18px; 11 | border-bottom: 1px solid $container-border-color; 12 | .history-button { 13 | width: 50%; 14 | color: $gray-text-color; 15 | padding-bottom: 5px; 16 | cursor: pointer; 17 | &.active { 18 | color: $dark-text-color; 19 | border-bottom: 3px solid $primary-color; 20 | } 21 | } 22 | .collection-button { 23 | width: 50%; 24 | color: $gray-text-color; 25 | padding-bottom: 5px; 26 | cursor: pointer; 27 | &.active { 28 | color: $dark-text-color; 29 | } 30 | } 31 | } 32 | .history-container { 33 | flex: 1; 34 | overflow: scroll; 35 | .history-item { 36 | border: 1px solid $container-border-color; 37 | border-radius: 5px; 38 | margin-top: 9px; 39 | padding: 9px; 40 | margin-left: 18px; 41 | margin-right: 18px; 42 | cursor: pointer; 43 | .history-item-name { 44 | font-size: 1rem; 45 | } 46 | .history-item-params { 47 | font-size: 0.8rem; 48 | white-space: nowrap; 49 | overflow: hidden; 50 | text-overflow: ellipsis; 51 | } 52 | .history-item-node-list { 53 | font-size: 0.8rem; 54 | color: gray; 55 | white-space: nowrap; 56 | overflow: hidden; 57 | text-overflow: ellipsis; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ui/src/components/RPC/RPCLeftPanel/RPCLeftPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as React from "react"; 3 | import "./RPCLeftPanel.css"; 4 | 5 | interface Props { 6 | className?: string; 7 | onClickHistoryItem: (rpc: { 8 | method: string; 9 | params: object[] | object; 10 | }) => void; 11 | } 12 | export class RPCLeftPanel extends React.Component { 13 | public render() { 14 | const { className } = this.props; 15 | return ( 16 |
    17 |
    18 |
    History
    19 |
    Collections
    20 |
    21 |
    22 | {_.map(_.range(10), index => { 23 | return ( 24 |
    29 |

    30 | Dummy_getBestBlockNumber 31 |

    32 |

    33 | parameter1 parameter2 parameter3 parameter4 34 |

    35 |

    36 | agent1 agent2 agent3 agent4 agent5 agant6 37 |

    38 |
    39 | ); 40 | })} 41 |
    42 |
    43 | ); 44 | } 45 | 46 | private onClickItem = () => { 47 | this.props.onClickHistoryItem({ 48 | method: "Dummy_getBestBlockNumber", 49 | params: ["DummyParam", 1, "number", "123"] 50 | }); 51 | }; 52 | } 53 | 54 | export default RPCLeftPanel; 55 | -------------------------------------------------------------------------------- /ui/src/components/RPC/RPCRightPanel/RPCRightPanel.scss: -------------------------------------------------------------------------------- 1 | @import "styles/variables.scss"; 2 | 3 | .rpc-right-panel { 4 | overflow: hidden; 5 | border: $container-border-color; 6 | border-radius: 3px; 7 | padding: 18px; 8 | 9 | .rpc-input { 10 | height: 250px; 11 | resize: none; 12 | } 13 | .rpc-response { 14 | height: 200px; 15 | resize: none; 16 | } 17 | .rpc-response-tab-container { 18 | height: 50px; 19 | margin-bottom: 5px; 20 | overflow: scroll; 21 | white-space: nowrap; 22 | 23 | .rpc-response-tab { 24 | margin-right: 8px; 25 | padding-left: 8px; 26 | padding-right: 8px; 27 | border: 1px solid #cfd4d9; 28 | cursor: pointer; 29 | 30 | &.active { 31 | background-color: #e9ecef; 32 | } 33 | span { 34 | line-height: 40px; 35 | } 36 | } 37 | } 38 | .select-btn { 39 | width: 50px; 40 | font-size: 0.7rem; 41 | padding: 3px; 42 | } 43 | a { 44 | text-decoration: underline !important; 45 | color: black; 46 | font-style: italic; 47 | &:hover { 48 | color: rgb(97, 97, 97); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import * as React from "react"; 3 | import "react-confirm-alert/src/react-confirm-alert.css"; 4 | import * as ReactDOM from "react-dom"; 5 | import { Provider } from "react-redux"; 6 | import "react-widgets/dist/css/react-widgets.css"; 7 | import { applyMiddleware, createStore } from "redux"; 8 | import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction"; 9 | import thunkMiddleware from "redux-thunk"; 10 | import App from "./components/App/App"; 11 | import appReducer from "./reducers"; 12 | import { unregister } from "./registerServiceWorker"; 13 | import "./styles/index.css"; 14 | 15 | const composeEnhancers = composeWithDevTools({}); 16 | const store = createStore( 17 | appReducer, 18 | composeEnhancers(applyMiddleware(thunkMiddleware)) 19 | ); 20 | 21 | ReactDOM.render( 22 | 23 | 24 | , 25 | document.getElementById("root") as HTMLElement 26 | ); 27 | unregister(); 28 | -------------------------------------------------------------------------------- /ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui/src/reducers/chainNetworks.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { ChainNetworksAction } from "../actions/chainNetworks"; 3 | import { ChainNetworks } from "../requests/types"; 4 | const merge = require("deepmerge").default; 5 | const overwriteMerge = ( 6 | destinationArray: any, 7 | sourceArray: any, 8 | options: any 9 | ) => sourceArray; 10 | 11 | export interface ChainNetworksState { 12 | chainNetworks: ChainNetworks | undefined; 13 | isFetching: boolean; 14 | lastUpdated?: number | null; 15 | } 16 | 17 | const initialState: ChainNetworksState = { 18 | chainNetworks: undefined, 19 | isFetching: false 20 | }; 21 | 22 | export const chainNetworksReducer = ( 23 | state = initialState, 24 | action: ChainNetworksAction 25 | ) => { 26 | switch (action.type) { 27 | case "RequestChainNetworks": { 28 | return { 29 | ...state, 30 | isFetching: true 31 | }; 32 | } 33 | case "SetChainNetworks": { 34 | return { 35 | ...state, 36 | chainNetworks: action.data, 37 | isFetching: false, 38 | lastUpdated: action.receivedAt 39 | }; 40 | } 41 | case "UpdateChainNetworks": { 42 | const chainNetworks = state.chainNetworks; 43 | if (!chainNetworks) { 44 | return { 45 | ...state 46 | }; 47 | } 48 | 49 | const newNodes = _.differenceBy( 50 | action.data.nodes, 51 | chainNetworks.nodes, 52 | "name" 53 | ); 54 | 55 | const updatedNodes = _.map(chainNetworks.nodes, node => { 56 | const findNode = _.find( 57 | action.data.nodes, 58 | actionNode => actionNode.name === node.name 59 | ); 60 | if (findNode) { 61 | return merge(node, findNode, { arrayMerge: overwriteMerge }); 62 | } else { 63 | return node; 64 | } 65 | }); 66 | 67 | const addedConnections = 68 | action.data.connectionsAdded && action.data.connectionsAdded.length > 0 69 | ? _.concat(chainNetworks.connections, action.data.connectionsAdded) 70 | : _.cloneDeep(chainNetworks.connections); 71 | 72 | const removedConnections = 73 | action.data.connectionsRemoved && 74 | action.data.connectionsRemoved.length > 0 75 | ? _.differenceWith( 76 | addedConnections, 77 | action.data.connectionsRemoved, 78 | _.isEqual 79 | ) 80 | : addedConnections; 81 | return { 82 | ...state, 83 | chainNetworks: { 84 | nodes: _.concat(updatedNodes, newNodes), 85 | connections: removedConnections 86 | } 87 | }; 88 | } 89 | } 90 | return state; 91 | }; 92 | -------------------------------------------------------------------------------- /ui/src/reducers/graph.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { GraphAction } from "../actions/graph"; 3 | import NetworkOutNodePeerGraph from "../components/Graph/GraphNode/NetworkOutNodePeerGraph/NetworkOutNodePeerGraph"; 4 | import NetworkOutAllGraph from "../components/Graph/NetworkOutAllGraph/NetworkOutAllGraph"; 5 | import { 6 | GraphNetworkOutAllAVGRow, 7 | GraphNetworkOutAllRow, 8 | GraphNetworkOutNodeExtensionRow, 9 | GraphNetworkOutNodePeerRow 10 | } from "../requests/types"; 11 | const merge = require("deepmerge").default; 12 | 13 | export interface GraphState { 14 | networkOutAllGraph: NetworkOutAllGraph; 15 | networkOutAllAVGGraph: NetworkOutAllAVGGraph; 16 | networkOutNodeExtensionGraph: NetworkOutNodeExtensionGraph; 17 | networkOutNodePeerGraph: NetworkOutNodePeerGraph; 18 | } 19 | 20 | export interface NetworkOutAllGraph { 21 | data: GraphNetworkOutAllRow[]; 22 | time: { 23 | fromTime: number; 24 | toTime: number; 25 | }; 26 | } 27 | 28 | export interface NetworkOutAllAVGGraph { 29 | data: GraphNetworkOutAllAVGRow[]; 30 | time: { 31 | fromTime: number; 32 | toTime: number; 33 | }; 34 | } 35 | 36 | export interface NetworkOutNodeExtensionGraph { 37 | nodeId: string; 38 | data: GraphNetworkOutNodeExtensionRow[]; 39 | time: { 40 | fromTime: number; 41 | toTime: number; 42 | }; 43 | } 44 | 45 | export interface NetworkOutNodePeerGraph { 46 | nodeId: string; 47 | data: GraphNetworkOutNodePeerRow[]; 48 | time: { 49 | fromTime: number; 50 | toTime: number; 51 | }; 52 | } 53 | 54 | const initialState: GraphState = { 55 | networkOutAllGraph: { 56 | data: [], 57 | time: { 58 | fromTime: moment() 59 | .subtract(7, "days") 60 | .unix(), 61 | toTime: moment().unix() 62 | } 63 | }, 64 | networkOutAllAVGGraph: { 65 | data: [], 66 | time: { 67 | fromTime: moment() 68 | .subtract(7, "days") 69 | .unix(), 70 | toTime: moment().unix() 71 | } 72 | }, 73 | networkOutNodeExtensionGraph: { 74 | nodeId: "", 75 | data: [], 76 | time: { 77 | fromTime: moment() 78 | .subtract(7, "days") 79 | .unix(), 80 | toTime: moment().unix() 81 | } 82 | }, 83 | networkOutNodePeerGraph: { 84 | nodeId: "", 85 | data: [], 86 | time: { 87 | fromTime: moment() 88 | .subtract(7, "days") 89 | .unix(), 90 | toTime: moment().unix() 91 | } 92 | } 93 | }; 94 | 95 | export const graphReducer = (state = initialState, action: GraphAction) => { 96 | switch (action.type) { 97 | case "ChangeNetworkOutAllFilters": 98 | return merge(state, { networkOutAllGraph: action.data }); 99 | case "SetNetworkOutAllGraph": 100 | return { 101 | ...state, 102 | networkOutAllGraph: { 103 | ...state.networkOutAllGraph, 104 | data: action.data 105 | } 106 | }; 107 | case "ChangeNetworkOutAllAVGFilters": 108 | return merge(state, { networkOutAllAVGGraph: action.data }); 109 | case "SetNetworkOutAllAVGGraph": 110 | return { 111 | ...state, 112 | networkOutAllAVGGraph: { 113 | ...state.networkOutAllAVGGraph, 114 | data: action.data 115 | } 116 | }; 117 | case "ChangeNetworkOutNodeExtensionFilters": 118 | return merge(state, { 119 | networkOutNodeExtensionGraph: action.data 120 | }); 121 | case "SetNetworkOutNodeExtensionGraph": 122 | return { 123 | ...state, 124 | networkOutNodeExtensionGraph: { 125 | ...state.networkOutNodeExtensionGraph, 126 | data: action.data 127 | } 128 | }; 129 | case "ChangeNetworkOutNodePeerFilters": 130 | return merge(state, { 131 | networkOutNodePeerGraph: action.data 132 | }); 133 | case "SetNetworkOutNodePeerGraph": 134 | return { 135 | ...state, 136 | networkOutNodePeerGraph: { 137 | ...state.networkOutNodePeerGraph, 138 | data: action.data 139 | } 140 | }; 141 | } 142 | return state; 143 | }; 144 | -------------------------------------------------------------------------------- /ui/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { chainNetworksReducer, ChainNetworksState } from "./chainNetworks"; 3 | import { graphReducer, GraphState } from "./graph"; 4 | import { logReducer, LogState } from "./log"; 5 | import { nodeInfoReducer, NodeState } from "./nodeInfo"; 6 | 7 | export interface ReducerConfigure { 8 | nodeInfoReducer: NodeState; 9 | chainNetworksReducer: ChainNetworksState; 10 | logReducer: LogState; 11 | graphReducer: GraphState; 12 | } 13 | 14 | const rootReducer = combineReducers({ 15 | nodeInfoReducer, 16 | chainNetworksReducer, 17 | logReducer, 18 | graphReducer 19 | } as any); 20 | export default rootReducer; 21 | -------------------------------------------------------------------------------- /ui/src/reducers/log.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { LogAction } from "../actions/log"; 3 | import Log from "../components/Log/Log"; 4 | import { getObjectFromStorage, saveObjectToStorage } from "../utils/storage"; 5 | const merge = require("deepmerge").default; 6 | const overwriteMerge = ( 7 | destinationArray: any, 8 | sourceArray: any, 9 | options: any 10 | ) => sourceArray; 11 | 12 | export interface LogState { 13 | filter: { 14 | nodeNames: string[]; 15 | levels: ("error" | "warn" | "info" | "debug" | "trace")[]; 16 | targets: string[]; 17 | }; 18 | search: string; 19 | time: { 20 | fromTime: number; 21 | toTime: number; 22 | }; 23 | page: number; 24 | itemPerPage: number; 25 | isFetchingLog: boolean; 26 | isFetchingTarget: boolean; 27 | targets?: string[] | null; 28 | lastUpdated?: number | null; 29 | logs?: Log[] | null; 30 | orderBy: "ASC" | "DESC"; 31 | fetchingUUIDForLog?: string | null; 32 | nodeColor: { 33 | [nodeName: string]: string; 34 | }; 35 | noMoreData: boolean; 36 | setAutoRefresh: boolean; 37 | setFromTime: boolean; 38 | setToTime: boolean; 39 | } 40 | 41 | const initialState: LogState = { 42 | filter: { 43 | nodeNames: [], 44 | levels: ["error", "warn", "info", "debug", "trace"], 45 | targets: [] 46 | }, 47 | time: { 48 | fromTime: moment() 49 | .subtract("days", 7) 50 | .unix(), 51 | toTime: moment().unix() 52 | }, 53 | search: "", 54 | page: 1, 55 | itemPerPage: 56 | ((getObjectFromStorage("itemPerPage") as { itemPerPage: number }) && 57 | (getObjectFromStorage("itemPerPage") as { itemPerPage: number }) 58 | .itemPerPage) || 59 | 15, 60 | isFetchingLog: false, 61 | isFetchingTarget: false, 62 | orderBy: "DESC", 63 | nodeColor: getObjectFromStorage("nodeColor") || {}, 64 | noMoreData: false, 65 | setAutoRefresh: false, 66 | setFromTime: true, 67 | setToTime: true 68 | }; 69 | 70 | export const logReducer = (state = initialState, action: LogAction) => { 71 | switch (action.type) { 72 | case "RequestTargets": { 73 | return { ...state, isFetchingTarget: true }; 74 | } 75 | case "SetTargets": { 76 | return { ...state, targets: action.data, isFetchingTarget: false }; 77 | } 78 | case "RequestLogs": { 79 | return { ...state, isFetchingLog: true, fetchingUUIDForLog: action.data }; 80 | } 81 | case "SetLogs": { 82 | return { ...state, logs: action.data, isFetchingLog: false }; 83 | } 84 | case "SetNodeColor": { 85 | const newNodeColor = { 86 | ...state.nodeColor, 87 | [action.data.nodeName]: action.data.color 88 | }; 89 | saveObjectToStorage("nodeColor", newNodeColor); 90 | return { 91 | ...state, 92 | nodeColor: newNodeColor 93 | }; 94 | } 95 | case "LoadMore": { 96 | return { 97 | ...state, 98 | page: action.data 99 | }; 100 | } 101 | case "SetNoMoreData": { 102 | return { 103 | ...state, 104 | noMoreData: true 105 | }; 106 | } 107 | case "SetAutoRefresh": { 108 | return { 109 | ...state, 110 | setAutoRefresh: action.data 111 | }; 112 | } 113 | case "ChangeFilters": { 114 | if (action.data.itemPerPage) { 115 | saveObjectToStorage("itemPerPage", { 116 | itemPerPage: action.data.itemPerPage 117 | }); 118 | } 119 | return merge({ ...state, noMoreData: false, page: 1 }, action.data, { 120 | arrayMerge: overwriteMerge 121 | }); 122 | } 123 | } 124 | return state; 125 | }; 126 | -------------------------------------------------------------------------------- /ui/src/reducers/nodeInfo.ts: -------------------------------------------------------------------------------- 1 | import { NodeInfoAction } from "../actions/nodeInfo"; 2 | import { NodeInfo } from "../requests/types"; 3 | const merge = require("deepmerge").default; 4 | const overwriteMerge = ( 5 | destinationArray: any, 6 | sourceArray: any, 7 | options: any 8 | ) => sourceArray; 9 | 10 | export interface NodeState { 11 | nodeInfos: { 12 | [name: string]: { 13 | info?: NodeInfo | null; 14 | isFetching: boolean; 15 | lastUpdated?: number | null; 16 | }; 17 | }; 18 | } 19 | 20 | const initialState: NodeState = { 21 | nodeInfos: {} 22 | }; 23 | 24 | export const nodeInfoReducer = ( 25 | state = initialState, 26 | action: NodeInfoAction 27 | ) => { 28 | switch (action.type) { 29 | case "RequestNodeInfo": { 30 | const nodeInfos = { 31 | ...state.nodeInfos, 32 | [action.name]: { 33 | isFetching: true 34 | } 35 | }; 36 | return { 37 | ...state, 38 | nodeInfos 39 | }; 40 | } 41 | case "SetNodeInfo": { 42 | const nodeInfos = { 43 | ...state.nodeInfos, 44 | [action.name]: { 45 | info: action.data, 46 | isFetching: false, 47 | lastUpdated: action.receivedAt 48 | } 49 | }; 50 | return { 51 | ...state, 52 | nodeInfos 53 | }; 54 | } 55 | case "UpdateNodeInfo": 56 | if (!state.nodeInfos[action.name]) { 57 | return { 58 | ...state 59 | }; 60 | } 61 | const updatedNodeInfos = { 62 | ...state.nodeInfos, 63 | [action.name]: { 64 | ...state.nodeInfos[action.name], 65 | info: merge(state.nodeInfos[action.name].info || {}, action.data, { 66 | arrayMerge: overwriteMerge 67 | }) 68 | } 69 | }; 70 | return { 71 | ...state, 72 | nodeInfos: updatedNodeInfos 73 | }; 74 | } 75 | return state; 76 | }; 77 | -------------------------------------------------------------------------------- /ui/src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | // In production, we register a service worker to serve assets from local cache. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on the 'N+1' visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === "localhost" || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === "[::1]" || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 19 | ) 20 | ); 21 | 22 | export default function register() { 23 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 24 | // The URL constructor is available in all browsers that support SW. 25 | const publicUrl = new URL( 26 | process.env.PUBLIC_URL!, 27 | window.location.toString() 28 | ); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener("load", () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | 39 | if (isLocalhost) { 40 | // This is running on localhost. Lets check if a service worker still exists or not. 41 | checkValidServiceWorker(swUrl); 42 | 43 | // Add some additional logging to localhost, pointing developers to the 44 | // service worker/PWA documentation. 45 | navigator.serviceWorker.ready.then(() => { 46 | console.log( 47 | "This web app is being served cache-first by a service " + 48 | "worker. To learn more, visit https://goo.gl/SC7cgQ" 49 | ); 50 | }); 51 | } else { 52 | // Is not local host. Just register service worker 53 | registerValidSW(swUrl); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | function registerValidSW(swUrl: string) { 60 | navigator.serviceWorker 61 | .register(swUrl) 62 | .then(registration => { 63 | registration.onupdatefound = () => { 64 | const installingWorker = registration.installing; 65 | if (installingWorker) { 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === "installed") { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the old content will have been purged and 70 | // the fresh content will have been added to the cache. 71 | // It's the perfect time to display a 'New content is 72 | // available; please refresh.' message in your web app. 73 | console.log("New content is available; please refresh."); 74 | } else { 75 | // At this point, everything has been precached. 76 | // It's the perfect time to display a 77 | // 'Content is cached for offline use.' message. 78 | console.log("Content is cached for offline use."); 79 | } 80 | } 81 | }; 82 | } 83 | }; 84 | }) 85 | .catch(error => { 86 | console.error("Error during service worker registration:", error); 87 | }); 88 | } 89 | 90 | function checkValidServiceWorker(swUrl: string) { 91 | // Check if the service worker can be found. If it can't reload the page. 92 | fetch(swUrl) 93 | .then(response => { 94 | // Ensure service worker exists, and that we really are getting a JS file. 95 | if ( 96 | response.status === 404 || 97 | response.headers.get("content-type")!.indexOf("javascript") === -1 98 | ) { 99 | // No service worker found. Probably a different app. Reload the page. 100 | navigator.serviceWorker.ready.then(registration => { 101 | registration.unregister().then(() => { 102 | window.location.reload(); 103 | }); 104 | }); 105 | } else { 106 | // Service worker found. Proceed as normal. 107 | registerValidSW(swUrl); 108 | } 109 | }) 110 | .catch(() => { 111 | console.log( 112 | "No internet connection found. App is running in offline mode." 113 | ); 114 | }); 115 | } 116 | 117 | export function unregister() { 118 | if ("serviceWorker" in navigator) { 119 | navigator.serviceWorker.ready.then(registration => { 120 | registration.unregister(); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /ui/src/requests/index.ts: -------------------------------------------------------------------------------- 1 | import RequestAgent from "../RequestAgent"; 2 | import { NodeInfo, UpdateCodeChainRequest } from "./types"; 3 | 4 | const startNode = async (nodeName: string, env: string, args: string) => { 5 | return await RequestAgent.getInstance().call("node_start", [ 6 | nodeName, 7 | { 8 | env, 9 | args 10 | } 11 | ]); 12 | }; 13 | 14 | const stopNode = async (nodeName: string) => { 15 | return await RequestAgent.getInstance().call("node_stop", [ 16 | nodeName 17 | ]); 18 | }; 19 | 20 | const updateNode = async (nodeName: string, req: UpdateCodeChainRequest) => { 21 | return await RequestAgent.getInstance().call("node_update", [ 22 | nodeName, 23 | req 24 | ]); 25 | }; 26 | 27 | export const Apis = { 28 | startNode, 29 | stopNode, 30 | updateNode 31 | }; 32 | -------------------------------------------------------------------------------- /ui/src/requests/types.ts: -------------------------------------------------------------------------------- 1 | export type NodeStatus = 2 | | "Run" 3 | | "Starting" 4 | | "Stop" 5 | | "Error" 6 | | "UFO" 7 | | "Updating"; 8 | export type SocketAddr = string; 9 | export type IpAddr = string; 10 | export type Tag = string; 11 | export type BlackList = WhiteList; 12 | export interface WhiteList { 13 | list: [IpAddr, Tag][]; 14 | enabled: boolean; 15 | } 16 | export enum CommonError { 17 | CodeChainIsNotRunning = 0, 18 | AgentNotFound = -1, 19 | InternalError = -32603 20 | } 21 | export interface NetworkNodeInfo { 22 | status: NodeStatus; 23 | address?: SocketAddr; 24 | version?: { version: string; hash: string }; 25 | bestBlockId?: { blockNumber: number; hash: string }; 26 | name: string; 27 | } 28 | export interface ChainNetworks { 29 | nodes: NetworkNodeInfo[]; 30 | connections: { nodeA: string; nodeB: string }[]; 31 | } 32 | export interface ChainNetworksUpdate { 33 | nodes?: { 34 | status?: NodeStatus; 35 | address?: SocketAddr; 36 | version?: { version: string; hash: string }; 37 | bestBlockId?: { blockNumber: number; hash: string }; 38 | name: string; 39 | }[]; 40 | connectionsAdded?: { nodeA: string; nodeB: string }[]; 41 | connectionsRemoved?: { nodeA: string; nodeB: string }[]; 42 | } 43 | export interface NodeInfo { 44 | name: string; 45 | startOption?: { env: string; args: string }; 46 | address?: SocketAddr; 47 | agentVersion?: string; 48 | status: NodeStatus; 49 | version?: { version: string; hash: string; binaryChecksum: string }; 50 | bestBlockId?: { blockNumber: number; hash: string }; 51 | pendingTransactions?: object[]; 52 | peers?: SocketAddr[]; 53 | whitelist?: WhiteList; 54 | blacklist?: BlackList; 55 | hardware?: { 56 | cpuUsage: number[]; 57 | diskUsage: { total: number; available: number; percentageUsed: number }; 58 | memoryUsage: { total: number; available: number; percentageUsed: number }; 59 | }; 60 | events?: string[]; 61 | } 62 | export interface NodeUpdateInfo { 63 | name: string; 64 | startOption?: { env: string; args: string }; 65 | address?: SocketAddr; 66 | agentVersion?: string; 67 | status?: NodeStatus; 68 | version?: { version: string; hash: string }; 69 | bestBlockId?: { blockNumber: number; hash: string }; 70 | pendingTransactions?: object[]; 71 | peers?: SocketAddr[]; 72 | whitelist?: { list: SocketAddr[]; enabled: boolean }; 73 | blacklist?: { list: SocketAddr[]; enabled: boolean }; 74 | hardware?: { 75 | cpuUsage: number[]; 76 | diskUsage: { total: number; available: number; percentageUsed: number }; 77 | memoryUsage: { total: number; available: number; percentageUsed: number }; 78 | }; 79 | events?: string[]; 80 | } 81 | 82 | export interface Log { 83 | id: string; 84 | nodeName: string; 85 | level: string; 86 | target: string; 87 | timestamp: string; 88 | message: string; 89 | } 90 | 91 | export type UpdateCodeChainRequest = 92 | | { 93 | type: "git"; 94 | commitHash: string; 95 | } 96 | | { 97 | type: "binary"; 98 | binaryURL: string; 99 | binaryChecksum: string; 100 | }; 101 | 102 | export interface GraphNetworkOutAllRow { 103 | nodeName: string; 104 | time: string; 105 | value: number; 106 | } 107 | 108 | export type GraphNetworkOutAllAVGRow = GraphNetworkOutAllRow; 109 | 110 | export interface GraphNetworkOutNodeExtensionRow { 111 | extension: string; 112 | time: string; 113 | value: number; 114 | } 115 | 116 | export interface GraphNetworkOutNodePeerRow { 117 | peer: string; 118 | time: string; 119 | value: number; 120 | } 121 | -------------------------------------------------------------------------------- /ui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "jest-canvas-mock"; 2 | 3 | (global as any).URL.createObjectURL = () => null; 4 | 5 | export default undefined; 6 | -------------------------------------------------------------------------------- /ui/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $header-height: 50px; 2 | $gnb-width: 70px; 3 | 4 | $header-background-color: #222222; 5 | $gnb-backgroun-color: #222222; 6 | 7 | $dark-background-color: #f4f5f6; 8 | $container-border-color: #cfcfcf; 9 | 10 | $dark-text-color: #222222; 11 | $gray-text-color: #6f6f6f; 12 | 13 | $primary-color: #f26420; 14 | -------------------------------------------------------------------------------- /ui/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "~animate.css/animate.css"; 2 | 3 | html, 4 | body { 5 | margin: 0; 6 | height: 100%; 7 | width: 100%; 8 | background-color: rgba(97, 121, 130, 0.15); 9 | overflow: hidden; 10 | } 11 | a { 12 | text-decoration: none !important; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/utils/getStatusClass.ts: -------------------------------------------------------------------------------- 1 | import { NodeStatus } from "../requests/types"; 2 | 3 | export const getStatusClass = (status: NodeStatus) => { 4 | switch (status) { 5 | case "Run": 6 | return "text-success"; 7 | case "Stop": 8 | return "text-secondary"; 9 | case "Error": 10 | return "text-danger"; 11 | case "Starting": 12 | case "Updating": 13 | return "text-warning"; 14 | case "UFO": 15 | return "text-info"; 16 | } 17 | throw new Error("Invalid status"); 18 | }; 19 | -------------------------------------------------------------------------------- /ui/src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | export const getObjectFromStorage = (key: string): T | null | undefined => { 2 | if (typeof Storage !== "undefined") { 3 | const item = sessionStorage.getItem(key); 4 | if (item) { 5 | try { 6 | return JSON.parse(item); 7 | } catch (e) { 8 | // nothing 9 | } 10 | } 11 | } 12 | return undefined; 13 | }; 14 | 15 | export const saveObjectToStorage = (key: string, data: object) => { 16 | if (typeof Storage !== "undefined") { 17 | sessionStorage.setItem(key, JSON.stringify(data)); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "preserve", 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /ui/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": false, 5 | "no-console": false, 6 | "object-literal-sort-keys": false, 7 | "no-var-requires": false, 8 | "array-type": false 9 | }, 10 | "jsRules": { 11 | "no-console": false, 12 | "object-literal-sort-keys": false 13 | }, 14 | "linterOptions": { 15 | "exclude": [ 16 | "config/**/*.js", 17 | "node_modules/**/*.ts", 18 | "coverage/lcov-report/*.js" 19 | ] 20 | } 21 | } 22 | --------------------------------------------------------------------------------