Skip to content

Commit

Permalink
Add help and support page
Browse files Browse the repository at this point in the history
  • Loading branch information
Atatakai authored and Atatakai committed May 30, 2024
1 parent 6217b24 commit 0fe96a6
Show file tree
Hide file tree
Showing 13 changed files with 316 additions and 13 deletions.
5 changes: 5 additions & 0 deletions electron/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,9 @@ module.exports = {
OperateDirectory,
OperateCmd,
Env,
dirs: {
VersionFile,
LogFile,
OperateInstallationLog,
},
};
94 changes: 94 additions & 0 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ const {
Menu,
Notification,
ipcMain,
dialog,
} = require('electron');
const { spawn } = require('child_process');
const path = require('path');
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 {
Expand All @@ -23,6 +25,7 @@ const {
OperateDirectory,
startDocker,
Env,
dirs,
} = require('./install');
const { killProcesses } = require('./processes');
const { isPortAvailable, findAvailablePort } = require('./ports');
Expand Down Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
2 changes: 1 addition & 1 deletion frontend/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
175 changes: 175 additions & 0 deletions frontend/components/HelpAndSupport/HelpAndSupport.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<CardTitle
title={
<Flex gap={10}>
<QuestionCircleOutlined />
Help & support
</Flex>
}
/>
);

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 (
<Card
title={<SettingsTitle />}
bordered={false}
extra={
<Button
size="large"
icon={<CloseOutlined />}
onClick={() => goto(PageState.Main)}
/>
}
>
<CardSection borderbottom="true" padding="16px 24px 24px" vertical>
<Title level={5} className="m-0 mb-16 text-base">
Frequently asked questions
</Title>
<a target="_blank" href={FAQ_URL}>
Read FAQ {UNICODE_SYMBOLS.EXTERNAL_LINK}
</a>
</CardSection>

<CardSection borderbottom="true" padding="16px 24px 24px" vertical>
<Title level={5} className="m-0 mb-8 text-base">
Ask for help
</Title>
<Paragraph type="secondary" className="mb-16 text-sm">
Get your questions answered by the community.
</Paragraph>
<a target="_blank" href={SUPPORT_URL}>
Olas community Discord server {UNICODE_SYMBOLS.EXTERNAL_LINK}
</a>
</CardSection>

<CardSection padding="16px 24px 24px" vertical align="start">
<Title level={5} className="m-0 mb-16 text-base ">
Export logs for troubleshooting
</Title>
<Button
type="primary"
ghost
size="large"
loading={isLoading}
onClick={onSaveLogs}
>
Export logs
</Button>
</CardSection>
</Card>
);
};
23 changes: 15 additions & 8 deletions frontend/components/Main/Main.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -29,13 +29,20 @@ export const Main = () => {
<Card
title={<MainHeader />}
extra={
<Button
type="default"
size="large"
onClick={() => goto(PageState.Settings)}
>
<SettingOutlined />
</Button>
<Flex gap={8}>
<Button
type="default"
size="large"
icon={<QuestionCircleOutlined />}
onClick={() => goto(PageState.HelpAndSupport)}
/>
<Button
type="default"
size="large"
icon={<SettingOutlined />}
onClick={() => goto(PageState.Settings)}
/>
</Flex>
}
style={{ borderTopColor: 'transparent' }}
>
Expand Down
1 change: 1 addition & 0 deletions frontend/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './HelpAndSupport/HelpAndSupport';
export * from './Settings/Settings';
export * from './Setup/Setup';
2 changes: 2 additions & 0 deletions frontend/constants/urls.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const BACKEND_URL: string = `http://localhost:${process.env.NODE_ENV === 'production' ? 8765 : 8000}/api`;
export const COW_SWAP_GNOSIS_XDAI_OLAS_URL: string =
'https://swap.cow.fi/#/100/swap/WXDAI/OLAS';

export const SUPPORT_URL =
'https://discord.com/channels/899649805582737479/1244588374736502847';
export const FAQ_URL = 'https://olas.network/operate#faq';
6 changes: 6 additions & 0 deletions frontend/context/ElectronApiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type ElectronApiContextProps = {
setAppHeight?: (height: unknown) => void;
notifyAgentRunning?: () => void;
showNotification?: (title: string, body?: string) => void;
saveLogs?: (data: {
store?: ElectronStore;
debugData?: Record<string, unknown>;
}) => Promise<{ success?: boolean; filePath?: string }>;
};

export const ElectronApiContext = createContext<ElectronApiContextProps>({
Expand All @@ -44,6 +48,7 @@ export const ElectronApiContext = createContext<ElectronApiContextProps>({
clear: async () => {},
},
setAppHeight: () => {},
saveLogs: async () => ({}),
});

export const ElectronApiProvider = ({ children }: PropsWithChildren) => {
Expand Down Expand Up @@ -80,6 +85,7 @@ export const ElectronApiProvider = ({ children }: PropsWithChildren) => {
},
setAppHeight: getElectronApiFunction('setAppHeight'),
showNotification: getElectronApiFunction('showNotification'),
saveLogs: getElectronApiFunction('saveLogs'),
}}
>
{children}
Expand Down
Loading

0 comments on commit 0fe96a6

Please sign in to comment.