Merge branch 'master' into HEAD

This commit is contained in:
Thomas Nordquist
2022-02-27 20:47:49 +01:00
63 changed files with 6212 additions and 8646 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" /> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
@@ -21,12 +22,15 @@
0% { 0% {
background-color: none; background-color: none;
} }
25% { 25% {
background-color: #595585; background-color: #595585;
} }
50% { 50% {
background-color: #595585; background-color: #595585;
} }
100% { 100% {
background-color: none; background-color: none;
} }
@@ -37,12 +41,15 @@
background-color: none; background-color: none;
color: inherit; color: inherit;
} }
25% { 25% {
background-color: #c0c8c0; background-color: #c0c8c0;
} }
50% { 50% {
background-color: #c0c8c0; background-color: #c0c8c0;
} }
100% { 100% {
background-color: none; background-color: none;
color: inherit; color: inherit;
@@ -139,6 +146,7 @@
.Resizer.disabled { .Resizer.disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.Resizer.disabled:hover { .Resizer.disabled:hover {
border-color: transparent; border-color: transparent;
} }
@@ -146,19 +154,26 @@
.example-enter { .example-enter {
opacity: 0; opacity: 0;
} }
.example-enter-active { .example-enter-active {
opacity: 1; opacity: 1;
transition: opacity 300ms ease-in; transition: opacity 300ms ease-in;
} }
.example-exit { .example-exit {
opacity: 1; opacity: 1;
} }
.example-exit-active { .example-exit-active {
opacity: 0; opacity: 0;
transition: opacity 300ms ease-in; transition: opacity 300ms ease-in;
} }
</style> </style>
<script>
global = globalThis //<- this should be enough
</script>
</head> </head>
<body> <body>
<div id="app" style="font: -webkit-control;"></div> <div id="app" style="font: -webkit-control;"></div>
<script> <script>
@@ -178,4 +193,5 @@
<script src="<%- file %>"></script> <script src="<%- file %>"></script>
<% }); %> <% }); %>
</body> </body>
</html> </html>

View File

@@ -4,29 +4,30 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "yarn rebuild && webpack --mode production", "build": "webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress", "dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=7.1.1 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -",
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest", "test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest",
"mochatest": "mocha --require ts-node/register src/**/*.spec.ts" "mochatest": "mocha --require ts-node/register src/**/*.spec.ts"
}, },
"engines": {
"node": "16"
},
"author": "", "author": "",
"license": "CC-BY-ND-4.0", "license": "CC-BY-ND-4.0",
"dependencies": { "dependencies": {
"@material-ui/core": "^4", "@material-ui/core": "4.12",
"@material-ui/icons": "^4", "@material-ui/icons": "^4",
"@material-ui/lab": "^4.0.0-alpha", "@material-ui/lab": "^4.0.0-alpha",
"@material-ui/styles": "^4", "@material-ui/styles": "4.11",
"@types/react-transition-group": "^4", "@types/react-transition-group": "^4",
"ace-builds": "^1.4.11", "ace-builds": "^1.4.11",
"axios": "^0.19.0", "axios": "^0.26.0",
"compare-versions": "^3.5.0", "compare-versions": "^3.5.0",
"copy-text-to-clipboard": "^2.1.0", "copy-text-to-clipboard": "^2.1.0",
"d3": "^5.9.7", "d3": "^5.9.7",
"d3-shape": "^1.3.5", "d3-shape": "^1.3.5",
"diff": "^4.0.1", "diff": "^4.0.1",
"dot-prop": "^5.0.0", "dot-prop": "^5.0.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"file-loader": "6", "file-loader": "6",
"get-value": "^3.0.1", "get-value": "^3.0.1",
"immutable": "^4.0.0-rc.12", "immutable": "^4.0.0-rc.12",
@@ -35,7 +36,6 @@
"json-to-ast": "^2.1.0", "json-to-ast": "^2.1.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"moment": "^2.24.0",
"moving-average": "^1.0.0", "moving-average": "^1.0.0",
"number-abbreviate": "^2.0.0", "number-abbreviate": "^2.0.0",
"parse-duration": "^0.1.1", "parse-duration": "^0.1.1",
@@ -56,6 +56,7 @@
"uuid": "7" "uuid": "7"
}, },
"devDependencies": { "devDependencies": {
"@babel/runtime": "^7.17.2",
"@types/d3": "^5.7.2", "@types/d3": "^5.7.2",
"@types/diff": "^4.0.1", "@types/diff": "^4.0.1",
"@types/get-value": "^3.0.1", "@types/get-value": "^3.0.1",
@@ -69,24 +70,24 @@
"@types/socket.io-client": "^1.4.32", "@types/socket.io-client": "^1.4.32",
"@types/uuid": "^7.0.2", "@types/uuid": "^7.0.2",
"@types/vis": "^4.21.9", "@types/vis": "^4.21.9",
"awesome-typescript-loader": "^5.2.1",
"chai": "^4.2.0", "chai": "^4.2.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"css-loader": "^3.0.0", "css-loader": "^3.0.0",
"hard-source-webpack-plugin": "^0.13.1", "html-webpack-plugin": "^5.5.0",
"heapdump": "^0.3.12", "lodash": "^4.17.21",
"html-webpack-plugin": "^4.0.0-beta.5", "mocha": "^9.2.1",
"mocha": "^7.1.1", "moment": "^2.29.1",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"style-loader": "^1", "style-loader": "^1",
"typescript": "^3.6.3", "ts-loader": "^9.2.6",
"webpack": "^4.28.2", "typescript": "^4.5.5",
"webpack-bundle-analyzer": "^3.0.3", "webpack": "^5.69.1",
"webpack-cli": "^3.3.6", "webpack-bundle-analyzer": "^4.5.0",
"webpack-dev-server": "^3.1.14" "webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
}, },
"peerDependencies": { "peerDependencies": {
"electron": "^5.0.5" "electron": "^17"
} }
} }

View File

@@ -9,12 +9,13 @@ import {
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage' import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { showError } from './Global' import { showError } from './Global'
import { remote } from 'electron'
import { promises as fsPromise } from 'fs' import { promises as fsPromise } from 'fs'
import * as path from 'path' import * as path from 'path'
import { ActionTypes, Action } from '../reducers/ConnectionManager' import { ActionTypes, Action } from '../reducers/ConnectionManager'
import { Subscription } from '../../../backend/src/DataSource/MqttSource' import { Subscription } from '../../../backend/src/DataSource/MqttSource'
import { connectionsMigrator } from './migrations/Connection' import { connectionsMigrator } from './migrations/Connection'
import { rendererRpc } from '../../../events'
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
export interface ConnectionDictionary { export interface ConnectionDictionary {
[s: string]: ConnectionOptions [s: string]: ConnectionOptions
@@ -72,7 +73,7 @@ async function openCertificate(): Promise<CertificateParameters> {
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.', certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
} }
const openDialogReturnValue = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), { const openDialogReturnValue = await rendererRpc.call(makeOpenDialogRpc(), {
properties: ['openFile'], properties: ['openFile'],
securityScopedBookmarks: true, securityScopedBookmarks: true,
}) })
@@ -83,7 +84,7 @@ async function openCertificate(): Promise<CertificateParameters> {
} }
const data = await fsPromise.readFile(selectedFile) const data = await fsPromise.readFile(selectedFile)
if (data.length > 16_384 || data.length < 128) { if (data.length > 16_384 || data.length < 64) {
throw rejectReasons.certificateSizeDoesNotMatch throw rejectReasons.certificateSizeDoesNotMatch
} }

View File

@@ -1,7 +1,7 @@
import { ActionTypes, ConfirmationRequest } from '../reducers/Global' import { ActionTypes, ConfirmationRequest } from '../reducers/Global'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
export const showError = (error?: string) => ({ export const showError = (error?: string | unknown) => ({
error, error,
type: ActionTypes.showError, type: ActionTypes.showError,
}) })

View File

