diff --git a/app/src/UpdateNotifier.tsx b/app/src/UpdateNotifier.tsx index 8314778..89e6c98 100644 --- a/app/src/UpdateNotifier.tsx +++ b/app/src/UpdateNotifier.tsx @@ -42,6 +42,10 @@ class UpdateNotifier extends React.Component { rendererEvents.subscribe(updateAvailable, this.handleUpdate) } + public componentWillUnmount() { + rendererEvents.unsubscribeAll(updateAvailable) + } + private fixUrl(url: string, version: string) { if (!/^http/.test(url)) { return `https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v${version}/${url}` @@ -49,9 +53,6 @@ class UpdateNotifier extends React.Component { return url } - public componentWillUnmount() { - rendererEvents.unsubscribeAll(updateAvailable) - } private handleUpdate = (updateInfo: UpdateInfo) => { this.updateInfo = updateInfo diff --git a/app/src/actions/Connection.ts b/app/src/actions/Connection.ts index e1cb023..2058527 100644 --- a/app/src/actions/Connection.ts +++ b/app/src/actions/Connection.ts @@ -4,6 +4,7 @@ import { Dispatch } from 'redux' import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent, removeConnection } from '../../../events' import { AppState } from '../reducers' import * as q from '../../../backend/src/Model' +import { showTree } from './Tree' export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch, getState: () => AppState) => { dispatch(connecting(connectionId)) @@ -14,6 +15,7 @@ export const connect = (options: MqttOptions, connectionId: string) => (dispatch const tree = new q.Tree() tree.updateWithConnection(rendererEvents, connectionId) dispatch(connected(tree)) + dispatch(showTree(tree)) } else if (dataSourceState.error) { dispatch(showError(dataSourceState.error)) dispatch(disconnect()) @@ -36,11 +38,12 @@ export const showError = (error?: string) => ({ type: ActionTypes.CONNECTION_SET_SHOW_ERROR, }) -export const disconnect = () => (dispatch: Dispatch, getState: () => AppState) => { +export const disconnect = () => (dispatch: Dispatch, getState: () => AppState) => { const { connectionId, tree } = getState().connection rendererEvents.emit(removeConnection, connectionId) tree && tree.stopUpdating() + dispatch(showTree(undefined)) dispatch({ type: ActionTypes.CONNECTION_SET_DISCONNECTED, }) diff --git a/app/src/actions/Settings.ts b/app/src/actions/Settings.ts index e6b0971..9077e1a 100644 --- a/app/src/actions/Settings.ts +++ b/app/src/actions/Settings.ts @@ -1,4 +1,8 @@ import { Action, ActionTypes, TopicOrder } from '../reducers/Settings' +import { ActionTypes as TreeActionTypes, Action as TreeAction } from '../reducers/Tree' +import { Dispatch } from 'redux' +import { AppState } from '../reducers' +import * as q from '../../../backend/src/Model' export const setAutoExpandLimit = (autoExpandLimit: number = 0): Action => { return { @@ -20,9 +24,44 @@ export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none): Action } } -export const filterTopics = (topicFilter: string): Action => { - return { +export const filterTopics = (filterStr: string) => (dispatch: Dispatch, getState: () => AppState) => { + const topicFilter = filterStr.toLowerCase() + + dispatch({ topicFilter, type: ActionTypes.SETTINGS_FILTER_TOPICS, + }) + + const { tree } = getState().connection + if (!tree) { + return } + + if (!topicFilter) { + dispatch({ + tree, + filter: '', + type: TreeActionTypes.TREE_SHOW_TREE, + }) + + return + } + + const resultTree = tree.leafes() + .filter(leaf => leaf.path().toLowerCase().indexOf(topicFilter) !== -1) + .map((node) => { + const clone = node.unconnectedClone() + q.TreeNodeFactory.insertNodeAtPosition(node.path().split('/'), clone) + return clone.firstNode() + }) + .reduce((a: q.TreeNode, b: q.TreeNode) => { + a.updateWithNode(b) + return a + }, new q.Tree()) + + dispatch({ + tree: resultTree, + filter: topicFilter, + type: TreeActionTypes.TREE_SHOW_TREE, + }) } diff --git a/app/src/actions/Sidebar.ts b/app/src/actions/Sidebar.ts index a353904..dce8a32 100644 --- a/app/src/actions/Sidebar.ts +++ b/app/src/actions/Sidebar.ts @@ -3,7 +3,7 @@ import { AppState } from '../reducers' import { makePublishEvent, rendererEvents } from '../../../events' export const clearRetainedTopic = () => (dispatch: Dispatch, getState: () => AppState) => { - const { selectedTopic } = getState().tooBigReducer + const { selectedTopic } = getState().tree const { connectionId } = getState().connection if (!selectedTopic || !connectionId) { diff --git a/app/src/actions/Tree.ts b/app/src/actions/Tree.ts index db1cccb..80144fb 100644 --- a/app/src/actions/Tree.ts +++ b/app/src/actions/Tree.ts @@ -1,10 +1,11 @@ -import { ActionTypes, CustomAction, AppState } from '../reducers' +import { AppState } from '../reducers' +import { ActionTypes } from '../reducers/Tree' import * as q from '../../../backend/src/Model' import { Dispatch } from 'redux' import { setTopic } from './Publish' export const selectTopic = (topic: q.TreeNode) => (dispatch: Dispatch, getState: () => AppState) => { - const { selectedTopic } = getState().tooBigReducer + const { selectedTopic } = getState().tree // Update publish topic if (selectedTopic && (selectedTopic.path() === getState().publish.topic || !getState().publish.topic)) { @@ -13,6 +14,13 @@ export const selectTopic = (topic: q.TreeNode) => (dispatch: Dispatch, getS dispatch({ selectedTopic: topic, - type: ActionTypes.selectTopic, + type: ActionTypes.TREE_SELECT_TOPIC, }) } + +export const showTree = (tree?: q.Tree) => { + return { + tree, + type: ActionTypes.TREE_SHOW_TREE, + } +} diff --git a/app/src/components/Settings.tsx b/app/src/components/Settings.tsx index adfcefe..ac9677a 100644 --- a/app/src/components/Settings.tsx +++ b/app/src/components/Settings.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import * as q from '../../../backend/src/Model' import { AppState } from '../reducers' import { @@ -19,6 +18,7 @@ import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { settingsActions } from '../actions' import { TopicOrder } from '../reducers/Settings' +import Topic from './Sidebar/Topic'; const styles: StyleRulesCallback = theme => ({ drawer: { @@ -40,7 +40,7 @@ const styles: StyleRulesCallback = theme => ({ }) interface Props { - actions?: any + actions: typeof settingsActions autoExpandLimit: number visible: boolean store?: any @@ -107,7 +107,7 @@ class Settings extends React.Component { } private onChangeAutoExpand = (e: React.ChangeEvent) => { - this.props.actions.setAutoExpandLimit(e.target.value) + this.props.actions.setAutoExpandLimit(parseInt(e.target.value, 10)) } private renderNodeOrder() { @@ -133,7 +133,7 @@ class Settings extends React.Component { } private onChangeSorting = (e: React.ChangeEvent) => { - this.props.actions.setNodeOrder(e.target.value) + this.props.actions.setTopicOrder(e.target.value as TopicOrder) } } diff --git a/app/src/components/Sidebar/Sidebar.tsx b/app/src/components/Sidebar/Sidebar.tsx index f7ae981..8649407 100644 --- a/app/src/components/Sidebar/Sidebar.tsx +++ b/app/src/components/Sidebar/Sidebar.tsx @@ -84,6 +84,10 @@ class Sidebar extends React.Component { } } + public componentWillUnmount() { + this.props.node && this.removeUpdateListener(this.props.node) + } + private registerUpdateListener(node: q.TreeNode) { node.onMerge.subscribe(this.updateNode) node.onMessage.subscribe(this.updateNode) @@ -215,7 +219,7 @@ class Sidebar extends React.Component { const mapStateToProps = (state: AppState) => { return { - node: state.tooBigReducer.selectedTopic, + node: state.tree.selectedTopic, } } diff --git a/app/src/components/Tree/Tree.tsx b/app/src/components/Tree/Tree.tsx index d2cf7c5..884abfb 100644 --- a/app/src/components/Tree/Tree.tsx +++ b/app/src/components/Tree/Tree.tsx @@ -19,6 +19,7 @@ interface Props { didSelectNode?: (node: q.TreeNode) => void connectionId?: string tree?: q.Tree + filter: string } class Tree extends React.Component { @@ -46,9 +47,14 @@ class Tree extends React.Component { if (nextProps.tree) { nextProps.tree.onMerge.subscribe(this.throttledTreeUpdate) } + this.setState(this.state) } } + public componentWillUnmount() { + this.props.tree && this.props.tree.onMerge.unsubscribe(this.throttledTreeUpdate) + } + public throttledTreeUpdate = () => { if (this.updateTimer) { return @@ -68,15 +74,9 @@ class Tree extends React.Component { }, Math.max(0, timeUntilNextUpdate)) } - public componentWillUnmount() { - if (this.props.connectionId) { - const event = makeConnectionMessageEvent(this.props.connectionId) - rendererEvents.unsubscribeAll(event) - } - } - public render() { - if (!this.props.tree) { + const { tree, filter } = this.props + if (!tree) { return null } @@ -84,17 +84,17 @@ class Tree extends React.Component { lineHeight: '1.1', cursor: 'default', } - + const key = `rootNode-${filter}` return (
@@ -109,7 +109,8 @@ class Tree extends React.Component { const mapStateToProps = (state: AppState) => { return { autoExpandLimit: state.settings.autoExpandLimit, - tree: state.connection.tree, + tree: state.tree.tree, + filter: state.tree.filter, } } diff --git a/app/src/components/Tree/TreeNodeSubnodes.tsx b/app/src/components/Tree/TreeNodeSubnodes.tsx index c198793..cb947ce 100644 --- a/app/src/components/Tree/TreeNodeSubnodes.tsx +++ b/app/src/components/Tree/TreeNodeSubnodes.tsx @@ -19,7 +19,17 @@ export interface Props { theme: Theme } -class TreeNodeSubnodes extends React.Component { +interface State { + alreadyAdded: number +} + +class TreeNodeSubnodes extends React.Component { + private renderMoreAnimationFrame?: any + constructor(props: Props) { + super(props) + this.state = { alreadyAdded: 10 } + } + private sortedNodes(): q.TreeNode[] { const { topicOrder, treeNode } = this.props @@ -39,17 +49,32 @@ class TreeNodeSubnodes extends React.Component { return nodes } + private renderMore() { + this.renderMoreAnimationFrame = window.requestAnimationFrame(() => { + this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 }) + }) + } + + public componentWillUnmount() { + window.cancelAnimationFrame(this.renderMoreAnimationFrame) + } + public render() { const edges = Object.values(this.props.treeNode.edges) if (edges.length === 0 || this.props.collapsed) { return null } + if (this.state.alreadyAdded < edges.length) { + const delta = Math.min(this.state.alreadyAdded, edges.length - this.state.alreadyAdded) + this.renderMore() + } + const listItemStyle = { padding: '3px 0px 0px 8px', } - const nodes = this.sortedNodes() + const nodes = this.sortedNodes().slice(0, this.state.alreadyAdded) const listItems = nodes.map(node => (
= (state = throw Error('No initial state') } trackEvent(action.type) - console.log(action, state) - switch (action.type) { - case ActionTypes.selectTopic: - if (!action.selectedTopic) { - return state - } - return { - ...state, - selectedTopic: action.selectedTopic, - } + switch (action.type) { case ActionTypes.showUpdateNotification: return { ...state, @@ -79,6 +68,7 @@ const reducer = combineReducers({ publish: publishReducer, connection: connectionReducer, settings: settingsReducer, + tree: treeReducer, }) export default reducer diff --git a/backend/src/Model/RingBuffer.ts b/backend/src/Model/RingBuffer.ts index 7891f4c..5128102 100644 --- a/backend/src/Model/RingBuffer.ts +++ b/backend/src/Model/RingBuffer.ts @@ -10,9 +10,19 @@ export class RingBuffer { private start: number = 0 private end: number = 0 - constructor(capacity: number, maxItems = Infinity) { + constructor(capacity: number, maxItems = Infinity, ringBuffer?: RingBuffer) { this.capacity = capacity this.maxItems = maxItems + + if (ringBuffer) { + this.items = ringBuffer.toArray() + this.end = this.items.length + this.usage = this.items.length + } + } + + public clone(): RingBuffer { + return new RingBuffer(this.capacity, this.maxItems, this) } public toArray() { diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index 9093b06..a3bb506 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -16,6 +16,17 @@ export class TreeNode { private cachedLeafes?: TreeNode[] private cachedLeafMessageCount?: number + public unconnectedClone() { + const node = new TreeNode() + node.message = this.message + node.mqttMessage = this.mqttMessage + node.messageHistory = this.messageHistory.clone() + node.messages = this.messages + node.lastUpdate = this.lastUpdate + + return node + } + constructor(sourceEdge?: Edge, message?: Message) { if (sourceEdge) { this.sourceEdge = sourceEdge diff --git a/backend/src/Model/TreeNodeFactory.ts b/backend/src/Model/TreeNodeFactory.ts index 3aeba27..3ac3e03 100644 --- a/backend/src/Model/TreeNodeFactory.ts +++ b/backend/src/Model/TreeNodeFactory.ts @@ -5,21 +5,29 @@ interface HasLength { } export abstract class TreeNodeFactory { - public static fromEdgesAndValue(edgeNames: string[], value?: T): TreeNode { + public static insertNodeAtPosition(edgeNames: string[], node: TreeNode) { let currentNode: TreeNode = new Tree() + let edge for (const edgeName of edgeNames) { - const edge = new Edge(edgeName) - const newNode = new TreeNode(edge) - edge.target = newNode + edge = new Edge(edgeName) currentNode.addEdge(edge) - currentNode = newNode + currentNode = new TreeNode(edge) + edge.target = currentNode } + node.sourceEdge = edge + node.sourceEdge!.target = node + } - currentNode.setMessage({ + public static fromEdgesAndValue(edgeNames: string[], value?: T): TreeNode { + const node = new TreeNode() + node.setMessage({ value, length: value ? value.length : 0, received: new Date(), }) - return currentNode + + this.insertNodeAtPosition(edgeNames, node) + + return node } }