Redesign topic details sidebar with clickable navigation and improved mobile layout (WIP - demo video test regression) (#1011)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com> Co-authored-by: Thomas Nordquist <thomasnordquist@users.noreply.github.com>
This commit is contained in:
@@ -154,7 +154,7 @@ const styles = (theme: Theme) => ({
|
||||
height: '100%',
|
||||
padding: '8px',
|
||||
flex: 1,
|
||||
overflow: 'hidden scroll',
|
||||
overflow: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
327
app/src/components/Sidebar/DetailsTab.tsx
Normal file
327
app/src/components/Sidebar/DetailsTab.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Box, Typography, IconButton, Chip, Tooltip } from '@mui/material'
|
||||
import { Theme } from '@mui/material/styles'
|
||||
import { withStyles } from '@mui/styles'
|
||||
import { AppState } from '../../reducers'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { sidebarActions } from '../../actions'
|
||||
import Copy from '../helper/Copy'
|
||||
import Save from '../helper/Save'
|
||||
import DateFormatter from '../helper/DateFormatter'
|
||||
import ValueRenderer from './ValueRenderer/ValueRenderer'
|
||||
import MessageHistory from './ValueRenderer/MessageHistory'
|
||||
import ActionButtons from './ValueRenderer/ActionButtons'
|
||||
import DeleteSelectedTopicButton from './ValueRenderer/DeleteSelectedTopicButton'
|
||||
import { useDecoder } from '../hooks/useDecoder'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'
|
||||
import SimpleBreadcrumb from './SimpleBreadcrumb'
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode<any>
|
||||
classes: any
|
||||
compareMessage?: q.Message
|
||||
sidebarActions: typeof sidebarActions
|
||||
}
|
||||
|
||||
function DetailsTab(props: Props) {
|
||||
const { node, compareMessage, classes } = props
|
||||
const decodeMessage = useDecoder(node)
|
||||
|
||||
const getDecodedValue = useCallback(() => {
|
||||
return node?.message && decodeMessage(node.message)?.message?.toUnicodeString()
|
||||
}, [node, decodeMessage])
|
||||
|
||||
const getData = () => {
|
||||
if (node?.message && node.message.payload) {
|
||||
return node.message.payload.base64Message
|
||||
}
|
||||
}
|
||||
|
||||
const handleMessageHistorySelect = useCallback(
|
||||
(message: q.Message) => {
|
||||
if (message !== compareMessage) {
|
||||
props.sidebarActions.setCompareMessage(message)
|
||||
} else {
|
||||
props.sidebarActions.setCompareMessage(undefined)
|
||||
}
|
||||
},
|
||||
[compareMessage, props.sidebarActions]
|
||||
)
|
||||
|
||||
const deleteTopic = useCallback(
|
||||
(topic?: q.TreeNode<any>, recursive: boolean = false) => {
|
||||
if (!topic) {
|
||||
return
|
||||
}
|
||||
props.sidebarActions.clearTopic(topic, recursive)
|
||||
},
|
||||
[props.sidebarActions]
|
||||
)
|
||||
|
||||
if (!node) {
|
||||
return (
|
||||
<Box className={classes.emptyState}>
|
||||
<Typography variant="body2" color="textSecondary" align="center">
|
||||
Select a topic to view details
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const [value] =
|
||||
node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
|
||||
const hasValue = Boolean(value)
|
||||
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
{/* Topic Section - Breadcrumb with actions */}
|
||||
<Box className={classes.topicSection}>
|
||||
<SimpleBreadcrumb node={node} />
|
||||
<Box className={classes.topicActions}>
|
||||
<Copy value={node.path()} />
|
||||
{node.childTopicCount() === 0 && (
|
||||
<Tooltip title="Delete this topic">
|
||||
<IconButton size="small" onClick={() => deleteTopic(node, false)} className={classes.iconButton}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{node.childTopicCount() > 0 && (
|
||||
<Tooltip title="Delete topic and all subtopics">
|
||||
<IconButton size="small" onClick={() => deleteTopic(node, true)} className={classes.iconButton}>
|
||||
<DeleteSweepIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Value Section - Simplified layout */}
|
||||
{hasValue && (
|
||||
<Box className={classes.valueSection}>
|
||||
{/* Metadata bar - Date on left, Retained/QoS on right */}
|
||||
<Box className={classes.metadataBar}>
|
||||
<Box className={classes.metadataLeft}>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
<DateFormatter date={node.message!.received} />
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box className={classes.metadataRight}>
|
||||
{node.message?.retain && (
|
||||
<Chip label="Retained" size="small" variant="outlined" color="primary" className={classes.chip} />
|
||||
)}
|
||||
<Chip
|
||||
label={`QoS ${node.message?.qos ?? 0}`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
className={classes.chip}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Action toolbar */}
|
||||
<Box className={classes.actionToolbar}>
|
||||
<Typography variant="subtitle2" className={classes.valueTitle}>
|
||||
Current Value
|
||||
</Typography>
|
||||
<Box className={classes.actionButtons}>
|
||||
<ActionButtons />
|
||||
</Box>
|
||||
<Box className={classes.valueActions}>
|
||||
<Copy getValue={getDecodedValue} />
|
||||
<Save getData={getData} />
|
||||
{node.message?.retain && <DeleteSelectedTopicButton />}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Value Display */}
|
||||
<Box className={classes.valueDisplay}>
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<ValueRenderer treeNode={node} message={node.message!} compareWith={compareMessage} />
|
||||
</React.Suspense>
|
||||
</Box>
|
||||
|
||||
{/* Message History */}
|
||||
<Box className={classes.historySection}>
|
||||
<MessageHistory onSelect={handleMessageHistorySelect} selected={compareMessage} node={node} />
|
||||
</Box>
|
||||
|
||||
{/* Stats Section - Moved to end of value section */}
|
||||
<Box className={classes.statsSection}>
|
||||
<Box className={classes.statsGrid}>
|
||||
<Box className={classes.statItem}>
|
||||
<Typography variant="body2" color="textSecondary" className={classes.statLabel}>
|
||||
Messages
|
||||
</Typography>
|
||||
<Typography variant="h6" className={classes.statValue}>
|
||||
{node.messages}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box className={classes.statItem}>
|
||||
<Typography variant="body2" color="textSecondary" className={classes.statLabel}>
|
||||
Subtopics
|
||||
</Typography>
|
||||
<Typography variant="h6" className={classes.statValue}>
|
||||
{node.childTopicCount()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box className={classes.statItem}>
|
||||
<Typography variant="body2" color="textSecondary" className={classes.statLabel}>
|
||||
Total
|
||||
</Typography>
|
||||
<Typography variant="h6" className={classes.statValue}>
|
||||
{node.leafMessageCount()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as 'column',
|
||||
gap: theme.spacing(3),
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
emptyState: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '200px',
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
// Topic section
|
||||
topicSection: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
gap: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
topicActions: {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(0.5),
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
},
|
||||
iconButton: {
|
||||
padding: theme.spacing(0.5),
|
||||
},
|
||||
// Stats section
|
||||
statsSection: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
statsGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: theme.spacing(1.5),
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
statItem: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as 'column',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(1.5, 1),
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase' as 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
},
|
||||
statValue: {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 600,
|
||||
lineHeight: 1,
|
||||
},
|
||||
// Value section
|
||||
valueSection: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as 'column',
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
metadataBar: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
flexWrap: 'wrap' as 'wrap',
|
||||
padding: theme.spacing(1),
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
metadataLeft: {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap' as 'wrap',
|
||||
},
|
||||
metadataRight: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
chip: {
|
||||
height: '24px',
|
||||
},
|
||||
actionToolbar: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
flexWrap: 'wrap' as 'wrap',
|
||||
},
|
||||
valueTitle: {
|
||||
fontWeight: 600,
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: '0.875rem',
|
||||
textTransform: 'uppercase' as 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
actionButtons: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
valueActions: {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(0.5),
|
||||
alignItems: 'center',
|
||||
},
|
||||
valueDisplay: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
historySection: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
})
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
compareMessage: state.sidebar.get('compareMessage'),
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
sidebarActions: bindActionCreators(sidebarActions, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(DetailsTab))
|
||||
53
app/src/components/Sidebar/PublishTab.tsx
Normal file
53
app/src/components/Sidebar/PublishTab.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { Theme } from '@mui/material/styles'
|
||||
import { withStyles } from '@mui/styles'
|
||||
|
||||
const Publish = React.lazy(() => import('./Publish/Publish'))
|
||||
|
||||
interface Props {
|
||||
connectionId?: string
|
||||
classes: any
|
||||
}
|
||||
|
||||
function PublishTab(props: Props) {
|
||||
const { classes } = props
|
||||
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Box className={classes.header}>
|
||||
<Typography variant="subtitle2" className={classes.title}>
|
||||
Publish Message
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Send messages to MQTT topics
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Publish connectionId={props.connectionId} />
|
||||
</React.Suspense>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as 'column',
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
header: {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
title: {
|
||||
fontWeight: 600,
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: '0.875rem',
|
||||
textTransform: 'uppercase' as 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
marginBottom: theme.spacing(0.5),
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(PublishTab)
|
||||
@@ -1,24 +1,19 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import NodeStats from './NodeStats'
|
||||
import ValuePanel from './ValueRenderer/ValuePanel'
|
||||
const ValuePanelAny = ValuePanel as any
|
||||
import { AppState } from '../../reducers'
|
||||
import { AccordionDetails } from '@mui/material'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { settingsActions, sidebarActions } from '../../actions'
|
||||
import { Theme } from '@mui/material/styles'
|
||||
import { withStyles } from '@mui/styles'
|
||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||
import TopicPanel from './TopicPanel/TopicPanel'
|
||||
import Panel from './Panel'
|
||||
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
|
||||
import { Tabs, Tab, Box, useMediaQuery, useTheme } from '@mui/material'
|
||||
import DetailsTab from './DetailsTab'
|
||||
import PublishTab from './PublishTab'
|
||||
|
||||
const throttle = require('lodash.throttle')
|
||||
|
||||
const Publish = React.lazy(() => import('./Publish/Publish'))
|
||||
|
||||
interface Props {
|
||||
nodePath?: string
|
||||
tree?: q.Tree<TopicViewModel>
|
||||
@@ -49,27 +44,55 @@ function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) {
|
||||
}, [node])
|
||||
}
|
||||
|
||||
function Sidebar(props: Props) {
|
||||
function SidebarNew(props: Props) {
|
||||
const { classes, tree, nodePath } = props
|
||||
const node = usePollingToFetchTreeNode(tree, nodePath || '')
|
||||
useUpdateNodeWhenNodeReceivesUpdates(node)
|
||||
const [tabValue, setTabValue] = useState(0)
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
|
||||
return (
|
||||
<div id="Sidebar" className={classes.drawer}>
|
||||
<div>
|
||||
<TopicPanel node={node} />
|
||||
<ValuePanelAny lastUpdate={node ? node.lastUpdate : 0} />
|
||||
<Panel>
|
||||
<span>Publish</span>
|
||||
<Publish connectionId={props.connectionId} />
|
||||
</Panel>
|
||||
<Panel detailsHidden={!node}>
|
||||
<span>Stats</span>
|
||||
<AccordionDetails className={classes.details}>
|
||||
<NodeStats node={node} />
|
||||
</AccordionDetails>
|
||||
</Panel>
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue)
|
||||
}
|
||||
|
||||
// On mobile, don't show tabs (mobile already has Topics/Details tabs at app level)
|
||||
// Just show the content directly
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div id="Sidebar" className={classes.root}>
|
||||
<Box className={classes.mobileContent}>
|
||||
<DetailsTab node={node} />
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Desktop: show tabs for Details/Publish
|
||||
return (
|
||||
<div id="Sidebar" className={classes.root}>
|
||||
<Box className={classes.tabsContainer}>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
variant="fullWidth"
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
className={classes.tabs}
|
||||
>
|
||||
<Tab label="Details" className={classes.tab} />
|
||||
<Tab label="Publish" className={classes.tab} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<Box className={classes.tabContent}>
|
||||
<Box sx={{ display: tabValue === 0 ? 'block' : 'none' }}>
|
||||
<DetailsTab node={node} />
|
||||
</Box>
|
||||
<Box sx={{ display: tabValue === 1 ? 'block' : 'none' }}>
|
||||
<PublishTab connectionId={props.connectionId} />
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -90,13 +113,38 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
drawer: {
|
||||
display: 'block' as 'block',
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
details: {
|
||||
padding: '0px 16px 8px 8px',
|
||||
display: 'block',
|
||||
tabsContainer: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
tabs: {
|
||||
minHeight: '48px',
|
||||
},
|
||||
tab: {
|
||||
minHeight: '48px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
textTransform: 'none' as 'none',
|
||||
padding: theme.spacing(1.5, 2),
|
||||
},
|
||||
tabContent: {
|
||||
flex: 1,
|
||||
overflowY: 'auto' as 'auto',
|
||||
overflowX: 'hidden' as 'hidden',
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
mobileContent: {
|
||||
flex: 1,
|
||||
overflowY: 'auto' as 'auto',
|
||||
overflowX: 'hidden' as 'hidden',
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(SidebarNew))
|
||||
|
||||
87
app/src/components/Sidebar/SimpleBreadcrumb.tsx
Normal file
87
app/src/components/Sidebar/SimpleBreadcrumb.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import { Link } from '@mui/material'
|
||||
import { Theme } from '@mui/material/styles'
|
||||
import { withStyles } from '@mui/styles'
|
||||
import { treeActions } from '../../actions'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode<any>
|
||||
classes: any
|
||||
actions: typeof treeActions
|
||||
}
|
||||
|
||||
function SimpleBreadcrumb(props: Props) {
|
||||
const { node, classes, actions } = props
|
||||
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
const branch = node.branch()
|
||||
const breadcrumbNodes = branch
|
||||
.map(n => n.sourceEdge)
|
||||
.filter(edge => Boolean(edge) && edge?.target)
|
||||
.map(edge => ({ name: edge?.name || '', target: edge!.target }))
|
||||
.filter(item => item.name !== '')
|
||||
|
||||
if (breadcrumbNodes.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.breadcrumbContainer}>
|
||||
{breadcrumbNodes.map((item, index) => (
|
||||
<span key={item.target.hash()}>
|
||||
{index > 0 && <span className={classes.separator}> / </span>}
|
||||
<Link
|
||||
component="button"
|
||||
variant="h6"
|
||||
className={classes.breadcrumbLink}
|
||||
onClick={() => actions.selectTopic(item.target)}
|
||||
underline="hover"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
breadcrumbContainer: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap' as 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: 0,
|
||||
},
|
||||
breadcrumbLink: {
|
||||
fontSize: '1rem',
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.primary,
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left' as 'left',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
padding: 0,
|
||||
lineHeight: 1.5,
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
separator: {
|
||||
color: theme.palette.text.secondary,
|
||||
userSelect: 'none' as 'none',
|
||||
},
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: bindActionCreators(treeActions, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withStyles(styles)(SimpleBreadcrumb))
|
||||
@@ -36,15 +36,17 @@ function ActionButtons(props: {
|
||||
>
|
||||
<ToggleButton className={props.classes.toggleButton} value="diff" id="valueRendererDisplayMode-diff">
|
||||
<Tooltip title="Show difference between the current and the last message">
|
||||
<span>
|
||||
<span className={props.classes.buttonContent}>
|
||||
<Code className={props.classes.toggleButtonIcon} />
|
||||
<span className={props.classes.buttonText}>Diff</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
<ToggleButton className={props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
|
||||
<Tooltip title="Raw / formatted JSON / formatted sparkplugb protojson">
|
||||
<span>
|
||||
<span className={props.classes.buttonContent}>
|
||||
<Reorder className={props.classes.toggleButtonIcon} />
|
||||
<span className={props.classes.buttonText}>Raw</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
@@ -55,9 +57,20 @@ function ActionButtons(props: {
|
||||
const styles = (theme: Theme) => ({
|
||||
toggleButton: {
|
||||
height: '36px',
|
||||
padding: theme.spacing(0.5, 1.5),
|
||||
},
|
||||
toggleButtonIcon: {
|
||||
verticalAlign: 'middle',
|
||||
fontSize: '1.25rem',
|
||||
},
|
||||
buttonContent: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: '0.875rem',
|
||||
textTransform: 'none' as 'none',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user