Add topic filter

This commit is contained in:
Thomas Nordquist
2019-01-21 15:07:53 +01:00
parent 4d21c63da9
commit 4c438bd00b
16 changed files with 286 additions and 53 deletions

View File

@@ -42,6 +42,10 @@ class UpdateNotifier extends React.Component<Props, {}> {
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<Props, {}> {
return url
}
public componentWillUnmount() {
rendererEvents.unsubscribeAll(updateAvailable)
}
private handleUpdate = (updateInfo: UpdateInfo) => {
this.updateInfo = updateInfo

View File

@@ -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<any>, 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<Action>, getState: () => AppState) => {
export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId, tree } = getState().connection
rendererEvents.emit(removeConnection, connectionId)
tree && tree.stopUpdating()
dispatch(showTree(undefined))
dispatch({
type: ActionTypes.CONNECTION_SET_DISCONNECTED,
})

View File

@@ -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<any>, 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,
})
}

View File

@@ -3,7 +3,7 @@ import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events'
export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const { selectedTopic } = getState().tooBigReducer
const { selectedTopic } = getState().tree
const { connectionId } = getState().connection
if (!selectedTopic || !connectionId) {

View File

@@ -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<any>, 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<any>, 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,
}
}

View File

@@ -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<Props, {}> {
}
private onChangeAutoExpand = (e: React.ChangeEvent<HTMLSelectElement>) => {
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<Props, {}> {
}
private onChangeSorting = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.setNodeOrder(e.target.value)
this.props.actions.setTopicOrder(e.target.value as TopicOrder)
}
}

View File

