diff --git a/.env b/.env index c8939d5..1d9843f 100644 --- a/.env +++ b/.env @@ -4,7 +4,4 @@ VITE_WEB_URL=http://localhost:5173 VITE_SENTRY_DSN= VITE_GTM_ID= -# TODO: remove this later -VITE_CDN_URL="https://public-bucket.speedytuner.app" - VITE_POCKETBASE_API_URL="https://api.hypertuner.cloud" diff --git a/src/hooks/useServerStorage.ts b/src/hooks/useServerStorage.ts index 0af06ba..096b6dd 100644 --- a/src/hooks/useServerStorage.ts +++ b/src/hooks/useServerStorage.ts @@ -1,6 +1,5 @@ import Pako from 'pako'; import * as Sentry from '@sentry/browser'; -import { fetchEnv } from '../utils/env'; import { API_URL } from '../pocketbase'; import { Collections } from '../@types/pocketbase-types'; import useDb from './useDb'; @@ -9,8 +8,6 @@ import { OnProgress, } from '../utils/http'; -export const CDN_URL = fetchEnv('VITE_CDN_URL'); - const useServerStorage = () => { const { getIni } = useDb(); diff --git a/src/pages/Diagnose.tsx b/src/pages/Diagnose.tsx index c82f4e3..6fddee5 100644 --- a/src/pages/Diagnose.tsx +++ b/src/pages/Diagnose.tsx @@ -1,4 +1,9 @@ -/* eslint-disable import/no-webpack-loader-syntax */ +import { + generatePath, + Link, + useMatch, + useNavigate, +} from 'react-router-dom'; import { useCallback, useEffect, @@ -19,20 +24,16 @@ import { FileTextOutlined, GlobalOutlined, } from '@ant-design/icons'; -import * as Sentry from '@sentry/browser'; import useBreakpoint from 'antd/lib/grid/hooks/useBreakpoint'; import { connect } from 'react-redux'; import PerfectScrollbar from 'react-perfect-scrollbar'; +import Pako from 'pako'; import { AppState, - ConfigState, - LogsState, + ToothLogsState, + TuneDataState, UIState, } from '../types/state'; -import { - loadCompositeLogs, - loadToothLogs, -} from '../utils/api'; import store from '../store'; import { formatBytes } from '../utils/numbers'; import CompositeCanvas from '../components/TriggerLogs/CompositeCanvas'; @@ -43,6 +44,10 @@ import TriggerLogsParser, { import ToothCanvas from '../components/TriggerLogs/ToothCanvas'; import Loader from '../components/Loader'; import { Colors } from '../utils/colors'; +import { Routes } from '../routes'; +import { removeFilenameSuffix } from '../pocketbase'; +import useServerStorage from '../hooks/useServerStorage'; +import { isAbortedRequest } from '../utils/error'; const { Content } = Layout; const { Step } = Steps; @@ -51,18 +56,27 @@ const edgeUnknown = 'Unknown'; const badgeStyle = { backgroundColor: Colors.TEXT }; -const mapStateToProps = (state: AppState) => ({ - ui: state.ui, - status: state.status, - config: state.config, - loadedLogs: state.logs, -}); - const margin = 30; const sidebarWidth = 250; const minCanvasHeightInner = 600; -const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: ConfigState, loadedLogs: LogsState }) => { +const mapStateToProps = (state: AppState) => ({ + ui: state.ui, + status: state.status, + config: state.config, + loadedToothLogs: state.toothLogs, + tuneData: state.tuneData, +}); + +const Diagnose = ({ + ui, + loadedToothLogs, + tuneData, +}: { + ui: UIState; + loadedToothLogs: ToothLogsState; + tuneData: TuneDataState; +}) => { const { lg } = useBreakpoint(); const { Sider } = Layout; const [progress, setProgress] = useState(0); @@ -73,11 +87,15 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: ConfigState const contentRef = useRef(null); const [canvasWidth, setCanvasWidth] = useState(0); const [canvasHeight, setCanvasHeight] = useState(0); + const routeMatch = useMatch(Routes.TUNE_DIAGNOSE_FILE); + const { fetchLogFileWithProgress } = useServerStorage(); + const navigate = useNavigate(); + const calculateCanvasSize = useCallback(() => { setCanvasWidth((contentRef.current?.clientWidth || 0) - margin); if (window.innerHeight > minCanvasHeightInner) { - setCanvasHeight(Math.round((window.innerHeight - 250) / 2)); + setCanvasHeight(Math.round(window.innerHeight - 250)); } else { setCanvasHeight(minCanvasHeightInner / 2); } @@ -91,47 +109,92 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: ConfigState store.dispatch({ type: 'ui/sidebarCollapsed', payload: collapsed }); }, }; - const [logs, setLogs] = useState(); - const [toothLogs, setToothLogs] = useState(); useEffect(() => { const controller = new AbortController(); const { signal } = controller; const loadData = async () => { - const pako = await import('pako'); + const logFileName = routeMatch?.params.fileName; + + if (!logFileName) { + return; + } + + // user didn't upload any logs + if (tuneData && (tuneData.toothLogFiles || []).length === 0) { + navigate(Routes.HUB); + + return; + } try { - const compositeRaw = await loadCompositeLogs((percent, total, edge) => { + const raw = await fetchLogFileWithProgress(tuneData.id, logFileName, (percent, total, edge) => { setProgress(percent); setFileSize(formatBytes(total)); setEdgeLocation(edge || edgeUnknown); }, signal); - const toothRaw = await loadToothLogs(undefined, signal); - - setFileSize(formatBytes(compositeRaw.byteLength)); + setFileSize(formatBytes(raw.byteLength)); setStep(1); - const resultComposite = (new TriggerLogsParser(pako.inflate(new Uint8Array(compositeRaw)))) - .parse() - .getCompositeLogs(); - const resultTooth = (new TriggerLogsParser(pako.inflate(new Uint8Array(toothRaw)))) - .parse() - .getToothLogs(); + const parser = new TriggerLogsParser(Pako.inflate(new Uint8Array(raw))).parse(); - setLogs(resultComposite); - setToothLogs(resultTooth); + let type = ''; + let result: CompositeLogEntry[] | ToothLogEntry[] = []; + + if (parser.isComposite()) { + type = 'composite'; + result = parser.getCompositeLogs(); + } + + if (parser.isTooth()) { + type = 'tooth'; + result = parser.getToothLogs(); + } + + store.dispatch({ + type: 'toothLogs/load', payload: { + fileName: logFileName, + logs: result, + type, + }, + }); setStep(2); } catch (error) { + if (isAbortedRequest(error as Error)) { + return; + } + setFetchError(error as Error); - Sentry.captureException(error); - console.error(error); } }; - loadData(); + // first visit, logs are not loaded yet + if (!loadedToothLogs.type && tuneData?.tuneId) { + loadData(); + } + + // file changed, reload + if (loadedToothLogs.type && loadedToothLogs.fileName !== routeMatch?.params.fileName) { + // setToothLogs(undefined); + // setCompositeLogs(undefined); + store.dispatch({ type: 'toothLogs/load', payload: {} }); + loadData(); + } + + // user navigated to logs root page + if (!routeMatch?.params.fileName && tuneData.toothLogFiles?.length) { + // either redirect to the first log or to the latest selected + if (loadedToothLogs.fileName) { + navigate(generatePath(Routes.TUNE_DIAGNOSE_FILE, { tuneId: tuneData.tuneId, fileName: loadedToothLogs.fileName })); + } else { + const firstLogFile = (tuneData.toothLogFiles || [])[0]; + navigate(generatePath(Routes.TUNE_DIAGNOSE_FILE, { tuneId: tuneData.tuneId, fileName: firstLogFile })); + } + } + calculateCanvasSize(); window.addEventListener('resize', calculateCanvasSize); @@ -140,12 +203,32 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: ConfigState controller.abort(); window.removeEventListener('resize', calculateCanvasSize); }; - }, [calculateCanvasSize, loadedLogs, ui.sidebarCollapsed]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [calculateCanvasSize, routeMatch?.params.fileName, ui.sidebarCollapsed, tuneData?.tuneId]); + + const graphSection = () => { + switch (loadedToothLogs.type) { + case 'composite': + return ; + case 'tooth': + return ; + default: + return null; + } + }; return ( <> - {!logs && !(loadedLogs.logs || []).length ? + {!loadedToothLogs.type ? : !ui.sidebarCollapsed && @@ -155,15 +238,26 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: ConfigState items={[ { label: ( - + Files ), key: 'files', children: ( - tooth.csv - composite.csv + {tuneData?.toothLogFiles?.map((fileName) => ( + + + {removeFilenameSuffix(fileName)} + + + ))} ), }, @@ -174,22 +268,9 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: ConfigState
- {toothLogs && logs + {loadedToothLogs.type ? - ( - - - - - ) + graphSection() : { const { lg } = useBreakpoint(); const { Sider } = Layout; @@ -188,8 +190,7 @@ const Logs = ({ setFileSize(formatBytes(raw.byteLength)); - const pako = await import('pako'); - worker.postMessage(pako.inflate(new Uint8Array(raw)).buffer); + worker.postMessage(raw); worker.onmessage = ({ data }) => { switch (data.type) { @@ -221,6 +222,10 @@ const Logs = ({ } }; } catch (error) { + if (isAbortedRequest(error as Error)) { + return; + } + setFetchError(error as Error); } }; @@ -264,12 +269,12 @@ const Logs = ({ window.removeEventListener('resize', calculateCanvasSize); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [calculateCanvasSize, config?.datalog, config?.outputChannels, loadedLogs, ui.sidebarCollapsed, routeMatch?.params.fileName]); + }, [calculateCanvasSize, config?.datalog, config?.outputChannels, ui.sidebarCollapsed, routeMatch?.params.fileName]); return ( <> - {!logs && !(loadedLogs.logs || []).length ? + {!(loadedLogs.logs || []).length ? : !ui.sidebarCollapsed && @@ -318,15 +323,15 @@ const Logs = ({ }, { label: ( - + Files ), key: 'files', children: ( - {tuneData.logFiles?.map((fileName) => ( - + {tuneData?.logFiles?.map((fileName) => ( + - {fileName} + {removeFilenameSuffix(fileName)} ))} diff --git a/src/pages/Upload.tsx b/src/pages/Upload.tsx index d70aa3b..75a042b 100644 --- a/src/pages/Upload.tsx +++ b/src/pages/Upload.tsx @@ -69,6 +69,7 @@ import { TunesRecordFull, TunesRecordPartial, } from '../types/dbData'; +import { removeFilenameSuffix } from '../pocketbase'; const { Item, useForm } = Form; @@ -139,8 +140,6 @@ const UploadPage = () => { const noop = () => { }; - const removeFilenameSuffix = (filename: string) => filename.replace(/(.+)(_\w{10})(\.\w+)$/, '$1$3'); - const goToNewTune = () => navigate(generatePath(Routes.TUNE_TUNE, { tuneId: newTuneId!, })); diff --git a/src/pocketbase.ts b/src/pocketbase.ts index f1b6c80..410e3e4 100644 --- a/src/pocketbase.ts +++ b/src/pocketbase.ts @@ -17,6 +17,8 @@ const formatError = (error: any) => { return message; }; +const removeFilenameSuffix = (filename: string) => filename.replace(/(.+)(_\w{10})(\.\w+)$/, '$1$3'); + // NOTE: PocketBase doesn't return ISO time, this may change here: https://github.com/pocketbase/pocketbase/issues/376 const formatTime = (time: string) => new Date(`${time}Z`).toLocaleString(); @@ -25,4 +27,5 @@ export { client, formatError, formatTime, + removeFilenameSuffix, }; diff --git a/src/store.ts b/src/store.ts index f5892ce..43736ea 100644 --- a/src/store.ts +++ b/src/store.ts @@ -9,6 +9,7 @@ import { AppState, ConfigState, LogsState, + ToothLogsState, TuneDataState, TuneState, UpdateTunePayload, @@ -25,6 +26,7 @@ const setTuneId = createAction('navigation/tuneId'); // logs const loadLogs = createAction('logs/load'); +const loadToothLogs = createAction('toothLogs/load'); // status bar const setStatus = createAction('status'); @@ -40,6 +42,7 @@ const initialState: AppState = { }, tuneData: {} as any, logs: {} as any, + toothLogs: {} as any, config: {} as any, ui: { sidebarCollapsed: false, @@ -66,6 +69,9 @@ const rootReducer = createReducer(initialState, (builder) => { .addCase(loadLogs, (state: AppState, action) => { state.logs = action.payload; }) + .addCase(loadToothLogs, (state: AppState, action) => { + state.toothLogs = action.payload; + }) .addCase(updateTune, (state: AppState, action) => { state.tune.constants[action.payload.name].value = action.payload.value; }) diff --git a/src/types/state.ts b/src/types/state.ts index ee97b76..a24a8be 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -3,6 +3,10 @@ import { Logs, TuneWithDetails, } from '@hyper-tuner/types'; +import { + CompositeLogEntry, + ToothLogEntry, +} from '../utils/logs/TriggerLogsParser'; import { TunesRecordFull } from './dbData'; export interface ConfigState extends Config {} @@ -16,6 +20,12 @@ export interface LogsState { logs: Logs; } +export interface ToothLogsState { + fileName: string; + type: 'tooth' | 'composite'; + logs: CompositeLogEntry[] | ToothLogEntry[]; +} + export interface UIState { sidebarCollapsed: boolean; } @@ -33,6 +43,7 @@ export interface AppState { tuneData: TuneDataState; config: ConfigState; logs: LogsState, + toothLogs: ToothLogsState, ui: UIState; status: StatusState; navigation: NavigationState; diff --git a/src/utils/api.ts b/src/utils/api.ts index b4db476..5ecd8b7 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -4,20 +4,18 @@ import store from '../store'; import stdDialogs from '../data/standardDialogs'; import help from '../data/help'; import { divider } from '../data/constants'; -import { - fetchWithProgress, - OnProgress, -} from './http'; import TuneParser from './tune/TuneParser'; -import useServerStorage, { CDN_URL } from '../hooks/useServerStorage'; +import useServerStorage from '../hooks/useServerStorage'; import { TunesRecordFull } from '../types/dbData'; import { iniLoadingError } from '../pages/auth/notifications'; // TODO: refactor this!! +// eslint-disable-next-line import/prefer-default-export export const loadTune = async (tuneData: TunesRecordFull | null) => { if (tuneData === null) { store.dispatch({ type: 'config/load', payload: null }); store.dispatch({ type: 'tune/load', payload: null }); + return; } @@ -62,24 +60,3 @@ export const loadTune = async (tuneData: TunesRecordFull | null) => { iniLoadingError((error as Error)); } }; - -export const loadLogs = (onProgress?: OnProgress, signal?: AbortSignal) => - fetchWithProgress( - `${CDN_URL}/public/temp/long.mlg.gz`, - onProgress, - signal, - ).then((response) => response); - -export const loadCompositeLogs = (onProgress?: OnProgress, signal?: AbortSignal) => - fetchWithProgress( - `${CDN_URL}/public/temp/composite_1.csv.gz`, - onProgress, - signal, - ).then((response) => response); - -export const loadToothLogs = (onProgress?: OnProgress, signal?: AbortSignal) => - fetchWithProgress( - `${CDN_URL}/public/temp/tooth_3.csv.gz`, - onProgress, - signal, - ).then((response) => response); diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 0000000..093879e --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const isAbortedRequest = (error: Error): boolean => error.message === 'The user aborted a request.'; diff --git a/src/utils/logs/TriggerLogsParser.ts b/src/utils/logs/TriggerLogsParser.ts index f7905ee..ad3dc3c 100644 --- a/src/utils/logs/TriggerLogsParser.ts +++ b/src/utils/logs/TriggerLogsParser.ts @@ -49,11 +49,9 @@ class TriggerLogsParser implements ParserInterface { this.parseCompositeLogs(this.raw); this.parseToothLogs(this.raw); - if (this.resultComposite.length > 0) { + if (this.resultComposite.length > this.resultTooth.length) { this.isCompositeLogs = true; - } - - if (this.resultTooth.length > 0) { + } else { this.isToothLogs = true; } diff --git a/src/workers/mlgParser.ts b/src/workers/mlgParser.ts index 4d75573..55542c8 100644 --- a/src/workers/mlgParser.ts +++ b/src/workers/mlgParser.ts @@ -1,6 +1,7 @@ /* eslint-disable no-bitwise */ import { Parser } from 'mlg-converter'; +import Pako from 'pako'; // eslint-disable-next-line no-restricted-globals const ctx: Worker = self as any; @@ -8,7 +9,7 @@ const ctx: Worker = self as any; ctx.addEventListener('message', ({ data }: { data: ArrayBuffer }) => { try { const t0 = performance.now(); - const result = new Parser(data).parse((progress) => { + const result = new Parser(Pako.inflate(new Uint8Array(data)).buffer).parse((progress) => { ctx.postMessage({ type: 'progress', progress,