diff --git a/app/src/actions/ConnectionManager.ts b/app/src/actions/ConnectionManager.ts index fce8cbed..f9924b39 100644 --- a/app/src/actions/ConnectionManager.ts +++ b/app/src/actions/ConnectionManager.ts @@ -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 { 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 } diff --git a/app/src/actions/Publish.ts b/app/src/actions/Publish.ts index a752251e..ab75050c 100644 --- a/app/src/actions/Publish.ts +++ b/app/src/actions/Publish.ts @@ -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, 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 { + 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, diff --git a/app/src/components/Sidebar/Publish/Publish.tsx b/app/src/components/Sidebar/Publish/Publish.tsx index 20c080a5..e9f96e45 100644 --- a/app/src/components/Sidebar/Publish/Publish.tsx +++ b/app/src/components/Sidebar/Publish/Publish.tsx @@ -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: {
+
@@ -163,6 +168,20 @@ const FormatJsonButton = React.memo(function FormatJsonButton(props: { ) }) +const OpenFileButton = React.memo(function OpenFileButton(props: { editorMode: string; openFile: () => void }) { + return ( + + + + + + ) +}) + const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) { const handleClickPublish = useCallback( (e: React.MouseEvent) => { diff --git a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx index 61c5b2f8..e20bab75 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx @@ -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 ? : null + const saveValue = value ? : null return ( - Value {copyValue} + + Value {copyValue} {saveValue} + {renderViewOptions()}
diff --git a/app/src/components/helper/Save.tsx b/app/src/components/helper/Save.tsx new file mode 100644 index 00000000..cf548194 --- /dev/null +++ b/app/src/components/helper/Save.tsx @@ -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 { + 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 { + 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 ? ( + + ) : ( + + ) + + return ( + +
{icon}
+
+ ) + } +} + +const mapDispatchToProps = (dispatch: any) => { + return { + actions: { + global: bindActionCreators(globalActions, dispatch), + }, + } +} + +export default connect(undefined, mapDispatchToProps)(Save) diff --git a/events/Events.ts b/events/Events.ts index 418652bc..d10b6667 100644 --- a/events/Events.ts +++ b/events/Events.ts @@ -54,3 +54,11 @@ export function makeConnectionMessageEvent(connectionId: string): Event = { 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', +} \ No newline at end of file diff --git a/events/OpenDialogRequest.ts b/events/OpenDialogRequest.ts index 6ef12546..fa799349 100644 --- a/events/OpenDialogRequest.ts +++ b/events/OpenDialogRequest.ts @@ -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 { @@ -6,3 +6,9 @@ export function makeOpenDialogRpc(): RpcEvent { + return { + topic: 'saveDialog', + } +} \ No newline at end of file diff --git a/src/electron.ts b/src/electron.ts index 7b4cf773..db66d853 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -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