@@ -84,6 +84,10 @@ class Sidebar extends React.Component<Props, State> {
}
}
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<Props, State> {
const mapStateToProps = (state: AppState) => {
return {
node: state.tooBigReducer.selectedTopic,
node: state.tree.selectedTopic,
}
}

View File

@@ -19,6 +19,7 @@ interface Props {
didSelectNode?: (node: q.TreeNode) => void
connectionId?: string
tree?: q.Tree
filter: string
}
class Tree extends React.Component<Props, {}> {
@@ -46,9 +47,14 @@ class Tree extends React.Component<Props, {}> {
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<Props, {}> {
}, 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<Props, {}> {
lineHeight: '1.1',
cursor: 'default',
}
const key = `rootNode-${filter}`
return (
<div style={style}>
<TreeNode
key={key}
animateChages={true}
isRoot={true}
treeNode={this.props.tree}
treeNode={tree}
name="/"
lastUpdate={tree.lastUpdate}
collapsed={false}
key="rootNode"
lastUpdate={this.props.tree.lastUpdate}
performanceCallback={this.performanceCallback}
/>
</div>
@@ -109,7 +109,8 @@ class Tree extends React.Component<Props, {}> {
const mapStateToProps = (state: AppState) => {
return {
autoExpandLimit: state.settings.autoExpandLimit,
tree: state.connection.tree,
tree: state.tree.tree,
filter: state.tree.filter,
}
}

View File

@@ -19,7 +19,17 @@ export interface Props {
theme: Theme
}
class TreeNodeSubnodes extends React.Component<Props, {}> {
interface State {
alreadyAdded: number
}
class TreeNodeSubnodes extends React.Component<Props, State> {
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<Props, {}> {
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 => (
<div key={node.hash()}>
<TreeNode

View File

@@ -11,7 +11,7 @@ import App from './App'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const composeEnhancers = /*(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || */ compose
const store = createStore(
reducers,
composeEnhancers(

View File

@@ -0,0 +1,84 @@
import { Action } from 'redux'
import { createReducer } from './lib'
export enum TopicOrder {
none = 'none',
messages = '#messages',
abc = 'abc',
topics = '#topics',
}
export interface SettingsState {
autoExpandLimit: number
visible: boolean
topicOrder: TopicOrder
topicFilter?: string
}
export type Action = SetAutoExpandLimit | ToggleVisibility | SetTopicOrder | FilterTopics
export enum ActionTypes {
SETTINGS_SET_AUTO_EXPAND_LIMIT = 'SETTINGS_SET_AUTO_EXPAND_LIMIT',
SETTINGS_TOGGLE_VISIBILITY = 'SETTINGS_TOGGLE_VISIBILITY',
SETTINGS_SET_TOPIC_ORDER = 'SETTINGS_SET_TOPIC_ORDER',
SETTINGS_FILTER_TOPICS = 'SETTINGS_FILTER_TOPICS',
}
const initialState: SettingsState = {
autoExpandLimit: 0,
topicOrder: TopicOrder.none,
visible: false,
}
export const settingsReducer = createReducer(initialState, {
SETTINGS_SET_AUTO_EXPAND_LIMIT: setAutoExpandLimit,
SETTINGS_TOGGLE_VISIBILITY: toggleVisibility,
SETTINGS_SET_TOPIC_ORDER: setTopicOrder,
SETTINGS_FILTER_TOPICS: filterTopics,
})
function setAutoExpandLimit(state: SettingsState, action: SetAutoExpandLimit) {
return {
...state,
autoExpandLimit: action.autoExpandLimit,
}
}
export interface SetAutoExpandLimit {
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT
autoExpandLimit: number
}
function toggleVisibility(state: SettingsState, action: ToggleVisibility) {
return {
...state,
visible: !state.visible,
}
}
export interface ToggleVisibility {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY
}
function setTopicOrder(state: SettingsState, action: SetTopicOrder) {
return {
...state,
topicOrder: action.topicOrder,
}
}
export interface SetTopicOrder {
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER
topicOrder: string
}
function filterTopics(state: SettingsState, action: FilterTopics) {
return {
...state,
topicFilter: action.topicFilter,
}
}
export interface FilterTopics {
type: ActionTypes.SETTINGS_FILTER_TOPICS
topicFilter: string
}

49
app/src/reducers/Tree.ts Normal file
View File

@@ -0,0 +1,49 @@
import * as q from '../../../backend/src/Model'
import { Action } from 'redux'
import { createReducer } from './lib'
export interface TreeState {
tree?: q.Tree
selectedTopic?: q.TreeNode
filter?: string
}
export type Action = ShowTree | SelectTopic
export enum ActionTypes {
TREE_SHOW_TREE = 'TREE_SHOW_TREE',
TREE_SELECT_TOPIC = 'TREE_SELECT_TOPIC',
}
export interface ShowTree {
type: ActionTypes.TREE_SHOW_TREE
tree?: q.Tree
filter?: string
}
export interface SelectTopic {
type: ActionTypes.TREE_SELECT_TOPIC
selectedTopic?: q.TreeNode
}
const initialState: TreeState = { }
export const treeReducer = createReducer(initialState, {
TREE_SHOW_TREE: showTree,
TREE_SELECT_TOPIC: selectTopic,
})
function showTree(state: TreeState, action: ShowTree) {
return {
...state,
tree: action.tree,
filter: action.filter,
}
}
function selectTopic(state: TreeState, action: SelectTopic) {
return {
...state,
selectedTopic: action.selectedTopic,
}
}

View File

@@ -6,35 +6,33 @@ import { trackEvent } from '../tracking'
import { PublishState, publishReducer } from './Publish'
import { ConnectionState, connectionReducer } from './Connection'
import { SettingsState, settingsReducer } from './Settings'
import { TreeState, treeReducer } from './Tree'
export enum ActionTypes {
selectTopic = 'SELECT_TOPIC',
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS',
}
export interface CustomAction extends Action {
type: ActionTypes,
selectedTopic?: q.TreeNode
showUpdateNotification?: boolean
showUpdateDetails?: boolean
}
export interface AppState {
tooBigReducer: TooBigOfState
tree: TreeState
settings: SettingsState,
publish: PublishState
connection: ConnectionState
}
export interface TooBigOfState {
selectedTopic?: q.TreeNode
showUpdateNotification?: boolean
showUpdateDetails: boolean
}
const initialBigState: TooBigOfState = {
selectedTopic: undefined,
showUpdateDetails: false,
}
@@ -43,17 +41,8 @@ const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (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