Load user tooth logs (#816)

This commit is contained in:
Piotr Rogowski 2022-10-19 20:45:38 +02:00 committed by GitHub
parent 9c746cdf43
commit 2fbc2be2e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 183 additions and 106 deletions

3
.env
View File

@ -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"

View File

@ -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();

View File

@ -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<HTMLDivElement | null>(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<CompositeLogEntry[]>();
const [toothLogs, setToothLogs] = useState<ToothLogEntry[]>();
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 <CompositeCanvas
data={loadedToothLogs.logs as CompositeLogEntry[]}
width={canvasWidth}
height={canvasHeight}
/>;
case 'tooth':
return <ToothCanvas
data={loadedToothLogs.logs}
width={canvasWidth}
height={canvasHeight}
/>;
default:
return null;
}
};
return (
<>
<Sider {...(siderProps as any)} className="app-sidebar">
{!logs && !(loadedLogs.logs || []).length ?
{!loadedToothLogs.type ?
<Loader />
:
!ui.sidebarCollapsed &&
@ -155,15 +238,26 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: ConfigState
items={[
{
label: (
<Badge size="small" style={badgeStyle} count={1} offset={[10, -3]}>
<Badge size="small" style={badgeStyle} count={tuneData?.toothLogFiles?.length} offset={[10, -3]}>
<FileTextOutlined />Files
</Badge>
),
key: 'files',
children: (
<PerfectScrollbar options={{ suppressScrollX: true }}>
<Typography.Paragraph>tooth.csv</Typography.Paragraph>
<Typography.Paragraph>composite.csv</Typography.Paragraph>
{tuneData?.toothLogFiles?.map((fileName) => (
<Typography.Paragraph key={fileName} ellipsis>
<Link
to={generatePath(Routes.TUNE_DIAGNOSE_FILE, { tuneId: tuneData.tuneId, fileName })}
style={
routeMatch?.params.fileName === fileName ?
{} : { color: 'inherit' }
}
>
{removeFilenameSuffix(fileName)}
</Link>
</Typography.Paragraph>
))}
</PerfectScrollbar>
),
},
@ -174,22 +268,9 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: ConfigState
<Layout style={{ width: '100%', textAlign: 'center', marginTop: 50 }}>
<Content>
<div ref={contentRef} style={{ width: '100%', marginRight: margin }}>
{toothLogs && logs
{loadedToothLogs.type
?
(
<Space direction="vertical" size="large">
<ToothCanvas
data={toothLogs!}
width={canvasWidth}
height={canvasHeight}
/>
<CompositeCanvas
data={logs!}
width={canvasWidth}
height={canvasHeight}
/>
</Space>
)
graphSection()
:
<Space
direction="vertical"

View File

@ -61,6 +61,8 @@ import Loader from '../components/Loader';
import { Colors } from '../utils/colors';
import useServerStorage from '../hooks/useServerStorage';
import { Routes } from '../routes';
import { removeFilenameSuffix } from '../pocketbase';
import { isAbortedRequest } from '../utils/error';
const { Content } = Layout;
const { Step } = Steps;
@ -84,10 +86,10 @@ const Logs = ({
loadedLogs,
tuneData,
}: {
ui: UIState,
config: ConfigState,
loadedLogs: LogsState,
tuneData: TuneDataState,
ui: UIState;
config: ConfigState;
loadedLogs: LogsState;
tuneData: TuneDataState;
}) => {
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 (
<>
<Sider {...(siderProps as any)} className="app-sidebar">
{!logs && !(loadedLogs.logs || []).length ?
{!(loadedLogs.logs || []).length ?
<Loader />
:
!ui.sidebarCollapsed &&
@ -318,15 +323,15 @@ const Logs = ({
},
{
label: (
<Badge size="small" style={badgeStyle} count={tuneData.logFiles?.length} offset={[10, -3]}>
<Badge size="small" style={badgeStyle} count={tuneData?.logFiles?.length} offset={[10, -3]}>
<FileTextOutlined />Files
</Badge>
),
key: 'files',
children: (
<PerfectScrollbar options={{ suppressScrollX: true }}>
{tuneData.logFiles?.map((fileName) => (
<Typography.Paragraph key={fileName}>
{tuneData?.logFiles?.map((fileName) => (
<Typography.Paragraph key={fileName} ellipsis>
<Link
to={generatePath(Routes.TUNE_LOGS_FILE, { tuneId: tuneData.tuneId, fileName })}
style={
@ -334,7 +339,7 @@ const Logs = ({
{} : { color: 'inherit' }
}
>
{fileName}
{removeFilenameSuffix(fileName)}
</Link>
</Typography.Paragraph>
))}

View File

@ -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!,
}));

View File

@ -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,
};

View File

@ -9,6 +9,7 @@ import {
AppState,
ConfigState,
LogsState,
ToothLogsState,
TuneDataState,
TuneState,
UpdateTunePayload,
@ -25,6 +26,7 @@ const setTuneId = createAction<string>('navigation/tuneId');
// logs
const loadLogs = createAction<LogsState>('logs/load');
const loadToothLogs = createAction<ToothLogsState>('toothLogs/load');
// status bar
const setStatus = createAction<string>('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;
})

View File

@ -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;

View File

@ -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);

2
src/utils/error.ts Normal file
View File

@ -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.';

View File

@ -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;
}

View File

@ -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,