@@ -13,6 +13,8 @@ import { connect } from 'react-redux'
import { globalActions, settingsActions } from '../actions' import { globalActions, settingsActions } from '../actions'
import { Theme, withStyles } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'
(window as any).global = window
const Settings = React.lazy(() => import('./SettingsDrawer/Settings')) const Settings = React.lazy(() => import('./SettingsDrawer/Settings'))
const ContentView = React.lazy(() => import('./Layout/ContentView')) const ContentView = React.lazy(() => import('./Layout/ContentView'))
@@ -66,6 +68,8 @@ class App extends React.PureComponent<Props, {}> {
return null return null
} }
const anyProps: any = {};
return ( return (
<div className={centerContent}> <div className={centerContent}>
<CssBaseline /> <CssBaseline />
@@ -73,7 +77,7 @@ class App extends React.PureComponent<Props, {}> {
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} /> <ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
{this.renderNotification()} {this.renderNotification()}
<React.Suspense fallback={<div></div>}> <React.Suspense fallback={<div></div>}>
<Settings /> <Settings {...anyProps} />
</React.Suspense> </React.Suspense>
<div className={centerContent}> <div className={centerContent}>
<div className={`${settingsVisible ? contentShift : content}`}> <div className={`${settingsVisible ? contentShift : content}`}>

View File

@@ -1,7 +1,5 @@
import * as React from 'react' import * as React from 'react'
import Add from '@material-ui/icons/Add'
import ClearAdornment from '../helper/ClearAdornment' import ClearAdornment from '../helper/ClearAdornment'
import Delete from '@material-ui/icons/Delete'
import Lock from '@material-ui/icons/Lock' import Lock from '@material-ui/icons/Lock'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { Button, Theme, Tooltip, Typography } from '@material-ui/core' import { Button, Theme, Tooltip, Typography } from '@material-ui/core'

View File

@@ -5,7 +5,7 @@ let heapdump: any
function writeHeapdump(path?: string) { function writeHeapdump(path?: string) {
if (!heapdump) { if (!heapdump) {
heapdump = require('heapdump') //<heapdump = require('heapdump')
} }
heapdump.writeSnapshot(path || `${Date.now()}.heapsnapshot`) heapdump.writeSnapshot(path || `${Date.now()}.heapsnapshot`)

View File

@@ -2,7 +2,6 @@ import * as React from 'react'
import PersistentStorage from '../utils/PersistentStorage' import PersistentStorage from '../utils/PersistentStorage'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied' import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import Warning from '@material-ui/icons/Warning' import Warning from '@material-ui/icons/Warning'
import { electronRendererTelemetry } from 'electron-telemetry'
import { Theme, withStyles } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'
import { Button, Modal, Paper, Toolbar, Typography } from '@material-ui/core' import { Button, Modal, Paper, Toolbar, Typography } from '@material-ui/core'
@@ -33,7 +32,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
} }
public componentDidCatch(error: Error, errorInfo: any) { public componentDidCatch(error: Error, errorInfo: any) {
electronRendererTelemetry.trackError(error) // electronRendererTelemetry.trackError(error)
console.log('did catch', error) console.log('did catch', error)
} }

View File

@@ -3,10 +3,17 @@ import DateFormatter from '../helper/DateFormatter'
import { AppState } from '../../reducers' import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Input, InputLabel, MenuItem, Select, StyleRulesCallback, Theme } from '@material-ui/core' import { Input, InputLabel, MenuItem, Select, Theme } from '@material-ui/core'
import { settingsActions } from '../../actions' import { settingsActions } from '../../actions'
import { withStyles } from '@material-ui/styles' import { withStyles } from '@material-ui/styles'
import * as moment from 'moment'
function importAll(r: any) {
r.keys().forEach(r);
}
// @ts-expect-error -- webpack require
importAll(require.context('moment/locale', true, /\.js$/));
const moment = require('moment')
interface Props { interface Props {
actions: { actions: {

View File

@@ -122,7 +122,7 @@ const EditorMode = memo(function EditorMode(props: {
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ') const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
updatePayload(str) updatePayload(str)
} catch (error) { } catch (error) {
props.globalActions.showError(`Format error: ${error.message}`) props.globalActions.showError(`Format error: ${(error as Error)?.message}`)
} }
} }
}, [props.payload]) }, [props.payload])

View File

