Compare commits
17 Commits
5ab491ee91
...
745f4fb59a
Author | SHA1 | Date |
---|---|---|
Piotr Rogowski | 745f4fb59a | |
Piotr Rogowski | 30b9b09ff1 | |
Piotr Rogowski | 6268325f80 | |
Piotr Rogowski | c79f8d0d6f | |
Piotr Rogowski | eafd652b83 | |
Piotr Rogowski | 608f20008e | |
Piotr Rogowski | a07a76d90e | |
dependabot[bot] | c0c06f866c | |
Piotr Rogowski | 7991d92d41 | |
Piotr Rogowski | cb6d31110c | |
dependabot[bot] | 62da1247da | |
Piotr Rogowski | c61a89237a | |
Piotr Rogowski | db7667f0c1 | |
Piotr Rogowski | 0717fcb1ce | |
Piotr Rogowski | 649a09f296 | |
Piotr Rogowski | 34c339b0c7 | |
Piotr Rogowski | d577b82174 |
|
@ -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"
|
||||
}
|
|
@ -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: "/"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
"editor.formatOnSave": true,
|
||||
"cSpell.words": [
|
||||
"baro",
|
||||
"devcontainers",
|
||||
"FOME",
|
||||
"hypertuner",
|
||||
"kbar",
|
||||
"MLVLG",
|
||||
"noisymime",
|
||||
"pocketbase",
|
||||
"prefs",
|
||||
|
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
10
rome.json
10
rome.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
@ -168,6 +168,7 @@ const LogCanvas = ({
|
|||
selectedFields2.length,
|
||||
plotSync.key,
|
||||
);
|
||||
|
||||
options2 = result2.options;
|
||||
plotData2 = [result2.xData, ...result2.yData];
|
||||
}
|
||||
|
|
|
@ -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 />,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = (
|
||||
<>
|
||||
|
|
|
@ -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);
|
|
@ -0,0 +1 @@
|
|||
export const bufferToFile = (buffer: ArrayBuffer, name: string) => new File([buffer], name);
|
|
@ -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, '');
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"incremental": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
|
Loading…
Reference in New Issue