Compare commits

..
Author SHA1 Message Date
Björn Dalfors efc9fb9736 dont use pull_request_target as it opens the repo for pwnage.. 2024-06-17 11:25:07 +02:00
Thomas Nordquist f539e03c7e Merge pull request #801 from thomasnordquist/feat/set-payload-from-file
feat: support save and load payload from file
2024-06-02 08:35:53 +02:00
Thomas Nordquist 724ea5acbf Merge pull request #804 from thomasnordquist/chore/add-sparkplug-to-demovideo
add sparkplug decoding to demo video
2024-06-02 08:34:48 +02:00
Björn Dalfors e009940530 fix inputs not being cleared 2024-06-01 22:24:41 +02:00
Björn Dalfors e19178780f add sparkplug decoding to demo video 2024-06-01 22:24:41 +02:00
Björn Dalfors 3229ef5643 Chore/fix workflow sha (#807)
* checkout merge commit of PR, not base branch head

https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target

* use link to test result, gif exceeds github allowed content length
2024-06-01 22:23:25 +02:00
Björn Dalfors b4a6199936 Move file operation to backend 2024-05-29 10:02:00 +02:00
Björn Dalfors bd6a1a0d2d Support specifying file encoding 2024-05-29 10:01:32 +02:00
Björn Dalfors 9d09ab2165 move filesystem operation to backend 2024-05-27 22:09:12 +02:00
Björn Dalfors f17640c9db feat: save value to file 2024-05-27 22:09:12 +02:00
Björn Dalfors 1ba0d07757 feat: support set payload from file when publishing 2024-05-27 22:09:12 +02:00
Thomas Nordquist 20a3202b5f Merge pull request #802 from thomasnordquist/tnordquist/fix-hot-reload
chore: fix webpack reload
2024-05-27 18:06:08 +02:00
16 changed files with 223 additions and 20 deletions
+1 -1
View File
@@ -35,4 +35,4 @@ jobs:
- name: Show URL
run: echo '${{ steps.upload.outputs.file-url }}'
id: artifact-upload-step
- run: echo '![test](${{ steps.upload.outputs.file-url }})' >> $GITHUB_STEP_SUMMARY
- run: echo '<picture><img src="${{ steps.upload.outputs.file-url }}"></picture>' >> $GITHUB_STEP_SUMMARY
+2 -3
View File
@@ -9,12 +9,11 @@ import {
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
import { Dispatch } from 'redux'
import { showError } from './Global'
import { promises as fsPromise } from 'fs'
import * as path from 'path'
import { ActionTypes, Action } from '../reducers/ConnectionManager'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
import { connectionsMigrator } from './migrations/Connection'
import { rendererRpc } from '../../../events'
import { rendererRpc, readFromFile } from '../../../events'
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
export interface ConnectionDictionary {
@@ -81,7 +80,7 @@ async function openCertificate(): Promise<CertificateParameters> {
throw rejectReasons.noCertificateSelected
}
const data = await fsPromise.readFile(selectedFile)
const data = await rendererRpc.call(readFromFile, { filePath: selectedFile })
if (data.length > 16_384 || data.length < 64) {
throw rejectReasons.certificateSizeDoesNotMatch
}
+47 -1
View File
@@ -2,7 +2,10 @@ import { Action, ActionTypes } from '../reducers/Publish'
import { AppState } from '../reducers'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Dispatch } from 'redux'
import { MqttMessage, makePublishEvent, rendererEvents } from '../../../events'
import { MqttMessage, makePublishEvent, rendererEvents, rendererRpc, readFromFile } from '../../../events'
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
import { showError } from './Global'
import { Base64 } from 'js-base64'
export const setTopic = (topic?: string): Action => {
return {
@@ -11,6 +14,49 @@ export const setTopic = (topic?: string): Action => {
}
}
export const openFile = (encoding: 'utf8' = 'utf8') => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const file = await getFileContent(encoding)
if (file) {
dispatch(
setPayload(file.data))
}
} catch (error) {
dispatch(showError(error))
}
}
type FileParameters = {
name: string,
data: string
}
async function getFileContent(encoding: string): Promise<FileParameters | undefined> {
const rejectReasons = {
noFileSelected: 'No file selected',
errorReadingFile: 'Error reading file'
}
const { canceled, filePaths } = await rendererRpc.call(makeOpenDialogRpc(), {
properties: ['openFile'],
securityScopedBookmarks: true,
})
if (canceled) {
return
}
const selectedFile = filePaths[0]
if (!selectedFile) {
throw rejectReasons.noFileSelected
}
try {
const data = await rendererRpc.call(readFromFile, { filePath: selectedFile, encoding })
return { name: selectedFile, data: data.toString(encoding) }
} catch (error) {
throw rejectReasons.errorReadingFile
}
}
export const setPayload = (payload?: string): Action => {
return {
payload,
+20 -1
View File
@@ -1,5 +1,5 @@
import Editor from './Editor'
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import { AttachFileOutlined, FormatAlignLeft } from '@material-ui/icons'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import PublishHistory from './PublishHistory'
@@ -116,6 +116,10 @@ const EditorMode = memo(function EditorMode(props: {
props.actions.setEditorMode(value)
}, [])
const openFile = useCallback(() => {
props.actions.openFile()
}, [])
const formatJson = useCallback(() => {
if (props.payload) {
try {
@@ -132,6 +136,7 @@ const EditorMode = memo(function EditorMode(props: {
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
<EditorModeSelect value={props.editorMode} onChange={updateMode} focusEditor={props.focusEditor} />
<FormatJsonButton editorMode={props.editorMode} focusEditor={props.focusEditor} formatJson={formatJson} />
<OpenFileButton editorMode={props.editorMode} openFile={openFile} />
<div style={{ float: 'right' }}>
<PublishButton publish={props.publish} focusEditor={props.focusEditor} />
</div>
@@ -163,6 +168,20 @@ const FormatJsonButton = React.memo(function FormatJsonButton(props: {
)
})
const OpenFileButton = React.memo(function OpenFileButton(props: { editorMode: string; openFile: () => void }) {
return (
<Tooltip title="Open file">
<Fab
style={{ width: '36px', height: '36px', margin: '0 8px' }}
onClick={props.openFile}
id="sidebar-publish-open-file"
>
<AttachFileOutlined style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
})
const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) {
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
@@ -1,6 +1,7 @@
import * as q from '../../../../../backend/src/Model'
import ActionButtons from './ActionButtons'
import Copy from '../../helper/Copy'
import Save from '../../helper/Save'
import DateFormatter from '../../helper/DateFormatter'
import MessageHistory from './MessageHistory'
import Panel from '../Panel'
@@ -59,6 +60,12 @@ function ValuePanel(props: Props) {
return node?.message && decodeMessage(node.message)?.message?.toUnicodeString()
}, [node, decodeMessage])
const getData = () => {
if (node?.message && node.message.payload) {
return node.message.payload.base64Message
}
}
function messageMetaInfo() {
if (!props.node || !props.node.message) {
return null
@@ -93,10 +100,13 @@ function ValuePanel(props: Props) {
const [value] =
node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
const copyValue = value ? <Copy getValue={getDecodedValue} /> : null
const saveValue = value ? <Save getData={getData} /> : null
return (
<Panel>
<span>Value {copyValue}</span>
<span>
Value {copyValue} {saveValue}
</span>
<span style={{ width: '100%' }}>
{renderViewOptions()}
<div style={{ marginBottom: '-8px', marginTop: '8px' }}>
+85
View File
@@ -0,0 +1,85 @@
import * as React from 'react'
import { connect } from 'react-redux'
import Check from '@material-ui/icons/Check'
import CustomIconButton from './CustomIconButton'
import { SaveAlt } from '@material-ui/icons'
import { bindActionCreators } from 'redux'
import { rendererRpc, writeToFile } from '../../../../events'
import { makeSaveDialogRpc } from '../../../../events/OpenDialogRequest'
import { globalActions } from '../../actions'
export async function saveToFile(data: string): Promise<string | undefined> {
const rejectReasons = {
errorWritingFile: 'Error writing file',
}
const { canceled, filePath } = await rendererRpc.call(makeSaveDialogRpc(), {
securityScopedBookmarks: true,
})
if (!canceled && filePath !== undefined) {
try {
const filename = await rendererRpc.call(writeToFile, { filePath, data })
return filePath
} catch (error) {
throw rejectReasons.errorWritingFile
}
}
}
interface Props {
getData: () => string | undefined
actions: {
global: typeof globalActions
}
}
interface State {
didSave: boolean
}
class Save extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = { didSave: false }
}
private handleClick = async (event: React.MouseEvent) => {
event.stopPropagation()
const data = this.props.getData()
if (data != undefined) {
const filename = await saveToFile(data)
this.props.actions.global.showNotification(`Saved to ${filename}`)
this.setState({ didSave: true })
setTimeout(() => {
this.setState({ didSave: false })
}, 1500)
}
}
public render() {
const icon = !this.state.didSave ? (
<SaveAlt fontSize="inherit" />
) : (
<Check fontSize="inherit" style={{ cursor: 'default' }} />
)
return (
<CustomIconButton onClick={this.handleClick} tooltip="Save to file">
<div style={{ marginTop: '2px' }}>{icon}</div>
</CustomIconButton>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
global: bindActionCreators(globalActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(Save)
+8
View File
@@ -54,3 +54,11 @@ export function makeConnectionMessageEvent(connectionId: string): Event<MqttMess
export const getAppVersion: RpcEvent<void, string> = {
topic: 'getAppVersion',
}
export const writeToFile: RpcEvent<{ filePath: string, data: string, encoding?: string }, void> = {
topic: 'writeFile',
}
export const readFromFile: RpcEvent<{ filePath: string, encoding?: string }, Buffer> = {
topic: 'readFromFile',
}
+7 -1
View File
@@ -1,4 +1,4 @@
import { OpenDialogOptions, OpenDialogReturnValue } from 'electron'
import { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
import { RpcEvent } from './EventSystem/Rpc'
export function makeOpenDialogRpc(): RpcEvent<OpenDialogOptions, OpenDialogReturnValue> {
@@ -6,3 +6,9 @@ export function makeOpenDialogRpc(): RpcEvent<OpenDialogOptions, OpenDialogRetur
topic: 'openDialog',
}
}
export function makeSaveDialogRpc(): RpcEvent<SaveDialogOptions, SaveDialogReturnValue> {
return {
topic: 'saveDialog',
}
}
+16 -2
View File
@@ -4,14 +4,15 @@ import ConfigStorage from '../backend/src/ConfigStorage'
import { app, BrowserWindow, Menu, dialog } from 'electron'
import { autoUpdater } from 'electron-updater'
import { ConnectionManager } from '../backend/src/index'
import { promises as fsPromise } from 'fs'
// import { electronTelemetryFactory } from 'electron-telemetry'
import { menuTemplate } from './MenuTemplate'
import buildOptions from './buildOptions'
import { waitForDevServer, isDev, runningUiTestOnCi, loadDevTools } from './development'
import { shouldAutoUpdate, handleAutoUpdate } from './autoUpdater'
import { registerCrashReporter } from './registerCrashReporter'
import { makeOpenDialogRpc } from '../events/OpenDialogRequest'
import { backendRpc, getAppVersion } from '../events'
import { makeOpenDialogRpc, makeSaveDialogRpc } from '../events/OpenDialogRequest'
import { backendRpc, getAppVersion, writeToFile, readFromFile } from '../events'
registerCrashReporter()
@@ -25,7 +26,20 @@ app.whenReady().then(() => {
backendRpc.on(makeOpenDialogRpc(), async request => {
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request)
})
backendRpc.on(makeSaveDialogRpc(), async request => {
return dialog.showSaveDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request)
})
backendRpc.on(getAppVersion, async () => app.getVersion())
backendRpc.on(writeToFile, async ({ filePath, data, encoding }) => {
await fsPromise.writeFile(filePath, Buffer.from(data, 'base64'), { encoding })
})
backendRpc.on(readFromFile, async ({ filePath, encoding }) => {
return fsPromise.readFile(filePath, { encoding })
})
})
autoUpdater.logger = log
+1
View File
@@ -19,6 +19,7 @@ export type SceneNames =
| 'settings'
| 'customize_subscriptions'
| 'keyboard_shortcuts'
| 'sparkplugb-decoding'
| 'end'
export class SceneBuilder {
+6
View File
@@ -21,6 +21,7 @@ import { showMenu } from './scenarios/showMenu'
import { showNumericPlot } from './scenarios/showNumericPlot'
import { showOffDiffCapability } from './scenarios/showOffDiffCapability'
import { showZoomLevel } from './scenarios/showZoomLevel'
import { showSparkPlugDecoding } from './scenarios/showSparkplugDecoding'
/**
* A convenience method that handles gracefully cleaning up the test run.
@@ -120,6 +121,11 @@ async function doStuff() {
await sleep(1000)
})
await scenes.record('sparkplugb-decoding', async () => {
await showText('SparkplugB Decoding', 2000, page, 'top')
await showSparkPlugDecoding(page)
})
// disable this scenario for now until expandTopic is sorted out
// await scenes.record('delete_retained_topics', async () => {
// await hideText(page)
+3 -2
View File
@@ -14,7 +14,7 @@ export async function publishTopic(browser: Page) {
const topicInput = await browser.locator('//input[contains(@value,"kitchen/lamp/state")][1]')
await clickOn(topicInput)
await deleteTextWithBackspaces(topicInput, 120, 5)
await writeText('set', topicInput, 300)
await writeText('set', topicInput)
const payloadInput = await browser.locator('//*[contains(@class, "ace_text-input")]')
await writeTextPayload(payloadInput, 'off')
@@ -34,5 +34,6 @@ export async function publishTopic(browser: Page) {
}
async function writeTextPayload(payloadInput: Locator, text: string) {
await payloadInput.fill(text)
await clickOn(payloadInput)
await writeText(text, payloadInput)
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { clickOn, deleteTextWithBackspaces, showText, sleep, writeText } from '.
export async function searchTree(text: string, browser: Page) {
const searchField = await browser.locator('//input[contains(@placeholder, "Search")]')
await clickOn(searchField, 1)
await writeText(text, searchField, 100)
await writeText(text, searchField)
await sleep(1500)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { Page } from 'playwright'
import { moveToCenterOfElement, clickOn, clickOnHistory, expandTopic, sleep, writeText } from '../util'
import { moveToCenterOfElement, clickOn, clickOnHistory, expandTopic, sleep } from '../util'
export async function showNumericPlot(browser: Page) {
await expandTopic('kitchen/coffee_maker', browser)
@@ -0,0 +1,9 @@
import { Page } from 'playwright'
import { expandTopic, sleep } from '../util'
export async function showSparkPlugDecoding(browser: Page) {
// spell-checker: disable-next-line
await expandTopic('spBv1.0/Sparkplug Devices/DDATA/JavaScript Edge Node/Emulated Device', browser)
await browser.screenshot({ path: 'screen_sparkplugb_decoding.png' })
await sleep(1000)
}
+5 -6
View File
@@ -19,15 +19,14 @@ export function sleep(ms: number, required = false) {
})
}
export async function writeText(text: string, element: Locator, delay = 0) {
return element.fill(text)
export async function writeText(text: string, element: Locator, delay = 30) {
element.pressSequentially(text, { delay })
}
export async function deleteTextWithBackspaces(element: Locator, delay = 0, count = 0) {
// @ts-ignore
const length = count > 0 ? count : (await element.textContent()).length
export async function deleteTextWithBackspaces(element: Locator, delay = 30, count = 0) {
const length = count > 0 ? count : (await element.inputValue()).length
for (let i = 0; i < length; i += 1) {
await element.press('Backspace')
await element.press('Backspace', { delay: 30 })
await sleep(delay)
}
}