@@ -13,6 +13,7 @@ import { Theme, withStyles } from '@material-ui/core/styles'
import { updateNotifierActions } from '../actions' import { updateNotifierActions } from '../actions'
import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@material-ui/core' import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@material-ui/core'
import { rendererRpc, getAppVersion } from '../../../events'
interface Props { interface Props {
showUpdateNotification: boolean showUpdateNotification: boolean
@@ -50,8 +51,12 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
super(props) super(props)
this.state = { newerVersions: [] } this.state = { newerVersions: [] }
const ownVersion = electron.remote.app.getVersion() this.checkForUpdates()
this.fetchReleases().then(releases => { }
private async checkForUpdates() {
const ownVersion = await rendererRpc.call(getAppVersion, undefined, 10000);
const releases = await this.fetchReleases();
const newerVersions = releases const newerVersions = releases
.filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion)) .filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion))
.filter(release => compareVersions(release.tag_name, ownVersion) > 0) .filter(release => compareVersions(release.tag_name, ownVersion) > 0)
@@ -61,7 +66,6 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
this.setState({ newerVersions }) this.setState({ newerVersions })
this.props.actions.showUpdateNotification(true) this.props.actions.showUpdateNotification(true)
} }
})
} }
private allowPrereleaseIfOwnVersionIsBeta(release: GithubRelease, ownVersion: string) { private allowPrereleaseIfOwnVersionIsBeta(release: GithubRelease, ownVersion: string) {

View File

@@ -1,12 +1,9 @@
import { rendererEvents } from '../../../events' import { rendererRpc } from '../../../events'
import { v4 } from 'uuid'
import { import {
storageStoreEvent, storageStoreEvent,
makeStorageResponseEvent,
storageLoadEvent, storageLoadEvent,
storageClearEvent, storageClearEvent,
makeStorageAcknowledgementEvent,
} from '../../../events/StorageEvents' } from '../../../events/StorageEvents'
export interface StorageIdentifier<Model> { export interface StorageIdentifier<Model> {
@@ -20,71 +17,23 @@ export interface PersistentStorage {
} }
class RemoteStorage implements PersistentStorage { class RemoteStorage implements PersistentStorage {
private timeoutCallback(event: any, callback: any, reject: any) {
setTimeout(() => {
reject('remote storage timeout')
rendererEvents.unsubscribe(event, callback)
}, 10000)
}
private expectAck(transactionId: string): Promise<void> {
const ack = makeStorageAcknowledgementEvent(transactionId)
return new Promise<void>((resolve, reject) => {
const callback = (msg: any) => {
if (msg && msg.error) {
reject(msg.error)
} else {
resolve()
}
rendererEvents.unsubscribe(ack, callback)
}
rendererEvents.subscribe(ack, callback)
this.timeoutCallback(ack, callback, reject)
})
}
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> { public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
const transactionId = v4() return rendererRpc.call(storageStoreEvent, {
const expectation = this.expectAck(transactionId)
rendererEvents.emit(storageStoreEvent, {
data, data,
transactionId,
store: identifier.id, store: identifier.id,
}) })
return expectation
} }
public load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined> { public async load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined> {
const transactionId = v4() const result = await rendererRpc.call(storageLoadEvent, {
const responseEvent = makeStorageResponseEvent(transactionId)
const promise = new Promise<Model>((resolve, reject) => {
const callback = (msg: any) => {
if (msg.error) {
reject(msg.error)
} else {
resolve(msg.data)
}
rendererEvents.unsubscribe(responseEvent, callback)
}
rendererEvents.subscribe(responseEvent, callback)
this.timeoutCallback(responseEvent, callback, reject)
})
rendererEvents.emit(storageLoadEvent, {
transactionId,
store: identifier.id, store: identifier.id,
}) }, 10000)
return promise return (result as any).data
} }
public clear(): Promise<void> { public clear(): Promise<void> {
const transactionId = v4() return rendererRpc.call(storageClearEvent, undefined, 10000)
const expectation = this.expectAck(transactionId)
rendererEvents.emit(storageClearEvent, { transactionId })
return expectation
} }
} }

View File

@@ -1,4 +1,4 @@
import { electronRendererTelemetry } from 'electron-telemetry' // import { electronRendererTelemetry } from 'electron-telemetry'
const telemetry = electronRendererTelemetry // const telemetry = electronRendererTelemetry
electronRendererTelemetry.registerErrorHandler() // electronRendererTelemetry.registerErrorHandler()

View File

@@ -1,27 +1,27 @@
import { electronRendererTelemetry } from 'electron-telemetry' // import { electronRendererTelemetry } from 'electron-telemetry'
// Used to determine long-time-stability and memory leaks // Used to determine long-time-stability and memory leaks
function trackProcessStatistics() { // function trackProcessStatistics() {
setInterval(() => { // setInterval(() => {
try { // try {
electronRendererTelemetry.trackCustomEvent({ // electronRendererTelemetry.trackCustomEvent({
name: 'heapStatistics', // name: 'heapStatistics',
payload: process.getHeapStatistics(), // payload: process.getHeapStatistics(),
}) // })
electronRendererTelemetry.trackCustomEvent({ // electronRendererTelemetry.trackCustomEvent({
name: 'cpuUsage', // name: 'cpuUsage',
payload: process.getCPUUsage(), // payload: process.getCPUUsage(),
}) // })
electronRendererTelemetry.trackCustomEvent({ // electronRendererTelemetry.trackCustomEvent({
name: 'runningSince', // name: 'runningSince',
payload: performance.now(), // payload: performance.now(),
}) // })
} catch (error) { // } catch (error) {
console.error(error) // console.error(error)
} // }
}, 60 * 1000) // }, 60 * 1000)
} // }
trackProcessStatistics() // trackProcessStatistics()
// Log reducer event names to determine what functionality is used and how to reproduce reported errors // Log reducer event names to determine what functionality is used and how to reproduce reported errors
export function trackEvent(name: string) { export function trackEvent(name: string) {
@@ -30,6 +30,6 @@ export function trackEvent(name: string) {
} }
const blacklist = ['CONNECTION_SET_HEALTH'] const blacklist = ['CONNECTION_SET_HEALTH']
if (blacklist.indexOf(name) === -1) { if (blacklist.indexOf(name) === -1) {
electronRendererTelemetry.trackEvent(name) // electronRendererTelemetry.trackEvent(name)
} }
} }

View File

@@ -4,18 +4,30 @@
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true, "strictNullChecks": true,
"strict": true, "strict": true,
"lib": ["es2017", "dom"], "lib": [
"es2017",
"dom"
],
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "./build/", "outDir": "./build/",
"sourceMap": true, "sourceMap": true,
"module": "esnext", "module": "esnext",
"target": "es2017", "target": "es2017",
"jsx": "react", "jsx": "react",
"types": ["react"], "types": [
"allowSyntheticDefaultImports": true "react"
],
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
}, },
"include": ["./src/**/*"], "include": [
"exclude": ["**/*.d.ts", ".src/**/*.png"], "./src/**/*"
],
"exclude": [
"**/*.d.ts",
".src/**/*.png",
"./node_modules"
],
"awesomeTypescriptLoaderOptions": { "awesomeTypescriptLoaderOptions": {
"useCache": true, "useCache": true,
"transpileModule": true, "transpileModule": true,

View File

@@ -1,7 +1,6 @@
// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); const webpack = require('webpack');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = { module.exports = {
entry: { entry: {
@@ -18,12 +17,11 @@ module.exports = {
splitChunks: { splitChunks: {
chunks: 'all', chunks: 'all',
minSize: 30000, minSize: 30000,
maxSize: 0,
minChunks: 1, minChunks: 1,
maxAsyncRequests: 5, maxAsyncRequests: 5,
maxInitialRequests: 3, maxInitialRequests: 3,
automaticNameDelimiter: '~', automaticNameDelimiter: '~',
name: true, // name: true,
cacheGroups: { cacheGroups: {
vendors: { vendors: {
test: /[\\/]node_modules[\\/](react|react-dom|@material-ui|popper\.js|react|react-redux|prop-types|jss|redux|scheduler|react-transition-group)[\\/]/, test: /[\\/]node_modules[\\/](react|react-dom|@material-ui|popper\.js|react|react-redux|prop-types|jss|redux|scheduler|react-transition-group)[\\/]/,
@@ -32,6 +30,7 @@ module.exports = {
priority: -10, priority: -10,
}, },
default: { default: {
name: 'default',
minChunks: 2, minChunks: 2,
priority: -20, priority: -20,
reuseExistingChunk: true, reuseExistingChunk: true,
@@ -55,13 +54,7 @@ module.exports = {
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
loader: 'awesome-typescript-loader', loader: 'ts-loader',
options: {
reportFiles: [
"src/**/*.{ts,tsx}",
"../backend/src/**/*.{ts,tsx}"
]
}
}, },
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
@@ -78,15 +71,6 @@ module.exports = {
}, },
], ],
}, },
{
test: /\.node$/,
use: {
loader: 'node-loader',
options: {
modules: true,
}
}
},
{ {
test: /\.proto$/, test: /\.proto$/,
use: [ use: [
@@ -98,18 +82,26 @@ module.exports = {
}, },
] ]
}, },
// {
// test: /\.node$/,
// use: {
// loader: 'node-loader',
// options: {
// modules: true,
// }
// }
// },
], ],
}, },
// node: { global: true },
plugins: [ plugins: [
new HtmlWebpackPlugin({ template: './index.html', file: './build/index.html', inject: false }), new HtmlWebpackPlugin({ template: './index.html', file: './build/index.html', inject: false }),
// new BundleAnalyzerPlugin(), // new BundleAnalyzerPlugin(),
new HardSourceWebpackPlugin(),
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.IgnorePlugin({ // new webpack.IgnorePlugin({
resourceRegExp: /\.\/build\/Debug\/addon/, // resourceRegExp: /\.\/build\/Debug\/addon/,
contextRegExp: /heapdump$/ // contextRegExp: /heapdump$/
}), // }),
], ],
// When importing a module whose path matches one of the following, just // When importing a module whose path matches one of the following, just

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ cache:
- '%LOCALAPPDATA%\electron-builder\cache' - '%LOCALAPPDATA%\electron-builder\cache'
install: install:
- ps: Install-Product node 10 - ps: Install-Product node 16
build_script: build_script:
- yarn - yarn

View File

@@ -11,6 +11,9 @@
"debug": "ts-node --inspect ./src/index.ts", "debug": "ts-node --inspect ./src/index.ts",
"postinstall": "yarn build" "postinstall": "yarn build"
}, },
"engines": {
"node": "16"
},
"author": "", "author": "",
"license": "CC-BY-ND-4.0", "license": "CC-BY-ND-4.0",
"nyc": { "nyc": {

View File

@@ -2,10 +2,8 @@ import * as FileAsync from 'lowdb/adapters/FileAsync'
import * as fs from 'fs-extra' import * as fs from 'fs-extra'
import * as lowdb from 'lowdb' import * as lowdb from 'lowdb'
import * as path from 'path' import * as path from 'path'
import { backendEvents } from '../../events' import { backendRpc } from '../../events'
import { import {
makeStorageAcknowledgementEvent,
makeStorageResponseEvent,
storageClearEvent, storageClearEvent,
storageLoadEvent, storageLoadEvent,
storageStoreEvent, storageStoreEvent,
@@ -32,57 +30,27 @@ export default class ConfigStorage {
} }
public async init() { public async init() {
backendEvents.subscribe(storageStoreEvent, async event => { backendRpc.on(storageStoreEvent, async event => {
const ack = makeStorageAcknowledgementEvent(event.transactionId)
try {
const db = await this.getDb() const db = await this.getDb()
await db.set(event.store, event.data).write() await db.set(event.store, event.data).write()
backendEvents.emit(ack, undefined) return
} catch (error) {
backendEvents.emit(ack, {
error,
transactionId: event.transactionId,
store: event.store,
})
throw error
}
}) })
backendEvents.subscribe(storageLoadEvent, async event => { backendRpc.on(storageLoadEvent, async event => {
const responseEvent = makeStorageResponseEvent(event.transactionId)
try {
const db = await this.getDb() const db = await this.getDb()
const data = await db.get(event.store).value() const data = await db.get(event.store).value()
backendEvents.emit(responseEvent, { return {
data, data,
transactionId: event.transactionId,
store: event.store, store: event.store,
})
} catch (error) {
backendEvents.emit(responseEvent, {
error,
transactionId: event.transactionId,
store: event.store,
})
throw error
} }
}) })
backendEvents.subscribe(storageClearEvent, async event => { backendRpc.on(storageClearEvent, async event => {
try {
const db = await this.getDb() const db = await this.getDb()
const keys = await db.keys().value() const keys = await db.keys().value()
for (const key of keys) { for (const key of keys) {
await db.unset(key).write() await db.unset(key).write()
} }
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), undefined)
} catch (error) {
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), {
error,
transactionId: event.transactionId,
})
throw error
}
}) })
} }
} }

