Compare commits

...

17 Commits

Author SHA1 Message Date
Piotr Rogowski 745f4fb59a
Add helper script 2023-09-11 11:30:39 +02:00
Piotr Rogowski 30b9b09ff1
Bump antd 2023-09-11 11:30:19 +02:00
Piotr Rogowski 6268325f80
Add fuzzy search for log fields 2023-09-11 11:15:39 +02:00
Piotr Rogowski c79f8d0d6f
Remove unused imports 2023-09-11 09:26:09 +02:00
Piotr Rogowski eafd652b83
Skip markers for now in MSL 2023-09-10 21:50:02 +02:00
Piotr Rogowski 608f20008e
Refactor compression, use gzip level 9 2023-09-10 18:05:56 +02:00
Piotr Rogowski a07a76d90e
Update dependencies 2023-09-10 17:21:33 +02:00
dependabot[bot] c0c06f866c
Bump actions/checkout from 3 to 4 (#1298)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-10 17:14:04 +02:00
Piotr Rogowski 7991d92d41
Better url matching 2023-08-10 17:27:40 +02:00
Piotr Rogowski cb6d31110c
Better url regex 2023-08-10 13:30:09 +02:00
dependabot[bot] 62da1247da
Bump vite from 4.4.8 to 4.4.9 (#1274)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.8 to 4.4.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.4.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-07 11:34:49 +02:00
Piotr Rogowski c61a89237a
Update dependabot interval 2023-08-06 13:44:38 +02:00
Piotr Rogowski db7667f0c1
Update dependencies 2023-08-06 13:30:39 +02:00
Piotr Rogowski 0717fcb1ce
Stricter lints part 1 2023-08-06 13:20:08 +02:00
Piotr Rogowski 649a09f296
Add support for GitHub Codespaces 2023-07-13 11:33:10 +02:00
Piotr Rogowski 34c339b0c7
Use Node v18 2023-07-13 11:27:27 +02:00
Piotr Rogowski d577b82174
Update dependencies 2023-07-12 17:04:07 +02:00
35 changed files with 1246 additions and 1103 deletions

View File

@ -0,0 +1,41 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "HyperTuner Cloud",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/vscode/devcontainers/base:ubuntu-20.04",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
// default dev server port
5173
],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Configure tool-specific properties.
"customizations": {
"vscode": {
// NOTE: keep this in sync with: .vscode/extensions.json
"extensions": [
"editorconfig.editorconfig",
"davidanson.vscode-markdownlint",
"streetsidesoftware.code-spell-checker",
"rome.rome"
]
}
},
// install dependencies
"postCreateCommand": "npm i",
// run dev server
"postStartCommand": "npm start",
"containerUser": "vscode",
"waitFor": "postCreateCommand",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"node": {
"version": "18"
},
"github-cli": "latest"
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@ -4,8 +4,8 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 20
interval: "weekly"
open-pull-requests-limit: 30
- package-ecosystem: "github-actions"
directory: "/"

View File

@ -42,7 +42,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v3

View File

@ -6,9 +6,9 @@ concurrency:
on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]
jobs:
lint:
@ -17,10 +17,10 @@ jobs:
NPM_GITHUB_TOKEN: ${{ secrets.NPM_GITHUB_PAT }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: "lts/*"
node-version-file: ".nvmrc"
cache: "npm"
- run: npm ci
- run: npm run lint

2
.nvmrc
View File

@ -1 +1 @@
v16
v18

View File

@ -3,9 +3,11 @@
"editor.formatOnSave": true,
"cSpell.words": [
"baro",
"devcontainers",
"FOME",
"hypertuner",
"kbar",
"MLVLG",
"noisymime",
"pocketbase",
"prefs",

View File

@ -2,23 +2,29 @@
This guide will help you set up this project.
## Requirements
## Using GitHub Codespaces
This project is configured to work with GitHub [Codespaces](https://github.com/features/codespaces). To use it, simply open the project in a Codespace using GitHub's UI.
## Local development
### Requirements
- Node Version Manager: [nvm](https://github.com/nvm-sh/nvm)
### Setup local environment variables
#### Setup local environment variables
```bash
cp .env .env.local
```
### Setup correct Node.js version
#### Setup correct Node.js version
```bash
nvm use
```
### Install dependencies and run in development mode
#### Install dependencies and run in development mode
```bash
# install packages

1875
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,8 @@
"start": "vite",
"build": "tsc && vite build",
"serve": "vite preview",
"lint": "tsc && rome ci src",
"lint": "tsc && npm run lint:rome",
"lint:rome": "rome ci src",
"lint:fix": "rome format --write src && rome check --apply src",
"lint:fix:unsafe": "rome check --apply-unsafe src",
"analyze": "npm run build && open stats.html",
@ -24,42 +25,43 @@
"@hyper-tuner/ini": "git+https://github.com/hyper-tuner/ini.git",
"@hyper-tuner/types": "git+https://github.com/hyper-tuner/types.git",
"@reduxjs/toolkit": "^1.9.5",
"@sentry/react": "^7.54.0",
"@sentry/tracing": "^7.54.0",
"antd": "^4.24.10",
"kbar": "^0.1.0-beta.40",
"@sentry/react": "^7.68.0",
"@sentry/tracing": "^7.68.0",
"antd": "^4.24.14",
"fuse.js": "^6.6.2",
"kbar": "^0.1.0-beta.43",
"lodash.debounce": "^4.0.8",
"mlg-converter": "^0.9.0",
"nanoid": "^4.0.2",
"pako": "^2.1.0",
"pocketbase": "^0.15.1",
"pocketbase": "^0.18.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-ga4": "^2.1.0",
"react-markdown": "^8.0.7",
"react-perfect-scrollbar": "^1.5.8",
"react-redux": "^8.0.7",
"react-router-dom": "^6.11.2",
"uplot": "^1.6.24",
"uplot-react": "^1.1.4",
"vite": "^4.3.9"
"react-redux": "^8.1.2",
"react-router-dom": "^6.15.0",
"uplot": "^1.6.25",
"uplot-react": "^1.1.5",
"vite": "^4.4.9"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.4.2",
"@total-typescript/ts-reset": "^0.5.1",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^20.2.5",
"@types/node": "^20.6.0",
"@types/pako": "^2.0.0",
"@types/react": "^18.2.8",
"@types/react-dom": "^18.2.4",
"@types/react-redux": "^7.1.25",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/react-redux": "^7.1.26",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.0.0",
"less": "^4.1.3",
"pocketbase-typegen": "^1.1.9",
"rollup-plugin-visualizer": "^5.9.0",
"@vitejs/plugin-react": "^4.0.4",
"less": "^4.2.0",
"pocketbase-typegen": "^1.1.13",
"rollup-plugin-visualizer": "^5.9.2",
"rome": "^12.1.3",
"typescript": "^5.1.3",
"typescript": "^5.2.2",
"vite-plugin-html": "^3.2.0",
"vite-plugin-pwa": "^0.16.3"
"vite-plugin-pwa": "^0.16.5"
}
}

View File

@ -18,17 +18,25 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"all": true,
"correctness": {
"all": true,
"noUnusedVariables": "warn"
},
"style": {
"all": true,
"noImplicitBoolean": "off",
"useEnumInitializers": "off",
"noNonNullAssertion": "off"
},
"suspicious": {
"all": true,
"noExplicitAny": "off"
},
"nursery": {
"all": true,
"useExhaustiveDependencies": "off",
"noForEach": "off"
}
}
}

18
scripts/decompress.js Normal file
View File

@ -0,0 +1,18 @@
const Pako = require('pako');
// get file from command line
const file = process.argv[2];
// read file
const fs = require('fs');
const data = fs.readFileSync(file);
const result = Pako.inflate(data, { to: 'string' });
// write file with suffix .decompressed
const path = require('path');
const basename = path.basename(file);
const extname = path.extname(file);
const filename = basename.slice(0, -extname.length);
const newFile = `${filename}.decompressed${extname}`;
fs.writeFileSync(newFile, result);

View File

@ -39,14 +39,14 @@ export enum IniFilesEcosystemOptions {
fome = 'fome',
}
export type IniFilesRecord = {
signature: string;
file: string;
ecosystem: IniFilesEcosystemOptions;
file: string;
signature: string;
};
export type StargazersRecord = {
user: RecordIdString;
tune: RecordIdString;
user: RecordIdString;
};
export enum TunesSourceOptions {
@ -70,32 +70,32 @@ export enum TunesVisibilityOptions {
unlisted = 'unlisted',
}
export type TunesRecord = {
author: RecordIdString;
tuneId: string;
source: TunesSourceOptions;
signature: string;
stars?: number;
vehicleName: string;
engineMake: string;
engineCode: string;
displacement: number;
cylindersCount: number;
aspiration: TunesAspirationOptions;
author: RecordIdString;
compression?: number;
customIniFile?: string;
cylindersCount: number;
displacement: number;
engineCode: string;
engineMake: string;
fuel?: string;
hp?: number;
ignition?: string;
injectorsSize?: number;
year?: number;
hp?: number;
stockHp?: number;
logFiles?: string[];
readme: string;
signature: string;
source: TunesSourceOptions;
stars?: number;
stockHp?: number;
tags?: TunesTagsOptions;
textSearch: string;
visibility: TunesVisibilityOptions;
tuneFile: string;
customIniFile?: string;
logFiles?: string[];
toothLogFiles?: string[];
tuneFile: string;
tuneId: string;
vehicleName: string;
visibility: TunesVisibilityOptions;
year?: number;
};
export type UsersRecord = {
@ -104,11 +104,12 @@ export type UsersRecord = {
};
// Response types include system fields and match responses from the PocketBase API
export type IniFilesResponse = Required<IniFilesRecord> & BaseSystemFields;
export type IniFilesResponse<Texpand = unknown> = Required<IniFilesRecord> &
BaseSystemFields<Texpand>;
export type StargazersResponse<Texpand = unknown> = Required<StargazersRecord> &
BaseSystemFields<Texpand>;
export type TunesResponse<Texpand = unknown> = Required<TunesRecord> & BaseSystemFields<Texpand>;
export type UsersResponse = Required<UsersRecord> & AuthSystemFields;
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand>;
// Types containing all Records and Responses, useful for creating typing helper functions

View File

@ -195,12 +195,13 @@ const ResultItem = forwardRef(
const RenderResults = () => {
const { results, rootActionId } = useMatches();
const onResultsRender = ({ item, active }: { item: any; active: boolean }) =>
typeof item === 'string' ? (
const onResultsRender = ({ item, active }: { item: any; active: boolean }) => {
return typeof item === 'string' ? (
<div style={groupNameStyle}>{item}</div>
) : (
<ResultItem action={item} active={active} currentRootActionId={rootActionId as string} />
);
};
return <KBarResults items={results} onRender={onResultsRender} />;
};

View File

@ -168,6 +168,7 @@ const LogCanvas = ({
selectedFields2.length,
plotSync.key,
);
options2 = result2.options;
plotData2 = [result2.xData, ...result2.yData];
}

View File

@ -21,9 +21,10 @@ import {
UnorderedListOutlined,
UpCircleOutlined,
} from '@ant-design/icons';
import React from 'react';
const Icon = ({ name }: { name: string }): JSX.Element => {
const map: { [index: string]: JSX.Element } = {
const Icon = ({ name }: { name: string }): React.JSX.Element => {
const map: { [index: string]: React.JSX.Element } = {
settings: <ControlOutlined />,
tuning: <CarOutlined />,
spark: <FireOutlined />,

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useLocation, useNavigate, Link, generatePath, useMatch } from 'react-router-dom';
import { Layout, Space, Button, Row, Col, Tooltip, Grid, Dropdown, Typography, Radio } from 'antd';
import {
@ -39,7 +39,7 @@ import { buildHyperTunerAppLink } from '../utils/url';
const { Header } = Layout;
const { useBreakpoint } = Grid;
const logsExtensionsIcons: { [key: string]: JSX.Element } = {
const logsExtensionsIcons: { [key: string]: React.JSX.Element } = {
mlg: <FileZipOutlined />,
msl: <FileTextOutlined />,
csv: <FileExcelOutlined />,
@ -264,39 +264,37 @@ const TopBar = ({
return list.length ? list : null;
}, [lg, sm]);
const userAuthMenuItems = useMemo(
() =>
currentUser
? [
{
key: 'profile',
icon: <UserOutlined />,
label: 'Profile',
onClick: () => navigate(Routes.PROFILE),
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: 'Logout',
onClick: logoutClick,
},
]
: [
{
key: 'login',
icon: <LoginOutlined />,
label: 'Login',
onClick: () => navigate(Routes.LOGIN),
},
{
key: 'sign-up',
icon: <UserAddOutlined />,
label: 'Sign Up',
onClick: () => navigate(Routes.SIGN_UP),
},
],
[currentUser, logoutClick, navigate],
);
const userAuthMenuItems = useMemo(() => {
return currentUser
? [
{
key: 'profile',
icon: <UserOutlined />,
label: 'Profile',
onClick: () => navigate(Routes.PROFILE),
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: 'Logout',
onClick: logoutClick,
},
]
: [
{
key: 'login',
icon: <LoginOutlined />,
label: 'Login',
onClick: () => navigate(Routes.LOGIN),
},
{
key: 'sign-up',
icon: <UserAddOutlined />,
label: 'Sign Up',
onClick: () => navigate(Routes.SIGN_UP),
},
];
}, [currentUser, logoutClick, navigate]);
const userMenuItems = [
...userAuthMenuItems,

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { connect } from 'react-redux';
import { Form, Divider, Col, Row, Popover, Space, Result } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
@ -81,7 +81,7 @@ const Dialog = ({
tune && config && Object.keys(tune.constants).length && Object.keys(config.constants).length;
const { storageSet } = useBrowserStorage();
const { findConstantOnPage } = useConfig(config);
const [panelsComponents, setPanelsComponents] = useState<(JSX.Element | null)[]>([]);
const [panelsComponents, setPanelsComponents] = useState<(React.JSX.Element | null)[]>([]);
const containerRef = useRef<HTMLDivElement | null>(null);
const renderHelp = (link?: string) =>

View File

@ -50,13 +50,13 @@ const SmartSelect = ({
style={{ maxWidth: 250 }}
>
{/* we need to preserve indexes here, skip INVALID option */}
{values.map((val: string, index) =>
val === 'INVALID' ? null : (
{values.map((val: string, index) => {
return val === 'INVALID' ? null : (
<Select.Option key={val} value={index} label={val}>
{val}
</Select.Option>
),
)}
);
})}
</Select>
);
};

View File

@ -9,26 +9,28 @@ const TextField = ({ title }: { title: string }) => {
const message = type ? title.substring(1) : title;
let messageTag = <span>{message}</span>;
// check if message contains a link and render it as a link
const linkRegex = /https?:\/\/[^\s]+/g;
const linkMatch = message.match(linkRegex);
if (linkMatch) {
const link = linkMatch[0];
const linkIndex = message.indexOf(link);
const beforeLink = message.substring(0, linkIndex);
const afterLink = message.substring(linkIndex + link.length);
// check if message contains url and render it as a link
const urlPattern = /(?<url>https?:\/\/[^:[\]@!$'(),; ]+)/;
const matches = message.split(urlPattern);
messageTag = (
<>
{beforeLink}
<a href={link} target='_blank' rel='noreferrer' style={{ color: 'inherit' }}>
{link}
</a>
{afterLink}
</>
);
if (!matches) {
return messageTag;
}
const parts = matches.map((part) => {
if (urlPattern.test(part)) {
return (
<a href={part} target='_blank' rel='noreferrer' style={{ color: 'inherit' }}>
{part}
</a>
);
}
return part;
});
messageTag = <span>{parts}</span>;
return (
<Typography.Paragraph style={{ display: 'flex', justifyContent: 'center' }}>
{type ? (

View File

@ -1,7 +1,10 @@
import { Tag } from 'antd';
import { TunesTagsOptions } from '../@types/pocketbase-types';
const TuneTag = ({ tag }: { tag: TunesTagsOptions | undefined }) =>
tag ? <Tag color={tag === TunesTagsOptions['base map'] ? 'green' : 'red'}>{tag}</Tag> : null;
const TuneTag = ({ tag }: { tag: TunesTagsOptions | undefined }) => {
return tag ? (
<Tag color={tag === TunesTagsOptions['base map'] ? 'green' : 'red'}>{tag}</Tag>
) : null;
};
export default TuneTag;

View File

@ -27,6 +27,7 @@ interface AuthValue {
}
const AuthContext = createContext<AuthValue | null>(null);
// rome-ignore lint/nursery/useHookAtTopLevel: <explanation>
const useAuth = () => useContext<AuthValue>(AuthContext as any);
const users = client.collection(Collections.Users);

View File

@ -50,6 +50,7 @@ const findDatalog = (config: ConfigType, name: string): DatalogEntry => {
};
const useConfig = (config: ConfigType | null) =>
// rome-ignore lint/nursery/useHookAtTopLevel: <explanation>
useMemo(
() => ({
isConfigReady: !!config?.constants,

View File

@ -1,10 +1,10 @@
import Pako from 'pako';
import * as Sentry from '@sentry/browser';
import { API_URL, removeFilenameSuffix } from '../pocketbase';
import { Collections } from '../@types/pocketbase-types';
import useDb from './useDb';
import { fetchWithProgress, OnProgress } from '../utils/http';
import { downloading } from '../pages/auth/notifications';
import { decompress } from '../utils/compression';
const useServerStorage = () => {
const { getIni } = useDb();
@ -15,7 +15,7 @@ const useServerStorage = () => {
const fetchTuneFile = async (recordId: string, filename: string): Promise<ArrayBuffer> => {
const response = await fetch(buildFileUrl(Collections.Tunes, recordId, filename));
return Pako.inflate(new Uint8Array(await response.arrayBuffer()));
return decompress(await response.arrayBuffer());
};
const fetchINIFile = async (signature: string): Promise<ArrayBuffer> => {
@ -32,7 +32,7 @@ const useServerStorage = () => {
const response = await fetch(buildFileUrl(Collections.IniFiles, ini.id, ini.file));
return Pako.inflate(new Uint8Array(await response.arrayBuffer()));
return decompress(await response.arrayBuffer());
};
const fetchLogFileWithProgress = (
@ -54,7 +54,7 @@ const useServerStorage = () => {
downloading();
const response = await fetch(buildFileUrl(collection, recordId, filename));
const data = Pako.inflate(new Uint8Array(await response.arrayBuffer()));
const data = decompress(await response.arrayBuffer());
const url = window.URL.createObjectURL(new Blob([data]));
anchorRef.href = url;

View File

@ -4,7 +4,6 @@ import { Layout, Tabs, Progress, Steps, Space, Divider, Typography, Badge, Grid
import { FileTextOutlined, GlobalOutlined } from '@ant-design/icons';
import { connect } from 'react-redux';
import PerfectScrollbar from 'react-perfect-scrollbar';
import Pako from 'pako';
import { AppState, ToothLogsState, TuneDataState, UIState } from '../types/state';
import store from '../store';
import { formatBytes } from '../utils/numbers';
@ -21,6 +20,7 @@ import { removeFilenameSuffix } from '../pocketbase';
import useServerStorage from '../hooks/useServerStorage';
import { isAbortedRequest } from '../utils/error';
import { collapsedSidebarWidth, sidebarWidth } from '../components/Tune/SideBar';
import { decompress } from '../utils/compression';
const { Content } = Layout;
@ -106,7 +106,7 @@ const Diagnose = ({
setFileSize(formatBytes(raw.byteLength));
setStep(1);
const parser = new TriggerLogsParser(Pako.inflate(new Uint8Array(raw))).parse();
const parser = new TriggerLogsParser(decompress(raw)).parse();
let type = '';
let result: CompositeLogEntry[] | ToothLogEntry[] = [];

View File

@ -12,6 +12,7 @@ import {
Badge,
Typography,
Grid,
Input,
} from 'antd';
import { FileTextOutlined, EditOutlined, GlobalOutlined } from '@ant-design/icons';
import { CheckboxValueType } from 'antd/es/checkbox/Group';
@ -19,7 +20,6 @@ import { connect } from 'react-redux';
import { Result as ParserResult } from 'mlg-converter/dist/types';
import PerfectScrollbar from 'react-perfect-scrollbar';
import { OutputChannel, Logs as LogsType, DatalogEntry } from '@hyper-tuner/types';
import LogParserWorker from '../workers/logParserWorker?worker';
import LogCanvas, { SelectedField } from '../components/Logs/LogCanvas';
import store from '../store';
@ -35,11 +35,29 @@ import { removeFilenameSuffix } from '../pocketbase';
import { isAbortedRequest } from '../utils/error';
import { WorkerOutput } from '../workers/logParserWorker';
import { collapsedSidebarWidth, sidebarWidth } from '../components/Tune/SideBar';
import Fuse from 'fuse.js';
import debounce from 'lodash.debounce';
const { Content } = Layout;
const edgeUnknown = 'Unknown';
const minCanvasHeightInner = 500;
const badgeStyle = { backgroundColor: Colors.TEXT };
const fieldsSectionStyle = { height: 'calc(50vh - 175px)' };
const searchInputStyle = {
width: 'auto',
position: 'sticky' as const,
top: 0,
marginBottom: 10,
};
const fuseOptions = {
shouldSort: true,
includeMatches: false,
includeScore: false,
ignoreLocation: false,
findAllMatches: false,
threshold: 0.4,
keys: ['label'], // TODO: handle expression
};
const mapStateToProps = (state: AppState) => ({
ui: state.ui,
@ -153,6 +171,18 @@ const Logs = ({
},
[config?.datalog, findOutputChannel, isConfigReady],
);
const [foundFields1, setFoundFields1] = useState<DatalogEntry[]>([]);
const [foundFields2, setFoundFields2] = useState<DatalogEntry[]>([]);
const fuse = new Fuse<DatalogEntry>(fields, fuseOptions);
const debounceSearch1 = debounce(async (searchText: string) => {
const result = fuse.search(searchText);
setFoundFields1(result.length > 0 ? result.map((item) => item.item) : fields);
}, 300);
const debounceSearch2 = debounce(async (searchText: string) => {
const result = fuse.search(searchText);
setFoundFields2(result.length > 0 ? result.map((item) => item.item) : fields);
}, 300);
useEffect(() => {
const worker = new LogParserWorker();
@ -263,7 +293,10 @@ const Logs = ({
}
if (config?.outputChannels) {
setFields(Object.values(config.datalog));
const fields = Object.values(config.datalog);
setFields(fields);
setFoundFields1(fields);
setFoundFields2(fields);
}
calculateCanvasSize();
@ -302,11 +335,24 @@ const Logs = ({
key: 'fields',
children: (
<>
<div style={showSingleGraph ? {} : { height: '45%' }}>
<Input
onChange={({ target }) => debounceSearch1(target.value)}
style={searchInputStyle}
placeholder='Search fields...'
allowClear
/>
<div
style={
showSingleGraph ? { height: 'calc(100vh - 250px)' } : fieldsSectionStyle
}
>
<PerfectScrollbar options={{ suppressScrollX: true }}>
<Checkbox.Group onChange={setSelectedFields1} value={selectedFields1}>
{fields.map((field) => (
<Row key={field.name}>
<Row
key={field.name}
hidden={!foundFields1.find((f) => f.name === field.name)}
>
<Checkbox key={field.name} value={field.name}>
{isExpression(field.label)
? stripExpression(field.label)
@ -320,11 +366,20 @@ const Logs = ({
{!showSingleGraph && (
<>
<Divider />
<div style={{ height: '45%' }}>
<Input
onChange={({ target }) => debounceSearch2(target.value)}
style={searchInputStyle}
placeholder='Search fields...'
allowClear
/>
<div style={fieldsSectionStyle}>
<PerfectScrollbar options={{ suppressScrollX: true }}>
<Checkbox.Group onChange={setSelectedFields2} value={selectedFields2}>
{fields.map((field) => (
<Row key={field.name}>
<Row
key={field.name}
hidden={!foundFields2.find((f) => f.name === field.name)}
>
<Checkbox key={field.name} value={field.name}>
{isExpression(field.label)
? stripExpression(field.label)

View File

@ -35,7 +35,6 @@ import { INI } from '@hyper-tuner/ini';
import { UploadRequestOption } from 'rc-upload/lib/interface';
import { UploadFile } from 'antd/lib/upload/interface';
import { generatePath, useMatch, useNavigate } from 'react-router-dom';
import Pako from 'pako';
import ReactMarkdown from 'react-markdown';
import { nanoid } from 'nanoid';
import {
@ -65,6 +64,8 @@ import {
TunesVisibilityOptions,
} from '../@types/pocketbase-types';
import { removeFilenameSuffix } from '../pocketbase';
import { bufferToFile } from '../utils/files';
import { compress } from '../utils/compression';
const { Item, useForm } = Form;
@ -103,8 +104,6 @@ const iniIcon = () => <FileTextOutlined />;
const tunePath = (tuneId: string) => generatePath(Routes.TUNE_ROOT, { tuneId });
const tuneParser = new TuneParser();
const bufferToFile = (buffer: ArrayBuffer, name: string) => new File([buffer], name);
const UploadPage = () => {
const [form] = useForm<TunesRecord>();
const routeMatch = useMatch(Routes.UPLOAD_WITH_TUNE_ID);
@ -190,13 +189,13 @@ const UploadPage = () => {
const tags = values.tags || ('' as TunesTagsOptions);
const compressedTuneFile = bufferToFile(
Pako.deflate(await tuneFile!.arrayBuffer()),
await compress(tuneFile!),
(tuneFile as UploadedFile).uid ? tuneFile!.name : removeFilenameSuffix(tuneFile!.name),
);
const compressedCustomIniFile = customIniFile
? bufferToFile(
Pako.deflate(await customIniFile!.arrayBuffer()),
await compress(customIniFile!),
(customIniFile as UploadedFile).uid
? customIniFile!.name
: removeFilenameSuffix(customIniFile!.name),
@ -206,7 +205,7 @@ const UploadPage = () => {
const compressedLogFiles = await Promise.all(
logFiles.map(async (file) =>
bufferToFile(
Pako.deflate(await file.arrayBuffer()),
await compress(file),
(file as UploadedFile).uid ? file.name : removeFilenameSuffix(file.name),
),
),
@ -215,7 +214,7 @@ const UploadPage = () => {
const compressedToothLogFiles = await Promise.all(
toothLogFiles.map(async (file) =>
bufferToFile(
Pako.deflate(await file.arrayBuffer()),
await compress(file),
(file as UploadedFile).uid ? file.name : removeFilenameSuffix(file.name),
),
),
@ -408,7 +407,6 @@ const UploadPage = () => {
}
default:
valid = false;
break;
}
return {

View File

@ -183,8 +183,8 @@ const Login = ({ formRole }: { formRole: FormRoles }) => {
});
}, [listAuthMethods]);
const OauthSection = () =>
isOauthEnabled ? (
const OauthSection = () => {
return isOauthEnabled ? (
<>
<Space direction='horizontal' style={{ width: '100%', justifyContent: 'center' }}>
{providersReady &&
@ -205,6 +205,7 @@ const Login = ({ formRole }: { formRole: FormRoles }) => {
<Divider />
</>
) : null;
};
const bottomLinksLogin = (
<>

5
src/utils/compression.ts Normal file
View File

@ -0,0 +1,5 @@
import Pako from 'pako';
export const compress = async (file: File) => Pako.gzip(await file.arrayBuffer(), { level: 9 });
export const decompress = (data: ArrayBuffer) => Pako.inflate(data);

1
src/utils/files.ts Normal file
View File

@ -0,0 +1 @@
export const bufferToFile = (buffer: ArrayBuffer, name: string) => new File([buffer], name);

View File

@ -1,7 +1,7 @@
import { ParserInterface } from '../ParserInterface';
class LogValidator implements ParserInterface {
private MLG_FORMAT_LENGTH = 6;
private mlgFormatLength = 6;
private isMLGLogs = false;
@ -32,7 +32,7 @@ class LogValidator implements ParserInterface {
}
private checkMLG() {
const fileFormat = new TextDecoder('utf8').decode(this.buffer.slice(0, this.MLG_FORMAT_LENGTH))
const fileFormat = new TextDecoder('utf8').decode(this.buffer.slice(0, this.mlgFormatLength))
.replace(/\x00/gu, '');

View File

@ -1,7 +1,9 @@
import { Result } from 'mlg-converter/dist/types';
import { Record, Result, BlockType } from 'mlg-converter/dist/types';
import { ParserInterface } from '../ParserInterface';
class MslLogParser implements ParserInterface {
private markerPrefix = 'MARK';
private raw = '';
private result: Result = {
@ -32,6 +34,7 @@ class MslLogParser implements ParserInterface {
continue;
}
// header line eg. Time SD: Present SD: Logging triggerScopeReady...
if (line.startsWith('Time') || line.startsWith('RPM')) {
unitsIndex = lineIndex + 1;
const fields = line.split('\t');
@ -50,15 +53,22 @@ class MslLogParser implements ParserInterface {
continue;
}
// markers: eg. MARK 000
if (line.startsWith(this.markerPrefix)) {
// TODO: parse markers
continue;
}
if (lineIndex > unitsIndex) {
// data line eg. 215.389 0 0 0 0 0 1 0 1...
const fields = line.split('\t');
const record = {
type: 'field' as const,
const record: Record = {
type: 'field' as BlockType, // TODO: use enum
timestamp: 0,
};
fields.forEach((value, fieldIndex) => {
(record as any)[this.result.fields[fieldIndex].name] = parseFloat(value);
record[this.result.fields[fieldIndex].name] = parseFloat(value);
});
this.result.records.push(record);

View File

@ -25,9 +25,9 @@ export interface ToothLogEntry {
}
class TriggerLogsParser implements ParserInterface {
private COMMENT_PREFIX = '#';
private commentPrefix = '#';
private MARKER_PREFIX = 'MARK';
private markerPrefix = 'MARK';
private isToothLogs = false;
@ -118,11 +118,11 @@ class TriggerLogsParser implements ParserInterface {
raw.split('\n').forEach((line) => {
const trimmed = line.trim();
if (trimmed.startsWith(this.COMMENT_PREFIX)) {
if (trimmed.startsWith(this.commentPrefix)) {
return;
}
if (trimmed.startsWith(this.MARKER_PREFIX)) {
if (trimmed.startsWith(this.markerPrefix)) {
const previous = this.resultTooth[this.resultTooth.length - 1] || {
toothTime: 0,
time: 0,
@ -160,11 +160,11 @@ class TriggerLogsParser implements ParserInterface {
raw.split('\n').forEach((line) => {
const trimmed = line.trim();
if (trimmed.startsWith(this.COMMENT_PREFIX)) {
if (trimmed.startsWith(this.commentPrefix)) {
return;
}
if (trimmed.startsWith(this.MARKER_PREFIX)) {
if (trimmed.startsWith(this.markerPrefix)) {
const previous = this.resultComposite[this.resultComposite.length - 1] || {
primaryLevel: 0,
secondaryLevel: 0,

View File

@ -1,8 +1,8 @@
import { Parser } from 'mlg-converter';
import { Result } from 'mlg-converter/dist/types';
import Pako from 'pako';
import LogValidator from '../utils/logs/LogValidator';
import MslLogParser from '../utils/logs/MslLogParser';
import { decompress } from '../utils/compression';
const ctx: Worker = self as any;
@ -40,7 +40,7 @@ const parseMlg = (raw: ArrayBufferLike, t0: number): Result =>
ctx.addEventListener('message', ({ data }: { data: ArrayBuffer }) => {
try {
const t0 = performance.now();
const raw = Pako.inflate(new Uint8Array(data)).buffer;
const raw = decompress(data).buffer;
const logParser = new LogValidator(raw);
if (logParser.isMLG()) {
@ -57,6 +57,7 @@ ctx.addEventListener('message', ({ data }: { data: ArrayBuffer }) => {
if (logParser.isMSL()) {
const mslResult = parseMsl(raw, t0);
ctx.postMessage({
type: 'metrics',
elapsed: elapsed(t0),

View File

@ -21,6 +21,7 @@
"incremental": false,
"noUncheckedIndexedAccess": false,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": [
"src"