mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-12 01:23:31 +00:00
Compare commits
39
Commits
v0.2.4
...
0.0.0-v0.2.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77654c7136 | ||
|
|
3bfc59d89a | ||
|
|
6bf84055d9 | ||
|
|
12d7d3ab64 | ||
|
|
b2913c7e0e | ||
|
|
352948f212 | ||
|
|
9a4dbe92a0 | ||
|
|
397b95f9e5 | ||
|
|
a411d21133 | ||
|
|
4f624cc130 | ||
|
|
f10bfafa1d | ||
|
|
10e37624a5 | ||
|
|
ebdfac39eb | ||
|
|
7cc032cdf4 | ||
|
|
4bc9039386 | ||
|
|
43b997fb18 | ||
|
|
6c883aa226 | ||
|
|
20cde3e2a4 | ||
|
|
ae7440f8ab | ||
|
|
03e228e5a0 | ||
|
|
14f2f9ff1e | ||
|
|
86544c1334 | ||
|
|
9e71c3132e | ||
|
|
817202fc20 | ||
|
|
931ec0113c | ||
|
|
365ebc78ab | ||
|
|
2a541a80dc | ||
|
|
a1d3f32f73 | ||
|
|
e9e6ea618d | ||
|
|
6021df7150 | ||
|
|
070b72b304 | ||
|
|
1f65a0f316 | ||
|
|
0f21f10c0d | ||
|
|
af2ff0149d | ||
|
|
8cd11cde3b | ||
|
|
fdbe6344d9 | ||
|
|
a56b41635c | ||
|
|
2c5c218fd1 | ||
|
|
749df70d5c |
@@ -8,7 +8,7 @@
|
||||
|
||||
| | | |
|
||||
|:---:|:---:|:---:|
|
||||
|[](https://user-images.githubusercontent.com/7721625/53954364-52551f00-40d6-11e9-93cf-d5a9601897ea.png)|[](https://user-images.githubusercontent.com/7721625/53954365-52551f00-40d6-11e9-823f-afd66f19ed01.png)|[](https://user-images.githubusercontent.com/7721625/53954366-52551f00-40d6-11e9-9738-74db830d03ac.png)|
|
||||
|[](https://mqtt-explorer.com/img/screen-composite.png)|[](https://mqtt-explorer.com/img/screen2.png)|[](https://mqtt-explorer.com/img/screen3.png)|
|
||||
|
||||
# The App has moved to [mqtt-explorer.com](https://mqtt-explorer.com)
|
||||
MQTT Explorer is a comprehensive and easy-to-use MQTT Client.
|
||||
|
||||
+6
-3
@@ -4,11 +4,12 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress"
|
||||
"build": "yarn rebuild && webpack --mode production",
|
||||
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
|
||||
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=5.0.0 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"license": "CC-BY-ND-4.0",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.0.0-alpha.6",
|
||||
"@material-ui/icons": "^4.0.0-alpha.1",
|
||||
@@ -60,7 +61,9 @@
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"css-loader": "^2.1.0",
|
||||
"hard-source-webpack-plugin": "^0.13.1",
|
||||
"heapdump": "^0.3.12",
|
||||
"html-webpack-plugin": "^4.0.0-beta.5",
|
||||
"node-loader": "^0.6.0",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"style-loader": "^0.23.1",
|
||||
"typescript": "^3.2.2",
|
||||
|
||||
@@ -78,6 +78,8 @@ export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppSta
|
||||
}
|
||||
|
||||
tree && tree.stopUpdating()
|
||||
tree && tree.destroy()
|
||||
|
||||
// Clear topic filter
|
||||
dispatch({
|
||||
topicFilter: '',
|
||||
@@ -88,4 +90,5 @@ export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppSta
|
||||
dispatch({
|
||||
type: ActionTypes.CONNECTION_SET_DISCONNECTED,
|
||||
})
|
||||
dispatch(showTree(undefined))
|
||||
}
|
||||
|
||||
+16
-8
@@ -53,20 +53,28 @@ const debouncedSelectTopic = debounce((topic: q.TreeNode<TopicViewModel>, dispat
|
||||
}
|
||||
}, 70)
|
||||
|
||||
export const resetStore = () => (dispatch: Dispatch<any>): AnyAction => {
|
||||
function destroyUnreferencedTree(state: AppState) {
|
||||
const visibleTree = state.tree.get('tree')
|
||||
const connectionTree = state.connection.tree
|
||||
|
||||
// Stop updates of old tree
|
||||
if (visibleTree && visibleTree !== connectionTree) {
|
||||
console.warn('destroy')
|
||||
visibleTree.stopUpdating()
|
||||
visibleTree.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
export const resetStore = () => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
|
||||
destroyUnreferencedTree(getState())
|
||||
|
||||
return dispatch({
|
||||
type: ActionTypes.TREE_RESET_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
export const showTree = (tree: q.Tree<TopicViewModel> | undefined) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
|
||||
const visibleTree = getState().tree.get('tree')
|
||||
const connectionTree = getState().connection.tree
|
||||
|
||||
// Stop updates of old tree
|
||||
if (visibleTree !== connectionTree && visibleTree) {
|
||||
visibleTree.stopUpdating()
|
||||
}
|
||||
destroyUnreferencedTree(getState())
|
||||
|
||||
return dispatch({
|
||||
tree,
|
||||
|
||||
@@ -89,6 +89,12 @@ class App extends React.PureComponent<Props, {}> {
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
const drawerWidth = 300
|
||||
const contentBaseStyle = {
|
||||
width: '100vw',
|
||||
overflow: 'hidden' as 'hidden',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}
|
||||
|
||||
return {
|
||||
heightProperty: {
|
||||
height: 'calc(100vh - 64px) !important',
|
||||
@@ -106,9 +112,7 @@ const styles = (theme: Theme) => {
|
||||
overflow: 'hidden' as 'hidden',
|
||||
},
|
||||
content: {
|
||||
width: '100vw',
|
||||
overflowX: 'hidden' as 'hidden',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
...contentBaseStyle,
|
||||
transition: theme.transitions.create('transform', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
@@ -116,9 +120,7 @@ const styles = (theme: Theme) => {
|
||||
transform: 'translateX(0px)',
|
||||
},
|
||||
contentShift: {
|
||||
overflowX: 'hidden' as 'hidden',
|
||||
width: '100vw',
|
||||
padding: 0,
|
||||
...contentBaseStyle,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
transition: theme.transitions.create('transform', {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
|
||||
@@ -77,6 +77,7 @@ const style = (theme: Theme) => ({
|
||||
position: 'fixed' as 'fixed',
|
||||
zIndex: 1000000,
|
||||
filter: theme.palette.type === 'light' ? undefined : 'invert(100%)',
|
||||
pointerEvents: 'none' as 'none',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import * as React from 'react'
|
||||
import ShowText from './ShowText'
|
||||
import Mouse from './Mouse'
|
||||
let heapdump: any
|
||||
|
||||
(window as any).demo = {}
|
||||
function writeHeapdump(path?: string) {
|
||||
if (!heapdump) {
|
||||
heapdump = require('heapdump')
|
||||
}
|
||||
|
||||
heapdump.writeSnapshot(path || `${Date.now()}.heapsnapshot`)
|
||||
return path
|
||||
}
|
||||
|
||||
(window as any).demo = {
|
||||
writeHeapdump,
|
||||
}
|
||||
|
||||
export default function render(props: any) {
|
||||
return (
|
||||
|
||||
@@ -37,7 +37,7 @@ class Notification extends React.Component<Props, {}> {
|
||||
<Snackbar
|
||||
anchorOrigin={snackbarAnchor}
|
||||
open={Boolean(this.props.message)}
|
||||
autoHideDuration={this.props.type === 'error' ? 10000 : 3000}
|
||||
autoHideDuration={this.props.type === 'error' ? 10000 : 2000}
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
<SnackbarContent
|
||||
|
||||
@@ -78,9 +78,6 @@ const styles: StyleRulesCallback = theme => ({
|
||||
paddingLeft: theme.spacing(6),
|
||||
transition: theme.transitions.create('width'),
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: 200,
|
||||
},
|
||||
},
|
||||
menuButton: {
|
||||
marginLeft: -12,
|
||||
|
||||
@@ -109,7 +109,7 @@ class Settings extends React.Component<Props, {}> {
|
||||
private toggleTheme() {
|
||||
const { actions, theme } = this.props
|
||||
|
||||
return <BooleanSwitch title="Dark Theme" tooltip="Enable dark theme" value={theme === 'light'} action={actions.settings.toggleTheme} />
|
||||
return <BooleanSwitch title="Dark Mode" tooltip="Enable dark theme" value={theme === 'dark'} action={actions.settings.toggleTheme} />
|
||||
}
|
||||
|
||||
private renderAutoExpand() {
|
||||
@@ -120,12 +120,12 @@ class Settings extends React.Component<Props, {}> {
|
||||
<div style={{ padding: '8px', display: 'flex' }}>
|
||||
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>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}
|
||||
style={{ flex: '1' }}
|
||||
value={autoExpandLimit}
|
||||
onChange={this.onChangeAutoExpand}
|
||||
input={<Input name="auto-expand" id="auto-expand-label-placeholder" />}
|
||||
name="auto-expand"
|
||||
className={classes.input}
|
||||
style={{ flex: '1' }}
|
||||
>
|
||||
{limits}
|
||||
</Select>
|
||||
|
||||
@@ -182,16 +182,16 @@ const style = (theme: Theme) => {
|
||||
deletion: {
|
||||
...gutterBaseStyle,
|
||||
backgroundColor: 'rgba(255, 10, 10, 0.3)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 10, 10, 0.6)',
|
||||
},
|
||||
// '&:hover': {
|
||||
// backgroundColor: 'rgba(255, 10, 10, 0.3)',
|
||||
// },
|
||||
},
|
||||
addition: {
|
||||
...gutterBaseStyle,
|
||||
backgroundColor: 'rgba(10, 255, 10, 0.3)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(10, 255, 10, 0.5)',
|
||||
},
|
||||
// '&:hover': {
|
||||
// backgroundColor: 'rgba(10, 255, 10, 0.5)',
|
||||
// },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ interface Props {
|
||||
}
|
||||
|
||||
interface State {
|
||||
node: q.TreeNode<TopicViewModel>
|
||||
compareMessage?: q.Message
|
||||
valueRenderWidth: number
|
||||
}
|
||||
@@ -49,8 +48,7 @@ class Sidebar extends React.Component<Props, State> {
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
console.error('Find and fix me #state')
|
||||
this.state = { node: new q.Tree(), valueRenderWidth: 300 }
|
||||
this.state = { valueRenderWidth: 300 }
|
||||
}
|
||||
|
||||
private registerUpdateListener(node: q.TreeNode<TopicViewModel>) {
|
||||
@@ -156,7 +154,6 @@ class Sidebar extends React.Component<Props, State> {
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
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 })
|
||||
|
||||
@@ -30,7 +30,7 @@ interface State {
|
||||
lastUpdate: number
|
||||
}
|
||||
|
||||
class Tree extends React.PureComponent<Props, State> {
|
||||
class TreeComponent extends React.PureComponent<Props, State> {
|
||||
private updateTimer?: any
|
||||
private perf: number = 0
|
||||
private renderTime = 0
|
||||
@@ -137,4 +137,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Tree)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
|
||||
|
||||
@@ -70,7 +70,7 @@ interface State {
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
class TreeNode extends React.Component<Props, State> {
|
||||
class TreeNodeComponent extends React.Component<Props, State> {
|
||||
private animationDirty: boolean = false
|
||||
|
||||
private cssAnimationWasSetAt?: number
|
||||
@@ -97,8 +97,8 @@ class TreeNode extends React.Component<Props, State> {
|
||||
treeNode.viewModel.change.subscribe(this.viewStateHasChanged)
|
||||
}
|
||||
|
||||
private viewStateHasChanged = (msg: void, viewModel: TopicViewModel) => {
|
||||
this.setState({ selected: viewModel.isSelected() })
|
||||
private viewStateHasChanged = (msg: void) => {
|
||||
this.props.treeNode.viewModel && this.setState({ selected: this.props.treeNode.viewModel.isSelected() })
|
||||
}
|
||||
|
||||
private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
|
||||
@@ -173,13 +173,6 @@ class TreeNode extends React.Component<Props, State> {
|
||||
this.addSubscriber(treeNode)
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (nextProps.treeNode !== this.props.treeNode) {
|
||||
this.removeSubscriber(this.props.treeNode)
|
||||
this.addSubscriber(nextProps.treeNode)
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const { treeNode } = this.props
|
||||
this.removeSubscriber(treeNode)
|
||||
@@ -243,4 +236,4 @@ class TreeNode extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles, { withTheme: true })(TreeNode)
|
||||
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
|
||||
|
||||
@@ -44,6 +44,7 @@ interface GithubAsset {
|
||||
id: number
|
||||
node_id: string
|
||||
url: string
|
||||
browser_download_url: string
|
||||
name: string
|
||||
label: string
|
||||
}
|
||||
@@ -170,7 +171,7 @@ class UpdateNotifier extends React.Component<Props, State> {
|
||||
{this.renderDownloads()}
|
||||
<Button
|
||||
className={this.props.classes.download}
|
||||
onClick={this.openGithub}
|
||||
onClick={this.openHomePage}
|
||||
>
|
||||
Github Page
|
||||
</Button>
|
||||
@@ -180,8 +181,8 @@ class UpdateNotifier extends React.Component<Props, State> {
|
||||
)
|
||||
}
|
||||
|
||||
private openGithub = () => {
|
||||
this.openUrl('https://github.com/thomasnordquist/MQTT-Explorer')
|
||||
private openHomePage = () => {
|
||||
this.openUrl('https://mqtt-explorer.com')
|
||||
}
|
||||
|
||||
private openUrl = (url: string) => {
|
||||
@@ -213,7 +214,7 @@ class UpdateNotifier extends React.Component<Props, State> {
|
||||
<div>
|
||||
<Button
|
||||
className={this.props.classes.download}
|
||||
onClick={() => this.openUrl(asset.url)}
|
||||
onClick={() => this.openUrl(asset.browser_download_url)}
|
||||
>
|
||||
<CloudDownload /> {asset.name}
|
||||
</Button>
|
||||
|
||||
@@ -47,7 +47,7 @@ class ConnectionHealthIndicator extends React.Component<Props, {}> {
|
||||
|
||||
return (
|
||||
<Tooltip title={`Connection health "${health}"`}>
|
||||
<div>
|
||||
<div style={{ display: 'inherit' }}>
|
||||
<DeviceHubOutlined className={`${[classes[health]]} ${this.props.withBackground ? classes.icon : ''}`} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -2,40 +2,35 @@ import * as React from 'react'
|
||||
import Check from '@material-ui/icons/Check'
|
||||
import CustomIconButton from './CustomIconButton'
|
||||
import FileCopy from '@material-ui/icons/FileCopy'
|
||||
import green from '@material-ui/core/colors/green'
|
||||
import { Snackbar, SnackbarContent, Tooltip } from '@material-ui/core'
|
||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { globalActions } from '../../actions'
|
||||
|
||||
const copy = require('copy-text-to-clipboard')
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
classes: any
|
||||
actions: {
|
||||
global: typeof globalActions
|
||||
}
|
||||
}
|
||||
|
||||
interface State {
|
||||
didCopy: boolean
|
||||
snackBarOpen: boolean
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
snackbar: {
|
||||
backgroundColor: green[600],
|
||||
color: theme.typography.button.color,
|
||||
},
|
||||
})
|
||||
|
||||
class Copy extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { didCopy: false, snackBarOpen: false }
|
||||
this.state = { didCopy: false }
|
||||
}
|
||||
|
||||
private handleClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
|
||||
copy(this.props.value)
|
||||
this.setState({ didCopy: true, snackBarOpen: true })
|
||||
this.props.actions.global.showNotification('Copied to clipboard')
|
||||
this.setState({ didCopy: true })
|
||||
setTimeout(() => {
|
||||
this.setState({ didCopy: false })
|
||||
}, 1500)
|
||||
@@ -53,23 +48,17 @@ class Copy extends React.Component<Props, State> {
|
||||
{icon}
|
||||
</CustomIconButton>
|
||||
</span>
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={this.state.snackBarOpen}
|
||||
autoHideDuration={2000}
|
||||
onClose={() => { this.setState({ snackBarOpen: false }) }}
|
||||
>
|
||||
<SnackbarContent
|
||||
className={this.props.classes.snackbar}
|
||||
message="Copied to clipboard"
|
||||
/>
|
||||
</Snackbar>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Copy)
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: {
|
||||
global: bindActionCreators(globalActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(Copy)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EventDispatcher } from '../../../events'
|
||||
|
||||
export class TopicViewModel {
|
||||
private selected: boolean
|
||||
public change = new EventDispatcher<void, TopicViewModel>(this)
|
||||
public change = new EventDispatcher<void, TopicViewModel>()
|
||||
|
||||
public constructor() {
|
||||
this.selected = false
|
||||
|
||||
+16
-2
@@ -14,6 +14,7 @@ module.exports = {
|
||||
path: `${__dirname}/build`,
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 30000,
|
||||
@@ -47,7 +48,7 @@ module.exports = {
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
// Add '.ts' and '.tsx' as resolvable extensions.
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
extensions: ['.ts', '.tsx', '.js', '.json', '.node'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@@ -77,6 +78,15 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: {
|
||||
loader: 'node-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -84,7 +94,11 @@ module.exports = {
|
||||
new HtmlWebpackPlugin({ template: './index.html', file: './build/index.html', inject: false }),
|
||||
// new BundleAnalyzerPlugin(),
|
||||
new HardSourceWebpackPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin()
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /\.\/build\/Debug\/addon/,
|
||||
contextRegExp: /heapdump$/
|
||||
}),
|
||||
],
|
||||
|
||||
// When importing a module whose path matches one of the following, just
|
||||
|
||||
+13
-1
@@ -2354,6 +2354,13 @@ he@1.2.x:
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
heapdump@^0.3.12:
|
||||
version "0.3.12"
|
||||
resolved "https://registry.yarnpkg.com/heapdump/-/heapdump-0.3.12.tgz#5623be7816a8a92ab2d42b1b422f9e829a58da89"
|
||||
integrity sha512-OSVdkxGd9xAJNEP0RA/ZvKEzGTEjCnkvmMEGF1ScR/87tH92v5Dpv6g/Fs8GvZA9UxdMpxU1G9NEC2D0OxZjJQ==
|
||||
dependencies:
|
||||
nan "^2.11.1"
|
||||
|
||||
hmac-drbg@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
@@ -3346,7 +3353,7 @@ multicast-dns@^6.0.1:
|
||||
dns-packet "^1.3.1"
|
||||
thunky "^1.0.2"
|
||||
|
||||
nan@^2.9.2:
|
||||
nan@^2.11.1, nan@^2.9.2:
|
||||
version "2.13.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
|
||||
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
|
||||
@@ -3438,6 +3445,11 @@ node-libs-browser@^2.0.0:
|
||||
util "^0.11.0"
|
||||
vm-browserify "0.0.4"
|
||||
|
||||
node-loader@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/node-loader/-/node-loader-0.6.0.tgz#c797ef51095ed5859902b157f6384f6361e05ae8"
|
||||
integrity sha1-x5fvUQle1YWZArFX9jhPY2HgWug=
|
||||
|
||||
node-object-hash@^1.2.0:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"postinstall": "yarn build"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"license": "CC-BY-ND-4.0",
|
||||
"nyc": {
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import * as FileAsync from 'lowdb/adapters/FileAsync'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as lowdb from 'lowdb'
|
||||
import * as path from 'path'
|
||||
import { backendEvents } from '../../events'
|
||||
import {
|
||||
makeStorageAcknowledgementEvent,
|
||||
makeStorageResponseEvent,
|
||||
storageClearEvent,
|
||||
storageLoadEvent,
|
||||
storageStoreEvent,
|
||||
makeStorageAcknowledgementEvent
|
||||
} from '../../events/StorageEvents'
|
||||
storageStoreEvent
|
||||
} from '../../events/StorageEvents'
|
||||
|
||||
export default class ConfigStorage {
|
||||
private file: string
|
||||
@@ -17,6 +19,10 @@ export default class ConfigStorage {
|
||||
}
|
||||
|
||||
private async getDb() {
|
||||
const pathInfo = path.parse(this.file)
|
||||
|
||||
// Ensure that Settings dir exists
|
||||
await fs.mkdirp(pathInfo.dir)
|
||||
const adapter = new FileAsync(this.file)
|
||||
if (!this.database) {
|
||||
this.database = await lowdb(adapter)
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface DataSourceState {
|
||||
}
|
||||
|
||||
export class DataSourceStateMachine {
|
||||
public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>(this)
|
||||
public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>()
|
||||
private state: DataSourceState = {
|
||||
error: undefined,
|
||||
connected: false,
|
||||
|
||||
@@ -16,7 +16,7 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
|
||||
public isTree = true
|
||||
private cachedHash = `${Math.random()}`
|
||||
private unmergedMessages: ChangeBuffer = new ChangeBuffer()
|
||||
public didReceive = new EventDispatcher<void, Tree<ViewModel>>(this)
|
||||
public didReceive = new EventDispatcher<void, Tree<ViewModel>>()
|
||||
|
||||
constructor() {
|
||||
super(undefined, undefined)
|
||||
@@ -27,6 +27,13 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
|
||||
this.didReceive.dispatch()
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy()
|
||||
this.updateSource && this.updateSource.unsubscribe(this.subscriptionEvent, this.handleNewData)
|
||||
this.updateSource = undefined
|
||||
this.didReceive.removeAllListeners()
|
||||
}
|
||||
|
||||
public updateWithConnection(emitter: EventBusInterface, connectionId: string, nodeFilter?: (node: TreeNode<ViewModel>) => boolean) {
|
||||
this.updateSource = emitter
|
||||
this.connectionId = connectionId
|
||||
|
||||
@@ -12,9 +12,9 @@ export class TreeNode<ViewModel> {
|
||||
public collapsed = false
|
||||
public messages: number = 0
|
||||
public lastUpdate: number = Date.now()
|
||||
public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>(this)
|
||||
public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>(this)
|
||||
public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>(this)
|
||||
public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>()
|
||||
public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>()
|
||||
public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>()
|
||||
public isTree = false
|
||||
|
||||
private cachedPath?: string
|
||||
@@ -99,6 +99,21 @@ export class TreeNode<ViewModel> {
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const edge of this.edgeArray) {
|
||||
edge.target.destroy()
|
||||
}
|
||||
this.edgeArray = []
|
||||
this.edges = {}
|
||||
this.cachedChildTopics = []
|
||||
this.sourceEdge = undefined
|
||||
this.onMerge.removeAllListeners()
|
||||
this.onEdgesChange.removeAllListeners()
|
||||
this.onMessage.removeAllListeners()
|
||||
this.messageHistory = new RingBuffer<Message>(1, 1)
|
||||
this.message = undefined
|
||||
}
|
||||
|
||||
public unconnectedClone() {
|
||||
const node = new TreeNode<ViewModel>()
|
||||
node.message = this.message
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'mocha'
|
||||
import { EventDispatcher } from '../../../../events'
|
||||
|
||||
import { expect } from 'chai'
|
||||
import 'mocha'
|
||||
|
||||
describe('EventDispatcher', async () => {
|
||||
it('should dispatch', async function () {
|
||||
const dispatcher = new EventDispatcher<string, string>('me')
|
||||
it('should dispatch', async function() {
|
||||
const dispatcher = new EventDispatcher<string, string>()
|
||||
this.timeout(300)
|
||||
|
||||
setTimeout(() => dispatcher.dispatch('hello'), 5)
|
||||
@@ -18,8 +17,8 @@ describe('EventDispatcher', async () => {
|
||||
expect(response).to.eq('hello')
|
||||
})
|
||||
|
||||
it('should unsubscribe', async function () {
|
||||
const dispatcher = new EventDispatcher<string, string>('me')
|
||||
it('should unsubscribe', async function() {
|
||||
const dispatcher = new EventDispatcher<string, string>()
|
||||
this.timeout(300)
|
||||
let incrementee = 0
|
||||
const callback = (msg: any) => {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { Base64Message } from './Model/Base64Message'
|
||||
import { DataSource, MqttSource } from './DataSource'
|
||||
import { UpdateInfo } from 'builder-util-runtime'
|
||||
import {
|
||||
AddMqttConnection,
|
||||
EventDispatcher,
|
||||
MqttMessage,
|
||||
addMqttConnectionEvent,
|
||||
backendEvents,
|
||||
checkForUpdates,
|
||||
makeConnectionMessageEvent,
|
||||
makeConnectionStateEvent,
|
||||
makePublishEvent,
|
||||
removeConnection,
|
||||
updateAvailable
|
||||
} from '../../events'
|
||||
|
||||
export class ConnectionManager {
|
||||
@@ -76,17 +72,3 @@ export class ConnectionManager {
|
||||
.forEach(conenctionId => this.removeConnection(conenctionId))
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -7,20 +7,15 @@ interface CallbackStore {
|
||||
|
||||
export class EventDispatcher<Message, Dispatcher> {
|
||||
private emitter = new EventEmitter()
|
||||
private dispatcher: Dispatcher
|
||||
private callbacks: Array<CallbackStore> = []
|
||||
|
||||
constructor(dispatcher: Dispatcher) {
|
||||
this.dispatcher = dispatcher
|
||||
}
|
||||
|
||||
public dispatch(msg: Message) {
|
||||
this.emitter.emit('event', msg)
|
||||
}
|
||||
|
||||
public subscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) {
|
||||
public subscribe(callback: (msg: Message) => void) {
|
||||
const wrappedCallback = (msg: Message) => {
|
||||
callback(msg, this.dispatcher)
|
||||
callback(msg)
|
||||
}
|
||||
this.emitter.on('event', wrappedCallback)
|
||||
|
||||
@@ -30,7 +25,7 @@ export class EventDispatcher<Message, Dispatcher> {
|
||||
})
|
||||
}
|
||||
|
||||
public unsubscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) {
|
||||
public unsubscribe(callback: (msg: Message) => void) {
|
||||
const item = this.callbacks.find(store => store.callback === callback)
|
||||
if (!item) {
|
||||
return
|
||||
|
||||
@@ -28,10 +28,6 @@ export function makeConnectionStateEvent(connectionId: string): Event<DataSource
|
||||
}
|
||||
}
|
||||
|
||||
export const checkForUpdates: Event<void> = {
|
||||
topic: 'app/update/check',
|
||||
}
|
||||
|
||||
export const updateAvailable: Event<UpdateInfo> = {
|
||||
topic: 'app/update/available',
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "MQTT-Explorer",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"description": "Explore your message queues",
|
||||
"main": "dist/src/electron.js",
|
||||
"scripts": {
|
||||
@@ -32,7 +32,6 @@
|
||||
"publish": [
|
||||
"github"
|
||||
],
|
||||
"provisioningProfile": "res/MQTT_Explorer_Store_Distribution_Profile.provisionprofile",
|
||||
"entitlements": "res/entitlements.mas.plist"
|
||||
},
|
||||
"linux": {
|
||||
@@ -51,7 +50,8 @@
|
||||
"buildResources": "res",
|
||||
"output": "build"
|
||||
},
|
||||
"afterAllArtifactBuild": "./dist/scripts/afterAllArtifactBuild.js"
|
||||
"afterAllArtifactBuild": "./dist/scripts/afterAllArtifactBuild.js",
|
||||
"afterPack": "./dist/scripts/afterPack.js"
|
||||
},
|
||||
"author": "Thomas Nordquist",
|
||||
"email": "xxnerowingerxx@gmail.com",
|
||||
@@ -70,7 +70,7 @@
|
||||
"app-builder-lib": "https://github.com/thomasnordquist/app-builder-lib.git",
|
||||
"axios": "^0.18.0",
|
||||
"chai": "^4.2.0",
|
||||
"electron": "4",
|
||||
"electron": "5",
|
||||
"electron-builder": "^20.38.5",
|
||||
"fs-extra": "^7.0.1",
|
||||
"mime": "^2.4.0",
|
||||
@@ -91,6 +91,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"about-window": "^1.12.1",
|
||||
"dot-prop": "^5.0.0",
|
||||
"electron-log": "^2.2.17",
|
||||
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
|
||||
"electron-updater": "^4.0.6",
|
||||
|
||||
+28
-11
@@ -1,6 +1,7 @@
|
||||
import * as builder from 'electron-builder'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as dotProp from 'dot-prop'
|
||||
|
||||
const linuxAppImage: builder.CliOptions = {
|
||||
x64: true,
|
||||
@@ -88,20 +89,36 @@ type Packages = 'portable' | 'nsis' | 'appx' | 'AppImage' | 'snap' | 'dmg' | 'zi
|
||||
|
||||
async function buildWithOptions(options: builder.CliOptions, buildInfo: BuildInfo) {
|
||||
fs.writeFileSync(path.join(options.projectDir!, 'buildOptions.json'), JSON.stringify(buildInfo))
|
||||
ensureAppNameForPackage(options, buildInfo.package)
|
||||
|
||||
await builder.build({
|
||||
...options,
|
||||
[buildInfo.platform]: [buildInfo.package],
|
||||
})
|
||||
}
|
||||
|
||||
// AppX must hav a different name since the store name is already taken (but not used)
|
||||
function ensureAppNameForPackage(options: builder.CliOptions, packageOption: Packages) {
|
||||
const jsonLocation = path.join((options.projectDir as string), 'package.json')
|
||||
const packageJsonStr = fs.readFileSync(jsonLocation).toString()
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(jsonLocation).toString())
|
||||
packageJson.build.productName = packageOption === 'appx' ? 'MQTT-Explorer' : 'MQTT Explorer'
|
||||
fs.writeFileSync(jsonLocation, JSON.stringify(packageJson, undefined, ' '))
|
||||
|
||||
// AppX must have a different name since the store name is already taken (but not used)
|
||||
if (buildInfo.package === 'appx') {
|
||||
dotProp.set(packageJson, 'build.productName', 'MQTT-Explorer')
|
||||
}
|
||||
|
||||
if (buildInfo.platform === 'mac') {
|
||||
console.log(buildInfo.package)
|
||||
const provisioningProfile = (buildInfo.package === 'mas') ? 'res/MQTT_Explorer_Store_Distribution_Profile.provisionprofile' : 'res/MQTTExplorerdmg.provisionprofile'
|
||||
dotProp.set(packageJson, 'build.mac.provisioningProfile', provisioningProfile)
|
||||
}
|
||||
|
||||
try {
|
||||
// Write modified package.json
|
||||
fs.writeFileSync(jsonLocation, JSON.stringify(packageJson))
|
||||
await builder.build({
|
||||
...options,
|
||||
[buildInfo.platform]: [buildInfo.package],
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
// Roll back changes to package.json
|
||||
fs.writeFileSync(jsonLocation, packageJsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
function build() {
|
||||
|
||||
Binary file not shown.
@@ -13,6 +13,8 @@ export default async function(info: any) {
|
||||
await exec('sudo', ['unsquashfs', snapFile])
|
||||
await exec('sudo', ['rm', snapFile])
|
||||
await exec('sudo', ['chmod', '-R', 'g-s', 'squashfs-root'])
|
||||
|
||||
// Add command line argument to disable the sandbox
|
||||
await exec('sudo', ['snap', 'run', 'snapcraft', 'pack', 'squashfs-root', '--output', snapFile])
|
||||
await exec('sudo', ['rm', '-rf', 'squashfs-root'])
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import { chdir } from 'process'
|
||||
import { exec } from './util'
|
||||
|
||||
|
||||
interface Target {
|
||||
name: 'appImage' | string
|
||||
}
|
||||
|
||||
interface Context {
|
||||
appOutDir: string // .../build/clean/build/linux-unpacked
|
||||
outDir: string // .../build/clean/build
|
||||
targets: [Target]
|
||||
}
|
||||
|
||||
export default async function(context: Context) {
|
||||
console.log(context)
|
||||
const isLinux = context.targets.find(target => target.name === 'appImage' || target.name === 'snap')
|
||||
if (!isLinux) {
|
||||
return
|
||||
}
|
||||
|
||||
const originalDir = process.cwd()
|
||||
const dirname = context.appOutDir
|
||||
chdir(dirname)
|
||||
|
||||
await exec('mv', ['mqtt-explorer', 'mqtt-explorer.bin'])
|
||||
const wrapperScript = `#!/bin/bash
|
||||
"\${BASH_SOURCE%/*}"/mqtt-explorer.bin "$@" --no-sandbox
|
||||
`
|
||||
fs.writeFileSync('mqtt-explorer', wrapperScript)
|
||||
await exec('chmod', ['+x', 'mqtt-explorer'])
|
||||
|
||||
chdir(originalDir)
|
||||
}
|
||||
+1
-1
@@ -36,7 +36,7 @@ rm ./app.mp4 || echo no need to delete ./app.mp4
|
||||
tmux new-session -d -s record ffmpeg -f x11grab -draw_mouse 0 -video_size $DIMENSIONS -i :$SCR -r 20 -vcodec rawvideo -pix_fmt yuv420p qrawvideorgb24.yuv
|
||||
|
||||
# Start tests
|
||||
node dist/src/spec/webdriverio.js
|
||||
node dist/src/spec/demoVideo.js
|
||||
TEST_EXIT_CODE=$?
|
||||
echo "Webriver exitet with $TEST_EXIT_CODE"
|
||||
|
||||
|
||||
+4
-6
@@ -93,9 +93,8 @@ const viewMenu: MenuItemConstructorOptions = {
|
||||
click: () => {
|
||||
const window = BrowserWindow.getFocusedWindow()
|
||||
if (window) {
|
||||
window.webContents.getZoomFactor((zoom) => {
|
||||
window.webContents.setZoomFactor(Math.min(zoom + 0.1, 2.0))
|
||||
})
|
||||
const zoom = window.webContents.getZoomFactor()
|
||||
window.webContents.setZoomFactor(Math.min(zoom + 0.1, 2.0))
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -105,9 +104,8 @@ const viewMenu: MenuItemConstructorOptions = {
|
||||
click: () => {
|
||||
const window = BrowserWindow.getFocusedWindow()
|
||||
if (window) {
|
||||
window.webContents.getZoomFactor((zoom) => {
|
||||
window.webContents.setZoomFactor(Math.max(zoom - 0.1, 0.5))
|
||||
})
|
||||
const zoom = window.webContents.getZoomFactor()
|
||||
window.webContents.setZoomFactor(Math.max(zoom - 0.1, 0.5))
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
+8
-11
@@ -1,10 +1,9 @@
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { BuildInfo } from 'electron-telemetry/build/Model'
|
||||
import { UpdateInfo } from '../events'
|
||||
import { updateNotifier } from '../backend/src/index'
|
||||
|
||||
export function shouldAutoUpdate(build: BuildInfo) {
|
||||
return build.package !== 'portable'
|
||||
return build.package !== 'portable' && build.platform !== 'mac'
|
||||
}
|
||||
|
||||
export function handleAutoUpdate() {
|
||||
@@ -12,15 +11,13 @@ export function handleAutoUpdate() {
|
||||
console.log('There is an update available')
|
||||
})
|
||||
|
||||
autoUpdater.on('error', () => {
|
||||
console.log('could not update due to error')
|
||||
autoUpdater.on('error', (error) => {
|
||||
console.error('could not update due to error', error)
|
||||
})
|
||||
|
||||
updateNotifier.onCheckUpdateRequest.subscribe(() => {
|
||||
try {
|
||||
autoUpdater.checkForUpdatesAndNotify()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
try {
|
||||
autoUpdater.checkForUpdatesAndNotify()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -18,9 +18,9 @@ export async function waitForDevServer() {
|
||||
|
||||
export function loadDevTools() {
|
||||
// Redux
|
||||
BrowserWindow.addDevToolsExtension(
|
||||
path.join(os.homedir(), '/Library/Application Support/Google/Chrome/Default/Extensions/lmhkpmbekcpmknklioeibfkpmmfibljd/2.17.0_0/')
|
||||
)
|
||||
// BrowserWindow.addDevToolsExtension(
|
||||
// path.join(os.homedir(), '/Library/Application Support/Google/Chrome/Default/Extensions/lmhkpmbekcpmknklioeibfkpmmfibljd/2.17.0_0/')
|
||||
// )
|
||||
}
|
||||
|
||||
export function isDev() {
|
||||
|
||||
+5
-2
@@ -14,6 +14,8 @@ if (!isDev() && !runningUiTestOnCi()) {
|
||||
const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions)
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('--no-sandbox')
|
||||
|
||||
autoUpdater.logger = log
|
||||
log.info('App starting...')
|
||||
|
||||
@@ -33,7 +35,7 @@ async function createWindow() {
|
||||
loadDevTools()
|
||||
}
|
||||
|
||||
const iconPath = path.join(__dirname, 'icon.png')
|
||||
const iconPath = path.join(__dirname, '..', '..', 'icon.png')
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1024,
|
||||
@@ -42,13 +44,14 @@ async function createWindow() {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
devTools: true,
|
||||
sandbox: false,
|
||||
},
|
||||
icon: iconPath,
|
||||
})
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
if (mainWindow) {
|
||||
runningUiTestOnCi && mainWindow.setFullScreen(true)
|
||||
runningUiTestOnCi() && mainWindow.setFullScreen(true)
|
||||
mainWindow.show()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
createFakeMousePointer,
|
||||
hideText,
|
||||
showText,
|
||||
sleep
|
||||
sleep,
|
||||
} from './util'
|
||||
|
||||
process.on('unhandledRejection', (error: Error) => {
|
||||
@@ -55,8 +55,9 @@ async function doStuff() {
|
||||
const browser = await webdriverio.remote(options)
|
||||
await createFakeMousePointer(browser)
|
||||
|
||||
|
||||
// Wait for Username input to be visible
|
||||
await browser.$(`//label[contains(text(), "Username")]/..//input`)
|
||||
await browser.$('//label[contains(text(), "Username")]/..//input')
|
||||
const scenes = new SceneBuilder()
|
||||
await scenes.record('connect', async () => {
|
||||
await connectTo('127.0.0.1', browser)
|
||||
@@ -64,10 +65,10 @@ async function doStuff() {
|
||||
})
|
||||
|
||||
await scenes.record('topic_updates', async () => {
|
||||
await showText('Topic overview', 2000, browser, 'top')
|
||||
await sleep(2000)
|
||||
await showText('Indicate topic updates', 2000, browser, 'bottom')
|
||||
await sleep(3000)
|
||||
await showText('Topic overview', 1000, browser, 'top')
|
||||
await sleep(1000)
|
||||
await showText('Indicate topic updates', 1000, browser, 'middle')
|
||||
await sleep(1000)
|
||||
})
|
||||
|
||||
await scenes.record('numeric_plots', async () => {
|
||||
@@ -133,7 +134,7 @@ async function doStuff() {
|
||||
await scenes.record('customize_subscriptions', async () => {
|
||||
await sleep(2000)
|
||||
await disconnect(browser)
|
||||
await showText('Customize Subscriptions', 3000, browser, 'top')
|
||||
await showText('Customize Subscriptions', 1000, browser, 'top')
|
||||
await showAdvancedConnectionSettings(browser)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as webdriverio from 'webdriverio'
|
||||
import mockMqtt, { stopUpdates as stopMqttUpdates } from './mock-mqtt'
|
||||
import {
|
||||
ClassNameMapping,
|
||||
countInstancesOf,
|
||||
createFakeMousePointer,
|
||||
getHeapDump,
|
||||
setFast,
|
||||
sleep
|
||||
} from './util'
|
||||
import { clearSearch, searchTree } from './scenarios/searchTree'
|
||||
import { connectTo } from './scenarios/connect'
|
||||
import { reconnect } from './scenarios/reconnect'
|
||||
|
||||
process.on('unhandledRejection', (error: Error) => {
|
||||
console.error('unhandledRejection', error.message, error.stack)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
const runningUiTestOnCi = os.platform() === 'darwin' ? [] : ['--runningUiTestOnCi']
|
||||
|
||||
console.log(`${__dirname}/../../../node_modules/.bin/electron`)
|
||||
const options = {
|
||||
host: '127.0.0.1', // Use localhost as chrome driver server
|
||||
port: 9515, // "9515" is the port opened by chrome driver.
|
||||
capabilities: {
|
||||
browserName: 'electron',
|
||||
chromeOptions: {
|
||||
binary: `${__dirname}/../../../node_modules/.bin/electron`,
|
||||
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'].concat(runningUiTestOnCi),
|
||||
},
|
||||
windowTypes: ['app', 'webview'],
|
||||
},
|
||||
}
|
||||
|
||||
async function doStuff() {
|
||||
console.log('Waiting for MQTT Broker on port 1880 (no auth)')
|
||||
await mockMqtt()
|
||||
console.log('start webdriver')
|
||||
|
||||
const browser = await webdriverio.remote(options)
|
||||
setFast()
|
||||
await createFakeMousePointer(browser)
|
||||
|
||||
// Wait for Username input to be visible
|
||||
await browser.$('//label[contains(text(), "Username")]/..//input')
|
||||
await connectTo('127.0.0.1', browser)
|
||||
stopMqttUpdates()
|
||||
await sleep(1000, true)
|
||||
|
||||
let heapDump = await getHeapDump(browser)
|
||||
const initialTreeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.Tree)
|
||||
const initialNodeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.TreeNode)
|
||||
console.log(initialTreeOccurrances, initialNodeOccurrances)
|
||||
|
||||
await doX(3, async () => {
|
||||
await reconnect(browser)
|
||||
})
|
||||
|
||||
await sleep(1000, true)
|
||||
|
||||
await doX(15, async () => {
|
||||
await searchTree('temp', browser)
|
||||
await reconnect(browser)
|
||||
})
|
||||
|
||||
await searchTree('ab', browser)
|
||||
await clearSearch(browser)
|
||||
|
||||
await searchTree('temp', browser)
|
||||
await clearSearch(browser)
|
||||
|
||||
await sleep(1000, true)
|
||||
|
||||
await waitForGarbageCollectorToDetermineLeak(browser, initialTreeOccurrances, initialNodeOccurrances)
|
||||
}
|
||||
|
||||
async function waitForGarbageCollectorToDetermineLeak(browser: any, initialTreeOccurrances: number, initialNodeOccurrances: number) {
|
||||
let delta = -1
|
||||
let lastTreeOccurances = -1
|
||||
let lastNodeOccurances = -1
|
||||
let leak = false
|
||||
while (delta < 0) {
|
||||
if (lastTreeOccurances !== -1) {
|
||||
await sleep(10000, true)
|
||||
}
|
||||
const heapDump = await getHeapDump(browser)
|
||||
const currentTreeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.Tree)
|
||||
const currentNodeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.TreeNode)
|
||||
|
||||
// Temporary "leaks" are expected due to React Fibers memoization
|
||||
if (Math.abs(initialTreeOccurrances - currentTreeOccurrances) > 1 || Math.abs(currentNodeOccurrances - initialNodeOccurrances) > 8) {
|
||||
console.error('Possible leak detected', initialTreeOccurrances, currentTreeOccurrances, initialNodeOccurrances, currentNodeOccurrances)
|
||||
leak = true
|
||||
} else {
|
||||
leak = false
|
||||
}
|
||||
|
||||
const treeDelta = lastTreeOccurances >= 0 ? currentTreeOccurrances - lastTreeOccurances : -1
|
||||
const nodeDelta = lastTreeOccurances >= 0 ? currentNodeOccurrances - lastNodeOccurances : -1
|
||||
delta = treeDelta + nodeDelta
|
||||
|
||||
lastTreeOccurances = currentTreeOccurrances
|
||||
lastNodeOccurances = currentNodeOccurrances
|
||||
}
|
||||
|
||||
if (leak) {
|
||||
console.error('leak')
|
||||
process.exit(100)
|
||||
}
|
||||
}
|
||||
|
||||
async function doX(x: number, action: () => Promise<any>) {
|
||||
for (let i = 0; i < x; i += 1) {
|
||||
await action()
|
||||
await sleep(10, true)
|
||||
}
|
||||
}
|
||||
|
||||
doStuff()
|
||||
@@ -30,16 +30,21 @@ function temperature(base = 18, sineCoefficient = 2, offset = 0) {
|
||||
return String(Math.round(temp * 100) / 100)
|
||||
}
|
||||
|
||||
export function stop() {
|
||||
export function stopUpdates() {
|
||||
for (const interval of intervals) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
intervals = []
|
||||
}
|
||||
|
||||
export function stop() {
|
||||
stopUpdates()
|
||||
try {
|
||||
client && client.end()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const intervals: any = []
|
||||
let intervals: any = []
|
||||
|
||||
function generateData(client: mqtt.MqttClient) {
|
||||
client.publish('livingroom/lamp/state', 'on', { retain: true, qos: 0 })
|
||||
|
||||
@@ -9,5 +9,5 @@ export async function connectTo(host: string, browser: Browser<void>) {
|
||||
await browser.saveScreenshot('screen1.png')
|
||||
|
||||
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
|
||||
clickOn(connectButton, browser)
|
||||
await clickOn(connectButton, browser)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ import { clickOn } from '../util'
|
||||
import { Browser } from 'webdriverio'
|
||||
|
||||
export async function disconnect(browser: Browser<void>) {
|
||||
const connectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
|
||||
clickOn(connectButton, browser)
|
||||
const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
|
||||
await clickOn(disconnectButton, browser)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { clickOn } from '../util'
|
||||
import { Browser } from 'webdriverio'
|
||||
|
||||
export async function reconnect(browser: Browser<void>) {
|
||||
const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
|
||||
await clickOn(disconnectButton, browser)
|
||||
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
|
||||
await clickOn(connectButton, browser)
|
||||
}
|
||||
@@ -10,9 +10,11 @@ export async function showJsonFormatting(browser: Browser<void>) {
|
||||
const payloadInput = await browser.$('//*[contains(@class, "ace_text-input")]')
|
||||
await clickOn(editor, browser)
|
||||
await browser.keys(['\uE009', 'A']) // Ctrl + A
|
||||
await sleep(200)
|
||||
await browser.keys(['\uE000']) // End keyboard modifier
|
||||
await sleep(200)
|
||||
await browser.keys(['\uE003']) // Backspace
|
||||
await sleep(500)
|
||||
await sleep(200)
|
||||
await writeTextPayload(payloadInput, '{"action": "setState", "state": "on" }')
|
||||
await sleep(300)
|
||||
await clickOn(formatJsonButton, browser)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clickOn, sleep, writeText, expandTopic, moveToCenterOfElement } from '../util'
|
||||
import { clickOn, sleep, writeText, expandTopic, moveToCenterOfElement, showText } from '../util'
|
||||
import { Browser } from 'webdriverio'
|
||||
|
||||
export async function showMenu(browser: Browser<void>) {
|
||||
@@ -19,5 +19,13 @@ export async function showMenu(browser: Browser<void>) {
|
||||
await clickOn(alphabetically, browser)
|
||||
await sleep(2000)
|
||||
|
||||
await showText('Dark Mode', 1500, browser, 'top')
|
||||
await sleep(1500)
|
||||
const themeSwitch = await browser.$('//*[contains(text(), "Dark Mode")]/..//input')
|
||||
await clickOn(themeSwitch, browser)
|
||||
await sleep(3000)
|
||||
await browser.saveScreenshot('screen_dark_mode.png')
|
||||
await clickOn(themeSwitch, browser)
|
||||
|
||||
await clickOn(menuButton, browser)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ export async function showOffDiffCapability(browser: Browser<void>) {
|
||||
const diffMessages = await browser.$('#valueRendererDisplayMode-diff')
|
||||
await clickOn(diffMessages, browser)
|
||||
|
||||
// const firstEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[1]/div')
|
||||
const secondEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[2]/div')
|
||||
await clickOn(secondEntry, browser)
|
||||
await sleep(2000)
|
||||
// // const firstEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[1]/div')
|
||||
// const secondEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[2]/div')
|
||||
// await clickOn(secondEntry, browser)
|
||||
// await sleep(2000)
|
||||
}
|
||||
|
||||
+41
-6
@@ -1,17 +1,27 @@
|
||||
import * as fs from 'fs'
|
||||
import { Browser, Element } from 'webdriverio'
|
||||
export { expandTopic } from './expandTopic'
|
||||
|
||||
let fast = false
|
||||
export function setFast() {
|
||||
fast = true
|
||||
}
|
||||
|
||||
export function sleep(ms: number, required = false) {
|
||||
return new Promise((resolve) => {
|
||||
if (required) {
|
||||
setTimeout(resolve, ms)
|
||||
} else {
|
||||
setTimeout(resolve, ms)
|
||||
setTimeout(resolve, fast ? 0 : ms)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function writeText(text: string, browser: Browser<void>, delay = 0) {
|
||||
if (fast) {
|
||||
return browser.keys(text.split(''))
|
||||
}
|
||||
|
||||
for (const c of text.split('')) {
|
||||
await browser.keys([c])
|
||||
await sleep(delay)
|
||||
@@ -42,11 +52,12 @@ export async function moveToCenterOfElement(element: Element<void>, browser: Bro
|
||||
const targetX = x + width / 2
|
||||
const targetY = y + height / 2
|
||||
|
||||
const duration = 500
|
||||
const duration = fast ? 1 : 500
|
||||
|
||||
const js = `window.demo.moveMouse(${targetX}, ${targetY}, ${duration});`
|
||||
await browser.execute(js)
|
||||
await sleep(duration + 500, true)
|
||||
await sleep(duration)
|
||||
await sleep(250, true)
|
||||
|
||||
await element.moveTo()
|
||||
}
|
||||
@@ -73,17 +84,41 @@ export async function createFakeMousePointer(browser: Browser<void>) {
|
||||
export async function showText(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys = []) {
|
||||
const js = `window.demo.showMessage('${text}', '${location}', ${duration});`
|
||||
|
||||
browser.execute(js)
|
||||
await browser.execute(js)
|
||||
}
|
||||
|
||||
type HeapDump = any
|
||||
|
||||
export async function getHeapDump(browser: Browser<void>): Promise<HeapDump> {
|
||||
const filename = 'heapdump.json'
|
||||
const js = `window.demo.writeHeapdump('${filename}');`
|
||||
await browser.execute(js)
|
||||
const buffer = fs.readFileSync(filename)
|
||||
fs.unlinkSync(filename)
|
||||
|
||||
return JSON.parse(buffer.toString())
|
||||
}
|
||||
|
||||
export enum ClassNameMapping {
|
||||
TreeNode = 'TreeNode_TreeNode',
|
||||
TreeNodeComponent = 'TreeNode_TreeNodeComponent',
|
||||
Tree = 'Tree_Tree',
|
||||
}
|
||||
|
||||
export async function countInstancesOf(heapDump: HeapDump, className: ClassNameMapping): Promise<number> {
|
||||
return heapDump.nodes
|
||||
.map((idx: number) => heapDump.strings[idx])
|
||||
.filter((s: string) => s === className).length
|
||||
}
|
||||
|
||||
export async function showKeys(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys: string[] = []) {
|
||||
const js = `window.demo.showMessage('${text}', '${location}', ${duration}, ${JSON.stringify(keys)});`
|
||||
|
||||
browser.execute(js)
|
||||
await browser.execute(js)
|
||||
}
|
||||
|
||||
export async function hideText(browser: Browser<void>) {
|
||||
const js = 'window.demo.hideMessage();'
|
||||
browser.execute(js)
|
||||
await browser.execute(js)
|
||||
await sleep(600)
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,5 +11,5 @@
|
||||
"lib": ["es2017", "dom"],
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/electron.ts", "src/spec/electron.ts", "src/spec/webdriverio.ts", "scripts/*.ts"]
|
||||
"include": ["src/electron.ts", "src/spec/electron.ts", "src/spec/demoVideo.ts", "src/spec/leakTest.ts", "scripts/*.ts"]
|
||||
}
|
||||
|
||||
@@ -1131,6 +1131,13 @@ dot-prop@^4.1.0:
|
||||
dependencies:
|
||||
is-obj "^1.0.0"
|
||||
|
||||
dot-prop@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.0.0.tgz#64b7968af349c3a9f966aa12658dbd5829f6b953"
|
||||
integrity sha512-RTmaF2jx3nOBO2GvtFqjnDLycjFUMqt+2pwRx7JVYa81lDauoj9aNkyrJI2ikR58FbBIchiIlRiGG+muLJ4oHQ==
|
||||
dependencies:
|
||||
is-obj "^1.0.0"
|
||||
|
||||
dotenv-expand@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.2.0.tgz#def1f1ca5d6059d24a766e587942c21106ce1275"
|
||||
@@ -1287,10 +1294,10 @@ electron-updater@^4.0.6:
|
||||
semver "^5.6.0"
|
||||
source-map-support "^0.5.9"
|
||||
|
||||
electron@4:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-4.1.4.tgz#41ba9e041f38c25c62a7db806884410654df058f"
|
||||
integrity sha512-MelOjntJvd33izEjR6H4N/Uii7y535z/b2BuYXJGLNSHL6o1IlyhUQmfiT87kWABayERgeuYERgvsyf956OOFw==
|
||||
electron@5:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-5.0.0.tgz#d8352e2c9625b3be0112ce0c1bf5c9b6f692557e"
|
||||
integrity sha512-++emIe4vLihiYiAVL+E8DT5vSNVFEIuQCRxA+VfpDRVBcog85UB28vi4ogRmMOK3UffzKdWV6e1jqp3T0KpBoA==
|
||||
dependencies:
|
||||
"@types/node" "^10.12.18"
|
||||
electron-download "^4.1.0"
|
||||
|
||||
Reference in New Issue
Block a user