View File

@@ -1,4 +1,4 @@
import * as Url from 'url' import { URL } from 'url'
import { Client, connect as mqttConnect } from 'mqtt' import { Client, connect as mqttConnect } from 'mqtt'
import { DataSource, DataSourceStateMachine } from './' import { DataSource, DataSourceStateMachine } from './'
@@ -41,13 +41,14 @@ export class MqttSource implements DataSource<MqttOptions> {
const urlStr = options.tls ? options.url.replace(/^(mqtt|ws):/, '$1s:') : options.url const urlStr = options.tls ? options.url.replace(/^(mqtt|ws):/, '$1s:') : options.url
let url let url
try { try {
url = Url.parse(urlStr) url = new URL(urlStr)
} catch (error) { } catch (error) {
this.stateMachine.setError(error) this.stateMachine.setError(error as Error)
throw error throw error
} }
const client = mqttConnect(url, {
const client = mqttConnect(url.toString(), {
resubscribe: false, resubscribe: false,
rejectUnauthorized: options.certValidation, rejectUnauthorized: options.certValidation,
username: options.username, username: options.username,

View File

@@ -1,6 +1,6 @@
import { ChangeBuffer } from './ChangeBuffer' import { ChangeBuffer } from './ChangeBuffer'
import { Destroyable } from './Destroyable' import { Destroyable } from './Destroyable'
import { EventBusInterface, EventDispatcher, makeConnectionMessageEvent, MqttMessage } from '../../../events' import { EventDispatcher, makeConnectionMessageEvent, MqttMessage, EventBusInterface } from '../../../events'
import { TreeNode } from './' import { TreeNode } from './'
import { TreeNodeFactory } from './TreeNodeFactory' import { TreeNodeFactory } from './TreeNodeFactory'

View File

@@ -1,11 +1,15 @@
{ {
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"skipLibCheck": true,
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true, "strictNullChecks": true,
"outDir": "./build", "outDir": "./build",
"strict": true, "strict": true,
"lib": ["es2017", "dom"], "lib": [
"es2017",
"dom"
],
"sourceMap": true "sourceMap": true
}, },
"includes": [ "includes": [

View File

@@ -1,50 +0,0 @@
import { IpcMain, ipcMain, ipcRenderer } from 'electron'
import { Event } from './Events'
import { IpcRendererEventBus } from './IpcRendererEventBus'
export interface EventBusInterface {
subscribe<MessageType>(event: Event<MessageType>, callback: (msg: MessageType) => void): void
unsubscribeAll<MessageType>(event: Event<MessageType>): void
emit<MessageType>(event: Event<MessageType>, msg: MessageType): void
unsubscribe<MessageType>(event: Event<MessageType>, callback: any): void
}
export interface CallbackStore {
wrappedCallback: any
callback: any
}
class IpcMainEventBus implements EventBusInterface {
private ipc: IpcMain
private client: any
constructor(ipc: IpcMain) {
this.ipc = ipc
}
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
console.log('subscribing', subscribeEvent.topic)
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
this.client = event.sender
callback(arg)
})
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
console.log('unsubscribeAll', event.topic)
this.ipc.removeAllListeners(event.topic)
}
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
throw new Error('Not implemented') // Todo: implement
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
if (!this.client.isDestroyed()) {
this.client.send(event.topic, msg)
}
}
}
export const rendererEvents = new IpcRendererEventBus(ipcRenderer)
export const backendEvents = new IpcMainEventBus(ipcMain)

View File

@@ -0,0 +1,4 @@
export interface CallbackStore {
wrappedCallback: any;
callback: any;
}

View File

@@ -0,0 +1,12 @@
import { ipcMain, ipcRenderer } from 'electron'
import { IpcMainEventBus } from './IpcMainEventBus'
import { IpcRendererEventBus } from './IpcRendererEventBus'
import { Rpc } from './Rpc'
export const rendererEvents = new IpcRendererEventBus(ipcRenderer)
export const backendEvents = new IpcMainEventBus(ipcMain)
// Preferred way to communicate typesafe
export const rendererRpc = new Rpc(rendererEvents)
export const backendRpc = new Rpc(backendEvents)

View File

@@ -0,0 +1,8 @@
import { Event } from '../Events';
export interface EventBusInterface {
subscribe<MessageType>(event: Event<MessageType>, callback: (msg: MessageType) => void): void;
unsubscribeAll<MessageType>(event: Event<MessageType>): void;
emit<MessageType>(event: Event<MessageType>, msg: MessageType): void;
unsubscribe<MessageType>(event: Event<MessageType>, callback: any): void;
}

View File

@@ -0,0 +1,34 @@
import { IpcMain } from 'electron';
import { Event } from '../Events';
import { EventBusInterface } from "./EventBusInterface";
export class IpcMainEventBus implements EventBusInterface {
private ipc: IpcMain;
private client: any;
constructor(ipc: IpcMain) {
this.ipc = ipc;
}
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
console.log('subscribing', subscribeEvent.topic);
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
this.client = event.sender;
callback(arg);
});
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
console.log('unsubscribeAll', event.topic);
this.ipc.removeAllListeners(event.topic);
}
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
throw new Error('Not implemented'); // Todo: implement
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
if (!this.client.isDestroyed()) {
this.client.send(event.topic, msg);
}
}
}

View File

