From 0fe96a699d87555c6fbc8836f33b823dce178135 Mon Sep 17 00:00:00 2001 From: Atatakai Date: Thu, 30 May 2024 12:18:41 +0400 Subject: [PATCH] Add help and support page --- electron/install.js | 5 + electron/main.js | 94 ++++++++++ electron/preload.js | 1 + frontend/client/types.ts | 2 +- .../HelpAndSupport/HelpAndSupport.tsx | 175 ++++++++++++++++++ frontend/components/Main/Main.tsx | 23 ++- frontend/components/index.ts | 1 + frontend/constants/urls.ts | 2 + frontend/context/ElectronApiProvider.tsx | 6 + frontend/context/MasterSafeProvider.tsx | 1 + frontend/enums/PageState.ts | 1 + frontend/pages/index.tsx | 4 +- frontend/styles/globals.scss | 14 +- 13 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 frontend/components/HelpAndSupport/HelpAndSupport.tsx diff --git a/electron/install.js b/electron/install.js index 5b9a88734..d30e914fd 100644 --- a/electron/install.js +++ b/electron/install.js @@ -370,4 +370,9 @@ module.exports = { OperateDirectory, OperateCmd, Env, + dirs: { + VersionFile, + LogFile, + OperateInstallationLog, + }, }; diff --git a/electron/main.js b/electron/main.js index 7035d0ea6..7fe6aae62 100644 --- a/electron/main.js +++ b/electron/main.js @@ -7,6 +7,7 @@ const { Menu, Notification, ipcMain, + dialog, } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); @@ -14,6 +15,7 @@ const fs = require('fs'); const os = require('os'); const next = require('next'); const http = require('http'); +const AdmZip = require('adm-zip'); const { TRAY_ICONS, TRAY_ICONS_PATHS } = require('./icons'); const { @@ -23,6 +25,7 @@ const { OperateDirectory, startDocker, Env, + dirs, } = require('./install'); const { killProcesses } = require('./processes'); const { isPortAvailable, findAvailablePort } = require('./ports'); @@ -544,3 +547,94 @@ process.on('uncaughtException', (error) => { }); }); }); + +// EXPORT LOGS +ipcMain.handle('save-logs', async (_, data) => { + // version.txt + const versionFile = dirs.VersionFile; + // logs.txt + const logFile = getSanitizedLogs({ name: 'log.txt', filePath: dirs.LogFile }); + // operate.log + const installationLog = getSanitizedLogs({ + name: 'installation_log.txt', + filePath: dirs.OperateInstallationLog, + }); + + const tempDir = os.tmpdir(); + + // OS info + const osInfo = ` + OS Type: ${os.type()} + OS Platform: ${os.platform()} + OS Arch: ${os.arch()} + OS Release: ${os.release()} + Total Memory: ${os.totalmem()} + Free Memory: ${os.freemem()} + `; + const osInfoFilePath = path.join(tempDir, 'os_info.txt'); + fs.writeFileSync(osInfoFilePath, osInfo); + + // Persistent store + let storeFilePath; + if (data.store) { + storeFilePath = path.join(tempDir, 'store.txt'); + fs.writeFileSync(storeFilePath, JSON.stringify(data.store, null, 2)); + } + + // Other debug data: balances, addresses, etc. + let debugDataFilePath; + if (data.debugData) { + debugDataFilePath = getSanitizedLogs({ + name: 'debug_data.txt', + data: JSON.stringify(data.debugData, null, 2), + }); + } + + // Create a zip archive + const zip = new AdmZip(); + zip.addLocalFile(versionFile); + zip.addLocalFile(logFile); + zip.addLocalFile(installationLog); + zip.addLocalFile(osInfoFilePath); + zip.addLocalFile(storeFilePath); + zip.addLocalFile(debugDataFilePath); + + // Show save dialog + const { filePath } = await dialog.showSaveDialog({ + title: 'Save Logs', + defaultPath: path.join(os.homedir(), 'pearl_logs.zip'), + filters: [{ name: 'Zip Files', extensions: ['zip'] }], + }); + + let result; + + if (filePath) { + // Write the zip file to the selected path + zip.writeZip(filePath); + result = { success: true, filePath }; + } else { + result = { success: false }; + } + + // Remove temporary files + fs.unlinkSync(logFile); + fs.unlinkSync(installationLog); + fs.unlinkSync(osInfoFilePath); + if (storeFilePath) fs.unlinkSync(storeFilePath); + if (debugDataFilePath) fs.unlinkSync(debugDataFilePath); + + return result; +}); + +function getSanitizedLogs({ name, filePath, data }) { + const logs = filePath ? fs.readFileSync(filePath, 'utf-8') : data; + const tempDir = os.tmpdir(); + + const usernameRegex = /\/Users\/([^/]+)/g; + const sanitizedData = logs.replace(usernameRegex, '/Users/*****'); + + const sanitizedLogsFilePath = path.join(tempDir, name); + fs.writeFileSync(sanitizedLogsFilePath, sanitizedData); + + return sanitizedLogsFilePath; +} diff --git a/electron/preload.js b/electron/preload.js index 8431dbe3c..6619ed143 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -20,4 +20,5 @@ contextBridge.exposeInMainWorld('electronAPI', { setAppHeight: (height) => ipcRenderer.send('set-height', height), showNotification: (title, description) => ipcRenderer.send('show-notification', title, description), + saveLogs: (data) => ipcRenderer.invoke('save-logs', data), }); diff --git a/frontend/client/types.ts b/frontend/client/types.ts index b15841131..f4e6937bd 100644 --- a/frontend/client/types.ts +++ b/frontend/client/types.ts @@ -25,7 +25,7 @@ export type ChainData = { export type Service = { name: string; hash: string; - keys: ServiceKeys; + keys: ServiceKeys[]; readme?: string; ledger: LedgerConfig; chain_data: ChainData; diff --git a/frontend/components/HelpAndSupport/HelpAndSupport.tsx b/frontend/components/HelpAndSupport/HelpAndSupport.tsx new file mode 100644 index 000000000..2e77cfdcd --- /dev/null +++ b/frontend/components/HelpAndSupport/HelpAndSupport.tsx @@ -0,0 +1,175 @@ +import { CloseOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import { Button, Card, Flex, message, Typography } from 'antd'; +import { useCallback, useEffect, useState } from 'react'; + +import { DeploymentStatus } from '@/client'; +import { FAQ_URL, SUPPORT_URL } from '@/constants'; +import { UNICODE_SYMBOLS } from '@/constants/unicode'; +import { PageState } from '@/enums'; +import { useBalance, usePageState, useServices } from '@/hooks'; +import { useElectronApi } from '@/hooks/useElectronApi'; +import { useMasterSafe } from '@/hooks/useMasterSafe'; +import { useStore } from '@/hooks/useStore'; + +import { CardTitle } from '../common/CardTitle'; +import { CardSection } from '../styled/CardSection'; + +const { Title, Paragraph } = Typography; + +const SettingsTitle = () => ( + + + Help & support + + } + /> +); + +export const HelpAndSupport = () => { + const { goto } = usePageState(); + const { saveLogs } = useElectronApi(); + + const { storeState } = useStore(); + const { + serviceStatus, + services, + hasInitialLoaded: isServiceLoaded, + } = useServices(); + const { + isBalanceLoaded, + totalEthBalance, + totalOlasBalance, + wallets, + walletBalances, + totalOlasStakedBalance, + } = useBalance(); + + const { + backupSafeAddress, + masterSafeAddress, + masterEoaAddress, + masterSafeOwners, + } = useMasterSafe(); + + const [isLoading, setIsLoading] = useState(false); + const [canSaveLogs, setCanSafeLogs] = useState(false); + + const onSaveLogs = useCallback(() => { + setIsLoading(true); + setCanSafeLogs(true); + }, []); + + const handleSaveLogs = useCallback(() => { + return saveLogs?.({ + store: storeState, + debugData: { + services: { + services: + services?.map((item) => ({ + ...item, + keys: item.keys.map((key) => key.address), + })) ?? 'undefined', + serviceStatus: serviceStatus + ? DeploymentStatus[serviceStatus] + : 'undefined', + }, + addresses: [ + { backupSafeAddress: backupSafeAddress ?? 'undefined' }, + { masterSafeAddress: masterSafeAddress ?? 'undefined' }, + { masterEoaAddress: masterEoaAddress ?? 'undefined' }, + { masterSafeOwners: masterSafeOwners ?? 'undefined' }, + ], + balances: [ + { wallets: wallets ?? 'undefined' }, + { walletBalances: walletBalances ?? 'undefined' }, + { totalOlasStakedBalance: totalOlasStakedBalance ?? 'undefined' }, + { totalEthBalance: totalEthBalance ?? 'undefined' }, + { totalOlasBalance: totalOlasBalance ?? 'undefined' }, + ], + }, + }).then((result) => { + if (result.success) { + message.success(`Logs saved to: ${result.filePath}`); + } else { + message.error('Save logs failed or cancelled'); + } + }); + }, [ + backupSafeAddress, + masterEoaAddress, + masterSafeAddress, + masterSafeOwners, + saveLogs, + serviceStatus, + services, + storeState, + totalEthBalance, + totalOlasBalance, + totalOlasStakedBalance, + walletBalances, + wallets, + ]); + + useEffect(() => { + // only save logs when all needed data is loaded + if (canSaveLogs && isBalanceLoaded && isServiceLoaded) { + handleSaveLogs()?.finally(() => { + setIsLoading(false); + setCanSafeLogs(false); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [canSaveLogs, isBalanceLoaded, isServiceLoaded]); + + return ( + } + bordered={false} + extra={ + + + + ); +}; diff --git a/frontend/components/Main/Main.tsx b/frontend/components/Main/Main.tsx index cfede1da7..84c575d53 100644 --- a/frontend/components/Main/Main.tsx +++ b/frontend/components/Main/Main.tsx @@ -1,4 +1,4 @@ -import { SettingOutlined } from '@ant-design/icons'; +import { QuestionCircleOutlined, SettingOutlined } from '@ant-design/icons'; import { Button, Card, Flex } from 'antd'; import { useEffect } from 'react'; @@ -29,13 +29,20 @@ export const Main = () => { } extra={ - + +