Skip to content

Commit f539e03

Browse files
Merge pull request #801 from thomasnordquist/feat/set-payload-from-file
feat: support save and load payload from file
2 parents 724ea5a + b4a6199 commit f539e03

File tree

8 files changed

+196
-9
lines changed

8 files changed

+196
-9
lines changed

app/src/actions/ConnectionManager.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import {
99
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
1010
import { Dispatch } from 'redux'
1111
import { showError } from './Global'
12-
import { promises as fsPromise } from 'fs'
1312
import * as path from 'path'
1413
import { ActionTypes, Action } from '../reducers/ConnectionManager'
1514
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
1615
import { connectionsMigrator } from './migrations/Connection'
17-
import { rendererRpc } from '../../../events'
16+
import { rendererRpc, readFromFile } from '../../../events'
1817
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
1918

2019
export interface ConnectionDictionary {
@@ -81,7 +80,7 @@ async function openCertificate(): Promise<CertificateParameters> {
8180
throw rejectReasons.noCertificateSelected
8281
}
8382

84-
const data = await fsPromise.readFile(selectedFile)
83+
const data = await rendererRpc.call(readFromFile, { filePath: selectedFile })
8584
if (data.length > 16_384 || data.length < 64) {
8685
throw rejectReasons.certificateSizeDoesNotMatch
8786
}

app/src/actions/Publish.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Action, ActionTypes } from '../reducers/Publish'
22
import { AppState } from '../reducers'
33
import { Base64Message } from '../../../backend/src/Model/Base64Message'
44
import { Dispatch } from 'redux'
5-
import { MqttMessage, makePublishEvent, rendererEvents } from '../../../events'
5+
import { MqttMessage, makePublishEvent, rendererEvents, rendererRpc, readFromFile } from '../../../events'
6+
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
7+
import { showError } from './Global'
8+
import { Base64 } from 'js-base64'
69

710
export const setTopic = (topic?: string): Action => {
811
return {
@@ -11,6 +14,49 @@ export const setTopic = (topic?: string): Action => {
1114
}
1215
}
1316

17+
export const openFile = (encoding: 'utf8' = 'utf8') => async (dispatch: Dispatch<any>, getState: () => AppState) => {
18+
try {
19+
const file = await getFileContent(encoding)
20+
if (file) {
21+
dispatch(
22+
setPayload(file.data))
23+
}
24+
} catch (error) {
25+
dispatch(showError(error))
26+
}
27+
}
28+
29+
type FileParameters = {
30+
name: string,
31+
data: string
32+
}
33+
async function getFileContent(encoding: string): Promise<FileParameters | undefined> {
34+
const rejectReasons = {
35+
noFileSelected: 'No file selected',
36+
errorReadingFile: 'Error reading file'
37+
}
38+
39+
const { canceled, filePaths } = await rendererRpc.call(makeOpenDialogRpc(), {
40+
properties: ['openFile'],
41+
securityScopedBookmarks: true,
42+
})
43+
44+
if (canceled) {
45+
return
46+
}
47+
48+
const selectedFile = filePaths[0]
49+
if (!selectedFile) {
50+
throw rejectReasons.noFileSelected
51+
}
52+
try {
53+
const data = await rendererRpc.call(readFromFile, { filePath: selectedFile, encoding })
54+
return { name: selectedFile, data: data.toString(encoding) }
55+
} catch (error) {
56+
throw rejectReasons.errorReadingFile
57+
}
58+
}
59+
1460
export const setPayload = (payload?: string): Action => {
1561
return {
1662
payload,

app/src/components/Sidebar/Publish/Publish.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Editor from './Editor'
2-
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
2+
import { AttachFileOutlined, FormatAlignLeft } from '@material-ui/icons'
33
import Message from './Model/Message'
44
import Navigation from '@material-ui/icons/Navigation'
55
import PublishHistory from './PublishHistory'
@@ -116,6 +116,10 @@ const EditorMode = memo(function EditorMode(props: {
116116
props.actions.setEditorMode(value)
117117
}, [])
118118

119+
const openFile = useCallback(() => {
120+
props.actions.openFile()
121+
}, [])
122+
119123
const formatJson = useCallback(() => {
120124
if (props.payload) {
121125
try {
@@ -132,6 +136,7 @@ const EditorMode = memo(function EditorMode(props: {
132136
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
133137
<EditorModeSelect value={props.editorMode} onChange={updateMode} focusEditor={props.focusEditor} />
134138
<FormatJsonButton editorMode={props.editorMode} focusEditor={props.focusEditor} formatJson={formatJson} />
139+
<OpenFileButton editorMode={props.editorMode} openFile={openFile} />
135140
<div style={{ float: 'right' }}>
136141
<PublishButton publish={props.publish} focusEditor={props.focusEditor} />
137142
</div>
@@ -163,6 +168,20 @@ const FormatJsonButton = React.memo(function FormatJsonButton(props: {
163168
)
164169
})
165170

171+
const OpenFileButton = React.memo(function OpenFileButton(props: { editorMode: string; openFile: () => void }) {
172+
return (
173+
<Tooltip title="Open file">
174+
<Fab
175+
style={{ width: '36px', height: '36px', margin: '0 8px' }}
176+
onClick={props.openFile}
177+
id="sidebar-publish-open-file"
178+
>
179+
<AttachFileOutlined style={{ fontSize: '20px' }} />
180+
</Fab>
181+
</Tooltip>
182+
)
183+
})
184+
166185
const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) {
167186
const handleClickPublish = useCallback(
168187
(e: React.MouseEvent) => {

app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as q from '../../../../../backend/src/Model'
22
import ActionButtons from './ActionButtons'
33
import Copy from '../../helper/Copy'
4+
import Save from '../../helper/Save'
45
import DateFormatter from '../../helper/DateFormatter'
56
import MessageHistory from './MessageHistory'
67
import Panel from '../Panel'
@@ -59,6 +60,12 @@ function ValuePanel(props: Props) {
5960
return node?.message && decodeMessage(node.message)?.message?.toUnicodeString()
6061
}, [node, decodeMessage])
6162

63+
const getData = () => {
64+
if (node?.message && node.message.payload) {
65+
return node.message.payload.base64Message
66+
}
67+
}
68+
6269
function messageMetaInfo() {
6370
if (!props.node || !props.node.message) {
6471
return null
@@ -93,10 +100,13 @@ function ValuePanel(props: Props) {
93100
const [value] =
94101
node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
95102
const copyValue = value ? <Copy getValue={getDecodedValue} /> : null
103+
const saveValue = value ? <Save getData={getData} /> : null
96104

97105
return (
98106
<Panel>
99-
<span>Value {copyValue}</span>
107+
<span>
108+
Value {copyValue} {saveValue}
109+
</span>
100110
<span style={{ width: '100%' }}>
101111
{renderViewOptions()}
102112
<div style={{ marginBottom: '-8px', marginTop: '8px' }}>

app/src/components/helper/Save.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as React from 'react'
2+
import { connect } from 'react-redux'
3+
import Check from '@material-ui/icons/Check'
4+
import CustomIconButton from './CustomIconButton'
5+
6+
import { SaveAlt } from '@material-ui/icons'
7+
import { bindActionCreators } from 'redux'
8+
import { rendererRpc, writeToFile } from '../../../../events'
9+
import { makeSaveDialogRpc } from '../../../../events/OpenDialogRequest'
10+
11+
import { globalActions } from '../../actions'
12+
13+
export async function saveToFile(data: string): Promise<string | undefined> {
14+
const rejectReasons = {
15+
errorWritingFile: 'Error writing file',
16+
}
17+
18+
const { canceled, filePath } = await rendererRpc.call(makeSaveDialogRpc(), {
19+
securityScopedBookmarks: true,
20+
})
21+
22+
if (!canceled && filePath !== undefined) {
23+
try {
24+
const filename = await rendererRpc.call(writeToFile, { filePath, data })
25+
return filePath
26+
} catch (error) {
27+
throw rejectReasons.errorWritingFile
28+
}
29+
}
30+
}
31+
32+
interface Props {
33+
getData: () => string | undefined
34+
actions: {
35+
global: typeof globalActions
36+
}
37+
}
38+
39+
interface State {
40+
didSave: boolean
41+
}
42+
43+
class Save extends React.PureComponent<Props, State> {
44+
constructor(props: Props) {
45+
super(props)
46+
this.state = { didSave: false }
47+
}
48+
49+
private handleClick = async (event: React.MouseEvent) => {
50+
event.stopPropagation()
51+
const data = this.props.getData()
52+
if (data != undefined) {
53+
const filename = await saveToFile(data)
54+
this.props.actions.global.showNotification(`Saved to ${filename}`)
55+
this.setState({ didSave: true })
56+
setTimeout(() => {
57+
this.setState({ didSave: false })
58+
}, 1500)
59+
}
60+
}
61+
62+
public render() {
63+
const icon = !this.state.didSave ? (
64+
<SaveAlt fontSize="inherit" />
65+
) : (
66+
<Check fontSize="inherit" style={{ cursor: 'default' }} />
67+
)
68+
69+
return (
70+
<CustomIconButton onClick={this.handleClick} tooltip="Save to file">
71+
<div style={{ marginTop: '2px' }}>{icon}</div>
72+
</CustomIconButton>
73+
)
74+
}
75+
}
76+
77+
const mapDispatchToProps = (dispatch: any) => {
78+
return {
79+
actions: {
80+
global: bindActionCreators(globalActions, dispatch),
81+
},
82+
}
83+
}
84+
85+
export default connect(undefined, mapDispatchToProps)(Save)

events/Events.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,11 @@ export function makeConnectionMessageEvent(connectionId: string): Event<MqttMess
5454
export const getAppVersion: RpcEvent<void, string> = {
5555
topic: 'getAppVersion',
5656
}
57+
58+
export const writeToFile: RpcEvent<{ filePath: string, data: string, encoding?: string }, void> = {
59+
topic: 'writeFile',
60+
}
61+
62+
export const readFromFile: RpcEvent<{ filePath: string, encoding?: string }, Buffer> = {
63+
topic: 'readFromFile',
64+
}

events/OpenDialogRequest.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import { OpenDialogOptions, OpenDialogReturnValue } from 'electron'
1+
import { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
22
import { RpcEvent } from './EventSystem/Rpc'
33

44
export function makeOpenDialogRpc(): RpcEvent<OpenDialogOptions, OpenDialogReturnValue> {
55
return {
66
topic: 'openDialog',
77
}
88
}
9+
10+
export function makeSaveDialogRpc(): RpcEvent<SaveDialogOptions, SaveDialogReturnValue> {
11+
return {
12+
topic: 'saveDialog',
13+
}
14+
}

src/electron.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import ConfigStorage from '../backend/src/ConfigStorage'
44
import { app, BrowserWindow, Menu, dialog } from 'electron'
55
import { autoUpdater } from 'electron-updater'
66
import { ConnectionManager } from '../backend/src/index'
7+
import { promises as fsPromise } from 'fs'
78
// import { electronTelemetryFactory } from 'electron-telemetry'
89
import { menuTemplate } from './MenuTemplate'
910
import buildOptions from './buildOptions'
1011
import { waitForDevServer, isDev, runningUiTestOnCi, loadDevTools } from './development'
1112
import { shouldAutoUpdate, handleAutoUpdate } from './autoUpdater'
1213
import { registerCrashReporter } from './registerCrashReporter'
13-
import { makeOpenDialogRpc } from '../events/OpenDialogRequest'
14-
import { backendRpc, getAppVersion } from '../events'
14+
import { makeOpenDialogRpc, makeSaveDialogRpc } from '../events/OpenDialogRequest'
15+
import { backendRpc, getAppVersion, writeToFile, readFromFile } from '../events'
1516

1617
registerCrashReporter()
1718

@@ -25,7 +26,20 @@ app.whenReady().then(() => {
2526
backendRpc.on(makeOpenDialogRpc(), async request => {
2627
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request)
2728
})
29+
30+
backendRpc.on(makeSaveDialogRpc(), async request => {
31+
return dialog.showSaveDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request)
32+
})
33+
2834
backendRpc.on(getAppVersion, async () => app.getVersion())
35+
36+
backendRpc.on(writeToFile, async ({ filePath, data, encoding }) => {
37+
await fsPromise.writeFile(filePath, Buffer.from(data, 'base64'), { encoding })
38+
})
39+
40+
backendRpc.on(readFromFile, async ({ filePath, encoding }) => {
41+
return fsPromise.readFile(filePath, { encoding })
42+
})
2943
})
3044

3145
autoUpdater.logger = log

0 commit comments

Comments
 (0)