Download tune and logs (#857)
This commit is contained in:
parent
2fc6398b1e
commit
49d659f817
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"hypertuner",
|
||||||
"kbar",
|
"kbar",
|
||||||
"pocketbase",
|
"pocketbase",
|
||||||
"prefs",
|
"prefs",
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
useLocation,
|
useLocation,
|
||||||
|
@ -31,10 +35,7 @@ import {
|
||||||
LoginOutlined,
|
LoginOutlined,
|
||||||
LineChartOutlined,
|
LineChartOutlined,
|
||||||
SlidersOutlined,
|
SlidersOutlined,
|
||||||
FileExcelOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
FileZipOutlined,
|
FileZipOutlined,
|
||||||
SaveOutlined,
|
|
||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
|
@ -44,6 +45,8 @@ import {
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
CarOutlined,
|
CarOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
FileExcelOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useKBar } from 'kbar';
|
import { useKBar } from 'kbar';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
@ -53,10 +56,17 @@ import { Routes } from '../routes';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { logOutSuccessful } from '../pages/auth/notifications';
|
import { logOutSuccessful } from '../pages/auth/notifications';
|
||||||
import { TuneDataState } from '../types/state';
|
import { TuneDataState } from '../types/state';
|
||||||
|
import { removeFilenameSuffix } from '../pocketbase';
|
||||||
|
import useServerStorage from '../hooks/useServerStorage';
|
||||||
|
|
||||||
const { Header } = Layout;
|
const { Header } = Layout;
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
const { SubMenu } = Menu;
|
|
||||||
|
const logsExtensionsIcons: { [key: string]: any } = {
|
||||||
|
'mlg': <FileZipOutlined />,
|
||||||
|
'msl': <FileTextOutlined />,
|
||||||
|
'csv': <FileExcelOutlined />,
|
||||||
|
};
|
||||||
|
|
||||||
const TopBar = ({
|
const TopBar = ({
|
||||||
tuneData,
|
tuneData,
|
||||||
|
@ -77,6 +87,8 @@ const TopBar = ({
|
||||||
const tabMatch = useMatch(`${Routes.TUNE_TAB}/*`);
|
const tabMatch = useMatch(`${Routes.TUNE_TAB}/*`);
|
||||||
const uploadMatch = useMatch(Routes.UPLOAD);
|
const uploadMatch = useMatch(Routes.UPLOAD);
|
||||||
const hubMatch = useMatch(Routes.HUB);
|
const hubMatch = useMatch(Routes.HUB);
|
||||||
|
const { downloadFile } = useServerStorage();
|
||||||
|
const downloadAnchorRef = useRef<HTMLAnchorElement | null>(null);
|
||||||
const logoutClick = useCallback(() => {
|
const logoutClick = useCallback(() => {
|
||||||
logout();
|
logout();
|
||||||
logOutSuccessful();
|
logOutSuccessful();
|
||||||
|
@ -92,6 +104,54 @@ const TopBar = ({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const downloadLogsItems = {
|
||||||
|
label: 'Logs',
|
||||||
|
icon: <LineChartOutlined />,
|
||||||
|
key: 'logs',
|
||||||
|
children: (tuneData?.logFiles || []).map((filename) => ({
|
||||||
|
key: filename,
|
||||||
|
label: removeFilenameSuffix(filename),
|
||||||
|
icon: logsExtensionsIcons[filename.slice(-3)],
|
||||||
|
onClick: () => downloadFile(tuneData!.id, filename, downloadAnchorRef.current!),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadToothLogsItems = {
|
||||||
|
label: 'Tooth logs',
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
key: 'toothLogs',
|
||||||
|
children: (tuneData?.toothLogFiles || []).map((filename) => ({
|
||||||
|
key: filename,
|
||||||
|
label: removeFilenameSuffix(filename),
|
||||||
|
icon: logsExtensionsIcons[filename.slice(-3)],
|
||||||
|
onClick: () => downloadFile(tuneData!.id, filename, downloadAnchorRef.current!),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadItems = [
|
||||||
|
{
|
||||||
|
label: 'Tune',
|
||||||
|
icon: <SlidersOutlined />,
|
||||||
|
key: 'tune',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'Download',
|
||||||
|
icon: <FileTextOutlined />,
|
||||||
|
key: 'download',
|
||||||
|
onClick: () => downloadFile(tuneData!.id, tuneData!.tuneFile, downloadAnchorRef.current!),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open in app',
|
||||||
|
icon: <DesktopOutlined />,
|
||||||
|
key: 'open',
|
||||||
|
onClick: () => window.open(`hypertuner://hypertuner.cloud/t/${tuneData!.tuneId}`, '_blank'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(tuneData?.logFiles || []).length > 0 ? { ...downloadLogsItems } : null,
|
||||||
|
(tuneData?.toothLogFiles || []).length > 0 ? { ...downloadToothLogsItems } : null,
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('keydown', handleGlobalKeyboard);
|
document.addEventListener('keydown', handleGlobalKeyboard);
|
||||||
|
|
||||||
|
@ -212,31 +272,9 @@ const TopBar = ({
|
||||||
{lg && 'Upload'}
|
{lg && 'Upload'}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Dropdown
|
{tuneData?.tuneId && <Dropdown
|
||||||
overlay={
|
overlay={
|
||||||
<Menu disabled>
|
<Menu triggerSubMenuAction="click" items={downloadItems} />
|
||||||
<SubMenu key="tune-sub" title="Tune" icon={<SlidersOutlined />}>
|
|
||||||
<Menu.Item key="download" icon={<SaveOutlined />}>
|
|
||||||
<a href="/tunes/202103.msq" target="__blank" rel="noopener noreferrer">
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="open" disabled icon={<DesktopOutlined />}>
|
|
||||||
Open in app
|
|
||||||
</Menu.Item>
|
|
||||||
</SubMenu>
|
|
||||||
<SubMenu key="logs-sub" title="Logs" icon={<LineChartOutlined />}>
|
|
||||||
<Menu.Item key="mlg" disabled icon={<FileZipOutlined />}>
|
|
||||||
MLG
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="msl" disabled icon={<FileTextOutlined />}>
|
|
||||||
MSL
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="csv" disabled icon={<FileExcelOutlined />}>
|
|
||||||
CSV
|
|
||||||
</Menu.Item>
|
|
||||||
</SubMenu>
|
|
||||||
</Menu>
|
|
||||||
}
|
}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
|
@ -244,7 +282,7 @@ const TopBar = ({
|
||||||
<Button icon={<CloudDownloadOutlined />}>
|
<Button icon={<CloudDownloadOutlined />}>
|
||||||
{downloadButton}
|
{downloadButton}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
overlay={<Menu items={userMenuItems} />}
|
overlay={<Menu items={userMenuItems} />}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
|
@ -254,6 +292,8 @@ const TopBar = ({
|
||||||
{sm && <DownOutlined />}
|
{sm && <DownOutlined />}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
{/* dummy anchor for file download */}
|
||||||
|
<a ref={downloadAnchorRef} style={{ display: 'none' }} />
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import Pako from 'pako';
|
import Pako from 'pako';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import { API_URL } from '../pocketbase';
|
import {
|
||||||
|
API_URL,
|
||||||
|
removeFilenameSuffix,
|
||||||
|
} from '../pocketbase';
|
||||||
import { Collections } from '../@types/pocketbase-types';
|
import { Collections } from '../@types/pocketbase-types';
|
||||||
import useDb from './useDb';
|
import useDb from './useDb';
|
||||||
import {
|
import {
|
||||||
fetchWithProgress,
|
fetchWithProgress,
|
||||||
OnProgress,
|
OnProgress,
|
||||||
} from '../utils/http';
|
} from '../utils/http';
|
||||||
|
import { downloading } from '../pages/auth/notifications';
|
||||||
|
|
||||||
const useServerStorage = () => {
|
const useServerStorage = () => {
|
||||||
const { getIni } = useDb();
|
const { getIni } = useDb();
|
||||||
|
@ -43,10 +47,28 @@ const useServerStorage = () => {
|
||||||
signal,
|
signal,
|
||||||
).then((response) => response);
|
).then((response) => response);
|
||||||
|
|
||||||
|
const downloadFile = async (recordId: string, filename: string, anchorRef: HTMLAnchorElement) => {
|
||||||
|
downloading();
|
||||||
|
|
||||||
|
const response = await fetch(buildFileUrl(Collections.Tunes, recordId, filename));
|
||||||
|
const data = Pako.inflate(new Uint8Array(await response.arrayBuffer()));
|
||||||
|
const url = window.URL.createObjectURL(new Blob([data]));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
anchorRef.href = url;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
anchorRef.target = '_blank';
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
anchorRef.download = removeFilenameSuffix(filename);
|
||||||
|
anchorRef.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetchTuneFile: (recordId: string, filename: string): Promise<ArrayBuffer> => fetchTuneFile(recordId, filename),
|
fetchTuneFile: (recordId: string, filename: string): Promise<ArrayBuffer> => fetchTuneFile(recordId, filename),
|
||||||
fetchINIFile: (signature: string): Promise<ArrayBuffer> => fetchINIFile(signature),
|
fetchINIFile: (signature: string): Promise<ArrayBuffer> => fetchINIFile(signature),
|
||||||
fetchLogFileWithProgress: (recordId: string, filename: string, onProgress?: OnProgress, signal?: AbortSignal): Promise<ArrayBuffer> => fetchLogFileWithProgress(recordId, filename, onProgress, signal),
|
fetchLogFileWithProgress: (recordId: string, filename: string, onProgress?: OnProgress, signal?: AbortSignal): Promise<ArrayBuffer> => fetchLogFileWithProgress(recordId, filename, onProgress, signal),
|
||||||
|
downloadFile: (recordId: string, filename: string, anchorRef: HTMLAnchorElement): Promise<void> => downloadFile(recordId, filename, anchorRef),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,12 @@ const signatureNotSupportedWarning = (message: string) => notification.warning({
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const downloading = () => notification.success({
|
||||||
|
message: 'Downloading...',
|
||||||
|
...baseOptions,
|
||||||
|
duration: 1,
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
error,
|
error,
|
||||||
emailNotVerified,
|
emailNotVerified,
|
||||||
|
@ -172,4 +178,5 @@ export {
|
||||||
iniLoadingError,
|
iniLoadingError,
|
||||||
tuneParsingError,
|
tuneParsingError,
|
||||||
signatureNotSupportedWarning,
|
signatureNotSupportedWarning,
|
||||||
|
downloading,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue