67 |
68 |
69 |
70 |
blinksocks gui
71 |
72 |
73 | this.$input = dom}
79 | onKeyPress={this.onKeyPress}
80 | onChange={this.onPasswordChange}
81 | value={password}
82 | />
83 |
84 |
85 |
password is invalid
86 |
87 |
88 | );
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/ui/src/containers/Landing/Landing.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | position: absolute;
3 | left: 0;
4 | right: 0;
5 | top: 0;
6 | bottom: 0;
7 | display: flex;
8 | flex-direction: column;
9 | justify-content: center;
10 | align-items: center;
11 | overflow: hidden;
12 |
13 | .background {
14 | position: absolute;
15 | left: -20px;
16 | right: -20px;
17 | top: -20px;
18 | bottom: -20px;
19 | filter: blur(15px);
20 | background: url("/images/landing.jpeg") no-repeat;
21 | background-size: 100% 100%;
22 | z-index: -1;
23 | }
24 |
25 | .content {
26 | display: flex;
27 | flex-direction: column;
28 | justify-content: center;
29 | align-items: center;
30 | margin-top: -20%;
31 |
32 | h3 {
33 | color: #fff;
34 | font-size: 2.5rem;
35 | margin-bottom: 40px;
36 | text-shadow: #484848 2px 3px 6px;
37 | }
38 |
39 | .input {
40 | display: flex;
41 | align-items: center;
42 |
43 | svg {
44 | position: relative;
45 | right: -35px;
46 | fill: #607d8b;
47 | }
48 |
49 | input {
50 | outline: none;
51 | width: 350px;
52 | height: 40px;
53 | padding: 10px 10px 10px 40px;
54 | margin-right: 10px;
55 | border: 1px solid transparent;
56 | border-radius: 3px;
57 | background-color: #dcdcdc;
58 | color: #484848;
59 | transition: background-color .1s linear;
60 |
61 | &:focus {
62 | background-color: #f4f4f4;
63 | }
64 |
65 | &.fail {
66 | border: 1px solid #f44336;
67 | }
68 | }
69 |
70 | button {
71 | width: 60px;
72 | height: 40px;
73 | border: 0;
74 | border-radius: 3px;
75 | background-color: coral;
76 | color: #fff;
77 | cursor: pointer;
78 | transition: background-color .1s linear;
79 |
80 | &:hover {
81 | background-color: #ff8d5f;
82 | }
83 | }
84 | }
85 |
86 | small {
87 | color: #f44336;
88 | width: 100%;
89 | padding: 5px 0 0 30px;
90 | font-size: 0.8rem;
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/ui/src/containers/Plugins/Plugins.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import styles from './Plugins.module.css';
5 |
6 | import { store } from '../../utils';
7 | import Title from '../../components/Title/Title';
8 |
9 | export default class Plugins extends React.Component {
10 |
11 | static propTypes = {
12 | match: PropTypes.object.isRequired,
13 | };
14 |
15 | render() {
16 | const { env } = store;
17 | if (typeof env.runType === 'undefined' || !env.os) {
18 | return null;
19 | }
20 | return (
21 |
22 |
Plugins
23 |
27 |
28 |
29 | No plugins available yet.
30 |
31 |
32 | );
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/ui/src/containers/Plugins/Plugins.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 100%;
3 | }
4 |
--------------------------------------------------------------------------------
/ui/src/containers/Plugins/SystemProxy/SystemProxy.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Button, RadioGroup, Radio, Intent } from '@blueprintjs/core';
4 | import styles from './SystemProxy.module.css';
5 |
6 | import { call } from '../../../utils';
7 |
8 | const PAC_TYPE_LOCAL = 0;
9 | const PAC_TYPE_REMOTE = 1;
10 |
11 | export default class SystemProxy extends React.Component {
12 |
13 | static propTypes = {
14 | platform: PropTypes.string.isRequired,
15 | };
16 |
17 | state = {
18 | pacType: PAC_TYPE_LOCAL,
19 | sysProxy: '',
20 | sysPac: '',
21 | };
22 |
23 | async componentDidMount() {
24 | try {
25 | const [sysProxy, sysPac] = await Promise.all([
26 | call('get_system_proxy', null, { timeout: 6e4 }),
27 | // call('get_system_pac'),
28 | ]);
29 | this.setState({ sysProxy, sysPac });
30 | } catch (err) {
31 | console.error(err);
32 | }
33 | }
34 |
35 | onSelectPacType = (e) => {
36 | this.setState({ pacType: +e.currentTarget.value });
37 | };
38 |
39 | onSystemProxyChange = (e) => {
40 | this.setState({ sysProxy: e.target.value });
41 | };
42 |
43 | onSysPacChange = (e) => {
44 | this.setState({ sysPac: e.target.value });
45 | };
46 |
47 | onSetSystemProxy = async () => {
48 | const { sysProxy } = this.state;
49 | if (!sysProxy) {
50 | return;
51 | }
52 | try {
53 | await call('set_system_proxy', { sysProxy });
54 | } catch (err) {
55 | console.error(err);
56 | }
57 | };
58 |
59 | onSetSystemPac = async () => {
60 | const { sysPac } = this.state;
61 | if (!sysPac) {
62 | return;
63 | }
64 | try {
65 | await call('set_system_pac', { sysPac });
66 | } catch (err) {
67 | console.error(err);
68 | }
69 | };
70 |
71 | render() {
72 | const { platform } = this.props;
73 | const { pacType, sysProxy, pac } = this.state;
74 | return (
75 |
76 |
System Proxy
77 | {platform === 'darwin' && (
78 |
79 | This plugin requires root privileges on macOS.
80 |
81 | )}
82 |
83 | Global HTTP Proxy
84 |
85 |
86 |
87 |
95 |
96 |
99 |
100 |
101 |
102 | PAC
103 |
109 |
110 |
111 |
112 | {pacType === PAC_TYPE_LOCAL && (
113 |
114 |
117 |
118 | )}
119 | {pacType === PAC_TYPE_REMOTE && (
120 |
121 |
122 |
123 |
131 |
132 |
135 |
136 | )}
137 |
138 |
139 | );
140 | }
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/ui/src/containers/Plugins/SystemProxy/SystemProxy.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 |
3 | section {
4 | margin-top: 20px;
5 | }
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Add/Add.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router-dom';
4 | import { Button } from '@blueprintjs/core';
5 |
6 | import { call } from '../../../utils';
7 |
8 | export default class Add extends React.Component {
9 |
10 | static propTypes = {
11 | match: PropTypes.object.isRequired,
12 | history: PropTypes.object.isRequired,
13 | };
14 |
15 | state = {
16 | remarks: '',
17 | isPending: false,
18 | };
19 |
20 | onRemarksChange = (e) => {
21 | this.setState({ remarks: e.target.value });
22 | };
23 |
24 | onAddSetting = async () => {
25 | const { history } = this.props;
26 | const { remarks } = this.state;
27 | try {
28 | this.setState({ isPending: true });
29 | const id = await call('add_setting', { remarks });
30 | history.push('/services/' + id + '/setting');
31 | } catch (err) {
32 | console.error(err);
33 | }
34 | this.setState({ isPending: false });
35 | };
36 |
37 | render() {
38 | const { remarks, isPending } = this.state;
39 | return (
40 |
41 |
42 |
43 | -
44 | Dashboard
45 |
46 | - Add Service
47 |
48 |
49 |
61 |
65 |
66 | );
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Graphs/Graphs.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 |
3 | .graphs {
4 | display: flex;
5 | margin-bottom: 20px;
6 |
7 | .graph {
8 | margin: 0 20px 0 0;
9 |
10 | > h3 {
11 | text-align: center;
12 | }
13 |
14 | .canvas {
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | width: 500px;
19 | height: 280px;
20 | padding: 10px;
21 | border: 1px solid #e5e5e5;
22 | border-radius: 3px;
23 | font-size: 1.2rem;
24 | color: #a0a0a0;
25 | }
26 | }
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Log/GoogleMap/GoogleMap.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { compose, withProps, withStateHandlers } from 'recompose';
4 | import { withScriptjs, withGoogleMap, GoogleMap, Marker, Polyline, InfoWindow } from 'react-google-maps';
5 |
6 | import styles from './GoogleMap.module.scss';
7 | import { GOOGLE_MAP_API_KEY } from '../../../../constants';
8 | import { call } from '../../../../utils';
9 |
10 | const CustomInfoWindow = ({ lat, lng, as, ips, country, regionName, city, org, hostname, onClose }) => (
11 |
15 |
30 |
31 | );
32 |
33 | const Map = compose(
34 | withProps({
35 | googleMapURL: `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAP_API_KEY}&v=3.exp&libraries=geometry,drawing,places`,
36 | loadingElement:
,
37 | containerElement:
,
38 | mapElement:
,
39 | }),
40 | withStateHandlers(() => ({
41 | infoWindow: {
42 | display: false,
43 | props: null,
44 | },
45 | }), {
46 | onToggleInfoWindow: () => (isOpen, props) => {
47 | return {
48 | infoWindow: {
49 | display: !isOpen,
50 | props: props,
51 | }
52 | };
53 | },
54 | onCloseInfoWindow: () => () => {
55 | return { infoWindow: { display: false, props: null } };
56 | },
57 | }),
58 | withScriptjs,
59 | withGoogleMap,
60 | )(({ defaultCenter, ips, infoWindow, onToggleInfoWindow, onCloseInfoWindow }) =>
61 |
66 | {ips.map((item, index) =>
67 | onToggleInfoWindow(infoWindow.display, item)}
70 | options={{
71 | position: { lat: item.lat, lng: item.lng },
72 | icon: item.self ? '' : {
73 | url: require('./destination.png'),
74 | scaledSize: { width: 16, height: 16 },
75 | anchor: { x: 8, y: 8 },
76 | },
77 | }}
78 | />
79 | )}
80 | {ips.map((item, index) =>
81 |
89 | )}
90 | {infoWindow.display && (
91 |
92 | )}
93 |
94 | );
95 |
96 | export default class _GoogleMap extends React.Component {
97 |
98 | static propTypes = {
99 | sid: PropTypes.string.isRequired,
100 | };
101 |
102 | state = {
103 | ips: [],
104 | };
105 |
106 | componentDidMount() {
107 | this.timer = window.setInterval(this.fetchIPs, 5e3);
108 | this.fetchIPs();
109 | }
110 |
111 | componentWillUnmount() {
112 | window.clearInterval(this.timer);
113 | }
114 |
115 | fetchIPs = async () => {
116 | try {
117 | const ips = await call('get_geoip', { id: this.props.sid });
118 | this.setState({ ips });
119 | } catch (err) {
120 | console.error(err);
121 | }
122 | };
123 |
124 | render() {
125 | const { ips } = this.state;
126 | const self = ips.filter(({ self }) => self)[0] || { lat: 21.289, lng: -175.253 };
127 | return (
128 |
129 |
133 |
134 | );
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Log/GoogleMap/GoogleMap.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 |
3 | .infoWindow {
4 | list-style: none;
5 | margin: 0;
6 | padding: 5px;
7 |
8 | li {
9 | padding: 3px 0;
10 | max-width: 280px;
11 | white-space: nowrap;
12 |
13 | b {
14 | display: inline-block;
15 | width: 65px;
16 | vertical-align: top;
17 | }
18 |
19 | pre {
20 | display: inline-block;
21 | margin: -2px 0 0;
22 | padding: 0;
23 | box-shadow: none;
24 | width: 100%;
25 | max-height: 50px;
26 | overflow: auto;
27 | font-family: inherit;
28 | }
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Log/GoogleMap/destination.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/ui/src/containers/Services/Log/GoogleMap/destination.png
--------------------------------------------------------------------------------
/ui/src/containers/Services/Log/Log.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | border: 1px solid #d5d8da;
3 | border-radius: 3px;
4 |
5 | .header {
6 | display: flex;
7 | align-items: center;
8 | height: 50px;
9 | padding: 0 10px;
10 | border-bottom: 1px solid #ddd;
11 | border-radius: 3px 3px 0 0;
12 | background-color: #f6f8fa;
13 |
14 | .tools {
15 | position: absolute;
16 | right: 40px;
17 |
18 | > span {
19 | cursor: pointer;
20 | font-size: 16px;
21 | padding: 5px;
22 | opacity: .5;
23 | transition: opacity .1s linear;
24 |
25 | &:hover {
26 | background-color: #eaeaea;
27 | border-radius: 2px;
28 | opacity: .85;
29 | }
30 | }
31 | }
32 | }
33 |
34 | .body {
35 | list-style: none;
36 | margin: 0;
37 | padding: 0;
38 |
39 | li {
40 | padding: 10px;
41 | border-bottom: 1px solid #ddd;
42 | cursor: pointer;
43 |
44 | .conn {
45 |
46 | .abstract {
47 |
48 | span {
49 | display: inline-block;
50 | margin-right: 5px;
51 |
52 | > svg {
53 | margin-top: 1px;
54 | }
55 | }
56 |
57 | .dot {
58 | width: 20px;
59 | height: 10px;
60 | }
61 | }
62 |
63 | .details {
64 | margin-left: 48px;
65 | margin-top: 5px;
66 | font-size: .8rem;
67 |
68 | & > span {
69 | margin-right: 5px;
70 | }
71 | }
72 | }
73 |
74 | &.empty {
75 | color: #30404d;
76 | text-align: center;
77 | }
78 |
79 | &:last-child {
80 | border-bottom: 0;
81 | }
82 |
83 | &:hover {
84 | background-color: #f7f7f7;
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Services.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Route, Link } from 'react-router-dom';
4 |
5 | import { call } from '../../utils';
6 | import { MenuRouter } from '../../components/MenuRouter';
7 |
8 | import Graphs from './Graphs/Graphs';
9 | import Setting from './Setting/Setting';
10 | import Log from './Log/Log';
11 |
12 | import styles from './Services.module.css';
13 |
14 | export default class Services extends React.Component {
15 |
16 | static propTypes = {
17 | match: PropTypes.object.isRequired,
18 | };
19 |
20 | state = {
21 | service: null,
22 | };
23 |
24 | async componentDidMount() {
25 | const { params: { id } } = this.props.match;
26 | try {
27 | const service = await call('get_service', { id }, { showProgress: true });
28 | this.setState({ service });
29 | } catch (err) {
30 | console.error(err);
31 | }
32 | }
33 |
34 | createRoutes() {
35 | const { match } = this.props;
36 | return [{
37 | path: match.url + '/graphs',
38 | text: 'Graphs',
39 | component: Graphs,
40 | }, {
41 | path: match.url + '/log',
42 | text: 'Log',
43 | component: Log,
44 | }, {
45 | path: match.url + '/setting',
46 | text: 'Setting',
47 | component: Setting,
48 | }];
49 | }
50 |
51 | render() {
52 | const { service } = this.state;
53 | const routes = this.createRoutes();
54 | return (
55 |
56 |
57 |
58 | -
59 | Dashboard
60 |
61 | - {service && service.remarks}
62 |
63 |
64 |
65 |
66 | {routes.map(({ path, exact, component }, i) => (
67 |
68 | ))}
69 |
70 |
71 |
72 | );
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Services.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 |
3 | .body {
4 | display: flex;
5 | height: 100%;
6 | margin-top: 10px;
7 | }
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Setting/AddressEditor/AddressEditor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classnames from 'classnames';
4 | import URL from 'url-parse';
5 | import styles from './AddressEditor.module.css';
6 |
7 | export default class AddressEditor extends React.Component {
8 |
9 | static propTypes = {
10 | address: PropTypes.string.isRequired,
11 | protocols: PropTypes.arrayOf(PropTypes.string),
12 | onChange: PropTypes.func,
13 | };
14 |
15 | static defaultProps = {
16 | protocols: [],
17 | onChange: (/* address */) => {
18 | },
19 | };
20 |
21 | state = {
22 | protocol: '',
23 | hostname: '',
24 | port: 0,
25 | };
26 |
27 | constructor(props) {
28 | super(props);
29 | const { protocol, hostname, port } = new URL(this.props.address);
30 | this.state = {
31 | protocol,
32 | hostname,
33 | port,
34 | };
35 | }
36 |
37 | onChange = () => {
38 | const { protocol, hostname, port } = this.state;
39 | this.props.onChange(`${protocol}//${hostname}:${port}`);
40 | };
41 |
42 | onProtocolChange = (e) => {
43 | this.setState({ protocol: e.target.value }, this.onChange);
44 | };
45 |
46 | onHostnameChange = (e) => {
47 | this.setState({ hostname: e.target.value }, this.onChange);
48 | };
49 |
50 | onPortChange = (e) => {
51 | this.setState({ port: e.target.value | 0 }, this.onChange);
52 | };
53 |
54 | render() {
55 | const { protocols } = this.props;
56 | const { protocol, hostname, port } = this.state;
57 | return (
58 |
59 |
60 |
66 |
67 |
68 |
75 |
76 |
77 |
86 |
87 |
88 | );
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Setting/AddressEditor/AddressEditor.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 |
3 | select {
4 | font-family: "Open Sans", "Helvetica Neue", sans-serif;
5 | }
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Setting/ClientEditor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classnames from 'classnames';
4 | import { TagInput } from '@blueprintjs/core';
5 |
6 | import AddressEditor from './AddressEditor/AddressEditor';
7 |
8 | import styles from './Editor.module.css';
9 |
10 | export default class ClientEditor extends React.Component {
11 |
12 | static propTypes = {
13 | client: PropTypes.object.isRequired,
14 | onChange: PropTypes.func,
15 | };
16 |
17 | static defaultProps = {
18 | onChange: (/* client */) => {
19 | },
20 | };
21 |
22 | static getDerivedStateFromProps({ client }) {
23 | return client;
24 | }
25 |
26 | state = {
27 | service: '',
28 | dns: [],
29 | dns_expire: 3600,
30 | timeout: 200,
31 | log_path: '',
32 | log_level: '',
33 | log_max_days: 30,
34 | };
35 |
36 | _isAdvancedShow = false;
37 |
38 | onChange = () => {
39 | this.props.onChange(this.state);
40 | };
41 |
42 | onToggleAdvanced = () => {
43 | this._isAdvancedShow = !this._isAdvancedShow;
44 | this.forceUpdate();
45 | };
46 |
47 | onAddressChange = (address) => {
48 | this.setState({ service: address }, this.onChange);
49 | };
50 |
51 | onLogPathChange = (e) => {
52 | this.setState({ log_path: e.target.value }, this.onChange);
53 | };
54 |
55 | onLogLevelChange = (e) => {
56 | this.setState({ log_level: e.target.value }, this.onChange);
57 | };
58 |
59 | onLogMaxDaysChange = (e) => {
60 | this.setState({ log_max_days: e.target.value | 0 }, this.onChange);
61 | };
62 |
63 | onTimeoutChange = (e) => {
64 | this.setState({ timeout: e.target.value | 0 }, this.onChange);
65 | };
66 |
67 | onDnsChange = (servers) => {
68 | this.setState({ dns: servers }, this.onChange);
69 | };
70 |
71 | onDnsExpireChange = (e) => {
72 | this.setState({ dns_expire: e.target.value | 0 }, this.onChange);
73 | };
74 |
75 | render() {
76 | const { service, log_path, log_level, log_max_days } = this.state;
77 | const { timeout, dns, dns_expire } = this.state;
78 | return (
79 |
80 |
88 |
101 | {this._isAdvancedShow && (
102 | <>
103 |
Log
104 |
114 |
115 | Log Level
116 |
117 |
123 |
124 |
125 |
135 |
DNS
136 |
137 | DNS Servers
138 |
144 |
145 |
155 |
Others
156 |
166 | >
167 | )}
168 |
169 | );
170 | }
171 |
172 | }
173 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Setting/Editor.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 |
3 | > section {
4 | margin-bottom: 10px;
5 | }
6 |
7 | .advanced {
8 | margin: 10px 0;
9 | }
10 |
11 | h3 {
12 | margin-top: 30px;
13 | padding-bottom: 10px;
14 | border-bottom: 1px solid #e8e8e8;
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Setting/ProtocolStatckEditor/ProtocolStackEditor.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | .presets {
3 | list-style: none;
4 | padding: 0;
5 | margin: 10px 0;
6 |
7 | :global(.sortable-chosen) {
8 | background-color: #fff;
9 | }
10 |
11 | :global(.sortable-ghost) {
12 | background-color: #cee0fd;
13 | }
14 |
15 | .preset {
16 | display: flex;
17 | align-items: center;
18 | justify-content: space-between;
19 | padding: 10px;
20 | margin-bottom: 5px;
21 | border-radius: 3px;
22 | background-color: #f6f6f6;
23 |
24 | .content {
25 | display: flex;
26 | align-items: center;
27 |
28 | .addressing {
29 | float: left;
30 | margin-top: -30px;
31 | margin-left: -10px;
32 | width: 0;
33 | height: 0;
34 | border-radius: 3px 0 0 0;
35 | border-top: 8px solid transparent;
36 | border-bottom: 8px solid transparent;
37 | border-right: 8px solid #2196f3;
38 | transform: rotate(45deg);
39 | }
40 |
41 | .dragger {
42 | margin-right: 5px;
43 | }
44 | }
45 |
46 | .operations {
47 | display: flex;
48 |
49 | svg {
50 | margin-right: 10px;
51 | cursor: pointer;
52 |
53 | &:last-child {
54 | margin-right: 0;
55 | }
56 | }
57 | }
58 |
59 | p {
60 | margin: 0;
61 | font-weight: 600;
62 | }
63 |
64 | small {
65 | margin-top: 5px;
66 | }
67 |
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Setting/Setting.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import { Prompt, matchPath } from 'react-router-dom';
5 | import { Button, Intent } from '@blueprintjs/core';
6 | import classnames from 'classnames';
7 | import omit from 'lodash/omit';
8 | import keyBy from 'lodash/keyBy';
9 | import cloneDeep from 'lodash/cloneDeep';
10 |
11 | import ClientEditor from './ClientEditor';
12 | import ServerEditor from './ServerEditor';
13 | import styles from './Setting.module.css';
14 |
15 | import { call, toast, store } from '../../../utils';
16 | import Title from '../../../components/Title/Title';
17 |
18 | @observer
19 | export default class Setting extends React.Component {
20 |
21 | static propTypes = {
22 | match: PropTypes.string.isRequired,
23 | };
24 |
25 | state = {
26 | id: '',
27 | config: null,
28 | client: null, // client only
29 | server: null,
30 | isClient: false,
31 | defs: null,
32 | isUnSaved: false,
33 | isSaving: false,
34 | };
35 |
36 | async componentDidMount() {
37 | const { params: { id } } = matchPath(this.props.match.url, { path: '/services/:id' });
38 | try {
39 | const [config, defs] = await Promise.all([
40 | call('get_config', { id }),
41 | call('get_preset_defs', null, { cache: true }),
42 | ]);
43 | const isClient = !!config.server;
44 | let client, server;
45 | if (isClient) {
46 | client = omit(config, 'server');
47 | server = config.server;
48 | } else {
49 | server = config;
50 | }
51 | // mix "._def" in each server.presets
52 | const map = keyBy(defs, 'name');
53 | for (const preset of server.presets) {
54 | preset._def = map[preset.name];
55 | }
56 | this.setState({ id, config, client, server, isClient, defs: map });
57 | } catch (err) {
58 | console.error(err);
59 | }
60 | }
61 |
62 | onClientChange = (client) => {
63 | this.setState({ client, isUnSaved: true });
64 | };
65 |
66 | onServerChange = (server) => {
67 | this.setState({ server, isUnSaved: true });
68 | };
69 |
70 | onSave = async () => {
71 | const { id } = this.state;
72 | const { isClient, client, server } = this.state;
73 | // drop "._def" of each server.presets
74 | const serverCopy = cloneDeep(server);
75 | for (const preset of serverCopy.presets) {
76 | delete preset._def;
77 | }
78 | let config;
79 | if (isClient) {
80 | config = { ...client, server: serverCopy };
81 | } else {
82 | config = serverCopy;
83 | }
84 | this.setState({ isSaving: true });
85 | try {
86 | await call('save_setting', { id, config });
87 | toast('configuration saved!', 'success');
88 | } catch (err) {
89 | console.error(err.message);
90 | }
91 | this.setState({ isSaving: false, isUnSaved: false });
92 | };
93 |
94 | onSaveAndRestart = async () => {
95 | const { id } = this.state;
96 | await this.onSave();
97 | try {
98 | await call('restart_service', { id });
99 | toast('service restart successfully!', 'success');
100 | } catch (err) {
101 | console.error(err.message);
102 | }
103 | };
104 |
105 | renderSaveButton = () => {
106 | const { id, isSaving, isUnSaved } = this.state;
107 | const props = {
108 | disabled: !isUnSaved,
109 | loading: isSaving,
110 | };
111 | if (store.isServiceRunning(id)) {
112 | return (
113 |
114 |
115 | This service is RUNNING now, save and restart to take effect immediately.
116 |
117 |
120 |
121 |
124 |
125 | );
126 | } else {
127 | return
;
128 | }
129 | };
130 |
131 | renderClient = () => {
132 | const { client, server, isClient, defs } = this.state;
133 | return (
134 | <>
135 |
136 | Local Service
137 | {client && (
138 |
142 | )}
143 |
144 |
145 | Remote Server
146 | {server && defs && (
147 |
153 | )}
154 |
155 | >
156 | );
157 | };
158 |
159 | renderServer = () => {
160 | const { server, isClient, defs } = this.state;
161 | return (
162 |
163 | Local Service
164 | {server && defs && (
165 |
171 | )}
172 |
173 | );
174 | };
175 |
176 | render() {
177 | const { id, isClient, isUnSaved } = this.state;
178 | return (
179 |
180 |
Setting
181 |
182 | {isUnSaved && !store.isServiceRunning(id) && (
183 |
184 | You have unsaved changes, remember to save before start/restart service.
185 |
188 |
189 | )}
190 |
191 | {isClient ? this.renderClient() : this.renderServer()}
192 |
193 | {this.renderSaveButton()}
194 |
195 | );
196 | }
197 |
198 | }
199 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Setting/Setting.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | margin-bottom: 20px;
3 |
4 | .warning {
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: center;
8 |
9 | &:before {
10 | margin-top: 5px;
11 | }
12 | }
13 |
14 | .body {
15 | & > section {
16 | padding: 20px;
17 | margin-bottom: 20px;
18 | border: 2px solid #f3f3f3;
19 | border-radius: 4px;
20 | }
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/ui/src/containers/Services/Setting/ToolTip/ToolTip.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tooltip, Icon, Position } from "@blueprintjs/core/lib/esm/index";
3 |
4 | const style = {
5 | cursor: 'pointer',
6 | position: 'relative',
7 | top: '3px',
8 | left: '3px',
9 | };
10 |
11 | export default ({ content }) => (
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/ui/src/containers/Settings/Settings.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 100%;
3 |
4 | h3 {
5 | padding: 0 10px;
6 | margin-bottom: 10px;
7 | border-left: 3px solid #4390ff;
8 | white-space: nowrap;
9 | overflow: hidden;
10 | text-overflow: ellipsis;
11 | }
12 |
13 | .table {
14 | width: 100%;
15 | margin-bottom: 20px;
16 |
17 | tbody {
18 | tr td {
19 | vertical-align: middle;
20 |
21 | &:nth-child(3) {
22 | text-align: center;
23 | }
24 |
25 | svg {
26 | cursor: pointer;
27 | }
28 | }
29 | }
30 | }
31 |
32 | .buttons {
33 | display: flex;
34 |
35 | & > button {
36 | margin-right: 10px;
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/ui/src/containers/Settings/UserItem/UserItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classnames from 'classnames';
4 |
5 | import styles from './UserItem.module.css';
6 |
7 | export default class UserItem extends React.Component {
8 |
9 | static propTypes = {
10 | user: PropTypes.object.isRequired,
11 | onChange: PropTypes.func,
12 | };
13 |
14 | static defaultProps = {
15 | onChange: (/* user */) => {
16 | },
17 | };
18 |
19 | $password = null;
20 |
21 | state = {
22 | user: null,
23 | isShowPassword: false,
24 | };
25 |
26 | static getDerivedStateFromProps({ user }) {
27 | return { user };
28 | }
29 |
30 | onChange = () => {
31 | this.props.onChange(this.state.user);
32 | };
33 |
34 | onPasswordChange = (e) => {
35 | this.setState({
36 | user: {
37 | ...this.state.user,
38 | password: e.target.value
39 | },
40 | }, this.onChange);
41 | };
42 |
43 | onTogglePasswordView = () => {
44 | this.setState({ isShowPassword: !this.state.isShowPassword });
45 | this.$password.focus();
46 | };
47 |
48 | onToggleMethod = (e, _name) => {
49 | const { user: { methods } } = this.state;
50 | const checked = e.target.checked;
51 | this.setState({
52 | user: {
53 | ...this.state.user,
54 | methods: methods.map(({ name, active }) => ({
55 | name,
56 | active: name === _name ? checked : active,
57 | })),
58 | },
59 | }, this.onChange);
60 | };
61 |
62 | render() {
63 | const { user, isShowPassword } = this.state;
64 | return (
65 |
66 |
67 |
70 |
71 |
72 | this.$password = dom}
74 | type={isShowPassword ? 'text' : 'password'}
75 | className="pt-input"
76 | placeholder="type password here"
77 | value={user && user.password}
78 | onChange={this.onPasswordChange}
79 | />
80 |
87 |
88 |
89 |
90 |
91 |
94 |
95 | {user && user.methods.map(({ name, active }) => (
96 |
101 | ))}
102 |
103 |
104 |
105 | );
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/ui/src/containers/Settings/UserItem/UserItem.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 |
3 | .password {
4 | margin-bottom: 10px;
5 | }
6 |
7 | .methods {
8 | display: flex;
9 | flex-wrap: wrap;
10 | margin-bottom: 0;
11 | max-height: 180px;
12 | overflow-y: auto;
13 |
14 | > label {
15 | width: 200px;
16 | margin-right: 20px;
17 | overflow: hidden;
18 | text-overflow: ellipsis;
19 | line-height: 13px;
20 | }
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/ui/src/i18n/index.js:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import XHR from 'i18next-xhr-backend';
3 | import LanguageDetector from 'i18next-browser-languagedetector';
4 |
5 | i18n
6 | .use(XHR)
7 | .use(LanguageDetector)
8 | .init({
9 | fallbackLng: 'en',
10 | debug: true,
11 | backend: {
12 | loadPath: '/locales/{{lng}}/{{ns}}.json'
13 | },
14 | react: {
15 | wait: false,
16 | bindI18n: 'languageChanged loaded',
17 | bindStore: 'added removed',
18 | nsMode: 'default'
19 | }
20 | });
21 |
22 | export default i18n;
23 |
--------------------------------------------------------------------------------
/ui/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import ReactGA from 'react-ga';
4 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
5 | import { FocusStyleManager } from "@blueprintjs/core";
6 | import { I18nextProvider } from 'react-i18next';
7 |
8 | import 'echarts/lib/chart/line';
9 | import 'echarts/lib/component/title';
10 | import 'echarts/lib/component/tooltip';
11 |
12 | import i18n from './i18n';
13 |
14 | import 'nprogress/nprogress.css';
15 | import 'normalize.css';
16 | import './index.css';
17 |
18 | import App from './containers/App/App';
19 | import Landing from './containers/Landing/Landing';
20 | // import registerServiceWorker from './registerServiceWorker';
21 |
22 | document.addEventListener('DOMContentLoaded', () => {
23 | ReactDOM.render(
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ,
32 | document.getElementById('root')
33 | );
34 | });
35 |
36 | // registerServiceWorker();
37 | FocusStyleManager.onlyShowFocusOnTabs();
38 |
39 | // initialize google analytics
40 | if (process.env.NODE_ENV === 'production') {
41 | ReactGA.initialize(process.env.GOOGLE_ANALYTICS_TRACKING_ID);
42 | }
43 |
--------------------------------------------------------------------------------
/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | @import '~@blueprintjs/core/lib/css/blueprint.css';
2 | @import '~@blueprintjs/icons/lib/css/blueprint-icons.css';
3 |
4 | @font-face {
5 | font-family: "Titillium Web";
6 | src: url('/fonts/TitilliumWeb-Light.ttf') format("truetype");
7 | }
8 |
9 | body {
10 | font-family: "Titillium Web", "Open Sans", "Helvetica Neue", sans-serif;
11 | padding: 0;
12 | margin: 0;
13 | background-color: #fff;
14 | }
15 |
16 | :global {
17 |
18 | .link-reset {
19 | color: #fff;
20 | text-decoration: none;
21 |
22 | &:hover {
23 | color: #fff;
24 | text-decoration: none;
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/ui/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/ui/src/utils/hash.js:
--------------------------------------------------------------------------------
1 | const jsSHA = require('jssha');
2 |
3 | export default function hash(algorithm, message) {
4 | const shaObj = new jsSHA(algorithm, 'TEXT');
5 | shaObj.update(message);
6 | return shaObj.getHash('HEX');
7 | }
8 |
--------------------------------------------------------------------------------
/ui/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import hash from './hash';
2 | import live from './live';
3 | import call from './rpc';
4 | import store from './store';
5 | import toast from './toast';
6 |
7 | export { hash, live, call, toast, store };
8 |
--------------------------------------------------------------------------------
/ui/src/utils/live.js:
--------------------------------------------------------------------------------
1 | import call from './rpc';
2 | import ws from './ws';
3 | import { KEEPALIVE_INTERVAL } from '../constants';
4 |
5 | const SERVER_PUSH_REGISTER_SUCCESS = 0;
6 | const SERVER_PUSH_REGISTER_ERROR = 1;
7 |
8 | export default async function live(method, args, callback) {
9 | // refine arguments
10 | if (typeof args === 'function') {
11 | callback = args;
12 | args = {};
13 | }
14 |
15 | // a timer for keepalive
16 | let timer = null;
17 |
18 | function onServerPush(response) {
19 | console.log(new Date().toISOString(), '[PUSH]', `[${method}]`, response);
20 | // reset timer
21 | // if (timer) {
22 | // window.clearInterval(timer);
23 | // timer = window.setInterval(keepalive, KEEPALIVE_INTERVAL);
24 | // }
25 | // handle message
26 | callback(response);
27 | }
28 |
29 | // listening for server push before send register request
30 | ws.on(method, onServerPush);
31 |
32 | // register a server push method
33 | const { code } = await call('_register_server_push', { method, args });
34 | switch (code) {
35 | case SERVER_PUSH_REGISTER_SUCCESS:
36 | break;
37 | case SERVER_PUSH_REGISTER_ERROR:
38 | ws.off(method, onServerPush);
39 | break;
40 | default:
41 | throw Error(`unknown register response status code: ${code}`);
42 | }
43 |
44 | async function keepalive() {
45 | try {
46 | await call('_keepalive_server_push', { method });
47 | } catch (err) {
48 | await unlive();
49 | }
50 | }
51 |
52 | // setup keepalive timer
53 | timer = window.setInterval(keepalive, KEEPALIVE_INTERVAL);
54 |
55 | // return a function to unregister server push
56 | async function unlive() {
57 | window.clearInterval(timer);
58 | ws.off(method, onServerPush);
59 | await call('_unregister_server_push', { method });
60 | }
61 |
62 | return unlive;
63 | }
64 |
--------------------------------------------------------------------------------
/ui/src/utils/rpc.js:
--------------------------------------------------------------------------------
1 | import NProgress from 'nprogress';
2 | import ws from './ws';
3 | import store from './store';
4 | import toast from './toast';
5 | import { RPC_TIMEOUT, SERVICE_STATUS_INIT, RPC_STATUS_ONLINE } from '../constants';
6 |
7 | const cacheStorage = {};
8 |
9 | function getCache(request) {
10 | try {
11 | const key = JSON.stringify(request);
12 | const value = cacheStorage[key];
13 | if (typeof value !== 'undefined') {
14 | return value;
15 | }
16 | } catch (err) {
17 | // ignore
18 | }
19 | return null;
20 | }
21 |
22 | function setCache(request, data) {
23 | try {
24 | const key = JSON.stringify(request);
25 | cacheStorage[key] = data;
26 | } catch (err) {
27 | // ignore
28 | }
29 | }
30 |
31 | function createRequest(method, args) {
32 | if (typeof method !== 'string' || method === '') {
33 | throw Error('method is invalid');
34 | }
35 | const request = { method };
36 | if (typeof args !== 'undefined' && args !== null) {
37 | request.args = args;
38 | }
39 | return request;
40 | }
41 |
42 | export default async function rpc(method, args, options) {
43 | const { cache = false, showProgress = false, timeout = RPC_TIMEOUT } = options || {};
44 |
45 | if (store.rpcStatus !== SERVICE_STATUS_INIT && store.rpcStatus !== RPC_STATUS_ONLINE) {
46 | throw Error('rpc service is offline');
47 | }
48 |
49 | const request = createRequest(method, args);
50 |
51 | // return directly if cached
52 | let cachedValue;
53 | if (cache && (cachedValue = getCache(request)) !== null) {
54 | return cachedValue;
55 | }
56 |
57 | return new Promise((resolve, reject) => {
58 | console.log(new Date().toISOString(), '[request]', request);
59 | showProgress && NProgress.start();
60 |
61 | let isTimeout = false;
62 |
63 | function onTimeout() {
64 | if (store.rpcStatus !== RPC_STATUS_ONLINE && !showProgress) {
65 | // prevent timeout toast when it's not online
66 | return;
67 | }
68 | const hint = `method "${method}" timeout`;
69 | isTimeout = true;
70 | showProgress && NProgress.done();
71 | toast(hint, 'warning');
72 | reject(Error(hint));
73 | }
74 |
75 | // timeout timer
76 | const timer = window.setTimeout(onTimeout, timeout);
77 |
78 | ws.emit('request', request, (response) => {
79 | if (isTimeout) {
80 | // drop response returned after timeout
81 | return;
82 | }
83 | console.log(new Date().toISOString(), '[response]', response);
84 | clearTimeout(timer);
85 | showProgress && NProgress.done();
86 | const { code, data, message } = response;
87 | if (code === 0) {
88 | if (cache) {
89 | setCache(request, data);
90 | }
91 | resolve(data);
92 | } else {
93 | const msg = `Server Respond Error: ${message || 'unknown error'}`;
94 | toast(msg, 'warning');
95 | reject(Error(msg));
96 | }
97 | });
98 | });
99 | }
100 |
--------------------------------------------------------------------------------
/ui/src/utils/store.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 | import { RPC_STATUS_INIT, SERVICE_STATUS_INIT, SERVICE_STATUS_RUNNING } from '../constants';
3 |
4 | const store = observable({
5 |
6 | // global environment variables
7 | env: observable.map({}),
8 |
9 | // global websocket status
10 | rpcStatus: RPC_STATUS_INIT,
11 |
12 | // global websocket latency in ms
13 | rpcLatency: 0,
14 |
15 | // services
16 | services: observable.map({
17 | //
: {
18 | // status: SERVICE_XXX,
19 | // connections: 0,
20 | // total_download_bytes: 0,
21 | // total_upload_bytes: 0,
22 | // download_speed: 0,
23 | // upload_speed: 0,
24 | // },
25 | // ...
26 | }),
27 |
28 | // custom methods
29 | isServiceRunning(id) {
30 | return this.getServiceStatusById(id) === SERVICE_STATUS_RUNNING;
31 | },
32 | getServiceStatusById(id) {
33 | return !this.services[id] ? SERVICE_STATUS_INIT : this.services[id].status;
34 | },
35 |
36 | });
37 |
38 | if (process.env.NODE_ENV === 'development') {
39 | window.store = store;
40 | }
41 |
42 | export default store;
43 |
--------------------------------------------------------------------------------
/ui/src/utils/toast.js:
--------------------------------------------------------------------------------
1 | import { Position, Toaster, Intent } from '@blueprintjs/core';
2 |
3 | const MyToaster = Toaster.create({
4 | position: Position.TOP,
5 | });
6 |
7 | export default function toast(message, intent = Intent.NONE) {
8 | if (typeof message === 'object') {
9 | MyToaster.show(message);
10 | } else {
11 | MyToaster.show({ message, intent });
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ui/src/utils/ws.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client';
2 | import toast from './toast';
3 | import store from './store';
4 | import { TOKEN_NAME, RPC_STATUS_ERROR, RPC_STATUS_OFFLINE, RPC_STATUS_ONLINE } from '../constants';
5 |
6 | const ws = io.connect('/', {
7 | query: {
8 | token: localStorage.getItem(TOKEN_NAME) || '',
9 | },
10 | });
11 |
12 | ws.on('connect', function onConnect() {
13 | store.rpcStatus = RPC_STATUS_ONLINE;
14 | });
15 |
16 | ws.on('connect_error', () => {
17 | store.rpcStatus = RPC_STATUS_OFFLINE;
18 | });
19 |
20 | ws.on('reconnecting', (attempts) => {
21 | if (attempts >= 10) {
22 | ws.close();
23 | store.rpcStatus = RPC_STATUS_ERROR;
24 | }
25 | });
26 |
27 | ws.on('reconnect_failed', () => {
28 | store.rpcStatus = RPC_STATUS_ERROR;
29 | });
30 |
31 | ws.on('pong', (latency) => {
32 | store.rpcLatency = latency;
33 | });
34 |
35 | ws.on('error', (message) => {
36 | switch (message) {
37 | case 'authentication error':
38 | const { pathname } = window.location;
39 | if (pathname !== '/landing') {
40 | toast({
41 | message: 'authentication error, taking you to verify page...',
42 | intent: 'danger',
43 | timeout: 1000,
44 | onDismiss() {
45 | window.location.replace('/landing');
46 | },
47 | });
48 | }
49 | break;
50 | default:
51 | console.error(message);
52 | break;
53 | }
54 | });
55 |
56 | export default ws;
57 |
--------------------------------------------------------------------------------