Compare commits

...
40 Commits
Author SHA1 Message Date
Thomas Nordquist 906eadf08b Add splash screen 2019-01-13 21:55:55 +01:00
Thomas Nordquist 225f536cd1 Tweak update notification settings 2019-01-13 21:09:20 +01:00
Thomas Nordquist 87f0f06a07 Remove debug code 2019-01-13 20:51:41 +01:00
Thomas Nordquist e294d9700f Add manual auto-update fallback 2019-01-13 20:49:36 +01:00
Thomas Nordquist fdece7ae91 Notify user if auto update failed 2019-01-13 17:37:13 +01:00
Thomas Nordquist d2bb098772 Track redux actions 2019-01-13 17:13:34 +01:00
Thomas Nordquist 490678d7b4 Fix sort topic function
Fixes #4
2019-01-13 15:26:32 +01:00
Thomas Nordquist 2d649b3f51 Refactor 2019-01-13 15:26:19 +01:00
Thomas Nordquist 0de67a48a6 Fix auto-expand selection
Fixes #5
2019-01-13 15:19:34 +01:00
Thomas Nordquist 1267bb27f7 Clean up 2019-01-13 15:19:09 +01:00
Thomas Nordquist 31b3077743 Remove unnecessary json editor options 2019-01-13 15:12:57 +01:00
Thomas Nordquist 7015c31a91 Remove unused code 2019-01-13 15:11:11 +01:00
Thomas Nordquist f486f5888c Change default server to iot.eclipse.org 2019-01-13 15:10:30 +01:00
Thomas Nordquist 33bd2b7340 Update Readme.md 2019-01-13 15:05:07 +01:00
Thomas Nordquist 241a99207d Add readme 2019-01-13 14:40:47 +01:00
Thomas Nordquist fa9f321d88 Add crash reporter 2019-01-13 14:38:31 +01:00
Thomas Nordquist 683a6a7245 Update build settings 2019-01-13 05:31:41 +01:00
Thomas Nordquist 37bb0b6ea1 Prepare travis builds 2019-01-13 04:41:58 +01:00
Thomas Nordquist 7e28865a73 Improve build 2019-01-13 03:13:00 +01:00
Thomas Nordquist c40949f38d Set scroll corners background 2019-01-13 02:27:05 +01:00
Thomas Nordquist 5cf5029530 Try catch autoupdater errors 2019-01-13 02:25:50 +01:00
Thomas Nordquist ab87b13eed Fix scrollbars for windows 2019-01-13 01:48:43 +01:00
Thomas Nordquist e83fe78755 Add "diff" for historic messages 2019-01-12 21:39:18 +01:00
Thomas Nordquist 8fa8ea5aa7 Update tslint.json 2019-01-12 20:26:56 +01:00
Thomas Nordquist d75122d44d Fix app style 2019-01-12 20:26:36 +01:00
Thomas Nordquist a677fb7a0c Add message history to frontend 2019-01-12 20:25:52 +01:00
Thomas Nordquist 6fbf04cd00 Improve node selection via MouseOver 2019-01-12 14:29:56 +01:00
Thomas Nordquist 48b7754a2b Add message history to TreeNode 2019-01-12 14:29:21 +01:00
Thomas Nordquist a8e082f084 Refactor 2019-01-11 19:07:56 +01:00
Thomas Nordquist b52b2d7696 Refactor 2019-01-11 18:52:12 +01:00
Thomas Nordquist 1586d0121c Fix layout 2019-01-11 10:32:38 +01:00
Thomas Nordquist 5425a80ada Fix topic selection 2019-01-11 10:32:29 +01:00
Thomas Nordquist f7843bc348 Add publish history & fix topic input 2019-01-11 10:17:02 +01:00
Thomas Nordquist c956afdee0 Fix node title updates 2019-01-11 09:10:41 +01:00
Thomas Nordquist 4e8727432d Fix button handler 2019-01-11 09:10:19 +01:00
Thomas Nordquist ea87c24fb5 Fix import 2019-01-11 00:42:00 +01:00
Thomas Nordquist c3ef9335c0 Add topic publishing sidebar 2019-01-11 00:40:05 +01:00
Thomas Nordquist eb375073f9 Fix Sidebar & Mouse event target area
Fix clipboard
Fix invalid state in sidebar due to missing event termination
2019-01-10 10:34:09 +01:00
Thomas Nordquist 269061bdc8 Work in progress 2019-01-08 23:58:24 +01:00
Thomas Nordquist b44f352804 WiP #broken 2019-01-08 16:39:18 +01:00
49 changed files with 2213 additions and 454 deletions
+26
View File
@@ -0,0 +1,26 @@
language: node_js
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
node_js:
- "10"
os:
- linux
- osx
install:
- npm install
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install snapd && sudo snap install snapcraft --classic; fi
script:
- npm run build
- npm run prepare-release
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then npm run package -- linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then npm run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then npm run package -- win; fi
+24
View File
@@ -0,0 +1,24 @@
# MQTT-Explorer
See the whole picture of your message queue.
The perfect tool to integrate new services, IoT devices in your network.
This application subscribes to all topics on your MQTT-Server and displays your message queue hierarchy, allowing you to drill-down to the topics that are of interest.
## Download
The app is prebuilt for Windows, Linux and Mac. [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
![screenshot](https://user-images.githubusercontent.com/7721625/51086078-340c1c00-1742-11e9-83a9-07f932055467.png)
## Develop
PRs and issues are welcome
Install with `npm run install`, build with `npm run build`
Start with `npm run start`
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## License
Not yet decided which licens exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
+52 -8
View File
@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<title>MQTT-Explorer</title>
<style>
body, html {
margin: 0;
@@ -14,17 +14,61 @@
50% {background-color: #3f51b5;}
100% {background-color: inherit;}
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0.0);
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(30,30,30,0.3);
}
::-webkit-scrollbar-thumb {
background-color: rgba(140,140,140,0.8);
#-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.8);
}
</style>
<style>
#splash {
z-index: 1000000;
background-color: #303030;
display: block;
width: 100vw;
height: 100vh;
position: fixed;
opacity: 1;
}
#splash1 {
margin: 37vh auto 0 auto;
height: 25vh;
width: 25vh;
background-image:url('./rings.svg');
background-size: cover;
}
#splash2 {
margin: 0 auto;
}
@keyframes unsplash {
0% {opacity: 1;}
100% {opacity: 0;}
}
</style>
<script src="http://localhost:35729/livereload.js"></script>
</head>
<body>
<div id="example"></div>
<!-- Dependencies -->
<!-- <script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script> -->
<!-- Main -->
<div id="splash"><div id="splash1"></div>
<!-- <div id="splash1">
<div id="splash2" style="color: #ddd" class="la-square-jelly-box la-3x">
<div>a</div>
<div>ads</div>
</div> -->
</div>
</div>
<div id="app" style="font:-webkit-control"></div>
<script src="./build/bundle.js"></script>
</body>
</html>
+487 -10
View File
@@ -483,6 +483,15 @@
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
},
"asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
"dev": true,
"requires": {
"safer-buffer": "~2.1.0"
}
},
"asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
@@ -516,6 +525,12 @@
}
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true
},
"assign-symbols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -536,6 +551,12 @@
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -569,6 +590,18 @@
}
}
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
"dev": true
},
"aws4": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true
},
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@@ -649,6 +682,15 @@
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
"integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true,
"requires": {
"tweetnacl": "^0.14.3"
}
},
"better-assert": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
@@ -748,6 +790,11 @@
"multicast-dns-service-types": "^1.1.0"
}
},
"brace": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz",
"integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -941,6 +988,12 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
"integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA=="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true
},
"chalk": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
@@ -1066,6 +1119,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"combined-stream": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
},
"commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
@@ -1129,6 +1191,19 @@
"typedarray": "^0.0.6"
}
},
"conf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
"dev": true,
"requires": {
"dot-prop": "^4.1.0",
"env-paths": "^1.0.0",
"make-dir": "^1.0.0",
"pkg-up": "^2.0.0",
"write-file-atomic": "^2.3.0"
}
},
"connect-history-api-fallback": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz",
@@ -1301,6 +1376,15 @@
"es5-ext": "^0.10.9"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"dev": true,
"requires": {
"assert-plus": "^1.0.0"
}
},
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
@@ -1427,6 +1511,12 @@
"rimraf": "^2.2.8"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -1451,6 +1541,11 @@
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
"integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw=="
},
"diff-match-patch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz",
"integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg=="
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@@ -1496,6 +1591,15 @@
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
"integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA=="
},
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
"dev": true,
"requires": {
"is-obj": "^1.0.0"
}
},
"duplexify": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz",
@@ -1507,11 +1611,53 @@
"stream-shift": "^1.0.0"
}
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"dev": true,
"requires": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"electron-nucleus": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/electron-nucleus/-/electron-nucleus-1.11.0.tgz",
"integrity": "sha512-n8Nz4D0z0SrBg5Y8JEIDdbNtradG2tozrSnpxB/PK9arJo5YujA4+4OAWiA5ggDi/UJs+zjlU/1iTVnPFgxtgw==",
"dev": true,
"requires": {
"electron-store": "^1.3.0",
"node-machine-id": "^1.1.10",
"request": "^2.85.0",
"ws": "^5.1.1"
},
"dependencies": {
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"
}
}
}
},
"electron-store": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz",
"integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==",
"dev": true,
"requires": {
"conf": "^1.3.0"
}
},
"elliptic": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
@@ -1602,6 +1748,12 @@
"tapable": "^1.0.0"
}
},
"env-paths": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=",
"dev": true
},
"errno": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
@@ -1803,6 +1955,12 @@
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
"extend-shallow": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@@ -1881,6 +2039,12 @@
}
}
},
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
@@ -2020,6 +2184,23 @@
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
"integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
"dev": true
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -2614,6 +2795,15 @@
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg="
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"dev": true,
"requires": {
"assert-plus": "^1.0.0"
}
},
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
@@ -2680,6 +2870,22 @@
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
"integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ=="
},
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
"dev": true
},
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"dev": true,
"requires": {
"ajv": "^6.5.5",
"har-schema": "^2.0.0"
}
},
"has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
@@ -2833,6 +3039,17 @@
"micromatch": "^3.1.9"
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"dev": true,
"requires": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
}
},
"https-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
@@ -3057,6 +3274,12 @@
}
}
},
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
"is-path-cwd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
@@ -3091,6 +3314,12 @@
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"dev": true
},
"is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@@ -3125,6 +3354,12 @@
"whatwg-fetch": ">=0.10.0"
}
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
"jquery": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
@@ -3135,16 +3370,34 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"dev": true
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
"json3": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
@@ -3158,6 +3411,18 @@
"minimist": "^1.2.0"
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"dev": true,
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.2.3",
"verror": "1.10.0"
}
},
"jss": {
"version": "9.8.7",
"resolved": "https://registry.npmjs.org/jss/-/jss-9.8.7.tgz",
@@ -3300,6 +3565,16 @@
"resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
"integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
@@ -3679,6 +3954,12 @@
}
}
},
"node-machine-id": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.10.tgz",
"integrity": "sha512-6SVxo3Ic2Qc09z1rCJh3No7ubizPLszImsMQnZZWfzeOC6SYU4orN214++c3ikB8uaP/A6dwSlO88A3ohI5oNA==",
"dev": true
},
"normalize-path": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
@@ -3705,6 +3986,12 @@
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"dev": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -3973,6 +4260,12 @@
"sha.js": "^2.4.8"
}
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@@ -3999,6 +4292,60 @@
"find-up": "^3.0.0"
}
},
"pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
"dev": true,
"requires": {
"find-up": "^2.1.0"
},
"dependencies": {
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"dev": true,
"requires": {
"locate-path": "^2.0.0"
}
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"dev": true,
"requires": {
"p-locate": "^2.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"dev": true,
"requires": {
"p-try": "^1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"dev": true,
"requires": {
"p-limit": "^1.1.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
"dev": true
}
}
},
"popper.js": {
"version": "1.14.6",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz",
@@ -4065,6 +4412,12 @@
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
},
"psl": {
"version": "1.1.31",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
"integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==",
"dev": true
},
"public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -4172,14 +4525,27 @@
}
},
"react": {
"version": "16.3.2",
"resolved": "https://registry.npmjs.org/react/-/react-16.3.2.tgz",
"integrity": "sha512-o5GPdkhciQ3cEph6qgvYB7LTOHw/GB0qRI6ZFNugj49qJCFfgHwVNjZ5u+b7nif4vOeMIOuYj3CeYe2IBD74lg==",
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.7.0.tgz",
"integrity": "sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A==",
"dev": true,
"requires": {
"fbjs": "^0.8.16",
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.0"
"prop-types": "^15.6.2",
"scheduler": "^0.12.0"
}
},
"react-ace": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-6.3.2.tgz",
"integrity": "sha512-eSk0fWvrBe2oqYIYX0njLddLG5H0hemWv5VVoQi5yDSPTjGlSSnzFwdgPyfuwRe8mSARZuRdprPQa5p61hKirw==",
"requires": {
"brace": "^0.11.1",
"diff-match-patch": "^1.0.4",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"prop-types": "^15.6.2"
}
},
"react-base16-styling": {
@@ -4194,14 +4560,15 @@
}
},
"react-dom": {
"version": "16.3.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.3.3.tgz",
"integrity": "sha512-ALCp7ZbSGkqRDtQoZozKVNgwXMxbxf/IGOUMC2A0yF6JHeZrS8e2cOotPT87Vf4b7PKCuUVKU4/RDEXxToA/yA==",
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.7.0.tgz",
"integrity": "sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg==",
"dev": true,
"requires": {
"fbjs": "^0.8.16",
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.0"
"prop-types": "^15.6.2",
"scheduler": "^0.12.0"
}
},
"react-event-listener": {
@@ -4351,6 +4718,34 @@
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"dev": true,
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -4438,6 +4833,16 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"scheduler": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.12.0.tgz",
"integrity": "sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"schema-utils": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
@@ -4920,6 +5325,23 @@
"extend-shallow": "^3.0.0"
}
},
"sshpk": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz",
"integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==",
"dev": true,
"requires": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
@@ -5191,6 +5613,24 @@
"repeat-string": "^1.6.1"
}
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"dev": true,
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
},
"dependencies": {
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
}
}
},
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
@@ -5201,6 +5641,21 @@
"resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"dev": true,
"requires": {
"safe-buffer": "^5.0.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
@@ -5395,6 +5850,17 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"dev": true,
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
},
"vm-browserify": {
"version": "0.0.4",
"resolved": "http://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
@@ -5732,6 +6198,17 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
"integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.2"
}
},
"ws": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz",
+5 -5
View File
@@ -4,8 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --mode production",
"postinstall": "npm run build"
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
@@ -15,8 +14,10 @@
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.12",
"electron-nucleus": "^1.11.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"react-transition-group": "^2.5.2",
"redux": "^4.0.1",
"webpack-livereload-plugin": "^2.2.0"
},
@@ -31,8 +32,7 @@
"jquery": "^3.3.1",
"lodash.throttle": "^4.1.1",
"moving-average": "^1.0.0",
"react": "^16.3.2",
"react-dom": "^16.3.3",
"react-ace": "^6.3.2",
"react-json-view": "^1.19.1",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0",
+42
View File
@@ -0,0 +1,42 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="45" height="45" viewBox="0 0 45 45" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
<g fill="none" fill-rule="evenodd" transform="translate(1 1)" stroke-width="2">
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="1.5s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="1.5s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-width"
begin="1.5s" dur="3s"
values="2;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="3s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="3s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-width"
begin="3s" dur="3s"
values="2;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="8">
<animate attributeName="r"
begin="0s" dur="1.5s"
values="6;1;2;3;4;5;6"
calcMode="linear"
repeatCount="indefinite" />
</circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+31 -25
View File
@@ -1,14 +1,17 @@
import * as React from 'react'
import { connect } from 'react-redux'
import * as q from '../../backend/src/Model'
import CssBaseline from '@material-ui/core/CssBaseline'
import { withStyles, Theme } from '@material-ui/core/styles'
import Tree from './components/Tree/Tree'
import TitleBar from './components/TitleBar'
import Sidebar from './components/Sidebar/Sidebar'
import Connection from './components/ConnectionSetup/Connection'
import Settings from './components/Settings'
import { Theme, withStyles } from '@material-ui/core/styles'
import { AppState } from './reducers'
import Connection from './components/ConnectionSetup/Connection'
import CssBaseline from '@material-ui/core/CssBaseline'
import Settings from './components/Settings'
import Sidebar from './components/Sidebar/Sidebar'
import TitleBar from './components/TitleBar'
import Tree from './components/Tree/Tree'
import UpdateNotifier from './UpdateNotifier'
import { connect } from 'react-redux'
interface State {
selectedNode?: q.TreeNode,
@@ -79,24 +82,27 @@ class App extends React.Component<Props, State> {
public render() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent } = this.getStyles()
return <div style={centerContent}>
<CssBaseline />
<Settings />
<div style={settingsVisible ? contentShift : content}>
<TitleBar />
<div style={centerContent}>
<div style={this.getStyles().left}>
<Tree connectionId={this.state.connectionId} didSelectNode={(node: q.TreeNode) => {
this.setState({ selectedNode: node })
}} />
return (
<div style={centerContent}>
<CssBaseline />
<Settings />
<div style={settingsVisible ? contentShift : content}>
<TitleBar />
<div style={centerContent}>
<div style={this.getStyles().left}>
<Tree connectionId={this.state.connectionId} didSelectNode={(node: q.TreeNode) => {
this.setState({ selectedNode: node })
}} />
</div>
<div style={this.getStyles().right}>
<Sidebar connectionId={this.state.connectionId} />
</div>
</div>
<div style={this.getStyles().right}>
<Sidebar node={this.state.selectedNode} />
</div>
</div>
</div>
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
</div >
</div>
<UpdateNotifier />
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
</div >
)
}
}
+219
View File
@@ -0,0 +1,219 @@
import * as React from 'react'
import {
Button,
IconButton,
Modal,
Paper,
Snackbar,
SnackbarContent,
Typography,
} from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { UpdateInfo, checkForUpdates, rendererEvents, updateAvailable } from '../../events'
import { green, red } from '@material-ui/core/colors'
import { AppState } from './reducers'
import Close from '@material-ui/icons/Close'
import CloudDownload from '@material-ui/icons/CloudDownload'
import { UpdateFileInfo } from 'builder-util-runtime'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { updateNotifierActions } from './actions'
interface Props {
showUpdateNotification: boolean
showUpdateDetails: boolean
classes: any
actions: any
}
class UpdateNotifier extends React.Component<Props, {}> {
private updateInfo?: UpdateInfo
constructor(props: any) {
super(props)
this.state = {
selectedNode: undefined,
}
}
public componentDidMount() {
rendererEvents.emit(checkForUpdates, undefined)
rendererEvents.subscribe(updateAvailable, this.handleUpdate)
}
public componentWillUnmount() {
rendererEvents.unsubscribeAll(updateAvailable)
}
private handleUpdate = (updateInfo: UpdateInfo) => {
this.updateInfo = updateInfo
this.props.actions.showUpdateNotification(true)
}
private onCloseNotification = (event: React.SyntheticEvent<any>, reason: string) => {
if (reason === 'clickaway') {
return
}
this.props.actions.showUpdateNotification(false)
}
private closeNotification = () => {
this.props.actions.showUpdateNotification(false)
}
private showDetails = () => {
this.props.actions.showUpdateNotification(false)
this.props.actions.showUpdateDetails(true)
}
private hideDetails = () => {
this.props.actions.showUpdateDetails(false)
}
public render() {
return (
<div>
{this.renderUpdateNotification()}
{this.renderUpdateDetails()}
</div>
)
}
private renderUpdateNotification() {
const snackbarAnchor: any = {
vertical: 'top',
horizontal: 'right',
}
return (
<Snackbar
anchorOrigin={snackbarAnchor}
open={this.props.showUpdateNotification}
autoHideDuration={7000}
onClose={this.onCloseNotification}
>
<SnackbarContent
className={this.props.classes.success}
message="Update available"
action={this.notificationActions()}
/>
</Snackbar>
)
}
private notificationActions() {
return [(
<Button key="undo" size="small" onClick={this.showDetails}>
Download
</Button>
), (
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={this.props.classes.close}
onClick={this.closeNotification}
>
<Close />
</IconButton>
),
]
}
private renderUpdateDetails() {
if (!this.updateInfo) {
return null
}
const releaseNotes = (this.updateInfo.releaseNotes as string) || ''
return (
<Modal
open={this.props.showUpdateDetails}
disableAutoFocus={true}
onClose={this.hideDetails}
>
<Paper className={this.props.classes.root}>
<Typography variant="h6" className={this.props.classes.title}>Version {this.updateInfo.version}</Typography>
<Typography className={this.props.classes.title}>Changelog</Typography>
<div className={this.props.classes.releaseNotes} dangerouslySetInnerHTML={{ __html: releaseNotes }} />
{this.renderDownloads(this.updateInfo)}
<Button className={this.props.classes.closeButton} color="secondary" onClick={this.hideDetails}>Close</Button>
</Paper>
</Modal>
)
}
private urlToFilename(url: string) {
const parts = url.split('/')
return parts[parts.length - 1]
}
private renderDownloads(updateInfo: UpdateInfo) {
console.log(updateInfo)
return updateInfo.files
.map((file: UpdateFileInfo, index: number) => (
<div key={index}>
<Button
className={this.props.classes.download}
href={file.url}
>
<IconButton><CloudDownload /></IconButton>{this.urlToFilename(file.url)}
</Button>
</div>
))
}
}
const styles = (theme: Theme) => ({
success: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
close: {
padding: theme.spacing.unit / 2,
},
root: {
minWidth: '350px',
maxWidth: '500px',
backgroundColor: theme.palette.background.default,
margin: '20vh auto auto auto',
padding: `${2 * theme.spacing.unit}px`,
outline: 'none',
},
title: {
color: theme.palette.text.primary,
},
releaseNotes: {
overflow: 'auto scroll',
color: theme.palette.text.secondary,
backgroundColor: 'rgba(60, 60, 60, 0.6)',
maxHeight: '28vh',
},
paper: {
padding: `${theme.spacing.unit * 2}px`,
color: theme.palette.text.secondary,
},
download: {
width: '100%',
},
closeButton: {
display: 'block',
margin: '0 0 0 auto',
},
})
const mapStateToProps = (state: AppState) => {
return {
showUpdateNotification: state.showUpdateNotification,
showUpdateDetails: state.showUpdateDetails,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(updateNotifierActions, dispatch),
}
}
export default withStyles(styles, { withTheme: true })(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
+10 -3
View File
@@ -1,14 +1,21 @@
import { ActionTypes } from '../reducers'
import { ActionTypes, NodeOrder, CustomAction } from '../reducers'
export const setAutoExpandLimit = (autoExpandLimit: number = 0) => {
export const setAutoExpandLimit = (autoExpandLimit: number = 0): CustomAction => {
return {
autoExpandLimit,
type: ActionTypes.setAutoExpandLimit,
}
}
export const toggleSettingsVisibility = () => {
export const toggleSettingsVisibility = (): CustomAction => {
return {
type: ActionTypes.toggleSettingsVisibility,
}
}
export const setNodeOrder = (nodeOrder: NodeOrder = NodeOrder.none): CustomAction => {
return {
nodeOrder,
type: ActionTypes.setNodeOrder,
}
}
+15
View File
@@ -0,0 +1,15 @@
import { ActionTypes, CustomAction } from '../reducers'
export const setPublishTopic = (topic: string): CustomAction => {
return {
publishTopic: topic,
type: ActionTypes.setPublishTopic,
}
}
export const setPublishPayload = (payload: string): CustomAction => {
return {
publishPayload: payload,
type: ActionTypes.setPublishPayload,
}
}
+9
View File
@@ -0,0 +1,9 @@
import { ActionTypes, CustomAction } from '../reducers'
import * as q from '../../../backend/src/Model'
export const selectTopic = (topic: q.TreeNode): CustomAction => {
return {
selectedTopic: topic,
type: ActionTypes.selectTopic,
}
}
+15
View File
@@ -0,0 +1,15 @@
import { ActionTypes, CustomAction } from '../reducers'
export const showUpdateNotification = (show: boolean): CustomAction => {
return {
type: ActionTypes.showUpdateNotification,
showUpdateNotification: show,
}
}
export const showUpdateDetails = (show: boolean): CustomAction => {
return {
type: ActionTypes.showUpdateDetails,
showUpdateDetails: show,
}
}
+4 -1
View File
@@ -1,3 +1,6 @@
import * as settingsActions from './Settings'
import * as sidebarActions from './Sidebar'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
export { settingsActions }
export { settingsActions, treeActions, sidebarActions, updateNotifierActions }
@@ -1,14 +1,22 @@
import * as React from 'react'
import {
Typography, Toolbar, Modal,
MenuItem, Button, Grid, Paper,
TextField, Switch, FormControlLabel,
Button,
CircularProgress,
FormControlLabel,
Grid,
MenuItem,
Modal,
Paper,
Switch,
TextField,
Toolbar,
Typography,
} from '@material-ui/core'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
import Notification from './Notification'
import { MqttOptions, DataSourceState } from '../../../../backend/src/DataSource'
import { DataSourceState, MqttOptions } from '../../../../backend/src/DataSource'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../../events'
import sha1 = require('sha1')
interface Props {
@@ -53,7 +61,7 @@ class Connection extends React.Component<Props, State> {
const defaultState = {
visible: true,
host: 'nodered',
host: 'iot.eclipse.org',
protocol: protocols[0],
port: 1883,
tls: false,
@@ -1,57 +0,0 @@
import * as React from 'react'
import { Snackbar, SnackbarContent } from '@material-ui/core'
import { red, green } from '@material-ui/core/colors'
import { withStyles, Theme } from '@material-ui/core/styles'
enum MessageType {
success = 'success',
error = 'error',
}
interface Props {
message: string
type: MessageType
onClose: () => void
classes: any
}
interface State {
snackBarOpen: boolean
}
class Notification extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { snackBarOpen: false }
}
public static styles = (theme: Theme) => ({
success: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
error: {
backgroundColor: red[600],
color: theme.typography.button.color,
},
})
public render() {
return <Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open = {this.state.snackBarOpen}
autoHideDuration = {6000}
onClose = {() => { this.setState({ snackBarOpen: false }) }}
>
<SnackbarContent
className = {this.props.classes[this.props.type]}
message = {this.props.message}
/>
</Snackbar>
}
}
export default withStyles(Notification.styles)(Notification)
+2 -2
View File
@@ -32,8 +32,8 @@ class Copy extends React.Component<Props, State> {
public render() {
const icon = !this.state.didCopy
? <FileCopy fontSize="inherit" style={ { cursor: 'pointer' } } onClick={this.handleClick} />
: <Check fontSize="inherit" style={ { cursor: 'default' } } />
? <FileCopy fontSize="inherit" style={{ cursor: 'pointer' }} onClick={this.handleClick} />
: <Check fontSize="inherit" style={{ cursor: 'default' }} />
return <span>
<span style={{ fontSize: '16px' }}>{icon}</span>
+88 -58
View File
@@ -1,23 +1,23 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import Drawer from '@material-ui/core/Drawer'
import IconButton from '@material-ui/core/IconButton'
import Paper from '@material-ui/core/Paper'
import Divider from '@material-ui/core/Divider'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import { AppState, NodeOrder } from '../reducers'
import {
Divider,
Drawer,
IconButton,
Input,
InputLabel,
MenuItem,
Select,
Typography,
} from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import ChevronRight from '@material-ui/icons/ChevronRight'
import { Typography, InputBase, Input, InputLabel } from '@material-ui/core'
import { withStyles, StyleRulesCallback } from '@material-ui/core/styles'
import { settingsActions } from '../actions'
import { AppState } from '../reducers'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions } from '../actions'
const styles: StyleRulesCallback = theme => ({
drawer: {
@@ -40,16 +40,14 @@ const styles: StyleRulesCallback = theme => ({
interface Props {
actions?: any
autoExpandLimit: number,
visible: boolean,
store?: any,
autoExpandLimit: number
visible: boolean
store?: any
classes: any
nodeOrder: NodeOrder
}
interface State {
}
class Settings extends React.Component<Props, State> {
class Settings extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
@@ -57,59 +55,91 @@ class Settings extends React.Component<Props, State> {
public render() {
const { classes, actions, visible } = this.props
return <Drawer
anchor = "left"
className = { classes.drawer }
open = { visible }
variant = "persistent"
>
<div
className = { classes.paper }
tabIndex = { 0 }
role = "button"
onClick = {(e: React.MouseEvent) => e.stopPropagation()}
onKeyDown = {(e: React.KeyboardEvent) => e.stopPropagation()}
return (
<Drawer
anchor="left"
className={classes.drawer}
open={visible}
variant="persistent"
>
<div
className={classes.paper}
tabIndex={0}
role="button"
>
<Typography className = { classes.title } variant = "h6" color = "inherit">
<IconButton onClick = {actions.toggleSettingsVisibility}>
<ChevronRight />
</IconButton>
Settings
</Typography>
<Divider />
<Typography className={classes.title} variant="h6" color="inherit">
<IconButton onClick={actions.toggleSettingsVisibility}>
<ChevronRight />
</IconButton>
Settings
</Typography>
<Divider />
{this.renderAutoExpand()}
</div>
</Drawer>
{this.renderAutoExpand()}
{this.renderNodeOrder()}
</div>
</Drawer>
)
}
private renderAutoExpand() {
const { classes, actions, autoExpandLimit } = this.props
const { classes, autoExpandLimit } = this.props
return (
<div style={{ padding: '8px' }}>
<InputLabel htmlFor="auto-expand">Auto Expand</InputLabel>
<Select
value={autoExpandLimit}
onChange={this.onChangeAutoExpand}
input={<Input name="auto-expand" id="auto-expand-label-placeholder" />}
name="auto-expand"
className={classes.input}
>
<MenuItem value={0}><em>Collapsed</em></MenuItem>
<MenuItem value={2}>Few</MenuItem>
<MenuItem value={3}>Some</MenuItem>
<MenuItem value={10}>Most</MenuItem>
<MenuItem value={1E6}>All</MenuItem>
</Select>
</div>
)
}
return <div style={{ padding: '8px' }}>
<InputLabel htmlFor="auto-expand">Auto Expand</InputLabel>
private onChangeAutoExpand = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.setAutoExpandLimit(e.target.value)
}
private renderNodeOrder() {
const { classes, nodeOrder } = this.props
return (
<div style={{ padding: '8px' }}>
<InputLabel htmlFor="auto-expand">Topic order</InputLabel>
<Select
value = { autoExpandLimit }
onChange = { (e: React.ChangeEvent<HTMLSelectElement>) => actions.setAutoExpandLimit(e.target.value) }
input = {<Input name="auto-expand" id="auto-expand-label-placeholder" />}
displayEmpty
name = "auto-expand"
className = {classes.input}
value={nodeOrder}
onChange={this.onChangeSorting}
input={<Input name="node-order" id="node-order-label-placeholder" />}
displayEmpty={true}
name="node-order"
className={classes.input}
>
<MenuItem value = {0}><em>Disabled</em></MenuItem>
<MenuItem value = {2}>Few</MenuItem>
<MenuItem value = {3}>Some</MenuItem>
<MenuItem value = {10}>Most</MenuItem>
<MenuItem value = {1E6}>All</MenuItem>
<MenuItem value={NodeOrder.none}><em>default</em></MenuItem>
<MenuItem value={NodeOrder.abc}>a-z</MenuItem>
<MenuItem value={NodeOrder.messages}>{NodeOrder.messages}</MenuItem>
<MenuItem value={NodeOrder.topics}>{NodeOrder.topics}</MenuItem>
</Select>
</div>
</div>)
}
private onChangeSorting = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.setNodeOrder(e.target.value)
}
}
const mapStateToProps = (state: AppState) => {
return {
autoExpandLimit: state.settings.autoExpandLimit,
nodeOrder: state.settings.nodeOrder,
visible: state.settings.visible,
}
}
+89
View File
@@ -0,0 +1,89 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Badge, Typography } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
interface HistoryItem {
title: string
value: string
}
interface Props {
items: HistoryItem[]
onClick?: (index: number, element: EventTarget) => void
classes: any
}
interface State {
collapsed: boolean
}
class MessageHistory extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = {
collapsed: true,
}
}
public renderHistory() {
const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }
const elements = this.props.items.map((element, index) => (
<div
key={index}
style={{
backgroundColor: 'rgba(80, 80, 80, 0.6)',
margin: '8px',
padding: '8px 8px 0 8px',
cursor: this.props.onClick ? 'pointer' : 'inherit',
}}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
>
<div><i>{element.title}</i></div>
<div style={messageStyle}>
<span><pre>{element.value}</pre></span>
</div>
</div>
))
return (
<div style={{ backgroundColor: 'rgba(60, 60, 60, 0.6)', marginTop: '16px' }}>
<Typography
onClick={this.toggle}
style={{ cursor: 'pointer' }}
>
{this.state.collapsed ? '▶' : '▼'} History
</Typography>
<div style={{ maxHeight: '230px', overflowY: 'scroll' }}>
{this.state.collapsed ? null : elements}
</div>
</div>
)
}
public render() {
const visible = this.props.items.length > 0 && this.state.collapsed
return (
<Badge
style={{ display: 'block', width: '100%' }}
invisible={!visible}
badgeContent={this.props.items.length}
color="primary"
classes={{ badge: this.props.classes.badge }}
>
{this.renderHistory()}
</Badge>
)
}
private toggle = () => {
this.setState({ collapsed: !this.state.collapsed })
}
}
const styles = (theme: Theme) => ({
badge: { top: '-8px', left:'64px' },
})
export default withStyles(styles)(MessageHistory)
@@ -0,0 +1,74 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Theme, withTheme } from '@material-ui/core/styles'
import Fade from '@material-ui/core/Fade'
import History from './History'
import Paper from '@material-ui/core/Paper'
import Popper from '@material-ui/core/Popper'
import ValueRenderer from './ValueRenderer'
interface Props {
node?: q.TreeNode
theme: Theme
onSelect: (message: q.Message) => void
}
interface State {
displayMessage?: q.Message,
anchorEl?: HTMLElement
}
class MessageHistory extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { }
}
private updateNode = () => {
this.setState(this.state)
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
nextProps.node && nextProps.node.onMessage.subscribe(this.updateNode)
}
public componentWillMount() {
this.props.node && this.props.node.onMessage.subscribe(this.updateNode)
}
public componentWillUnMount() {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
}
public render() {
const { node } = this.props
if (!node) {
return null
}
const history = node.messageHistory.toArray()
const historyElements = history.map(message => ({
title: message.received.toGMTString(),
value: message.value,
}))
return (
<div>
<History
items={historyElements}
onClick={this.displayMessage}
/>
</div>
)
}
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray()[index]
this.props.onSelect(message)
}
}
export default withTheme()(MessageHistory)
+7 -6
View File
@@ -1,6 +1,5 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
// import Drawer from '@material-ui/core/Drawer'
import { Typography } from '@material-ui/core'
interface Props {
@@ -15,11 +14,13 @@ class NodeStats extends React.Component<Props, {}> {
public render() {
const { node } = this.props
return <div>
<Typography>Messages: #{node.messages}</Typography>
<Typography>Subtopics: {node.leafCount()}</Typography>
<Typography>Messages Subtopics: #{node.leafMessageCount()}</Typography>
</div>
return (
<div>
<Typography>Messages: #{node.messages}</Typography>
<Typography>Subtopics: {node.leafCount()}</Typography>
<Typography>Messages Subtopics: #{node.leafMessageCount()}</Typography>
</div>
)
}
}
@@ -0,0 +1,7 @@
interface Message {
topic: string
payload?: string
sent: Date
}
export default Message
@@ -0,0 +1,234 @@
// tslint:disable-next-line
import 'react-ace'
import 'brace/mode/json'
import 'brace/mode/text'
import 'brace/mode/xml'
import 'brace/theme/monokai'
import * as React from 'react'
import * as brace from 'brace'
import * as q from '../../../../../backend/src/Model'
import {
Button,
Fab,
FormControlLabel,
InputAdornment,
Radio,
RadioGroup,
TextField,
Typography,
} from '@material-ui/core'
import { makePublishEvent, rendererEvents } from '../../../../../events'
import { default as AceEditor } from 'react-ace'
import { AppState } from '../../../reducers'
import History from '../History'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions'
interface Props {
node?: q.TreeNode
connectionId?: string
topic?: string
payload?: string
actions: any
}
interface State {
mode: string
history: Message[]
}
class Publish extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { mode: 'json', history: [] }
}
private updatePayload = (value: string, event?: any) => {
this.props.actions.setPublishPayload(value)
}
private updateTopic = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
this.props.actions.setPublishTopic(e.target.value)
}
private updateMode = (e: React.ChangeEvent<{}>, value: string) => {
this.setState({ mode: value })
}
private publish = (e: React.MouseEvent) => {
e.stopPropagation()
const topic = this.currentTopic() || ''
const payload = this.props.payload
if (this.props.connectionId && topic) {
rendererEvents.emit(makePublishEvent(this.props.connectionId), { topic, payload })
this.addMessageToHistory(topic, payload)
}
}
private addMessageToHistory(topic: string, payload?: string) {
// Remove duplicates
let filteredHistory = this.state.history.filter(e => e.payload !== payload || e.topic !== topic)
filteredHistory = filteredHistory.slice(-7)
const history: Message[] = [...filteredHistory, { topic, payload, sent: new Date() }]
this.setState({ history })
}
public render() {
return (
<div style={{ flexGrow: 1, marginLeft: '8px' }}>
{this.topic()}
{this.editor()}
{this.history()}
</div>
)
}
private currentTopic(): string | undefined {
const { node, topic } = this.props
const selectedNodePath = (node ? node.path() : undefined)
return (topic !== undefined) ? topic : selectedNodePath
}
private topic() {
const topicStr = this.currentTopic() || ''
return (
<div>
<Typography>Topic</Typography>
<TextField
placeholder="example/topic"
style={{ width: '100%' }}
margin="normal"
value={topicStr}
multiline={true}
onChange={this.updateTopic}
InputProps={{ onBlur: this.onTopicBlur }}
/>
</div>
)
}
private onTopicBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (!e.target.value) {
this.props.actions.setPublishTopic(undefined)
}
}
private editorOptions = {
showLineNumbers: false,
tabSize: 2,
}
private publishButton() {
return (
<Button
variant="contained"
size="small"
color="primary"
onClick={this.publish}
>
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
)
}
private editorMode() {
const labelStyle = { margin: '0 8px 0 8px' }
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={this.state.mode}
onChange={this.updateMode}
row={true}
>
<FormControlLabel
value="text"
style={labelStyle}
control={<Radio color="primary" />}
label="raw"
labelPlacement="top"
/>
<FormControlLabel
value="xml"
style={labelStyle}
control={<Radio color="primary" />}
label="xml"
labelPlacement="top"
/>
<FormControlLabel
value="json"
style={labelStyle}
control={<Radio color="primary" />}
label="json"
labelPlacement="top"
/>
</RadioGroup>
<div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()}
</div>
</div>
</div>
)
}
private history() {
const items = this.state.history.map(message => ({
title: message.topic,
value: message.payload,
}))
return <History items={items} onClick={this.didSelectHistoryEntry} />
}
private didSelectHistoryEntry = (index: number) => {
let message = this.state.history[index]
this.props.actions.setPublishTopic(message.topic)
this.props.actions.setPublishPayload(message.payload)
}
private editor() {
return (
<div style={{ width: '100%', display: 'block' }}>
{this.editorMode()}
<AceEditor
mode={this.state.mode}
theme="monokai"
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
showGutter={true}
value={this.props.payload}
onChange={this.updatePayload}
setOptions={this.editorOptions}
editorProps={{ $blockScrolling: true }}
/>
</div>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(sidebarActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
topic: state.sidebar.publishTopic,
payload: state.sidebar.publishPayload,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Publish)
+104 -36
View File
@@ -1,32 +1,48 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
import ExpandMore from '@material-ui/icons/ExpandMore'
import {
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Fade,
Paper,
Popper,
Typography,
} from '@material-ui/core'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { AppState } from '../../reducers'
import Copy from '../Copy'
import ValueRenderer from './ValueRenderer'
import ExpandMore from '@material-ui/icons/ExpandMore'
import MessageHistory from './MessageHistory'
import NodeStats from './NodeStats'
import Publish from './Publish/Publish'
import Topic from './Topic'
import ValueRenderer from './ValueRenderer'
import { connect } from 'react-redux'
interface Props {
node?: q.TreeNode,
classes: any,
theme: Theme,
connectionId?: string,
}
interface State {
node: q.TreeNode
node: q.TreeNode,
compareMessage?: q.Message
}
class Sidebar extends React.Component<Props, State> {
private valueRef: any = React.createRef<HTMLDivElement>()
private updateNode = () => {
this.setState(this.state)
}
constructor(props: any) {
super(props)
console.warn('Find and fix me #state')
console.error('Find and fix me #state')
this.state = { node: new q.Tree() }
}
@@ -50,6 +66,10 @@ class Sidebar extends React.Component<Props, State> {
this.props.node && this.removeUpdateListener(this.props.node)
nextProps.node && this.registerUpdateListener(nextProps.node)
this.props.node && this.setState({ node: this.props.node })
if (this.props.node !== nextProps.node) {
this.setState({ compareMessage: undefined })
}
}
private registerUpdateListener(node: q.TreeNode) {
@@ -63,44 +83,92 @@ class Sidebar extends React.Component<Props, State> {
}
public render() {
return <div className={this.props.classes.drawer}>
{this.renderNode()}
</div>
return (
<div className={this.props.classes.drawer}>
{this.renderNode()}
</div>
)
}
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
private renderNode() {
const { classes, node } = this.props
const copyTopic = node ? <Copy value={node.path()} /> : null
const copyValue = node && node.message ? <Copy value={node.message.value} /> : null
const summeryStyle = { minHeight: '0' }
return (
<div>
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Topic {copyTopic}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<Topic node={this.props.node} didSelectNode={this.updateNode} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<div ref={this.valueRef}><ValueRenderer message={this.props.node && this.props.node.message} /></div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} node={this.props.node} /></div>
<Popper open={Boolean(this.state.compareMessage)} anchorEl={this.valueRef.current} placement="left" transition={true}>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper>
<ValueRenderer message={this.state.compareMessage} />
</Paper>
</Fade>
)}
</Popper>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Publish</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<Publish node={this.props.node} connectionId={this.props.connectionId} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
{this.renderNodeStats()}
</ExpansionPanel>
</div>
)
}
return <div>
<ExpansionPanel key="topic" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Topic {copyTopic}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Topic node={this.props.node} didSelectNode={this.updateNode} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<ValueRenderer node={this.state.node} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<NodeStats node={this.state.node} />
</ExpansionPanelDetails>
</ExpansionPanel>
</div>
private handleMessageHistorySelect = (message: q.Message) => {
if (message !== this.state.compareMessage) {
this.setState({ compareMessage: message })
} else {
this.setState({ compareMessage: undefined })
}
}
private renderNodeStats() {
if (!this.props.node) {
return null
}
return (
<ExpansionPanelDetails style={this.detailsStyle}>
<NodeStats node={this.props.node} />
</ExpansionPanelDetails>
)
}
}
export default withStyles(Sidebar.styles, { withTheme: true })(Sidebar)
const mapStateToProps = (state: AppState) => {
return {
node: state.selectedTopic,
}
}
export default withStyles(Sidebar.styles, { withTheme: true })(connect(mapStateToProps)(Sidebar))
+4 -2
View File
@@ -31,7 +31,8 @@ class Topic extends React.Component<Props, {}> {
.map(node => node.sourceEdge)
.filter(edge => Boolean(edge))
.map(edge =>
[<Button
[(
<Button
onClick={() => this.setState({ node: edge!.target })}
size="small"
color="secondary"
@@ -39,7 +40,8 @@ class Topic extends React.Component<Props, {}> {
key={edge!.hash()}
>
{edge!.name}
</Button>],
</Button>
)],
)
if (breadCrumps.length === 0) {
+29 -37
View File
@@ -1,71 +1,63 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Theme, withTheme } from '@material-ui/core/styles'
import { default as ReactJson } from 'react-json-view'
import { withTheme, Theme } from '@material-ui/core/styles'
interface Props {
node?: q.TreeNode | undefined
message?: q.Message
theme: Theme
}
interface State {
node?: q.TreeNode | undefined
}
class ValueRenderer extends React.Component<Props, State> {
private updateNode: () => void
constructor(props: any) {
super(props)
this.state = {}
this.updateNode = () => {
this.setState(this.state)
}
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
nextProps.node && nextProps.node.onMessage.subscribe(this.updateNode)
}
class ValueRenderer extends React.Component<Props, {}> {
public render() {
const node = this.props.node
if (!node || !node.message) {
return <div>{this.renderValue()}</div>
}
public renderValue() {
const { message } = this.props
if (!message) {
return null
}
let json
try {
json = JSON.parse(node.message.value)
json = JSON.parse(message.value)
} catch (error) {
return this.renderRawValue(node.message.value)
return this.renderRawValue(message.value)
}
if (typeof json === 'string') {
return this.renderRawValue(node.message.value)
return this.renderRawValue(message.value)
} else if (typeof json === 'number') {
return this.renderRawValue(node.message.value)
return this.renderRawValue(message.value)
} else if (typeof json === 'boolean') {
return this.renderRawValue(node.message.value)
return this.renderRawValue(message.value)
} else {
const theme = this.props.theme.palette.type === 'dark' ? 'monokai' : 'bright:inverted'
return <ReactJson
style={{ width: '100%' }}
src={json}
theme={theme}
onEdit={(val) => {
console.log(val)
}} />
const theme = (this.props.theme.palette.type === 'dark') ? 'monokai' : 'bright:inverted'
return (
<ReactJson
style={{ width: '100%' }}
src={json}
theme={theme}
/>
)
}
}
private renderRawValue(value: string) {
const style: React.CSSProperties = {
backgroundColor: 'rgba(80, 80, 80, 0.6)',
wordBreak: 'break-all',
width: '100%',
overflow: 'scroll',
overflowX: 'scroll',
overflowY: 'clip',
display: 'block',
lineHeight: '1.2em',
padding: '12px 5px 12px 5px',
maxWidth: '40vw',
color: this.props.theme.palette.text.primary,
}
return <pre style={style}><code>{value}</code></pre>
+30 -20
View File
@@ -1,14 +1,16 @@
import * as React from 'react'
import { connect } from 'react-redux'
import * as q from '../../../../backend/src/Model'
import { Typography } from '@material-ui/core'
import TreeNode from './TreeNode'
import { makeConnectionMessageEvent, rendererEvents } from '../../../../events'
import { AppState } from '../../reducers'
import TreeNode from './TreeNode'
import { connect } from 'react-redux'
const MovingAverage = require('moving-average')
declare const performance: any
declare const window: any
const timeInterval = 10 * 1000
const average = MovingAverage(timeInterval)
@@ -48,14 +50,16 @@ class Tree extends React.Component<Props, TreeState> {
}
const expectedRenderTime = average.forecast()
const updateInterval = Math.max(expectedRenderTime * 5, 300)
const updateInterval = Math.max(expectedRenderTime * 20, 300)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
this.updateTimer = setTimeout(() => {
this.lastUpdate = performance.now()
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.setState(state)
window.requestAnimationFrame(() => {
this.lastUpdate = performance.now()
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.setState(state)
})
}, Math.max(0, timeUntilNextUpdate))
}
@@ -93,26 +97,32 @@ class Tree extends React.Component<Props, TreeState> {
}
public render() {
console.log('render called')
const style: React.CSSProperties = {
lineHeight: '1.1',
cursor: 'default',
}
return <Typography style={ style }>
const performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
return (
<div style={style}>
<TreeNode
animateChages = { true }
autoExpandLimit = { this.props.autoExpandLimit }
isRoot = { true }
didSelectNode = { this.props.didSelectNode }
treeNode = { this.state.tree }
name = "/"
collapsed = { false }
animateChages={true}
autoExpandLimit={this.props.autoExpandLimit}
isRoot={true}
didSelectNode={this.props.didSelectNode}
treeNode={this.state.tree}
name="/"
collapsed={false}
key="rootNode"
performanceCallback={(ms: number) => {
average.push(Date.now(), ms)
}}
lastUpdate={this.state.tree.lastUpdate}
/>
</Typography>
</div>
)
}
}
+74 -51
View File
@@ -1,4 +1,7 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { treeActions } from '../../actions'
import * as q from '../../../../backend/src/Model'
import { withStyles, Theme } from '@material-ui/core/styles'
@@ -29,6 +32,8 @@ const styles = (theme: Theme) => {
}
interface Props {
actions: any
lastUpdate: number
animateChages: boolean
isRoot?: boolean
treeNode: q.TreeNode
@@ -38,6 +43,7 @@ interface Props {
didSelectNode?: (node: q.TreeNode) => void
classes: any
autoExpandLimit: number
style?: React.CSSProperties
}
interface State {
@@ -48,6 +54,7 @@ class TreeNode extends React.Component<Props, State> {
private dirtySubnodes: boolean = true
private dirtyEdges: boolean = true
private dirtyMessage: boolean = true
private animationDirty: boolean = false
private cssAnimationWasSetAt?: number
@@ -103,6 +110,7 @@ class TreeNode extends React.Component<Props, State> {
|| this.dirtyEdges
|| this.dirtyMessage
|| this.dirtySubnodes
|| this.animationDirty
|| shouldRenderToRemoveCssAnimation
}
@@ -111,6 +119,12 @@ class TreeNode extends React.Component<Props, State> {
const renderTime = performance.now() - this.willUpdateTime
this.props.performanceCallback(renderTime)
}
// setTimeout(() => {
// this.setState(this.state)
// }, 500)
// this.addCssAnimation()
// setTimeout(this.removeCssAnimation, 500)
}
public componentWillUpdate() {
@@ -131,64 +145,73 @@ class TreeNode extends React.Component<Props, State> {
return this.props.treeNode.edgeCount() > this.props.autoExpandLimit
}
// private addCssAnimation = () => {
// const element = this.titleRef.current
// if ((this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes) && element && isElementInViewport(element)) {
// element.style.animation = 'example 0.5s'
// }
// }
//
// private removeCssAnimation = () => {
// const element = this.titleRef.current
// if (element && element.style.animation) {
// element.style.animation = ''
// }
// }
public render() {
const animationStyle = this.indicatingChangeAnimationStyle()
const { classes } = this.props
const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes
this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
return <div
key={this.props.treeNode.hash()}
className={`${classes.node} ${!this.props.isRoot ? classes.hover : ''}`}
onClick={(event) => {
event.stopPropagation()
this.toggle()
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}}
>
<span ref={this.titleRef} style={animationStyle}>
<TreeNodeTitle
onClick={() => this.toggle()}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
didSelectNode={this.props.didSelectNode}
toggleCollapsed={() => this.toggle()}
/>
</span>
{ this.renderNodes() }
</div>
const shouldStartAnimation = (isDirty && !this.animationDirty) && !this.props.isRoot
const animation = shouldStartAnimation ? { animation: 'example 0.5s' } : {}
this.animationDirty = shouldStartAnimation
return (
<div
key={this.props.treeNode.hash()}
className={`${classes.node} ${!this.props.isRoot ? classes.hover : ''}`}
onClick={this.didClickNode}
style={this.props.style}
>
<span ref={this.titleRef} style={animation}>
<TreeNodeTitle
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
lastUpdate={this.props.treeNode.lastUpdate}
/>
</span>
{this.renderNodes()}
</div>
)
}
private didClickNode = (event: React.MouseEvent) => {
event.stopPropagation()
this.toggle()
this.props.actions.selectTopic(this.props.treeNode)
}
private renderNodes() {
return <TreeNodeSubnodes
animateChanges={this.props.animateChages}
collapsed={this.collapsed()}
autoExpandLimit={this.props.autoExpandLimit}
didSelectNode={this.props.didSelectNode}
toggleCollapsed={() => this.toggle()}
treeNode={this.props.treeNode}
/>
}
private indicatingChangeAnimationStyle(): React.CSSProperties {
if (this.props.isRoot) {
return {}
}
if (this.cssAnimationWasSetAt && (performance.now() - this.cssAnimationWasSetAt) > 500) {
this.cssAnimationWasSetAt = undefined
return {}
}
const isInViewPort = this.titleRef.current && isElementInViewport(this.titleRef.current)
const isDirty = this.dirtyMessage || this.dirtyEdges
if (this.props.animateChages && isDirty && isInViewPort) {
if (!this.cssAnimationWasSetAt) {
this.cssAnimationWasSetAt = performance.now()
}
return { animation: 'example 0.5s' }
}
return {}
return (
<TreeNodeSubnodes
animateChanges={this.props.animateChages}
collapsed={this.collapsed()}
autoExpandLimit={this.props.autoExpandLimit}
didSelectNode={this.props.didSelectNode}
treeNode={this.props.treeNode}
lastUpdate={this.props.treeNode.lastUpdate}
/>
)
}
}
export default withStyles(styles)(TreeNode)
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(treeActions, dispatch),
}
}
export default withStyles(styles)(connect(null, mapDispatchToProps)(TreeNode))
+53 -26
View File
@@ -1,52 +1,79 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { withTheme, Theme } from '@material-ui/core/styles'
import { AppState, NodeOrder } from '../../reducers'
import { Theme, withTheme } from '@material-ui/core/styles'
import TreeNode from './TreeNode'
import { connect } from 'react-redux'
export interface Props {
lastUpdate: number
nodeOrder?: NodeOrder
animateChanges: boolean
treeNode: q.TreeNode
autoExpandLimit: number
collapsed?: boolean | undefined
didSelectNode?: (node: q.TreeNode) => void
toggleCollapsed: () => void
theme: Theme
}
class TreeNodeSubnodes extends React.Component<Props, {}> {
private sortedNodes(): q.TreeNode[] {
const { nodeOrder, treeNode } = this.props
let edges = Object.values(treeNode.edges)
if (nodeOrder === NodeOrder.abc) {
edges = edges.sort((a, b) => a.name.localeCompare(b.name))
}
let nodes = edges.map(edge => edge.target)
if (nodeOrder === NodeOrder.messages) {
nodes = nodes.sort((a, b) => b.leafMessageCount() - a.leafMessageCount())
}
if (nodeOrder === NodeOrder.topics) {
nodes = nodes.sort((a, b) => b.leafCount() - a.leafCount())
}
return nodes
}
public render() {
const edges = Object.values(this.props.treeNode.edges)
if (edges.length === 0 || this.props.collapsed) {
return null
}
const listItemStyle = {
padding: '3px 8px 0px 8px',
padding: '3px 0px 0px 8px',
}
if (edges.length > 0 && !this.props.collapsed) {
const listItems = edges
.map(edge => edge.target)
.map(node => (
<div
key={node.hash()}
const nodes = this.sortedNodes()
const listItems = nodes.map(node => (
<div key={node.hash()}>
<TreeNode
animateChages={this.props.animateChanges}
treeNode={node}
didSelectNode={this.props.didSelectNode}
autoExpandLimit={this.props.autoExpandLimit}
lastUpdate={node.lastUpdate}
style={listItemStyle}
>
<TreeNode
animateChages={this.props.animateChanges}
treeNode={node}
didSelectNode={this.props.didSelectNode}
autoExpandLimit={this.props.autoExpandLimit}
/>
</div>
))
/>
</div>
))
return <span
style={{ display: 'block', clear: 'both' }}
>
{this.props.collapsed ? null : listItems}
return (
<span style={{ display: 'block', clear: 'both' }} >
{listItems}
</span>
}
return null
)
}
}
export default withTheme()(TreeNodeSubnodes)
const mapStateToProps = (state: AppState) => {
return {
nodeOrder: state.settings.nodeOrder,
}
}
export default withTheme()(connect(mapStateToProps)(TreeNodeSubnodes))
+27 -26
View File
@@ -1,14 +1,17 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { treeActions } from '../../actions'
import * as q from '../../../../backend/src/Model'
import { withTheme, Theme } from '@material-ui/core/styles'
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
treeNode: q.TreeNode
actions: any
name?: string | undefined
collapsed?: boolean | undefined
toggleCollapsed: () => void
didSelectNode?: (node: q.TreeNode) => void
theme: Theme
lastUpdate: number
}
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
@@ -24,25 +27,23 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
}
}
private didSelectNode = (event: React.MouseEvent) => {
event.stopPropagation()
if (this.props.treeNode.message) {
this.props.actions.selectTopic(this.props.treeNode)
}
}
public render() {
const style: React.CSSProperties = {
lineHeight: '1em',
whiteSpace: 'nowrap',
}
return <span
style={style}
onClick={() => {
this.toggle()
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}}
onMouseOver={() => {
if (this.props.treeNode.message) {
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}
}}
>
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
return (
<span style={style} onMouseOver={this.didSelectNode}>
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
)
}
private renderSourceEdge() {
@@ -58,25 +59,19 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
private renderValue() {
const style: React.CSSProperties = {
width: '15em',
maxWidth: '15em',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '0',
paddingLeft: '5px',
marginLeft: '5px',
display: 'inline-block',
}
return this.props.treeNode.message
? <span
style={style}
> = {this.props.treeNode.message.value.toString()}</span>
? <span style={style}> = {this.props.treeNode.message.value.toString()}</span>
: null
}
private toggle() {
this.props.toggleCollapsed()
}
private renderExpander() {
if (this.props.treeNode.edgeCount() === 0) {
return null
@@ -95,4 +90,10 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
}
}
export default withTheme()(TreeNodeTitle)
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(treeActions, dispatch),
}
}
export default withTheme()(connect(null, mapDispatchToProps)(TreeNodeTitle))
+19 -8
View File
@@ -1,19 +1,24 @@
import './tracking'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
import reducers, { AppState, NodeOrder } from './reducers'
import App from './App'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
import reducers from './reducers'
import App from './App'
declare var document: any
const initialAppState = {
const initialAppState: AppState = {
settings: {
autoExpandLimit: 0,
nodeOrder: NodeOrder.none,
visible: false,
},
sidebar: {},
selectedTopic: undefined,
showUpdateDetails: false,
}
const store = createStore(reducers, initialAppState)
@@ -24,11 +29,17 @@ const theme = createMuiTheme({
typography: { useNextVariants: true },
})
const splash = document.getElementById('splash')
if (splash) {
splash.style.animation = 'unsplash 1s ease-out 0s 1 normal forwards'
setTimeout(() => splash.remove(), 1100)
}
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<Provider store={store}>
<App name="" />
</Provider>
</MuiThemeProvider>,
document.getElementById('example'),
document.getElementById('app'),
)
+85 -10
View File
@@ -1,47 +1,122 @@
import { Reducer, Action } from 'redux'
import * as q from '../../../backend/src/Model'
import { Action, Reducer } from 'redux'
import { trackEvent } from '../tracking'
export enum ActionTypes {
setAutoExpandLimit = 'SET_AUTO_EXPAND_LIMIT',
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY',
setNodeOrder = 'SET_NODE_ORDER',
selectTopic = 'SELECT_TOPIC',
setPublishTopic = 'SET_PUBLISH_TOPIC',
setPublishPayload = 'SET_PUBLISH_PAYLOAD',
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS',
}
interface SettingsAction extends Action {
export interface CustomAction extends Action {
type: ActionTypes,
autoExpandLimit: number
autoExpandLimit?: number
nodeOrder?: NodeOrder
selectedTopic?: q.TreeNode
publishTopic?: string
publishPayload?: string
showUpdateNotification?: boolean
showUpdateDetails?: boolean
}
export interface SidebarState {
publishTopic?: string
publishPayload?: string
}
export interface AppState {
settings: SettingsModel
settings: SettingsState,
selectedTopic?: q.TreeNode
sidebar: SidebarState
showUpdateNotification?: boolean
showUpdateDetails: boolean
}
export interface SettingsModel {
export interface SettingsState {
autoExpandLimit: number
visible: boolean
nodeOrder: NodeOrder
}
const reducer: Reducer<AppState | undefined, SettingsAction> = (state, action) => {
export enum NodeOrder {
none = 'none',
messages = '#messages',
abc = 'abc',
topics = '#topics',
}
const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) => {
if (!state) {
throw Error('No initial state')
}
console.log(action)
trackEvent(action.type)
console.log(action, state)
switch (action.type) {
case ActionTypes.setAutoExpandLimit:
if (action.autoExpandLimit === undefined) {
return state
}
return {
...state,
settings: {
visible: state.settings.visible,
...state.settings,
autoExpandLimit: action.autoExpandLimit,
},
}
case ActionTypes.setPublishTopic:
return {
...state,
sidebar: { ...state.sidebar, publishTopic: action.publishTopic },
}
case ActionTypes.setPublishPayload:
return {
...state,
sidebar: { ...state.sidebar, publishPayload: action.publishPayload },
}
case ActionTypes.toggleSettingsVisibility:
return {
...state,
settings: {
...state.settings,
visible: !state.settings.visible,
autoExpandLimit: state.settings.autoExpandLimit,
},
}
case ActionTypes.selectTopic:
if (!action.selectedTopic) {
return state
}
return {
...state,
selectedTopic: action.selectedTopic,
}
case ActionTypes.setNodeOrder:
if (!action.nodeOrder) {
return state
}
return {
...state,
settings: { ...state.settings, nodeOrder: action.nodeOrder },
}
case ActionTypes.showUpdateNotification:
return {
...state,
showUpdateNotification: action.showUpdateNotification,
}
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
return state
}
return {
...state,
showUpdateDetails: action.showUpdateDetails,
}
default:
return state
}
+21
View File
@@ -0,0 +1,21 @@
let userId = window.localStorage.getItem('userId')
const sha1 = require('sha1')
if (!userId) {
userId = sha1(sha1(Math.random()) + sha1(performance.now()) + sha1(Date.now())).slice(0, 8) as string
window.localStorage.setItem('userId', userId)
}
const Nucleus = require('electron-nucleus')('5c3b3e0443b7cc00eec3782b', {
userId,
disableInDev: true,
})
export default Nucleus
export function trackEvent(name: string) {
if (name.match(/^@@redux/)) {
return
}
Nucleus.track(name)
}
+1
View File
@@ -7,6 +7,7 @@ interface DataSource<DataSourceOptions> {
connect(options: DataSourceOptions): DataSourceStateMachine
disconnect(): void
onMessage(messageCallback: MessageCallback): void
publish(topic: string, payload: any): void
topicSeparator: string
stateMachine: DataSourceStateMachine
}
+4
View File
@@ -69,6 +69,10 @@ export class MqttSource implements DataSource<MqttOptions> {
return this.stateMachine
}
public publish(topic: string, payload: any) {
this.client && this.client.publish(topic, payload)
}
public disconnect() {
this.client && this.client.end()
}
+2
View File
@@ -1,3 +1,5 @@
export interface Message {
value?: any | undefined
length: number
received: Date
}
+60
View File
@@ -0,0 +1,60 @@
interface Lengthwise {
length: number
}
export class RingBuffer<T extends Lengthwise> {
private capacity: number
private maxItems: number
private usage: number = 0
private items: (any)[] = []
private start: number = 0
private end: number = 0
constructor(capacity: number, maxItems = Infinity) {
this.capacity = capacity
this.maxItems = maxItems
}
public toArray() {
return this.items.slice(this.start, this.end)
}
public add(item: T) {
const size = item.length
this.enforceCapacityConstraints(size)
this.usage += size
this.items[this.end] = item
this.end += 1
}
private enforceCapacityConstraints(addedItemSize: number) {
const remainingSize = this.capacity - (this.usage + addedItemSize)
if (remainingSize < 0) {
this.freeSomeSpace(Math.abs(remainingSize))
}
while ((this.end - this.start) >= this.maxItems) {
this.dropFirst()
}
}
private freeSpace(): number {
return this.capacity - this.usage
}
private isEmpty(): boolean {
return this.usage === 0
}
private freeSomeSpace(requiredSpace: number) {
while (this.freeSpace() < requiredSpace && !this.isEmpty()) {
this.dropFirst()
}
}
private dropFirst() {
const freedSpace = this.items[this.start].length
this.usage -= freedSpace
delete this.items[this.start]
this.start += 1
}
}
+14 -4
View File
@@ -1,12 +1,14 @@
import { Edge, Message } from './'
import { Edge, Message, RingBuffer } from './'
import { EventDispatcher } from '../../../events'
export class TreeNode {
public sourceEdge?: Edge
public message?: Message
public messageHistory: RingBuffer<Message> = new RingBuffer<Message>(3000, 100)
public edges: {[s: string]: Edge} = {}
public collapsed = false
public messages: number = 0
public lastUpdate: number = Date.now()
public onMerge = new EventDispatcher<void, TreeNode>(this)
public onEdgesChange = new EventDispatcher<void, TreeNode>(this)
@@ -20,15 +22,23 @@ export class TreeNode {
sourceEdge.target = this
}
this.setMessage(message)
message && this.setMessage(message)
this.onMerge.subscribe(() => {
this.cachedLeafes = undefined
this.cachedLeafMessageCount = undefined
this.lastUpdate = Date.now()
})
this.onEdgesChange.subscribe(() => {
this.lastUpdate = Date.now()
})
this.onMessage.subscribe(() => {
this.lastUpdate = Date.now()
})
}
public setMessage(value: any) {
this.message = value
public setMessage(message: Message) {
this.messageHistory.add(message)
this.message = message
this.messages += 1
}
+11 -3
View File
@@ -1,7 +1,11 @@
import { Edge, Message, Tree, TreeNode } from './'
import { Edge, Tree, TreeNode } from './'
interface HasLength {
length: number
}
export abstract class TreeNodeFactory {
public static fromEdgesAndValue(edgeNames: string[], value: any): TreeNode {
public static fromEdgesAndValue<T extends HasLength>(edgeNames: string[], value: T): TreeNode {
let currentNode: TreeNode = new Tree()
for (const edgeName of edgeNames) {
const edge = new Edge(edgeName)
@@ -11,7 +15,11 @@ export abstract class TreeNodeFactory {
currentNode = newNode
}
currentNode.setMessage({ value })
currentNode.setMessage({
value,
length: value.length,
received: new Date(),
})
return currentNode
}
}
+1
View File
@@ -4,3 +4,4 @@ export { Message } from './Message'
export { TreeNodeFactory } from './TreeNodeFactory'
export { Tree } from './Tree'
export { Hashable } from './Hashable'
export * from './RingBuffer'
+46
View File
@@ -0,0 +1,46 @@
import { RingBuffer } from '../'
import { expect } from 'chai'
import 'mocha'
describe('RingBuffer', () => {
it('should add a new value', () => {
const b = new RingBuffer(30)
b.add('hello')
expect(b.toArray()[0]).to.eq('hello')
})
it('should add a new value after the first', () => {
const b = new RingBuffer(30)
b.add('hello')
b.add('world')
expect(b.toArray()[1]).to.eq('world')
})
it('should remove old values if buffer size is reached', () => {
const b = new RingBuffer(6)
b.add('hello')
b.add('world')
expect(b.toArray()[0]).to.eq('world')
})
it('items bigger then the buffer should be stored anyway', () => {
const b = new RingBuffer(3)
b.add('hello')
expect(b.toArray()[0]).to.eq('hello')
b.add('world')
expect(b.toArray()[0]).to.eq('world')
})
it('max item count should be respected', () => {
const b = new RingBuffer(100, 3)
b.add('a')
b.add('b')
b.add('c')
b.add('d')
b.add('e')
expect(b.toArray().length).to.eq(3)
expect(b.toArray()[0]).to.eq('c')
expect(b.toArray()[1]).to.eq('d')
expect(b.toArray()[2]).to.eq('e')
})
})
+31 -7
View File
@@ -1,11 +1,21 @@
import {
addMqttConnectionEvent, backendEvents,
makeConnectionStateEvent, removeConnection,
makeConnectionMessageEvent, AddMqttConnection,
AddMqttConnection,
EventDispatcher,
Message,
addMqttConnectionEvent,
backendEvents,
checkForUpdates,
makeConnectionMessageEvent,
makeConnectionStateEvent,
makePublishEvent,
removeConnection,
updateAvailable,
} from '../../events'
import { MqttSource, DataSource } from './DataSource'
import { DataSource, MqttSource } from './DataSource'
class ConnectionManager {
import { UpdateInfo } from 'builder-util-runtime'
export class ConnectionManager {
private connections: {[s: string]: DataSource<any>} = {}
public manageConnections() {
@@ -26,6 +36,9 @@ class ConnectionManager {
connection.connect(options)
this.handleNewMessagesForConnection(connectionId, connection)
backendEvents.subscribe(makePublishEvent(connectionId), (msg: Message) => {
this.connections[connectionId].publish(msg.topic, msg.payload)
})
}
private handleNewMessagesForConnection(connectionId: string, connection: MqttSource) {
@@ -46,5 +59,16 @@ class ConnectionManager {
}
}
const connectionManager = new ConnectionManager()
connectionManager.manageConnections()
class UpdateNotifier {
public onCheckUpdateRequest = new EventDispatcher<void, UpdateNotifier>(this)
constructor() {
backendEvents.subscribe(checkForUpdates, () => {
this.onCheckUpdateRequest.dispatch()
})
}
public notify(updateInfo: UpdateInfo) {
backendEvents.emit(updateAvailable, updateInfo)
}
}
export const updateNotifier = new UpdateNotifier()
+24 -8
View File
@@ -1,17 +1,15 @@
const { app, BrowserWindow } = require('electron')
const { autoUpdater } = require("electron-updater")
const log = require('electron-log');
const { ConnectionManager, updateNotifier } = require('./backend/build/backend/src/index.js')
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
log.info('App starting...');
// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
try {
require('./backend/build/backend/src/index.js')
} catch (err) {
console.error(err)
}
const connectionManager = new ConnectionManager()
connectionManager.manageConnections()
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
@@ -47,7 +45,25 @@ function createWindow () {
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
createWindow()
autoUpdater.checkForUpdatesAndNotify()
let updateInfo
autoUpdater.on('update-available', (info) => {
updateInfo = info
})
autoUpdater.on('error', () => {
if (updateInfo) {
updateNotifier.notify(updateInfo)
}
})
updateNotifier.onCheckUpdateRequest.subscribe(() => {
try {
autoUpdater.checkForUpdatesAndNotify()
} catch (error) {
console.error(error)
}
})
})
// Quit when all windows are closed.
+2 -2
View File
@@ -1,5 +1,6 @@
import { IpcMain, IpcRenderer, ipcMain, ipcRenderer } from 'electron'
import { Event } from './Events'
import { ipcMain, ipcRenderer, IpcRenderer, IpcMain } from 'electron'
interface EventBusInterface {
subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void): void
@@ -46,7 +47,6 @@ class IpcRendererEventBus implements EventBusInterface {
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
console.log(event.topic, msg)
this.ipc.send(event.topic, msg)
}
}
+20 -2
View File
@@ -1,4 +1,8 @@
import { MqttOptions, DataSourceState } from '../backend/src/DataSource'
import { DataSourceState, MqttOptions } from '../backend/src/DataSource'
import { UpdateInfo } from 'builder-util-runtime'
export { UpdateInfo } from 'builder-util-runtime'
export interface Event<MessageType> {
topic: string
@@ -23,11 +27,25 @@ export function makeConnectionStateEvent(connectionId: string): Event<DataSource
}
}
interface Message {
export const checkForUpdates: Event<void> = {
topic: 'app/update/check',
}
export const updateAvailable: Event<UpdateInfo> = {
topic: 'app/update/available',
}
export interface Message {
topic: string,
payload: any
}
export function makePublishEvent(connectionId: string): Event<Message> {
return {
topic: `conn/publish/${connectionId}`,
}
}
export function makeConnectionMessageEvent(connectionId: string): Event<Message> {
return {
topic: `conn/${connectionId}`,
+45 -2
View File
@@ -621,6 +621,11 @@
}
}
},
"brace": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz",
"integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1083,6 +1088,11 @@
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
"diff-match-patch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz",
"integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg=="
},
"dmg-builder": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-6.5.3.tgz",
@@ -2160,8 +2170,7 @@
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
"dev": true
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-yaml": {
"version": "3.12.0",
@@ -2277,11 +2286,24 @@
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"loud-rejection": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
@@ -4078,6 +4100,15 @@
"through2": "~0.2.3"
}
},
"prop-types": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
"requires": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
}
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -4140,6 +4171,18 @@
"strip-json-comments": "~2.0.1"
}
},
"react-ace": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-6.3.2.tgz",
"integrity": "sha512-eSk0fWvrBe2oqYIYX0njLddLG5H0hemWv5VVoQi5yDSPTjGlSSnzFwdgPyfuwRe8mSARZuRdprPQa5p61hKirw==",
"requires": {
"brace": "^0.11.1",
"diff-match-patch": "^1.0.4",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"prop-types": "^15.6.2"
}
},
"read-config-file": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-3.2.0.tgz",
+10 -12
View File
@@ -1,14 +1,16 @@
{
"name": "MQ(TT)-Explorer",
"version": "0.0.1",
"name": "MQTT-Explorer",
"version": "0.0.3",
"description": "Explore your message queues",
"main": "electron.js",
"scripts": {
"start": "electron .",
"install": "cd app; npm install; cd ..",
"test": "npm run test-backend",
"build": "cd app; npm run build; cd ..; cd backend; npm run build; cd ..",
"test-backend": "cd backend && npm run test && cd ..",
"release": "npm run test && ./release.sh"
"prepare-release": "./prepare-release.sh",
"package": "ts-node package.ts"
},
"repository": {
"type": "git",
@@ -16,12 +18,11 @@
},
"build": {
"appId": "mqtt-explorer",
"nodeGypRebuild": false,
"mac": {
"category": "de.t7n.apps.mq-explorer",
"files": [
"**/*",
"!**/*.ts",
"!app/node_modules"
"category": "de.t7n.apps.mqtt-explorer",
"publish": [
"github"
]
},
"linux": {
@@ -58,12 +59,9 @@
"tslint-strict-null-checks": "^1.0.1"
},
"dependencies": {
"@types/electron": "^1.6.10",
"@types/socket.io": "^2.1.2",
"electron-log": "^2.2.17",
"electron-updater": "^4.0.6",
"mqtt": "^2.18.8",
"sha1": "^1.1.1",
"socket.io": "^2.2.0"
"sha1": "^1.1.1"
}
}
+16 -5
View File
@@ -5,7 +5,7 @@ const linux: builder.CliOptions = {
ia32: true,
armv7l: true,
arm64: true,
linux: ['snap', 'AppImage', 'deb', 'pacman'],
linux: ['AppImage', 'deb', 'snap'],
projectDir: './build/clean',
publish: 'onTag',
}
@@ -15,7 +15,7 @@ const win: builder.CliOptions = {
ia32: true,
armv7l: false,
arm64: false,
win: ['portable'],
win: ['portable', 'nsis'],
projectDir: './build/clean',
publish: 'onTag',
}
@@ -31,9 +31,20 @@ const mac: builder.CliOptions = {
}
async function buildAll() {
await builder.build(linux)
await builder.build(mac)
await builder.build(win)
console.log(process.argv[2])
switch (process.argv[2]) {
case 'win':
await builder.build(win)
break
case 'linux':
await builder.build(linux)
break
case 'mac':
await builder.build(mac)
break
default:
await builder.build(mac)
}
}
buildAll()
+22 -9
View File
@@ -1,24 +1,37 @@
#!/bin/bash
DIR=build/clean
rm -rf "$DIR"
mkdir -p "$DIR"
git clone .git "$DIR"
set -e
ORIGINAL_DIR=`pwd`
DIR=build/clean
rm -rf "$DIR" || echo "Directory did not exist"
mkdir -p "$DIR"
git clone .git "$DIR"
cd $DIR
cd app && npm install; cd ..
cd backend && npm install; cd ..
# App
cd app
npm install
cd ..
# Backend
cd backend
npm install;
#npm run test
cd ..
# Build
npm run build
rm -rf app/node_modules
cd "$ORIGINAL_DIR"
node_modules/.bin/ts-node build.ts
exit 0
docker run --rm -ti \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
--env GH_TOKEN="$GH_TOKEN" \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
+3 -2
View File
@@ -1,11 +1,12 @@
{
"extends": "tslint-config-airbnb",
"extends": ["tslint-config-airbnb", "tslint-react"],
"rules": {
"semicolon": [true, "never"],
"max-line-length": [true, 200],
"member-access": true,
"no-else-after-return": false,
"align": false,
"variable-name": false
"variable-name": false,
"jsx-no-lambda": false
}
}