@@ -1,26 +1,32 @@
import { CallbackStore, EventBusInterface } from './EventBus' import { CallbackStore } from "./CallbackStore"
import { Event } from './Events' import { EventBusInterface } from "./EventBusInterface"
import { Event } from '../Events'
import { IpcRenderer } from 'electron' import { IpcRenderer } from 'electron'
export class IpcRendererEventBus implements EventBusInterface { export class IpcRendererEventBus implements EventBusInterface {
private ipc: IpcRenderer private ipc: IpcRenderer
private callbacks: Array<CallbackStore> = [] private callbacks: Array<CallbackStore> = []
constructor(ipc: IpcRenderer) { constructor(ipc: IpcRenderer) {
this.ipc = ipc this.ipc = ipc
} }
public subscribe<MessageType>(event: Event<MessageType>, callback: (msg: MessageType) => void) { public subscribe<MessageType>(event: Event<MessageType>, callback: (msg: MessageType) => void) {
const wrappedCallback = (_: any, arg: any) => { const wrappedCallback = (_: any, arg: any) => {
callback(arg) callback(arg)
} }
console.log("subscribing", event.topic)
this.ipc.on(event.topic, wrappedCallback) this.ipc.on(event.topic, wrappedCallback)
this.callbacks.push({ this.callbacks.push({
callback, callback,
wrappedCallback, wrappedCallback,
}) })
} }
public unsubscribeAll<MessageType>(event: Event<MessageType>) { public unsubscribeAll<MessageType>(event: Event<MessageType>) {
this.ipc.removeAllListeners(event.topic) this.ipc.removeAllListeners(event.topic)
} }
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) { public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
const item = this.callbacks.find(store => store.callback === callback) const item = this.callbacks.find(store => store.callback === callback)
if (!item) { if (!item) {
@@ -29,6 +35,7 @@ export class IpcRendererEventBus implements EventBusInterface {
this.ipc.removeListener(event.topic, item.wrappedCallback) this.ipc.removeListener(event.topic, item.wrappedCallback)
this.callbacks = this.callbacks.filter(a => a !== item) this.callbacks = this.callbacks.filter(a => a !== item)
} }
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) { public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
this.ipc.send(event.topic, msg) this.ipc.send(event.topic, msg)
} }

53
events/EventSystem/Rpc.ts Normal file
View File

@@ -0,0 +1,53 @@
import { Event } from '../Events';
import { EventBusInterface } from './EventBusInterface'
import { v4 } from 'uuid';
export type RpcEvent<RequstType, ResponseType> = {
topic: string;
};
export class Rpc {
constructor(private participant: EventBusInterface) { }
async call<RpcRequest, RpcResponse>(event: RpcEvent<RpcRequest, RpcResponse>, request: RpcRequest, timeout: number = 0): Promise<RpcResponse> {
return new Promise((resolve, reject) => {
let id = v4();
let responseEvent: Event<any> = { topic: `${event.topic}/response/${id}` };
let requestEvent: Event<any> = { topic: `${event.topic}/request` };
let callback = (result: { id: string; payload: RpcResponse; error: unknown }) => {
this.participant.unsubscribe(responseEvent as any, callback);
if (result.error) {
reject(result.error)
} else {
resolve(result.payload);
}
console.log("received", result)
};
this.participant.subscribe(responseEvent, callback);
this.participant.emit(requestEvent, { id, payload: request });
if (timeout > 0) {
setTimeout(() => {
reject(new Error(`Did not respond to ${event.topic} within ${timeout}ms`))
this.participant.unsubscribe(responseEvent as any, callback);
}, 10000)
}
});
}
async on<RpcRequest, RpcResponse>(event: RpcEvent<RpcRequest, RpcResponse>, handler: (request: RpcRequest) => Promise<RpcResponse>) {
this.participant.subscribe<RpcRequest>({ topic: `${event.topic}/request` } as RpcEvent<any, any>, async (request) => {
let payload;
let error;
try {
payload = await handler((request as any).payload);
} catch (e) {
error = e
}
const id = (request as any).id
console.log(`${event.topic}/response/${id}`, payload, error)
this.participant.emit({ topic: `${event.topic}/response/${id}` }, { id, payload, error });
});
}
}

View File

@@ -1,8 +1,9 @@
import { Base64Message } from '../backend/src/Model/Base64Message' import { Base64Message } from '../backend/src/Model/Base64Message'
import { DataSourceState, MqttOptions } from '../backend/src/DataSource' import { DataSourceState, MqttOptions } from '../backend/src/DataSource'
import { UpdateInfo } from 'builder-util-runtime' import { UpdateInfo } from 'builder-util-runtime'
import { RpcEvent } from './EventSystem/Rpc';
export interface Event<MessageType> { export type Event<MessageType> = {
topic: string topic: string
} }
@@ -49,3 +50,7 @@ export function makeConnectionMessageEvent(connectionId: string): Event<MqttMess
topic: `conn/${connectionId}`, topic: `conn/${connectionId}`,
} }
} }
export const getAppVersion: RpcEvent<void, string> = {
topic: `getAppVersion`
}

View File

@@ -0,0 +1,8 @@
import { OpenDialogOptions, OpenDialogReturnValue } from 'electron';
import { RpcEvent } from './EventSystem/Rpc';
export function makeOpenDialogRpc(): RpcEvent<OpenDialogOptions, OpenDialogReturnValue> {
return {
topic: `openDialog`
};
}

View File

View File

@@ -1,39 +1,22 @@
import { Event } from './' import { RpcEvent } from './EventSystem/Rpc'
interface StorageEvent { export interface StoreCommand {
transactionId: string store: string
data: any
} }
export interface StoreCommand extends StorageEvent { export interface LoadCommand {
store?: string
data?: any
error?: any
}
export interface LoadCommand extends StorageEvent {
store: string store: string
} }
export const storageStoreEvent: Event<StoreCommand> = { export const storageStoreEvent: RpcEvent<StoreCommand, void> = {
topic: 'storage/store', topic: 'storage/store',
} }
export const storageLoadEvent: Event<LoadCommand> = { export const storageLoadEvent: RpcEvent<LoadCommand, StoreCommand> = {
topic: 'storage/load', topic: 'storage/load',
} }
export function makeStorageAcknowledgementEvent(transactionId: string): Event<StoreCommand> { export const storageClearEvent: RpcEvent<void, void> = {
return {
topic: `storage/ack/${transactionId}`,
}
}
export function makeStorageResponseEvent(transactionId: string): Event<StoreCommand> {
return {
topic: `storage/response/${transactionId}`,
}
}
export const storageClearEvent: Event<StorageEvent> = {
topic: 'storage/clear', topic: 'storage/clear',
} }

View File

@@ -1,3 +1,4 @@
export * from './Events' export * from './Events'
export * from './EventDispatcher' export * from './EventSystem/EventDispatcher'
export * from './EventBus' export * from './EventSystem/EventBus'
export * from './EventSystem/EventBusInterface'

View File

@@ -3,6 +3,9 @@
"version": "0.4.0-beta1", "version": "0.4.0-beta1",
"description": "Explore your message queues", "description": "Explore your message queues",
"main": "dist/src/electron.js", "main": "dist/src/electron.js",
"engines": {
"node": "16"
},
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"test": "yarn test:app && yarn test:backend", "test": "yarn test:app && yarn test:backend",
@@ -62,14 +65,14 @@
"buildResources": "res", "buildResources": "res",
"output": "build" "output": "build"
}, },
"afterPack": "./dist/scripts/afterPack.js", "afterPack": "./dist/scripts/afterPack.js"
"afterSign": "./scripts/afterSign.js"
}, },
"author": "Thomas Nordquist", "author": "Thomas Nordquist",
"email": "xxnerowingerxx@gmail.com", "email": "xxnerowingerxx@gmail.com",
"homepage": "https://github.com", "homepage": "https://github.com",
"license": "CC-BY-ND-4.0", "license": "CC-BY-ND-4.0",
"devDependencies": { "devDependencies": {
"@babel/runtime": "^7.17.2",
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/fs-extra": "8", "@types/fs-extra": "8",
"@types/lowdb": "^1.0.6", "@types/lowdb": "^1.0.6",
@@ -79,12 +82,13 @@
"@types/node": "^12.6.8", "@types/node": "^12.6.8",
"@types/semver": "7", "@types/semver": "7",
"@types/sha1": "^1.1.1", "@types/sha1": "^1.1.1",
"builder-util-runtime": "^8.2.5", "@types/uuid": "^8.3.4",
"builder-util-runtime": "^9",
"chai": "^4.2.0", "chai": "^4.2.0",
"cspell": "^4.0.28", "cspell": "^4.0.28",
"electron": "8.2.3", "electron": "17",
"electron-builder": "22.5.1", "electron-builder": "22",
"electron-notarize": "^0.3.0", "electron-notarize": "^1.1.1",
"mocha": "7.1", "mocha": "7.1",
"mustache": "4", "mustache": "4",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
@@ -92,32 +96,26 @@
"prettier": "2", "prettier": "2",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"source-map-support": "^0.5.9", "source-map-support": "^0.5.9",
"spectron": "10", "spectron": "19",
"ts-node": "^8.2.0", "ts-node": "^10.5.0",
"tslint": "6", "typescript": "^4.5.5",
"tslint-config-airbnb": "^5.11.1", "webdriverio": "7.16"
"tslint-react": "^4.0.0",
"tslint-react-recommended": "^1.0.15",
"tslint-strict-null-checks": "^1.0.1",
"typescript": "^3.2.2",
"webdriverio": "6"
}, },
"dependencies": { "dependencies": {
"about-window": "^1.12.1", "about-window": "^1.12.1",
"axios": "^0.19.0", "axios": "^0.19.0",
"dot-prop": "^5.0.0", "dot-prop": "^5.0.0",
"electron-log": "4.1.1", "electron-log": "4.4.6",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist", "electron-updater": "^4.6",
"electron-updater": "^4.0.6",
"fs-extra": "9", "fs-extra": "9",
"js-base64": "^2.5.1", "js-base64": "^2.5.1",
"json-to-ast": "^2.1.0", "json-to-ast": "^2.1.0",
"lowdb": "^1.0.0", "lowdb": "^1.0.0",
"mime": "^2.4.4", "mime": "^2.4.4",
"mqtt": "^3.0.0", "mqtt": "^4.3.6",
"protobufjs": "~6.11.2", "protobufjs": "~6.11.2",
"sha1": "^1.1.1", "sha1": "^1.1.1",
"uuid": "^8.3.2",
"yarn-run-all": "^3.1.1" "yarn-run-all": "^3.1.1"
}, }
"optionalDependencies": {}
} }

