Add About dialog to sidebar with license compliance tests (#971)
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:
@@ -21,6 +21,12 @@ export const toggleSettingsVisibility = () => (dispatch: Dispatch<any>) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const toggleAboutDialogVisibility = () => (dispatch: Dispatch<any>) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.toggleAboutDialogVisibility,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const requestConfirmation = (title: string, inquiry: string) => (dispatch: Dispatch<any>) => {
|
export const requestConfirmation = (title: string, inquiry: string) => (dispatch: Dispatch<any>) => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const confirmationRequest = {
|
const confirmationRequest = {
|
||||||
|
|||||||
133
app/src/components/AboutDialog.spec.ts
Normal file
133
app/src/components/AboutDialog.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AboutDialog License Compliance Tests
|
||||||
|
*
|
||||||
|
* These tests verify that the About dialog properly displays required
|
||||||
|
* attribution information as mandated by the CC-BY-ND-4.0 license.
|
||||||
|
*
|
||||||
|
* CC-BY-ND-4.0 (Creative Commons Attribution-NoDerivatives 4.0 International):
|
||||||
|
* - BY (Attribution): Must credit the original author (Thomas Nordquist)
|
||||||
|
* - ND (NoDerivatives): Cannot create derivative works without permission
|
||||||
|
* - Requires: Author name, license notice, and LICENSE NOTICE comment
|
||||||
|
*/
|
||||||
|
describe('AboutDialog License Compliance', () => {
|
||||||
|
const aboutDialogPath = path.join(__dirname, 'AboutDialog.tsx')
|
||||||
|
let aboutDialogContent: string
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
aboutDialogContent = fs.readFileSync(aboutDialogPath, 'utf-8')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should contain the license notice in the component file', () => {
|
||||||
|
expect(aboutDialogContent).to.include('LICENSE NOTICE')
|
||||||
|
expect(aboutDialogContent).to.include('CC-BY-ND-4.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display author attribution (Thomas Nordquist)', () => {
|
||||||
|
// Verify the author is displayed in the component
|
||||||
|
expect(aboutDialogContent).to.match(/Author.*Thomas Nordquist/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display CC-BY-ND-4.0 license', () => {
|
||||||
|
// Verify the license is displayed in the component
|
||||||
|
expect(aboutDialogContent).to.match(/License.*CC-BY-ND-4.0/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have data-testid attributes for license verification', () => {
|
||||||
|
// These attributes allow automated testing of the rendered component
|
||||||
|
expect(aboutDialogContent).to.include('data-testid="about-author"')
|
||||||
|
expect(aboutDialogContent).to.include('data-testid="about-license"')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('License Violation Detection', () => {
|
||||||
|
it('removing author attribution violates CC-BY-ND-4.0 license', () => {
|
||||||
|
// CC-BY-ND-4.0 Attribution (BY) requirement:
|
||||||
|
// Must credit the original author "Thomas Nordquist"
|
||||||
|
const hasAuthor = aboutDialogContent.includes('Thomas Nordquist')
|
||||||
|
|
||||||
|
if (!hasAuthor) {
|
||||||
|
throw new Error(
|
||||||
|
'LICENSE VIOLATION: Author attribution "Thomas Nordquist" is missing. ' +
|
||||||
|
'This violates the CC-BY-ND-4.0 Attribution (BY) requirement. ' +
|
||||||
|
'The author must be properly credited in the About dialog.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(hasAuthor).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removing license notice violates CC-BY-ND-4.0 license', () => {
|
||||||
|
// CC-BY-ND-4.0 requires the license identifier to be displayed
|
||||||
|
const hasLicense = aboutDialogContent.includes('CC-BY-ND-4.0')
|
||||||
|
|
||||||
|
if (!hasLicense) {
|
||||||
|
throw new Error(
|
||||||
|
'LICENSE VIOLATION: License notice "CC-BY-ND-4.0" is missing. ' +
|
||||||
|
'This violates CC-BY-ND-4.0 license notice requirements. ' +
|
||||||
|
'The license identifier must be displayed in the About dialog.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(hasLicense).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removing LICENSE NOTICE comment violates CC-BY-ND-4.0 license', () => {
|
||||||
|
// CC-BY-ND-4.0 requires attribution notice in source code
|
||||||
|
const hasLicenseNotice = aboutDialogContent.includes('LICENSE NOTICE')
|
||||||
|
|
||||||
|
if (!hasLicenseNotice) {
|
||||||
|
throw new Error(
|
||||||
|
'LICENSE VIOLATION: LICENSE NOTICE comment is missing from source code. ' +
|
||||||
|
'This violates CC-BY-ND-4.0 source code attribution requirements. ' +
|
||||||
|
'The LICENSE NOTICE comment must be retained in the component source.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(hasLicenseNotice).to.be.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AboutDialog Functionality Tests
|
||||||
|
*
|
||||||
|
* These tests verify that the About dialog is accessible and functional.
|
||||||
|
*/
|
||||||
|
describe('AboutDialog Accessibility', () => {
|
||||||
|
const detailsTabPath = path.join(__dirname, 'Sidebar', 'DetailsTab.tsx')
|
||||||
|
const appPath = path.join(__dirname, 'App.tsx')
|
||||||
|
|
||||||
|
it('should be accessible from the DetailsTab component', () => {
|
||||||
|
const detailsTabContent = fs.readFileSync(detailsTabPath, 'utf-8')
|
||||||
|
|
||||||
|
// Verify the About button exists in DetailsTab
|
||||||
|
expect(detailsTabContent).to.include('About')
|
||||||
|
|
||||||
|
// Verify it triggers the toggle action
|
||||||
|
expect(detailsTabContent).to.include('toggleAboutDialogVisibility')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be integrated in the App component', () => {
|
||||||
|
const appContent = fs.readFileSync(appPath, 'utf-8')
|
||||||
|
|
||||||
|
// Verify AboutDialog is imported
|
||||||
|
expect(appContent).to.include('AboutDialog')
|
||||||
|
|
||||||
|
// Verify it's rendered with state
|
||||||
|
expect(appContent).to.include('aboutDialogVisible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have About button with Info icon in DetailsTab', () => {
|
||||||
|
const detailsTabContent = fs.readFileSync(detailsTabPath, 'utf-8')
|
||||||
|
|
||||||
|
// Verify the button text
|
||||||
|
expect(detailsTabContent).to.include('About MQTT Explorer')
|
||||||
|
|
||||||
|
// Verify Info icon is used
|
||||||
|
expect(detailsTabContent).to.match(/import.*Info.*from.*@mui\/icons-material/)
|
||||||
|
})
|
||||||
|
})
|
||||||
136
app/src/components/AboutDialog.tsx
Normal file
136
app/src/components/AboutDialog.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Link,
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
} from '@mui/material'
|
||||||
|
import { rendererRpc, getAppVersion } from '../../../events'
|
||||||
|
import FavoriteIcon from '@mui/icons-material/Favorite'
|
||||||
|
|
||||||
|
// Fallback version if RPC call fails (e.g., in browser mode during initialization)
|
||||||
|
const FALLBACK_VERSION = '0.4.0-beta.5'
|
||||||
|
|
||||||
|
interface AboutDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* About Dialog Component
|
||||||
|
*
|
||||||
|
* This component displays application information including version, author, and license.
|
||||||
|
*
|
||||||
|
* LICENSE NOTICE (CC-BY-ND-4.0):
|
||||||
|
* This component is licensed under Creative Commons Attribution-NoDerivatives 4.0 International.
|
||||||
|
*
|
||||||
|
* REQUIRED ATTRIBUTION:
|
||||||
|
* - Author: Thomas Nordquist
|
||||||
|
* - License: CC-BY-ND-4.0
|
||||||
|
*
|
||||||
|
* RESTRICTIONS:
|
||||||
|
* - BY (Attribution): You must give appropriate credit to the author
|
||||||
|
* - ND (NoDerivatives): You may not create derivative works without permission
|
||||||
|
*
|
||||||
|
* Removing or modifying this attribution violates the license terms.
|
||||||
|
* For full license text: https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
||||||
|
*/
|
||||||
|
export function AboutDialog(props: AboutDialogProps) {
|
||||||
|
const [version, setVersion] = React.useState<string>(FALLBACK_VERSION)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Fetch version from backend
|
||||||
|
rendererRpc
|
||||||
|
.call(getAppVersion, undefined, 5000)
|
||||||
|
.then(v => setVersion(v))
|
||||||
|
.catch(() => {
|
||||||
|
// Fallback to hardcoded version if RPC fails
|
||||||
|
console.warn('Failed to fetch app version, using fallback')
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>About MQTT Explorer</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
<strong>Version:</strong> {version}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" gutterBottom data-testid="about-license">
|
||||||
|
<strong>License:</strong> CC-BY-ND-4.0
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
<strong>Description:</strong> Explore your message queues
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }} data-testid="about-author">
|
||||||
|
<Avatar
|
||||||
|
src="https://github.com/thomasnordquist.png"
|
||||||
|
alt="Thomas Nordquist"
|
||||||
|
sx={{ width: 56, height: 56 }}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 500 }}>
|
||||||
|
Thomas Nordquist
|
||||||
|
</Typography>
|
||||||
|
<Link
|
||||||
|
href="https://github.com/thomasnordquist"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
sx={{ display: 'block', fontSize: '0.875rem' }}
|
||||||
|
>
|
||||||
|
@thomasnordquist
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="https://paypal.me/ThomasNordquist"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
mt: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FavoriteIcon sx={{ fontSize: '1rem', color: 'error.main' }} />
|
||||||
|
Support via PayPal
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
<strong>Homepage:</strong>{' '}
|
||||||
|
<Link href="https://thomasnordquist.github.io/MQTT-Explorer/" target="_blank" rel="noopener noreferrer">
|
||||||
|
https://thomasnordquist.github.io/MQTT-Explorer/
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
<strong>Bug Report:</strong>{' '}
|
||||||
|
<Link
|
||||||
|
href="https://github.com/thomasnordquist/MQTT-Explorer/issues"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://github.com/thomasnordquist/MQTT-Explorer/issues
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={props.onClose} color="primary" variant="contained">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import Notification from './Layout/Notification'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import TitleBar from './Layout/TitleBar'
|
import TitleBar from './Layout/TitleBar'
|
||||||
import UpdateNotifier from './UpdateNotifier'
|
import UpdateNotifier from './UpdateNotifier'
|
||||||
|
import { AboutDialog } from './AboutDialog'
|
||||||
import { AppState } from '../reducers'
|
import { AppState } from '../reducers'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { ConfirmationRequest } from '../reducers/Global'
|
import { ConfirmationRequest } from '../reducers/Global'
|
||||||
@@ -28,6 +29,7 @@ interface Props {
|
|||||||
settingsActions: typeof settingsActions
|
settingsActions: typeof settingsActions
|
||||||
launching: boolean
|
launching: boolean
|
||||||
confirmationRequests: Array<ConfirmationRequest>
|
confirmationRequests: Array<ConfirmationRequest>
|
||||||
|
aboutDialogVisible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends React.PureComponent<Props, {}> {
|
class App extends React.PureComponent<Props, {}> {
|
||||||
@@ -75,6 +77,10 @@ class App extends React.PureComponent<Props, {}> {
|
|||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
|
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
|
||||||
|
<AboutDialog
|
||||||
|
open={this.props.aboutDialogVisible}
|
||||||
|
onClose={() => this.props.actions.toggleAboutDialogVisibility()}
|
||||||
|
/>
|
||||||
{this.renderNotification()}
|
{this.renderNotification()}
|
||||||
<React.Suspense fallback={<div></div>}>
|
<React.Suspense fallback={<div></div>}>
|
||||||
<Settings {...anyProps} />
|
<Settings {...anyProps} />
|
||||||
@@ -158,6 +164,7 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
highlightTopicUpdates: state.settings.get('highlightTopicUpdates'),
|
highlightTopicUpdates: state.settings.get('highlightTopicUpdates'),
|
||||||
launching: state.globalState.get('launching'),
|
launching: state.globalState.get('launching'),
|
||||||
confirmationRequests: state.globalState.get('confirmationRequests'),
|
confirmationRequests: state.globalState.get('confirmationRequests'),
|
||||||
|
aboutDialogVisible: state.globalState.get('aboutDialogVisible'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import * as q from '../../../../backend/src/Model'
|
import * as q from '../../../../backend/src/Model'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Box, Typography, IconButton, Chip, Tooltip } from '@mui/material'
|
import { Box, Typography, IconButton, Chip, Tooltip, Button } from '@mui/material'
|
||||||
import { Theme } from '@mui/material/styles'
|
import { Theme } from '@mui/material/styles'
|
||||||
import { withStyles } from '@mui/styles'
|
import { withStyles } from '@mui/styles'
|
||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { sidebarActions } from '../../actions'
|
import { sidebarActions, globalActions } from '../../actions'
|
||||||
import Copy from '../helper/Copy'
|
import Copy from '../helper/Copy'
|
||||||
import Save from '../helper/Save'
|
import Save from '../helper/Save'
|
||||||
import DateFormatter from '../helper/DateFormatter'
|
import DateFormatter from '../helper/DateFormatter'
|
||||||
@@ -17,6 +17,7 @@ import DeleteSelectedTopicButton from './ValueRenderer/DeleteSelectedTopicButton
|
|||||||
import { useDecoder } from '../hooks/useDecoder'
|
import { useDecoder } from '../hooks/useDecoder'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'
|
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'
|
||||||
|
import Info from '@mui/icons-material/Info'
|
||||||
import SimpleBreadcrumb from './SimpleBreadcrumb'
|
import SimpleBreadcrumb from './SimpleBreadcrumb'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,6 +25,7 @@ interface Props {
|
|||||||
classes: any
|
classes: any
|
||||||
compareMessage?: q.Message
|
compareMessage?: q.Message
|
||||||
sidebarActions: typeof sidebarActions
|
sidebarActions: typeof sidebarActions
|
||||||
|
globalActions: typeof globalActions
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailsTab(props: Props) {
|
function DetailsTab(props: Props) {
|
||||||
@@ -67,6 +69,19 @@ function DetailsTab(props: Props) {
|
|||||||
<Typography variant="body2" color="textSecondary" align="center">
|
<Typography variant="body2" color="textSecondary" align="center">
|
||||||
Select a topic to view details
|
Select a topic to view details
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{/* About Button - always show even when no topic selected */}
|
||||||
|
<Box className={classes.aboutSection}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Info />}
|
||||||
|
onClick={() => props.globalActions.toggleAboutDialogVisibility()}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
About MQTT Explorer
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -180,6 +195,19 @@ function DetailsTab(props: Props) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* About Section - always visible at bottom */}
|
||||||
|
<Box className={classes.aboutSection}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Info />}
|
||||||
|
onClick={() => props.globalActions.toggleAboutDialogVisibility()}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
About MQTT Explorer
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -195,10 +223,17 @@ const styles = (theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexDirection: 'column' as 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
minHeight: '200px',
|
minHeight: '200px',
|
||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
|
gap: theme.spacing(3),
|
||||||
|
},
|
||||||
|
aboutSection: {
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
},
|
},
|
||||||
// Topic section
|
// Topic section
|
||||||
topicSection: {
|
topicSection: {
|
||||||
@@ -321,6 +356,7 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
const mapDispatchToProps = (dispatch: any) => {
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
return {
|
return {
|
||||||
sidebarActions: bindActionCreators(sidebarActions, dispatch),
|
sidebarActions: bindActionCreators(sidebarActions, dispatch),
|
||||||
|
globalActions: bindActionCreators(globalActions, dispatch),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import * as q from '../../../../backend/src/Model'
|
import * as q from '../../../../backend/src/Model'
|
||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
|
<<<<<<< HEAD
|
||||||
|
import { AccordionDetails, Button } from '@mui/material'
|
||||||
|
=======
|
||||||
|
>>>>>>> origin/master
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { settingsActions, sidebarActions } from '../../actions'
|
import { globalActions, settingsActions, sidebarActions } from '../../actions'
|
||||||
import { Theme } from '@mui/material/styles'
|
import { Theme } from '@mui/material/styles'
|
||||||
import { withStyles } from '@mui/styles'
|
import { withStyles } from '@mui/styles'
|
||||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||||
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
|
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
|
||||||
|
<<<<<<< HEAD
|
||||||
|
import Info from '@mui/icons-material/Info'
|
||||||
|
=======
|
||||||
import { Tabs, Tab, Box, useMediaQuery, useTheme } from '@mui/material'
|
import { Tabs, Tab, Box, useMediaQuery, useTheme } from '@mui/material'
|
||||||
import DetailsTab from './DetailsTab'
|
import DetailsTab from './DetailsTab'
|
||||||
import PublishTab from './PublishTab'
|
import PublishTab from './PublishTab'
|
||||||
|
>>>>>>> origin/master
|
||||||
|
|
||||||
const throttle = require('lodash.throttle')
|
const throttle = require('lodash.throttle')
|
||||||
|
|
||||||
@@ -18,6 +26,7 @@ interface Props {
|
|||||||
nodePath?: string
|
nodePath?: string
|
||||||
tree?: q.Tree<TopicViewModel>
|
tree?: q.Tree<TopicViewModel>
|
||||||
actions: typeof sidebarActions
|
actions: typeof sidebarActions
|
||||||
|
globalActions: typeof globalActions
|
||||||
settingsActions: typeof settingsActions
|
settingsActions: typeof settingsActions
|
||||||
classes: any
|
classes: any
|
||||||
connectionId?: string
|
connectionId?: string
|
||||||
@@ -52,6 +61,37 @@ function SidebarNew(props: Props) {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
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>
|
||||||
|
<Panel>
|
||||||
|
<span>About</span>
|
||||||
|
<AccordionDetails className={classes.details}>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Info />}
|
||||||
|
onClick={() => props.globalActions.toggleAboutDialogVisibility()}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
About MQTT Explorer
|
||||||
|
</Button>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Panel>
|
||||||
|
=======
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
setTabValue(newValue)
|
setTabValue(newValue)
|
||||||
}
|
}
|
||||||
@@ -64,6 +104,7 @@ function SidebarNew(props: Props) {
|
|||||||
<Box className={classes.mobileContent}>
|
<Box className={classes.mobileContent}>
|
||||||
<DetailsTab node={node} />
|
<DetailsTab node={node} />
|
||||||
</Box>
|
</Box>
|
||||||
|
>>>>>>> origin/master
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -108,6 +149,7 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
const mapDispatchToProps = (dispatch: any) => {
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators(sidebarActions, dispatch),
|
actions: bindActionCreators(sidebarActions, dispatch),
|
||||||
|
globalActions: bindActionCreators(globalActions, dispatch),
|
||||||
settingsActions: bindActionCreators(settingsActions, dispatch),
|
settingsActions: bindActionCreators(settingsActions, dispatch),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export enum ActionTypes {
|
|||||||
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY',
|
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY',
|
||||||
requestConfirmation = 'REQUEST_CONFIRMATION',
|
requestConfirmation = 'REQUEST_CONFIRMATION',
|
||||||
removeConfirmationRequest = 'REMOVE_CONFIRMATION_REQUEST',
|
removeConfirmationRequest = 'REMOVE_CONFIRMATION_REQUEST',
|
||||||
|
toggleAboutDialogVisibility = 'TOGGLE_ABOUT_DIALOG_VISIBILITY',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfirmationRequest {
|
export interface ConfirmationRequest {
|
||||||
@@ -36,6 +37,7 @@ interface GlobalStateInterface {
|
|||||||
launching: boolean
|
launching: boolean
|
||||||
settingsVisible: boolean
|
settingsVisible: boolean
|
||||||
confirmationRequests: Array<ConfirmationRequest>
|
confirmationRequests: Array<ConfirmationRequest>
|
||||||
|
aboutDialogVisible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GlobalState = Record<GlobalStateInterface>
|
export type GlobalState = Record<GlobalStateInterface>
|
||||||
@@ -48,6 +50,7 @@ const initialStateFactory = Record<GlobalStateInterface>({
|
|||||||
launching: true,
|
launching: true,
|
||||||
settingsVisible: false,
|
settingsVisible: false,
|
||||||
confirmationRequests: [],
|
confirmationRequests: [],
|
||||||
|
aboutDialogVisible: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> = (
|
export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> = (
|
||||||
@@ -63,6 +66,9 @@ export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> =
|
|||||||
case ActionTypes.toggleSettingsVisibility:
|
case ActionTypes.toggleSettingsVisibility:
|
||||||
return state.set('settingsVisible', !state.get('settingsVisible'))
|
return state.set('settingsVisible', !state.get('settingsVisible'))
|
||||||
|
|
||||||
|
case ActionTypes.toggleAboutDialogVisibility:
|
||||||
|
return state.set('aboutDialogVisible', !state.get('aboutDialogVisible'))
|
||||||
|
|
||||||
case ActionTypes.showError:
|
case ActionTypes.showError:
|
||||||
return state.set('error', action.error)
|
return state.set('error', action.error)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user