View File

@@ -1,5 +1,4 @@
import * as fs from 'fs-extra' import * as fs from 'fs-extra'
import * as path from 'path'
import { chdir } from 'process' import { chdir } from 'process'
import { exec } from './util' import { exec } from './util'

View File

@@ -53,12 +53,12 @@ async function uploadAsset() {
try { try {
uploadUrl = await createDraft(tag) uploadUrl = await createDraft(tag)
} catch (error) { } catch (error) {
console.error('failed to create draft', error.stack) console.error('failed to create draft', (error as Error).stack)
process.exit(1) process.exit(1)
} }
} }
} catch (error) { } catch (error) {
console.error('failed to find tag release', error.stack) console.error('failed to find tag release', (error as Error).stack)
process.exit(1) process.exit(1)
} }

View File

@@ -23,7 +23,7 @@ function redirectOutputFor(child: ChildProcess) {
} }
async function waitFor(child: ChildProcess) { async function waitFor(child: ChildProcess) {
return new Promise(resolve => { return new Promise<void>(resolve => {
child.once('close', () => resolve()) child.once('close', () => resolve())
}) })
} }

View File

@@ -1,7 +1,7 @@
import { autoUpdater, UpdateInfo } from 'electron-updater' import { autoUpdater, UpdateInfo } from 'electron-updater'
import { BuildInfo } from 'electron-telemetry/build/Model' // import { BuildInfo } from 'electron-telemetry/build/Model'
export function shouldAutoUpdate(build: BuildInfo) { export function shouldAutoUpdate(build: any) {
return ( return (
build.package !== 'portable' && build.package !== 'portable' &&
build.package !== 'appx' && build.package !== 'appx' &&

View File

@@ -1,8 +1,8 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import { BuildInfo } from 'electron-telemetry/build/Model' // import { BuildInfo } from 'electron-telemetry/build/Model'
let buildOptions: BuildInfo = { let buildOptions: any = {
platform: process.platform, platform: process.platform,
package: 'unpacked', package: 'unpacked',
} as any } as any

View File

@@ -1,23 +1,31 @@
import * as log from 'electron-log' import * as log from 'electron-log'
import * as path from 'path' import * as path from 'path'
import ConfigStorage from '../backend/src/ConfigStorage' import ConfigStorage from '../backend/src/ConfigStorage'
import { app, BrowserWindow, Menu } from 'electron' import { app, BrowserWindow, Menu, dialog } from 'electron'
import { autoUpdater } from 'electron-updater' import { autoUpdater } from 'electron-updater'
import { ConnectionManager } from '../backend/src/index' import { ConnectionManager } from '../backend/src/index'
import { electronTelemetryFactory } from 'electron-telemetry' // import { electronTelemetryFactory } from 'electron-telemetry'
import { menuTemplate } from './MenuTemplate' import { menuTemplate } from './MenuTemplate'
import buildOptions from './buildOptions' import buildOptions from './buildOptions'
import { waitForDevServer, isDev, runningUiTestOnCi, loadDevTools } from './development' import { waitForDevServer, isDev, runningUiTestOnCi, loadDevTools } from './development'
import { shouldAutoUpdate, handleAutoUpdate } from './autoUpdater' import { shouldAutoUpdate, handleAutoUpdate } from './autoUpdater'
import { registerCrashReporter } from './registerCrashReporter' import { registerCrashReporter } from './registerCrashReporter'
import { makeOpenDialogRpc } from '../events/OpenDialogRequest'
import { backendRpc, getAppVersion } from '../events'
registerCrashReporter() registerCrashReporter()
if (!isDev() && !runningUiTestOnCi()) { // if (!isDev() && !runningUiTestOnCi()) {
const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions) // const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions)
} // }
app.commandLine.appendSwitch('--no-sandbox') app.commandLine.appendSwitch('--no-sandbox')
app.whenReady().then(() => {
backendRpc.on(makeOpenDialogRpc(), async (request) => {
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request)
})
backendRpc.on(getAppVersion, async () => app.getVersion())
})
autoUpdater.logger = log autoUpdater.logger = log
log.info('App starting...') log.info('App starting...')
@@ -45,6 +53,8 @@ async function createWindow() {
height: 720, height: 720,
show: false, show: false,
webPreferences: { webPreferences: {
...({ enableRemoteModule: true } as any),
contextIsolation: false,
nodeIntegration: true, nodeIntegration: true,
devTools: true, devTools: true,
sandbox: false, sandbox: false,

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
import { clickOn, expandTopic, moveToCenterOfElement, sleep, writeText } from '../util' import { clickOn, expandTopic, moveToCenterOfElement, sleep, writeText } from '../util'
export async function clearOldTopics(browser: Browser) { export async function clearOldTopics(browser: Browser<'async'>) {
const topics = ['hello', 'test 123'] const topics = ['hello', 'test 123']
for (const topic of topics) { for (const topic of topics) {
await expandTopic(topic, browser) await expandTopic(topic, browser)

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
import { clickOn, setTextInInput } from '../util' import { clickOn, setTextInInput } from '../util'
export async function connectTo(host: string, browser: Browser) { export async function connectTo(host: string, browser: Browser<'async'>) {
await setTextInInput('Host', host, browser) await setTextInInput('Host', host, browser)
await browser.saveScreenshot('screen1.png') await browser.saveScreenshot('screen1.png')

View File

@@ -1,7 +1,7 @@
import { Browser } from 'webdriverio' import { Browser } from 'webdriverio'
import { clickOn } from '../util' import { clickOn } from '../util'
export async function copyTopicToClipboard(browser: Browser) { export async function copyTopicToClipboard(browser: Browser<'async'>) {
const copyButton = await browser.$('//span[contains(text(), "Topic")]//button') const copyButton = await browser.$('//span[contains(text(), "Topic")]//button')
await clickOn(copyButton, browser, 1) await clickOn(copyButton, browser, 1)
} }

View File

@@ -1,7 +1,7 @@
import { Browser } from 'webdriverio' import { Browser } from 'webdriverio'
import { clickOn } from '../util' import { clickOn } from '../util'
export async function copyValueToClipboard(browser: Browser) { export async function copyValueToClipboard(browser: Browser<'async'>) {
const copyButton = await browser.$('//span[contains(text(), "Value")]//button') const copyButton = await browser.$('//span[contains(text(), "Value")]//button')
await clickOn(copyButton, browser, 1) await clickOn(copyButton, browser, 1)
} }

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
import { clickOn } from '../util' import { clickOn } from '../util'
export async function disconnect(browser: Browser) { export async function disconnect(browser: Browser<'async'>) {
const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]') const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
await clickOn(disconnectButton, browser) await clickOn(disconnectButton, browser)
} }

View File

@@ -9,7 +9,7 @@ import {
showText, showText,
} from '../util' } from '../util'
export async function publishTopic(browser: Browser) { export async function publishTopic(browser: Browser<'async'>) {
await expandTopic('kitchen/lamp/state', browser) await expandTopic('kitchen/lamp/state', browser)
const topicInput = await browser.$('//input[contains(@value,"kitchen/lamp/state")][1]') const topicInput = await browser.$('//input[contains(@value,"kitchen/lamp/state")][1]')
await clickOn(topicInput, browser) await clickOn(topicInput, browser)
@@ -17,8 +17,7 @@ export async function publishTopic(browser: Browser) {
await writeText('set', browser, 300) await writeText('set', browser, 300)
const payloadInput = await browser.$('//*[contains(@class, "ace_text-input")]') const payloadInput = await browser.$('//*[contains(@class, "ace_text-input")]')
await writeTextPayload(payloadInput, '{"action": "setState", "state": "on" }') await writeTextPayload(payloadInput, 'off')
await sleep(500) await sleep(500)
const formatJsonButton = await browser.$('#sidebar-publish-format-json') const formatJsonButton = await browser.$('#sidebar-publish-format-json')
await clickOn(formatJsonButton, browser) await clickOn(formatJsonButton, browser)

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
import { clickOn } from '../util' import { clickOn } from '../util'
export async function reconnect(browser: Browser) { export async function reconnect(browser: Browser<'async'>) {
const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]') const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
await clickOn(disconnectButton, browser) await clickOn(disconnectButton, browser)
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]') const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')

View File

@@ -1,14 +1,14 @@
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
import { clickOn, deleteTextWithBackspaces, showText, sleep, writeText } from '../util' import { clickOn, deleteTextWithBackspaces, showText, sleep, writeText } from '../util'
export async function searchTree(text: string, browser: Browser) { export async function searchTree(text: string, browser: Browser<'async'>) {
const searchField = await browser.$('//input[contains(@placeholder, "Search")]') const searchField = await browser.$('//input[contains(@placeholder, "Search")]')
await clickOn(searchField, browser, 1) await clickOn(searchField, browser, 1)
await writeText(text, browser, 100) await writeText(text, browser, 100)
await sleep(1500) await sleep(1500)
} }
export async function clearSearch(browser: Browser) { export async function clearSearch(browser: Browser<'async'>) {
const searchField = await browser.$('//input[contains(@placeholder, "Search")]') const searchField = await browser.$('//input[contains(@placeholder, "Search")]')
await clickOn(searchField, browser, 1) await clickOn(searchField, browser, 1)
await deleteTextWithBackspaces(searchField, browser, 100) await deleteTextWithBackspaces(searchField, browser, 100)

View File

@@ -1,7 +1,7 @@
import { Browser } from 'webdriverio' import { Browser } from 'webdriverio'
import { clickOn, sleep, setInputText } from '../util' import { clickOn, sleep, setInputText } from '../util'
export async function showAdvancedConnectionSettings(browser: Browser) { export async function showAdvancedConnectionSettings(browser: Browser<'async'>) {
const advancedSettingsButton = await browser.$('//button/span[contains(text(),"Advanced")]') const advancedSettingsButton = await browser.$('//button/span[contains(text(),"Advanced")]')
const addButton = await browser.$('//button/span[contains(text(),"Add")]') const addButton = await browser.$('//button/span[contains(text(),"Add")]')
const topicInput = await browser.$('//*[contains(@class, "advanced-connection-settings-topic-input")]//input') const topicInput = await browser.$('//*[contains(@class, "advanced-connection-settings-topic-input")]//input')
@@ -24,7 +24,7 @@ export async function showAdvancedConnectionSettings(browser: Browser) {
await clickOn(connectButton, browser) await clickOn(connectButton, browser)
} }
async function deleteFirstSubscribedTopic(browser: Browser) { async function deleteFirstSubscribedTopic(browser: Browser<'async'>) {
const deleteButton = await browser.$('.advanced-connection-settings-topic-list button') const deleteButton = await browser.$('.advanced-connection-settings-topic-list button')
await clickOn(deleteButton, browser) await clickOn(deleteButton, browser)
} }

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
import { expandTopic, sleep } from '../util' import { expandTopic, sleep } from '../util'
export async function showJsonPreview(browser: Browser) { export async function showJsonPreview(browser: Browser<'async'>) {
await expandTopic('actuality/showcase', browser) await expandTopic('actuality/showcase', browser)
await browser.saveScreenshot('screen3.png') await browser.saveScreenshot('screen3.png')
await sleep(1000) await sleep(1000)

View File

@@ -1,7 +1,7 @@
import { Browser } from 'webdriverio' import { Browser } from 'webdriverio'
import { clickOn, showText, sleep } from '../util' import { clickOn, showText, sleep } from '../util'
export async function showMenu(browser: Browser) { export async function showMenu(browser: Browser<'async'>) {
const menuButton = await browser.$('//button[contains(@aria-label, "Menu")]') const menuButton = await browser.$('//button[contains(@aria-label, "Menu")]')
await clickOn(menuButton, browser) await clickOn(menuButton, browser)

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
import { moveToCenterOfElement, clickOn, clickOnHistory, expandTopic, sleep, writeText } from '../util' import { moveToCenterOfElement, clickOn, clickOnHistory, expandTopic, sleep, writeText } from '../util'
export async function showNumericPlot(browser: Browser) { export async function showNumericPlot(browser: Browser<'async'>) {
await expandTopic('kitchen/coffee_maker', browser) await expandTopic('kitchen/coffee_maker', browser)
let heater = await valuePreviewGuttersShowChartIcon('heater', browser) let heater = await valuePreviewGuttersShowChartIcon('heater', browser)
await moveToCenterOfElement(heater, browser) await moveToCenterOfElement(heater, browser)
@@ -42,7 +42,7 @@ export async function showNumericPlot(browser: Browser) {
await clickOnHistory(browser) await clickOnHistory(browser)
} }
async function valuePreviewGuttersShowChartIcon(name: string, browser: Browser) { async function valuePreviewGuttersShowChartIcon(name: string, browser: Browser<'async'>) {
for (let retries = 0; retries < 2; retries += 1) { for (let retries = 0; retries < 2; retries += 1) {
try { try {
return await browser.$(`//*[contains(@data-test-type, "ShowChart")][contains(@data-test, "${name}")]`) return await browser.$(`//*[contains(@data-test-type, "ShowChart")][contains(@data-test, "${name}")]`)
@@ -53,23 +53,23 @@ async function valuePreviewGuttersShowChartIcon(name: string, browser: Browser)
return browser.$(`//*[contains(@data-test-type, "ShowChart")][contains(@data-test, "${name}")]`) return browser.$(`//*[contains(@data-test-type, "ShowChart")][contains(@data-test, "${name}")]`)
} }
async function chartSettings(name: string, browser: Browser) { async function chartSettings(name: string, browser: Browser<'async'>) {
const settings = await browser.$(`//*[contains(@data-test-type, "ChartSettings")][contains(@data-test, "${name}")]`) const settings = await browser.$(`//*[contains(@data-test-type, "ChartSettings")][contains(@data-test, "${name}")]`)
return clickOn(settings, browser) return clickOn(settings, browser)
} }
async function clickAway(name: string, browser: Browser) { async function clickAway(name: string, browser: Browser<'async'>) {
const settings = await browser.$(`//*[contains(@data-test-type, "ChartPaper")][contains(@data-test, "${name}")]`) const settings = await browser.$(`//*[contains(@data-test-type, "ChartPaper")][contains(@data-test, "${name}")]`)
await moveToCenterOfElement(settings, browser) await moveToCenterOfElement(settings, browser)
await browser.keys(['Escape']) await browser.keys(['Escape'])
} }
async function removeChart(name: string, browser: Browser) { async function removeChart(name: string, browser: Browser<'async'>) {
const remove = await browser.$(`//*[contains(@data-test-type, "RemoveChart")][contains(@data-test, "${name}")]`) const remove = await browser.$(`//*[contains(@data-test-type, "RemoveChart")][contains(@data-test, "${name}")]`)
return clickOn(remove, browser) return clickOn(remove, browser)
} }
async function clickOnMenuPoint(name: string, browser: Browser) { async function clickOnMenuPoint(name: string, browser: Browser<'async'>) {
const item = await browser.$(`//li/span[contains(text(), "${name}")]`) const item = await browser.$(`//li/span[contains(text(), "${name}")]`)
return clickOn(item, browser) return clickOn(item, browser)
} }

View File

@@ -2,7 +2,7 @@ import { Browser, Element } from 'webdriverio'
import { clickOn, showText, sleep } from '../util' import { clickOn, showText, sleep } from '../util'
// Expects a topic with at least two messages to be selected // Expects a topic with at least two messages to be selected
export async function showOffDiffCapability(browser: Browser) { export async function showOffDiffCapability(browser: Browser<'async'>) {
await showText('Compare messages', 2000, browser, 'top') await showText('Compare messages', 2000, browser, 'top')
await showText('Show raw message', 2000, browser, 'bottom') await showText('Show raw message', 2000, browser, 'bottom')

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
import { showKeys, showText, sleep } from '../util' import { showKeys, showText, sleep } from '../util'
export async function showZoomLevel(browser: Browser) { export async function showZoomLevel(browser: Browser<'async'>) {
await showKeys('Zoom in', 2000, browser, 'top', ['Ctrl', '+']) await showKeys('Zoom in', 2000, browser, 'top', ['Ctrl', '+'])
await sleep(2000) await sleep(2000)
await showKeys('Zoom out', 2000, browser, 'middle', ['Ctrl', '-']) await showKeys('Zoom out', 2000, browser, 'middle', ['Ctrl', '-'])

View File

@@ -1,7 +1,7 @@
import { clickOn } from './' import { clickOn } from './'
import { Browser, Element } from 'webdriverio' import { Browser, Element } from 'webdriverio'
export async function expandTopic(path: string, browser: Browser) { export async function expandTopic(path: string, browser: Browser<'async'>) {
const originalTopics = path.split('/') const originalTopics = path.split('/')
let topics = path.split('/') let topics = path.split('/')
while (topics.length > 0 && !(await topicMatches(topics, browser))) { while (topics.length > 0 && !(await topicMatches(topics, browser))) {
@@ -18,7 +18,7 @@ export async function expandTopic(path: string, browser: Browser) {
} }
} }
async function topicMatches(topics: Array<string>, browser: Browser) { async function topicMatches(topics: Array<string>, browser: Browser<'async'>) {
const result = await browser.$(topicSelector(topics)) const result = await browser.$(topicSelector(topics))
return result.isExisting() return result.isExisting()
} }

View File

@@ -18,7 +18,7 @@ export function sleep(ms: number, required = false) {
}) })
} }
export async function writeText(text: string, browser: Browser, delay = 0) { export async function writeText(text: string, browser: Browser<'async'>, delay = 0) {
if (fast) { if (fast) {
return browser.keys(text.split('')) return browser.keys(text.split(''))
} }
@@ -29,7 +29,7 @@ export async function writeText(text: string, browser: Browser, delay = 0) {
} }
} }
export async function deleteTextWithBackspaces(element: Element, browser: Browser, delay = 0, count = 0) { export async function deleteTextWithBackspaces(element: Element<'async'>, browser: Browser<'async'>, delay = 0, count = 0) {
const length = count > 0 ? count : (await element.getValue()).length const length = count > 0 ? count : (await element.getValue()).length
for (let i = 0; i < length; i += 1) { for (let i = 0; i < length; i += 1) {
await browser.keys(['Backspace']) await browser.keys(['Backspace'])
@@ -37,13 +37,13 @@ export async function deleteTextWithBackspaces(element: Element, browser: Browse
} }
} }
export async function setInputText(input: Element, text: string, browser: Browser) { export async function setInputText(input: Element<'async'>, text: string, browser: Browser<'async'>) {
await clickOn(input, browser, 1) await clickOn(input, browser, 1)
await deleteTextWithBackspaces(input, browser) await deleteTextWithBackspaces(input, browser)
await input.setValue(text) await input.setValue(text)
} }
export async function setTextInInput(name: string, text: string, browser: Browser) { export async function setTextInInput(name: string, text: string, browser: Browser<'async'>) {
const input = await browser.$(`//label[contains(text(), "${name}")]/..//input`) const input = await browser.$(`//label[contains(text(), "${name}")]/..//input`)
await clickOn(input, browser, 1) await clickOn(input, browser, 1)
await browser.$(`//label[contains(text(), "${name}")]/..//input`) await browser.$(`//label[contains(text(), "${name}")]/..//input`)
@@ -52,7 +52,7 @@ export async function setTextInInput(name: string, text: string, browser: Browse
await input.setValue(text) await input.setValue(text)
} }
export async function moveToCenterOfElement(element: Element, browser: Browser) { export async function moveToCenterOfElement(element: Element<'async'>, browser: Browser<'async'>) {
const { x, y } = await element.getLocation() const { x, y } = await element.getLocation()
const { width, height } = await element.getSize() const { width, height } = await element.getSize()
@@ -69,12 +69,12 @@ export async function moveToCenterOfElement(element: Element, browser: Browser)
await element.moveTo() await element.moveTo()
} }
export async function clickOnHistory(browser: Browser) { export async function clickOnHistory(browser: Browser<'async'>) {
const messageHistory = await browser.$('//span/*[contains(text(), "History")]') const messageHistory = await browser.$('//span/*[contains(text(), "History")]')
await clickOn(messageHistory, browser) await clickOn(messageHistory, browser)
} }
export async function clickOn(element: Element, browser: Browser, clicks = 1) { export async function clickOn(element: Element<'async'>, browser: Browser<'async'>, clicks = 1) {
await moveToCenterOfElement(element, browser) await moveToCenterOfElement(element, browser)
for (let i = 0; i < clicks; i += 1) { for (let i = 0; i < clicks; i += 1) {
await element.click() await element.click()
@@ -82,7 +82,7 @@ export async function clickOn(element: Element, browser: Browser, clicks = 1) {
} }
} }
export async function createFakeMousePointer(browser: Browser) { export async function createFakeMousePointer(browser: Browser<'async'>) {
const js = 'window.demo.enableMouse();' const js = 'window.demo.enableMouse();'
await browser.execute(js) await browser.execute(js)
@@ -91,7 +91,7 @@ export async function createFakeMousePointer(browser: Browser) {
export async function showText( export async function showText(
text: string, text: string,
duration: number = 0, duration: number = 0,
browser: Browser, browser: Browser<'async'>,
location: 'top' | 'bottom' | 'middle' = 'bottom', location: 'top' | 'bottom' | 'middle' = 'bottom',
keys = [] keys = []
) { ) {
@@ -102,7 +102,7 @@ export async function showText(
type HeapDump = any type HeapDump = any
export async function getHeapDump(browser: Browser): Promise<HeapDump> { export async function getHeapDump(browser: Browser<'async'>): Promise<HeapDump> {
const filename = 'heapdump.json' const filename = 'heapdump.json'
const js = `window.demo.writeHeapdump('${filename}');` const js = `window.demo.writeHeapdump('${filename}');`
await browser.execute(js) await browser.execute(js)
@@ -125,7 +125,7 @@ export async function countInstancesOf(heapDump: HeapDump, className: ClassNameM
export async function showKeys( export async function showKeys(
text: string, text: string,
duration: number = 0, duration: number = 0,
browser: Browser, browser: Browser<'async'>,
location: 'top' | 'bottom' | 'middle' = 'bottom', location: 'top' | 'bottom' | 'middle' = 'bottom',
keys: Array<string> = [] keys: Array<string> = []
) { ) {
@@ -134,7 +134,7 @@ export async function showKeys(
await browser.execute(js) await browser.execute(js)
} }
export async function hideText(browser: Browser) { export async function hideText(browser: Browser<'async'>) {
const js = 'window.demo.hideMessage();' const js = 'window.demo.hideMessage();'
await browser.execute(js) await browser.execute(js)
await sleep(600) await sleep(600)

View File

@@ -8,9 +8,15 @@
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"sourceRoot": "src/", "sourceRoot": "src/",
"lib": ["es2017", "dom"], "lib": [
"es2017",
"dom"
],
"sourceMap": true, "sourceMap": true,
"types": ["node"] "types": [
"node"
],
"skipLibCheck": true
}, },
"include": [ "include": [
"src/electron.ts", "src/electron.ts",
@@ -18,5 +24,8 @@
"src/spec/demoVideo.ts", "src/spec/demoVideo.ts",
"src/spec/leakTest.ts", "src/spec/leakTest.ts",
"scripts/*.ts" "scripts/*.ts"
],
"exclude": [
"node_modules"
] ]
} }

7246
yarn.lock

File diff suppressed because it is too large Load Diff