Compare commits
11 Commits
4f9afdefe9
...
0bc6ccc1c9
Author | SHA1 | Date |
---|---|---|
Piotr Rogowski | 0bc6ccc1c9 | |
Piotr Rogowski | 0ebea1852f | |
Piotr Rogowski | 916a3bafd4 | |
dependabot[bot] | 956ca6d329 | |
Piotr Rogowski | a1e2049c6c | |
Piotr Rogowski | ff6894c16f | |
Piotr Rogowski | bab8af76d5 | |
Karol Piecuch | 3ea4470583 | |
Karol Piecuch | f06382b5a1 | |
Piotr Rogowski | 10b6150a7a | |
Karol Piecuch | 26e5f28119 |
|
@ -16,11 +16,12 @@
|
|||
"vscode": {
|
||||
// NOTE: keep this in sync with: .vscode/extensions.json
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"biomejs.biome"
|
||||
]
|
||||
"editorconfig.editorconfig",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
// install dependencies
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'next/core-web-vitals',
|
||||
'plugin:@typescript-eslint/strict-type-checked',
|
||||
'plugin:@typescript-eslint/stylistic-type-checked',
|
||||
'prettier',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'prettier'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
root: true,
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
"prettier/prettier": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off", // handled by typescript
|
||||
"react-hooks/exhaustive-deps": 'off',
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
|
||||
// TODO: enable later
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-misused-promises": [
|
||||
"error",
|
||||
{
|
||||
"checksVoidReturn": false,
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
# automatically normalize line endings
|
||||
* text=auto
|
||||
|
||||
# force bash scripts to always use LF line endings
|
||||
*.sh text eol=lf
|
|
@ -0,0 +1,3 @@
|
|||
build/
|
||||
coverage/
|
||||
public/
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"jsxSingleQuote": false,
|
||||
"plugins": []
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
"editorconfig.editorconfig",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"biomejs.biome"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
{
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[less]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"cSpell.words": [
|
||||
"antd",
|
||||
"baro",
|
||||
"biomejs",
|
||||
"Codespaces",
|
||||
"Datalog",
|
||||
"devcontainer",
|
||||
"devcontainers",
|
||||
"FOME",
|
||||
|
@ -24,13 +26,22 @@
|
|||
"kbar",
|
||||
"MLVLG",
|
||||
"noisymime",
|
||||
"pageview",
|
||||
"pako",
|
||||
"Piotr",
|
||||
"Plottable",
|
||||
"pocketbase",
|
||||
"prefs",
|
||||
"reduxjs",
|
||||
"Rogowski",
|
||||
"rusefi",
|
||||
"secl",
|
||||
"Sider",
|
||||
"speeduino",
|
||||
"tacho",
|
||||
"Texpand",
|
||||
"typegen",
|
||||
"uplot",
|
||||
"vite",
|
||||
"vitejs"
|
||||
]
|
||||
|
|
56
biome.json
56
biome.json
|
@ -1,56 +0,0 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.1.2/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"lineWidth": 100,
|
||||
"indentSize": 2
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"trailingComma": "all"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": [
|
||||
".devcontainer",
|
||||
".vscode",
|
||||
"node_modules"
|
||||
]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"all": true,
|
||||
"correctness": {
|
||||
"all": true,
|
||||
"noUnusedVariables": "warn"
|
||||
},
|
||||
"style": {
|
||||
"all": true,
|
||||
"noImplicitBoolean": "off",
|
||||
"useEnumInitializers": "off",
|
||||
"noNonNullAssertion": "off",
|
||||
"useNamingConvention": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"all": true,
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"all": true,
|
||||
"useExhaustiveDependencies": "off",
|
||||
"useImportRestrictions": "off",
|
||||
"noExcessiveComplexity": "off",
|
||||
"noConfusingVoidType": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
|
@ -14,52 +14,57 @@
|
|||
"start": "vite",
|
||||
"build": "tsc && vite build && generate-version",
|
||||
"serve": "vite preview",
|
||||
"lint": "npm run lint:biome && tsc",
|
||||
"lint:biome": "biome check src",
|
||||
"lint:fix": "biome format --write src && biome check --apply src",
|
||||
"lint:fix:unsafe": "biome check src --apply-unsafe src",
|
||||
"lint": "npm run lint:eslint && tsc",
|
||||
"lint:eslint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"analyze": "npm run build && open stats.html",
|
||||
"typegen": "pocketbase-typegen --json ../cloud-backend/pb_schema.json --out src/@types/pocketbase-types.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hyper-tuner/ini": "github:hyper-tuner/ini",
|
||||
"@hyper-tuner/types": "github:hyper-tuner/types",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@sentry/react": "^7.70.0",
|
||||
"@sentry/tracing": "^7.70.0",
|
||||
"@reduxjs/toolkit": "^1.9.7",
|
||||
"@sentry/react": "^7.74.1",
|
||||
"@sentry/tracing": "^7.74.1",
|
||||
"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": "^5.0.1",
|
||||
"nanoid": "^5.0.2",
|
||||
"pako": "^2.1.0",
|
||||
"pocketbase": "^0.18.0",
|
||||
"pocketbase": "^0.18.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-redux": "^8.1.2",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.17.0",
|
||||
"react-update-notification": "^1.2.0",
|
||||
"uplot": "^1.6.26",
|
||||
"uplot-react": "^1.1.5",
|
||||
"vite": "^4.4.9"
|
||||
"vite": "^4.4.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.2.2",
|
||||
"@total-typescript/ts-reset": "^0.5.1",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^20.6.3",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/react": "^18.2.22",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-redux": "^7.1.26",
|
||||
"@types/node": "^20.8.6",
|
||||
"@types/pako": "^2.0.1",
|
||||
"@types/react": "^18.2.28",
|
||||
"@types/react-dom": "^18.2.13",
|
||||
"@types/react-redux": "^7.1.27",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"@vitejs/plugin-react": "^4.1.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"less": "^4.2.0",
|
||||
"pocketbase-typegen": "^1.1.13",
|
||||
"pocketbase-typegen": "^1.2.0",
|
||||
"prettier": "^3.0.3",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
* This file was @generated using pocketbase-typegen
|
||||
*/
|
||||
|
||||
import type PocketBase from 'pocketbase';
|
||||
import type { RecordService } from 'pocketbase';
|
||||
|
||||
export enum Collections {
|
||||
IniFiles = 'iniFiles',
|
||||
Stargazers = 'stargazers',
|
||||
|
@ -126,3 +129,13 @@ export type CollectionResponses = {
|
|||
tunes: TunesResponse;
|
||||
users: UsersResponse;
|
||||
};
|
||||
|
||||
// Type for usage with type asserted PocketBase instance
|
||||
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions
|
||||
|
||||
export type TypedPocketBase = PocketBase & {
|
||||
collection(idOrName: 'iniFiles'): RecordService<IniFilesResponse>;
|
||||
collection(idOrName: 'stargazers'): RecordService<StargazersResponse>;
|
||||
collection(idOrName: 'tunes'): RecordService<TunesResponse>;
|
||||
collection(idOrName: 'users'): RecordService<UsersResponse>;
|
||||
};
|
||||
|
|
35
src/App.tsx
35
src/App.tsx
|
@ -1,5 +1,6 @@
|
|||
import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { INI } from '@hyper-tuner/ini';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Layout, Modal, Result } from 'antd';
|
||||
import { ReactNode, Suspense, lazy, useCallback, useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -32,7 +33,10 @@ import TuneParser from './utils/tune/TuneParser';
|
|||
|
||||
import 'react-perfect-scrollbar/dist/css/styles.css';
|
||||
import 'uplot/dist/uPlot.min.css';
|
||||
import { useAuth } from './contexts/AuthContext';
|
||||
import './css/App.less';
|
||||
import { isProduction } from './utils/env';
|
||||
import { UpdateStatus } from 'react-update-notification/lib/types';
|
||||
|
||||
const Tune = lazy(() => import('./pages/Tune'));
|
||||
const Logs = lazy(() => import('./pages/Logs'));
|
||||
|
@ -64,7 +68,7 @@ const NewVersionPrompt = () => {
|
|||
});
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
if (status === 'checking' || status === 'current') {
|
||||
if (status === UpdateStatus.checking || status === UpdateStatus.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -79,18 +83,20 @@ const NewVersionPrompt = () => {
|
|||
onOk={reloadPage}
|
||||
okText="Reload the page"
|
||||
okButtonProps={{ icon: <ReloadOutlined /> }}
|
||||
onCancel={() => setOpen(false)}
|
||||
onCancel={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
cancelText="I'll do it later"
|
||||
cancelButtonProps={{ icon: <ClockCircleOutlined /> }}
|
||||
destroyOnClose
|
||||
>
|
||||
<p>To enjoy the new features, it's time to refresh the page!</p>
|
||||
<p>To enjoy the new features, it's time to refresh the page!</p>
|
||||
<p>You can refresh later at your convenience.</p>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const App = ({ ui, tuneData }: { ui: UIState; tuneData: TuneDataState }) => {
|
||||
const App = ({ ui, tuneData }: { ui: UIState; tuneData: TuneDataState | null }) => {
|
||||
const margin = ui.sidebarCollapsed ? collapsedSidebarWidth : sidebarWidth;
|
||||
const { getTune } = useDb();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -98,6 +104,23 @@ const App = ({ ui, tuneData }: { ui: UIState; tuneData: TuneDataState }) => {
|
|||
const tunePathMatch = useMatch(`${Routes.TUNE_ROOT}/*`);
|
||||
const tuneId = tunePathMatch?.params.tuneId;
|
||||
const { fetchINIFile, fetchTuneFile } = useServerStorage();
|
||||
const { currentUser } = useAuth();
|
||||
|
||||
if (isProduction) {
|
||||
if (currentUser) {
|
||||
Sentry.setContext('user', {
|
||||
id: currentUser.id,
|
||||
email: currentUser.email,
|
||||
username: currentUser.username,
|
||||
});
|
||||
}
|
||||
|
||||
if (tuneId) {
|
||||
Sentry.setContext('tune', {
|
||||
tuneId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const loadTune = async (data: TunesResponse | null) => {
|
||||
if (data === null) {
|
||||
|
@ -173,14 +196,14 @@ const App = ({ ui, tuneData }: { ui: UIState; tuneData: TuneDataState }) => {
|
|||
setIsLoading(false);
|
||||
}
|
||||
|
||||
getTune(tuneId).then(async (tune) => {
|
||||
getTune(tuneId).then((tune) => {
|
||||
if (!tune) {
|
||||
tuneNotFound();
|
||||
navigate(Routes.HUB);
|
||||
return;
|
||||
}
|
||||
|
||||
loadTune(tune!);
|
||||
loadTune(tune);
|
||||
store.dispatch({
|
||||
type: 'tuneData/load',
|
||||
payload: JSON.parse(JSON.stringify(tune)),
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Colors } from '../utils/colors';
|
|||
|
||||
const AuthorName = ({ author }: { author: UsersResponse }) => (
|
||||
<Space>
|
||||
{author.verifiedAuthor === true && (
|
||||
{author.verifiedAuthor && (
|
||||
<Tooltip title="Verified author">
|
||||
<CheckCircleFilled style={{ color: Colors.ACCENT }} />
|
||||
</Tooltip>
|
||||
|
|
|
@ -97,6 +97,7 @@ const groupNameStyle = {
|
|||
opacity: 0.5,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const ResultItem = forwardRef(
|
||||
(
|
||||
{
|
||||
|
@ -187,11 +188,13 @@ const ResultItem = forwardRef(
|
|||
const RenderResults = () => {
|
||||
const { results, rootActionId } = useMatches();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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} />
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
<ResultItem action={item} active={active} currentRootActionId={rootActionId!} />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -213,7 +216,9 @@ const ActionsProvider = (props: CommandPaletteProps) => {
|
|||
name: 'Info',
|
||||
subtitle: 'Basic information about this tune.',
|
||||
icon: <InfoCircleOutlined />,
|
||||
perform: () => navigate(buildTuneUrl(navigation.tuneId!, Routes.TUNE_ROOT)),
|
||||
perform: () => {
|
||||
navigate(buildTuneUrl(navigation.tuneId!, Routes.TUNE_ROOT));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'LogsAction',
|
||||
|
@ -221,7 +226,9 @@ const ActionsProvider = (props: CommandPaletteProps) => {
|
|||
name: 'Logs',
|
||||
subtitle: 'Log viewer.',
|
||||
icon: <FundOutlined />,
|
||||
perform: () => navigate(buildTuneUrl(navigation.tuneId!, Routes.TUNE_LOGS)),
|
||||
perform: () => {
|
||||
navigate(buildTuneUrl(navigation.tuneId!, Routes.TUNE_LOGS));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'DiagnoseAction',
|
||||
|
@ -229,16 +236,16 @@ const ActionsProvider = (props: CommandPaletteProps) => {
|
|||
name: 'Diagnose',
|
||||
subtitle: 'Tooth and composite logs viewer.',
|
||||
icon: <SettingOutlined />,
|
||||
perform: () => navigate(buildTuneUrl(navigation.tuneId!, Routes.TUNE_DIAGNOSE)),
|
||||
perform: () => {
|
||||
navigate(buildTuneUrl(navigation.tuneId!, Routes.TUNE_DIAGNOSE));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mapSubMenuItems = (
|
||||
rootMenuName: string,
|
||||
rootMenu: MenuType,
|
||||
subMenus: {
|
||||
[name: string]: SubMenuType | GroupMenuType | GroupChildMenuType;
|
||||
},
|
||||
subMenus: Record<string, SubMenuType | GroupMenuType | GroupChildMenuType>,
|
||||
groupMenuName: string | null = null,
|
||||
) => {
|
||||
Object.keys(subMenus).forEach((subMenuName: string) => {
|
||||
|
@ -252,6 +259,7 @@ const ActionsProvider = (props: CommandPaletteProps) => {
|
|||
|
||||
const subMenu = subMenus[subMenuName];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if ((subMenu as GroupMenuType).type === 'groupMenu') {
|
||||
// recurrence
|
||||
mapSubMenuItems(
|
||||
|
@ -273,7 +281,9 @@ const ActionsProvider = (props: CommandPaletteProps) => {
|
|||
section: rootMenu.title,
|
||||
name: subMenu.title,
|
||||
icon: <Icon name={subMenuName} />,
|
||||
perform: () => navigate(url),
|
||||
perform: () => {
|
||||
navigate(url);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -322,7 +332,9 @@ const CommandPalette = (props: CommandPaletteProps) => {
|
|||
name: 'Hub',
|
||||
subtitle: 'Public tunes and logs.',
|
||||
icon: <CarOutlined />,
|
||||
perform: () => navigate(Routes.HUB),
|
||||
perform: () => {
|
||||
navigate(Routes.HUB);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ToggleSidebar',
|
||||
|
@ -336,7 +348,9 @@ const CommandPalette = (props: CommandPaletteProps) => {
|
|||
name: 'Upload',
|
||||
subtitle: 'Upload tune and logs.',
|
||||
icon: <CloudUploadOutlined />,
|
||||
perform: () => navigate(Routes.UPLOAD),
|
||||
perform: () => {
|
||||
navigate(Routes.UPLOAD);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'LoginAction',
|
||||
|
@ -344,7 +358,9 @@ const CommandPalette = (props: CommandPaletteProps) => {
|
|||
name: 'Login',
|
||||
subtitle: 'Login using email, Google or GitHub account.',
|
||||
icon: <LoginOutlined />,
|
||||
perform: () => navigate(Routes.LOGIN),
|
||||
perform: () => {
|
||||
navigate(Routes.LOGIN);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SignUpAction',
|
||||
|
@ -352,7 +368,9 @@ const CommandPalette = (props: CommandPaletteProps) => {
|
|||
name: 'Sign-up',
|
||||
subtitle: 'Create new account.',
|
||||
icon: <UserAddOutlined />,
|
||||
perform: () => navigate(Routes.SIGN_UP),
|
||||
perform: () => {
|
||||
navigate(Routes.SIGN_UP);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'LogoutAction',
|
||||
|
@ -367,7 +385,9 @@ const CommandPalette = (props: CommandPaletteProps) => {
|
|||
name: 'About',
|
||||
subtitle: 'About this app / sponsor.',
|
||||
icon: <InfoCircleOutlined />,
|
||||
perform: () => navigate(Routes.ABOUT),
|
||||
perform: () => {
|
||||
navigate(Routes.ABOUT);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import UplotReact from 'uplot-react';
|
|||
import { Colors } from '../../utils/colors';
|
||||
import { colorHsl, formatNumberMs } from '../../utils/numbers';
|
||||
import { isNumber } from '../../utils/tune/expression';
|
||||
import keyboardZoomPlugin from '../../utils/uPlot/keyboardZoomPlugin';
|
||||
import mouseZoomPlugin from '../../utils/uPlot/mouseZoomPlugin';
|
||||
import touchZoomPlugin from '../../utils/uPlot/touchZoomPlugin';
|
||||
|
||||
export interface SelectedField {
|
||||
|
@ -52,10 +54,11 @@ const LogCanvas = ({
|
|||
|
||||
const generateFieldsToPlot = useCallback(
|
||||
(selectedFields: SelectedField[]) => {
|
||||
const temp: { [index: string]: PlottableField } = {};
|
||||
const temp: Record<string, PlottableField> = {};
|
||||
|
||||
data.forEach((_entry) => {
|
||||
selectedFields.forEach(({ label, scale, transform, units, format }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!temp[label]) {
|
||||
temp[label] = {
|
||||
min: 0,
|
||||
|
@ -76,7 +79,7 @@ const LogCanvas = ({
|
|||
|
||||
const generatePlotConfig = useCallback(
|
||||
(
|
||||
fieldsToPlot: { [index: string]: PlottableField },
|
||||
fieldsToPlot: Record<string, PlottableField>,
|
||||
selectedFieldsLength: number,
|
||||
plotSyncKey: string,
|
||||
) => {
|
||||
|
@ -106,6 +109,7 @@ const LogCanvas = ({
|
|||
}
|
||||
|
||||
let value = entry[label];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (value !== undefined) {
|
||||
value = (value as number) * field.scale + field.transform;
|
||||
}
|
||||
|
@ -144,7 +148,7 @@ const LogCanvas = ({
|
|||
sync: { key: plotSyncKey },
|
||||
points: { size: 7 },
|
||||
},
|
||||
plugins: [touchZoomPlugin()],
|
||||
plugins: [touchZoomPlugin(), mouseZoomPlugin(), keyboardZoomPlugin()],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
import React from 'react';
|
||||
|
||||
const Icon = ({ name }: { name: string }): React.JSX.Element => {
|
||||
const map: { [index: string]: React.JSX.Element } = {
|
||||
const map: Record<string, React.JSX.Element> = {
|
||||
settings: <ControlOutlined />,
|
||||
tuning: <CarOutlined />,
|
||||
spark: <FireOutlined />,
|
||||
|
|
|
@ -16,39 +16,47 @@ const mapStateToProps = (state: AppState) => ({
|
|||
|
||||
const Firmware = ({ tune }: { tune: TuneState }) => {
|
||||
const [width, setWidth] = useState(1000);
|
||||
const calculateWidth = () => setWidth(window.innerWidth - 130);
|
||||
const calculateWidth = () => {
|
||||
setWidth(window.innerWidth - 130);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
calculateWidth();
|
||||
window.addEventListener('resize', calculateWidth);
|
||||
|
||||
return () => window.removeEventListener('resize', calculateWidth);
|
||||
return () => {
|
||||
window.removeEventListener('resize', calculateWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<InfoCircleOutlined />
|
||||
<Typography.Text ellipsis style={{ maxWidth: width }}>
|
||||
{`${tune.details.signature} - ${tune.details.writeDate} - ${tune.details.author}`}
|
||||
{[tune.details.signature, tune.details.writeDate, tune.details.author]
|
||||
.filter(Boolean)
|
||||
.join(' - ')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBar = ({ tune }: { tune: TuneState }) => (
|
||||
<Footer className="app-status-bar">
|
||||
<Row>
|
||||
<Col span={20}>{tune?.details?.author && <Firmware tune={tune} />}</Col>
|
||||
<Col span={4} style={{ textAlign: 'right' }}>
|
||||
<Link to={Routes.ABOUT}>
|
||||
<Space>
|
||||
<GithubOutlined />
|
||||
GitHub
|
||||
</Space>
|
||||
</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</Footer>
|
||||
);
|
||||
const StatusBar = ({ tune }: { tune: TuneState | null }) => {
|
||||
return (
|
||||
<Footer className="app-status-bar">
|
||||
<Row>
|
||||
<Col span={20}>{tune && <Firmware tune={tune} />}</Col>
|
||||
<Col span={4} style={{ textAlign: 'right' }}>
|
||||
<Link to={Routes.ABOUT}>
|
||||
<Space>
|
||||
<GithubOutlined />
|
||||
GitHub
|
||||
</Space>
|
||||
</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</Footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(StatusBar);
|
||||
|
|
|
@ -39,17 +39,13 @@ import { buildHyperTunerAppLink } from '../utils/url';
|
|||
const { Header } = Layout;
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const logsExtensionsIcons: { [key: string]: React.JSX.Element } = {
|
||||
const logsExtensionsIcons: Record<string, React.JSX.Element> = {
|
||||
mlg: <FileZipOutlined />,
|
||||
msl: <FileTextOutlined />,
|
||||
csv: <FileExcelOutlined />,
|
||||
};
|
||||
|
||||
const TopBar = ({
|
||||
tuneData,
|
||||
}: {
|
||||
tuneData: TuneDataState | null;
|
||||
}) => {
|
||||
const TopBar = ({ tuneData }: { tuneData: TuneDataState | null }) => {
|
||||
const { xs, sm, lg } = useBreakpoint();
|
||||
const { pathname } = useLocation();
|
||||
const { currentUser, logout } = useAuth();
|
||||
|
@ -76,7 +72,9 @@ const TopBar = ({
|
|||
navigate(0);
|
||||
}, [logout, navigate]);
|
||||
|
||||
const toggleCommandPalette = useCallback(() => query.toggle(), [query]);
|
||||
const toggleCommandPalette = useCallback(() => {
|
||||
query.toggle();
|
||||
}, [query]);
|
||||
|
||||
const handleGlobalKeyboard = useCallback((e: KeyboardEvent) => {
|
||||
if (isToggleSidebar(e)) {
|
||||
|
@ -146,8 +144,8 @@ const TopBar = ({
|
|||
if (tuneData?.customIniFile) {
|
||||
downloadFile(
|
||||
Collections.Tunes,
|
||||
tuneData!.id,
|
||||
tuneData!.customIniFile,
|
||||
tuneData.id,
|
||||
tuneData.customIniFile,
|
||||
downloadAnchorRef.current!,
|
||||
);
|
||||
} else {
|
||||
|
@ -163,7 +161,9 @@ const TopBar = ({
|
|||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleGlobalKeyboard);
|
||||
|
||||
return () => document.removeEventListener('keydown', handleGlobalKeyboard);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleGlobalKeyboard);
|
||||
};
|
||||
}, [currentUser, handleGlobalKeyboard]);
|
||||
|
||||
const tabs = useMemo(
|
||||
|
@ -181,7 +181,9 @@ const TopBar = ({
|
|||
}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => navigate(e.target.value)}
|
||||
onChange={(e) => {
|
||||
navigate(e.target.value as string);
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={buildTuneUrl(Routes.HUB)}>
|
||||
<Space>
|
||||
|
@ -271,7 +273,9 @@ const TopBar = ({
|
|||
key: 'profile',
|
||||
icon: <UserOutlined />,
|
||||
label: 'Profile',
|
||||
onClick: () => navigate(Routes.PROFILE),
|
||||
onClick: () => {
|
||||
navigate(Routes.PROFILE);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
|
@ -285,13 +289,17 @@ const TopBar = ({
|
|||
key: 'login',
|
||||
icon: <LoginOutlined />,
|
||||
label: 'Login',
|
||||
onClick: () => navigate(Routes.LOGIN),
|
||||
onClick: () => {
|
||||
navigate(Routes.LOGIN);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'sign-up',
|
||||
icon: <UserAddOutlined />,
|
||||
label: 'Sign Up',
|
||||
onClick: () => navigate(Routes.SIGN_UP),
|
||||
onClick: () => {
|
||||
navigate(Routes.SIGN_UP);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [currentUser, logoutClick, navigate]);
|
||||
|
@ -306,7 +314,9 @@ const TopBar = ({
|
|||
key: 'about',
|
||||
icon: <InfoCircleOutlined />,
|
||||
label: 'About',
|
||||
onClick: () => navigate(Routes.ABOUT),
|
||||
onClick: () => {
|
||||
navigate(Routes.ABOUT);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import uPlot from 'uplot';
|
|||
import UplotReact from 'uplot-react';
|
||||
import { Colors } from '../../utils/colors';
|
||||
import { CompositeLogEntry } from '../../utils/logs/TriggerLogsParser';
|
||||
import keyboardZoomPlugin from '../../utils/uPlot/keyboardZoomPlugin';
|
||||
import mouseZoomPlugin from '../../utils/uPlot/mouseZoomPlugin';
|
||||
import touchZoomPlugin from '../../utils/uPlot/touchZoomPlugin';
|
||||
import LogsPagination from './LogsPagination';
|
||||
|
||||
|
@ -103,7 +105,7 @@ const CompositeCanvas = ({ data, width, height }: Props) => {
|
|||
drag: { y: false },
|
||||
points: { size: 7 },
|
||||
},
|
||||
plugins: [touchZoomPlugin()],
|
||||
plugins: [touchZoomPlugin(), mouseZoomPlugin(), keyboardZoomPlugin()],
|
||||
});
|
||||
}, [data, width, height, indexFrom, indexTo]);
|
||||
|
||||
|
|
|
@ -3,7 +3,10 @@ import uPlot from 'uplot';
|
|||
import UplotReact from 'uplot-react';
|
||||
import { Colors } from '../../utils/colors';
|
||||
import { EntryType, ToothLogEntry } from '../../utils/logs/TriggerLogsParser';
|
||||
import keyboardZoomPlugin from '../../utils/uPlot/keyboardZoomPlugin';
|
||||
import mouseZoomPlugin from '../../utils/uPlot/mouseZoomPlugin';
|
||||
import touchZoomPlugin from '../../utils/uPlot/touchZoomPlugin';
|
||||
|
||||
import LogsPagination from './LogsPagination';
|
||||
|
||||
const { bars } = uPlot.paths;
|
||||
|
@ -69,7 +72,7 @@ const ToothCanvas = ({ data, width, height }: Props) => {
|
|||
drag: { y: false },
|
||||
points: { size: 7 },
|
||||
},
|
||||
plugins: [touchZoomPlugin()],
|
||||
plugins: [touchZoomPlugin(), mouseZoomPlugin(), keyboardZoomPlugin()],
|
||||
});
|
||||
}, [data, width, height, indexFrom, indexTo]);
|
||||
|
||||
|
|
|
@ -25,9 +25,7 @@ import SmartNumber from './Dialog/SmartNumber';
|
|||
import SmartSelect from './Dialog/SmartSelect';
|
||||
import TextField from './Dialog/TextField';
|
||||
|
||||
interface DialogsAndCurves {
|
||||
[name: string]: DialogType | CurveType | TableType;
|
||||
}
|
||||
type DialogsAndCurves = Record<string, DialogType | CurveType | TableType>;
|
||||
|
||||
interface RenderedPanel {
|
||||
type: string;
|
||||
|
@ -72,8 +70,8 @@ const Dialog = ({
|
|||
name,
|
||||
}: {
|
||||
ui: UIState;
|
||||
config: ConfigType;
|
||||
tune: TuneType;
|
||||
config: ConfigType | null;
|
||||
tune: TuneType | null;
|
||||
name: string;
|
||||
url: string;
|
||||
}) => {
|
||||
|
@ -100,8 +98,8 @@ const Dialog = ({
|
|||
|
||||
const renderCurve = useCallback(
|
||||
(curve: CurveType) => {
|
||||
const x = tune.constants[curve.xBins[0]];
|
||||
const y = tune.constants[curve.yBins[0]];
|
||||
const x = tune!.constants[curve.xBins[0]];
|
||||
const y = tune!.constants[curve.yBins[0]];
|
||||
const xConstant = findConstantOnPage(curve.xBins[0]) as ScalarConstantType;
|
||||
const yConstant = findConstantOnPage(curve.yBins[0]) as ScalarConstantType;
|
||||
|
||||
|
@ -109,7 +107,7 @@ const Dialog = ({
|
|||
<Curve
|
||||
key={curve.yBins[0]}
|
||||
// disabled={false} // TODO: evaluate condition
|
||||
help={config.help[curve.yBins[0]]}
|
||||
help={config!.help[curve.yBins[0]]}
|
||||
xLabel={curve.labels[0]}
|
||||
yLabel={curve.labels[1]}
|
||||
xUnits={xConstant.units}
|
||||
|
@ -124,9 +122,9 @@ const Dialog = ({
|
|||
|
||||
const renderTable = useCallback(
|
||||
(table: TableType | RenderedPanel) => {
|
||||
const x = tune.constants[table.xBins[0]];
|
||||
const y = tune.constants[table.yBins[0]];
|
||||
const z = tune.constants[table.zBins[0]];
|
||||
const x = tune?.constants[table.xBins[0]];
|
||||
const y = tune?.constants[table.yBins[0]];
|
||||
const z = tune?.constants[table.zBins[0]];
|
||||
|
||||
if (!(x && y)) {
|
||||
// TODO: handle this (rusEFI: fuel/lambdaTableTbl)
|
||||
|
@ -142,20 +140,21 @@ const Dialog = ({
|
|||
key={table.map}
|
||||
xData={parseXy(x.value as string)}
|
||||
yData={parseXy(y.value as string).reverse()}
|
||||
zData={parseZ(z.value as string)}
|
||||
xUnits={x.units as string}
|
||||
yUnits={y.units as string}
|
||||
zData={parseZ(z?.value as string)}
|
||||
xUnits={x.units!}
|
||||
yUnits={y.units!}
|
||||
zDigits={zConstant.digits}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[findConstantOnPage, tune.constants],
|
||||
[findConstantOnPage, tune?.constants],
|
||||
);
|
||||
|
||||
const resolvedDialogs: DialogsAndCurves = {};
|
||||
|
||||
const resolveDialogs = (source: DialogsType, dialogName: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!source[dialogName]) {
|
||||
return;
|
||||
}
|
||||
|
@ -166,20 +165,23 @@ const Dialog = ({
|
|||
Object.keys(source[dialogName].panels).forEach((panelName: string) => {
|
||||
const currentDialog = source[panelName];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!currentDialog) {
|
||||
// resolve 2D map / curve panel
|
||||
if (config.curves[panelName]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (config!.curves[panelName]) {
|
||||
resolvedDialogs[panelName] = {
|
||||
...config.curves[panelName],
|
||||
...config!.curves[panelName],
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// resolve 3D map / table panel
|
||||
if (config.tables[panelName]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (config!.tables[panelName]) {
|
||||
resolvedDialogs[panelName] = {
|
||||
...config.tables[panelName],
|
||||
...config!.tables[panelName],
|
||||
};
|
||||
|
||||
return;
|
||||
|
@ -192,11 +194,11 @@ const Dialog = ({
|
|||
|
||||
if (currentDialog.fields.length > 0) {
|
||||
// resolve in root scope
|
||||
resolvedDialogs[panelName] = config.dialogs[panelName];
|
||||
resolvedDialogs[panelName] = config!.dialogs[panelName];
|
||||
}
|
||||
|
||||
// recursion
|
||||
resolveDialogs(config.dialogs, panelName);
|
||||
resolveDialogs(config!.dialogs, panelName);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -212,7 +214,7 @@ const Dialog = ({
|
|||
|
||||
if ('fields' in currentDialog) {
|
||||
type = PanelTypes.FIELDS;
|
||||
fields = (currentDialog as DialogType).fields.filter((field) => field.title !== '');
|
||||
fields = currentDialog.fields.filter((field) => field.title !== '');
|
||||
} else if ('zBins' in currentDialog) {
|
||||
type = PanelTypes.TABLE;
|
||||
}
|
||||
|
@ -243,6 +245,7 @@ const Dialog = ({
|
|||
const generatePanelsComponents = useCallback(
|
||||
() =>
|
||||
panels.map((panel: RenderedPanel) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||
if (panel.type === PanelTypes.FIELDS && panel.fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -252,21 +255,22 @@ const Dialog = ({
|
|||
<Divider>{panel.title}</Divider>
|
||||
{panel.fields.map((field: FieldType) => {
|
||||
const constant = findConstantOnPage(field.name);
|
||||
const tuneField = tune.constants[field.name];
|
||||
const help = config.help[field.name];
|
||||
const tuneField = tune!.constants[field.name];
|
||||
const help = config!.help[field.name];
|
||||
let input;
|
||||
let enabled = true;
|
||||
const fieldKey = `${panel.name}-${field.title}`;
|
||||
|
||||
if (field.condition) {
|
||||
// TODO: optimize it
|
||||
enabled = evaluateExpression(field.condition, tune.constants, config);
|
||||
enabled = evaluateExpression(field.condition, tune!.constants, config!) as boolean;
|
||||
}
|
||||
|
||||
if (field.name === '_fieldText_' && enabled) {
|
||||
return <TextField key={fieldKey} title={field.title} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!tuneField) {
|
||||
// TODO: handle this?
|
||||
// name: "rpmwarn", title: "Warning",
|
||||
|
@ -279,7 +283,7 @@ const Dialog = ({
|
|||
input = (
|
||||
<SmartSelect
|
||||
defaultValue={`${tuneField.value}`}
|
||||
values={constant.values as string[]}
|
||||
values={constant.values}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
);
|
||||
|
@ -312,7 +316,9 @@ const Dialog = ({
|
|||
{field.title}
|
||||
{help && (
|
||||
<Popover
|
||||
content={help.split('\\n').map((line) => <div key={line}>{line}</div>)}
|
||||
content={help.split('\\n').map((line) => (
|
||||
<div key={line}>{line}</div>
|
||||
))}
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Popover>
|
||||
|
@ -325,8 +331,8 @@ const Dialog = ({
|
|||
);
|
||||
})}
|
||||
|
||||
{panel.type === PanelTypes.CURVE && renderCurve(panel)}
|
||||
{panel.type === PanelTypes.TABLE && renderTable(panel)}
|
||||
{panel.type === (PanelTypes.CURVE as string) && renderCurve(panel)}
|
||||
{panel.type === (PanelTypes.TABLE as string) && renderTable(panel)}
|
||||
</Col>
|
||||
);
|
||||
}),
|
||||
|
@ -350,7 +356,9 @@ const Dialog = ({
|
|||
const tableConfig = config.tables[name];
|
||||
|
||||
// standalone dialog / page
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!dialogConfig) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (curveConfig) {
|
||||
return (
|
||||
<div ref={containerRef} className="large-container">
|
||||
|
@ -360,6 +368,7 @@ const Dialog = ({
|
|||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (tableConfig) {
|
||||
return (
|
||||
<div ref={containerRef} className="large-container">
|
||||
|
@ -375,7 +384,7 @@ const Dialog = ({
|
|||
|
||||
return (
|
||||
<div ref={containerRef} className="large-container">
|
||||
{renderHelp(dialogConfig?.help)}
|
||||
{renderHelp(dialogConfig.help)}
|
||||
<Form labelCol={{ span: 10 }} wrapperCol={{ span: 10 }}>
|
||||
<Row gutter={20}>{panelsComponents}</Row>
|
||||
</Form>
|
||||
|
|
|
@ -21,7 +21,7 @@ const Curve = ({
|
|||
yLabel: string;
|
||||
xData: number[];
|
||||
yData: number[];
|
||||
help: string;
|
||||
help?: string;
|
||||
xUnits?: string;
|
||||
yUnits?: string;
|
||||
}) => {
|
||||
|
@ -31,10 +31,9 @@ const Curve = ({
|
|||
const [plotData, setPlotData] = useState<uPlot.AlignedData>();
|
||||
const [canvasWidth, setCanvasWidth] = useState(0);
|
||||
|
||||
const calculateWidth = useCallback(
|
||||
() => setCanvasWidth(containerRef.current?.clientWidth || 0),
|
||||
[],
|
||||
);
|
||||
const calculateWidth = useCallback(() => {
|
||||
setCanvasWidth(containerRef.current?.clientWidth || 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setPlotData([xData, yData]);
|
||||
|
@ -77,7 +76,9 @@ const Curve = ({
|
|||
calculateWidth();
|
||||
window.addEventListener('resize', calculateWidth);
|
||||
|
||||
return () => window.removeEventListener('resize', calculateWidth);
|
||||
return () => {
|
||||
window.removeEventListener('resize', calculateWidth);
|
||||
};
|
||||
}, [xData, xLabel, xUnits, yData, yLabel, yUnits, sm, canvasWidth, calculateWidth]);
|
||||
|
||||
if (!sm) {
|
||||
|
@ -86,9 +87,11 @@ const Curve = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Typography.Paragraph>
|
||||
<Typography.Text type="secondary">{help}</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
{help && (
|
||||
<Typography.Paragraph>
|
||||
<Typography.Text type="secondary">{help}</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
<UplotReact options={options!} data={plotData!} />
|
||||
<div ref={containerRef}>
|
||||
<Table
|
||||
|
|
|
@ -17,7 +17,7 @@ const SmartNumber = ({
|
|||
disabled: boolean;
|
||||
}) => {
|
||||
const isSlider = (u: string) => ['%', 'C'].includes(`${u}`.toUpperCase());
|
||||
const sliderMarks: { [value: number]: string } = {};
|
||||
const sliderMarks: Record<number, string> = {};
|
||||
const step = digits ? 10 ** -digits : 1;
|
||||
const val = formatNumber(defaultValue, digits);
|
||||
sliderMarks[min] = `${min}${units}`;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Alert, Typography } from 'antd';
|
||||
|
||||
const TextField = ({ title }: { title: string }) => {
|
||||
const types: { [char: string]: 'info' | 'warning' } = {
|
||||
const types: Record<string, 'info' | 'warning' | undefined> = {
|
||||
'#': 'info',
|
||||
'!': 'warning',
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ const TextField = ({ title }: { title: string }) => {
|
|||
const urlPattern = /(?<url>https?:\/\/[^:[\]@!$'(),; ]+)/;
|
||||
const matches = message.split(urlPattern);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!matches) {
|
||||
return messageTag;
|
||||
}
|
||||
|
@ -20,7 +21,7 @@ const TextField = ({ title }: { title: string }) => {
|
|||
const parts = matches.map((part) => {
|
||||
if (urlPattern.test(part)) {
|
||||
return (
|
||||
<a href={part} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>
|
||||
<a key={part} href={part} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>
|
||||
{part}
|
||||
</a>
|
||||
);
|
||||
|
|
|
@ -100,9 +100,7 @@ const SideBar = ({
|
|||
const mapSubMenuItems = useCallback(
|
||||
(
|
||||
rootMenuName: string,
|
||||
subMenus: {
|
||||
[name: string]: SubMenuType | GroupMenuType | GroupChildMenuType;
|
||||
},
|
||||
subMenus: Record<string, SubMenuType | GroupMenuType | GroupChildMenuType>,
|
||||
groupMenuName: string | null = null,
|
||||
): ItemType[] => {
|
||||
const items: ItemType[] = [];
|
||||
|
@ -122,6 +120,7 @@ const SideBar = ({
|
|||
|
||||
const subMenu = subMenus[subMenuName];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if ((subMenu as GroupMenuType).type === 'groupMenu') {
|
||||
// recurrence
|
||||
items.push({
|
||||
|
@ -146,7 +145,9 @@ const SideBar = ({
|
|||
key: url,
|
||||
icon: <Icon name={subMenuName} />,
|
||||
label: subMenu.title,
|
||||
onClick: () => navigate(url),
|
||||
onClick: () => {
|
||||
navigate(url);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -22,12 +22,12 @@ interface AuthValue {
|
|||
logout: () => void;
|
||||
initResetPassword: (email: string) => Promise<void>;
|
||||
listAuthMethods: () => Promise<AuthMethodsList>;
|
||||
oAuth: (provider: OAuthProviders, code: string, codeVerifier: string) => Promise<void>;
|
||||
oAuth: (provider: OAuthProviders, code: string, codeVerifier: string) => void;
|
||||
updateUsername: (username: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthValue | null>(null);
|
||||
// biome-ignore lint/nursery/useHookAtTopLevel: False positive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
||||
const useAuth = () => useContext<AuthValue>(AuthContext as any);
|
||||
|
||||
const users = client.collection(Collections.Users);
|
||||
|
@ -98,7 +98,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
|
|||
return Promise.reject(new Error(formatError(error as ClientResponseError)));
|
||||
}
|
||||
},
|
||||
logout: async () => {
|
||||
logout: () => {
|
||||
client.authStore.clear();
|
||||
},
|
||||
initResetPassword: async (email: string) => {
|
||||
|
@ -117,7 +117,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
|
|||
return Promise.reject(new Error(formatError(error as ClientResponseError)));
|
||||
}
|
||||
},
|
||||
oAuth: async (provider: OAuthProviders, code: string, codeVerifier: string) => {
|
||||
oAuth: (provider: OAuthProviders, code: string, codeVerifier: string) => {
|
||||
users.authWithOAuth2(
|
||||
provider,
|
||||
code,
|
||||
|
|
|
@ -101,6 +101,10 @@
|
|||
// Radio buttons
|
||||
@radio-solid-checked-color: @white;
|
||||
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
// Scrollbar
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
|
|
|
@ -25,7 +25,7 @@ class BrowserStorage {
|
|||
this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
public async isAvailable(): Promise<boolean> {
|
||||
public isAvailable() {
|
||||
return !!this.storage;
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,9 @@ const useBrowserStorage = () => {
|
|||
storageGet: (key: string) => storage.get(key),
|
||||
storageGetSync: (key: string) => storage.getSync(key),
|
||||
storageSet: (key: string, value: string) => storage.set(key, value),
|
||||
storageDelete: (key: string) => storage.delete(key),
|
||||
storageDelete: (key: string) => {
|
||||
storage.delete(key);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -10,9 +10,10 @@ import { useMemo } from 'react';
|
|||
|
||||
const findConstantOnPage = (config: ConfigType, fieldName: string): Constant => {
|
||||
const foundPage =
|
||||
config.constants.pages.find((page: PageType) => fieldName in page.data) ||
|
||||
config.constants.pages.find((page: PageType) => fieldName in page.data) ??
|
||||
({ data: {} } as PageType);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!foundPage) {
|
||||
throw new Error(`Constant [${fieldName}] not found`);
|
||||
}
|
||||
|
@ -22,6 +23,8 @@ const findConstantOnPage = (config: ConfigType, fieldName: string): Constant =>
|
|||
|
||||
const findOutputChannel = (config: ConfigType, name: string): OutputChannel | SimpleConstant => {
|
||||
const result = config.outputChannels[name];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!result) {
|
||||
throw new Error(`Output channel [${name}] not found`);
|
||||
}
|
||||
|
@ -42,6 +45,8 @@ const findDatalogNameByLabel = (config: ConfigType, label: string): string => {
|
|||
|
||||
const findDatalog = (config: ConfigType, name: string): DatalogEntry => {
|
||||
const result = config.datalog[name];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!result) {
|
||||
throw new Error(`Datalog entry [${name}] not found`);
|
||||
}
|
||||
|
@ -50,7 +55,6 @@ const findDatalog = (config: ConfigType, name: string): DatalogEntry => {
|
|||
};
|
||||
|
||||
const useConfig = (config: ConfigType | null) =>
|
||||
// biome-ignore lint/nursery/useHookAtTopLevel: False positive
|
||||
useMemo(
|
||||
() => ({
|
||||
isConfigReady: !!config?.constants,
|
||||
|
|
|
@ -21,7 +21,7 @@ if (isProduction) {
|
|||
});
|
||||
|
||||
if (import.meta.env.VITE_GTM_ID) {
|
||||
ReactGA.initialize(import.meta.env.VITE_GTM_ID);
|
||||
ReactGA.initialize(import.meta.env.VITE_GTM_ID as string);
|
||||
ReactGA.send({ hitType: 'pageview', page: window.location.hash });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ const Diagnose = ({
|
|||
tuneData,
|
||||
}: {
|
||||
ui: UIState;
|
||||
loadedToothLogs: ToothLogsState;
|
||||
loadedToothLogs: ToothLogsState | null;
|
||||
tuneData: TuneDataState | null;
|
||||
}) => {
|
||||
const { lg } = Grid.useBreakpoint();
|
||||
|
@ -85,7 +85,7 @@ const Diagnose = ({
|
|||
}
|
||||
|
||||
// user didn't upload any logs
|
||||
if (tuneData && (tuneData.toothLogFiles || []).length === 0) {
|
||||
if ((tuneData?.toothLogFiles || []).length === 0) {
|
||||
navigate(Routes.HUB);
|
||||
|
||||
return;
|
||||
|
@ -141,12 +141,12 @@ const Diagnose = ({
|
|||
};
|
||||
|
||||
// first visit, logs are not loaded yet
|
||||
if (!loadedToothLogs.type && tuneData?.tuneId) {
|
||||
if (!loadedToothLogs?.type && tuneData?.tuneId) {
|
||||
loadData();
|
||||
}
|
||||
|
||||
// file changed, reload
|
||||
if (loadedToothLogs.type && loadedToothLogs.fileName !== routeMatch?.params.fileName) {
|
||||
if (loadedToothLogs?.type && loadedToothLogs.fileName !== routeMatch?.params.fileName) {
|
||||
// setToothLogs(undefined);
|
||||
// setCompositeLogs(undefined);
|
||||
store.dispatch({ type: 'toothLogs/load', payload: {} });
|
||||
|
@ -154,9 +154,9 @@ const Diagnose = ({
|
|||
}
|
||||
|
||||
// user navigated to logs root page
|
||||
if (!routeMatch?.params.fileName && tuneData && tuneData.toothLogFiles?.length) {
|
||||
if (!routeMatch?.params.fileName && tuneData && tuneData.toothLogFiles.length) {
|
||||
// either redirect to the first log or to the latest selected
|
||||
if (loadedToothLogs.fileName) {
|
||||
if (loadedToothLogs?.fileName) {
|
||||
navigate(
|
||||
generatePath(Routes.TUNE_DIAGNOSE_FILE, {
|
||||
tuneId: tuneData.tuneId,
|
||||
|
@ -164,7 +164,7 @@ const Diagnose = ({
|
|||
}),
|
||||
);
|
||||
} else {
|
||||
const firstLogFile = (tuneData.toothLogFiles || [])[0];
|
||||
const firstLogFile = tuneData.toothLogFiles[0];
|
||||
navigate(
|
||||
generatePath(Routes.TUNE_DIAGNOSE_FILE, {
|
||||
tuneId: tuneData.tuneId,
|
||||
|
@ -185,7 +185,7 @@ const Diagnose = ({
|
|||
}, [calculateCanvasSize, routeMatch?.params.fileName, ui.sidebarCollapsed, tuneData?.tuneId]);
|
||||
|
||||
const Graph = () => {
|
||||
switch (loadedToothLogs.type) {
|
||||
switch (loadedToothLogs?.type) {
|
||||
case 'composite':
|
||||
return (
|
||||
<CompositeCanvas
|
||||
|
@ -206,7 +206,7 @@ const Diagnose = ({
|
|||
return (
|
||||
<>
|
||||
<Sider {...siderProps} className="app-sidebar">
|
||||
{loadedToothLogs.type ? (
|
||||
{loadedToothLogs?.type ? (
|
||||
!ui.sidebarCollapsed && (
|
||||
<Tabs
|
||||
defaultActiveKey="files"
|
||||
|
@ -217,7 +217,7 @@ const Diagnose = ({
|
|||
<Badge
|
||||
size="small"
|
||||
style={badgeStyle}
|
||||
count={tuneData?.toothLogFiles?.length}
|
||||
count={tuneData?.toothLogFiles.length}
|
||||
offset={[10, -3]}
|
||||
>
|
||||
<FileTextOutlined />
|
||||
|
@ -227,7 +227,7 @@ const Diagnose = ({
|
|||
key: 'files',
|
||||
children: (
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||
{tuneData?.toothLogFiles?.map((fileName) => (
|
||||
{tuneData?.toothLogFiles.map((fileName) => (
|
||||
<Typography.Paragraph key={fileName} ellipsis>
|
||||
<Link
|
||||
to={generatePath(Routes.TUNE_DIAGNOSE_FILE, {
|
||||
|
@ -255,7 +255,7 @@ const Diagnose = ({
|
|||
<Layout className="logs-container">
|
||||
<Content>
|
||||
<div ref={contentRef}>
|
||||
{loadedToothLogs.type ? (
|
||||
{loadedToothLogs?.type ? (
|
||||
<Graph />
|
||||
) : (
|
||||
<Space direction="vertical" size="large">
|
||||
|
@ -269,7 +269,7 @@ const Diagnose = ({
|
|||
title: 'Downloading',
|
||||
subTitle: fileSize,
|
||||
description: fetchError ? (
|
||||
fetchError!.message
|
||||
fetchError.message
|
||||
) : (
|
||||
<Space>
|
||||
<GlobalOutlined />
|
||||
|
|
|
@ -34,12 +34,13 @@ const Hub = () => {
|
|||
const [total, setTotal] = useState(0);
|
||||
const searchRef = useRef<InputRef | null>(null);
|
||||
const { currentUser } = useAuth();
|
||||
const goToEdit = (tuneId: string) =>
|
||||
const goToEdit = (tuneId: string) => {
|
||||
navigate(
|
||||
generatePath(Routes.UPLOAD_WITH_TUNE_ID, {
|
||||
tuneId,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const loadData = debounce(async (searchText: string) => {
|
||||
setIsLoading(true);
|
||||
|
@ -53,6 +54,7 @@ const Hub = () => {
|
|||
aspiration: aspirationMapper[tune.aspiration],
|
||||
updated: formatTime(tune.updated),
|
||||
}));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
||||
setDataSource(mapped as any);
|
||||
} catch (_error) {
|
||||
// request cancelled
|
||||
|
@ -83,10 +85,12 @@ const Hub = () => {
|
|||
|
||||
// searchRef.current?.focus(); // autofocus
|
||||
|
||||
return () => window.removeEventListener('keydown', handleGlobalKeyboard);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleGlobalKeyboard);
|
||||
};
|
||||
}, [page]);
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
const columns: ColumnsType<TunesResponse<TunesResponseExpand>> = [
|
||||
{
|
||||
title: 'Tunes',
|
||||
render: (tune: TunesResponse<TunesResponseExpand>) => (
|
||||
|
@ -119,7 +123,7 @@ const Hub = () => {
|
|||
dataIndex: 'vehicleName',
|
||||
key: 'vehicleName',
|
||||
responsive: ['sm'],
|
||||
render: (vehicleName: string, tune: TunesResponse) => (
|
||||
render: (vehicleName: string, tune) => (
|
||||
<Space direction="vertical">
|
||||
{vehicleName}
|
||||
<TuneTag tag={tune.tags} />
|
||||
|
@ -161,7 +165,7 @@ const Hub = () => {
|
|||
dataIndex: 'authorUsername',
|
||||
key: 'authorUsername',
|
||||
responsive: ['sm'],
|
||||
render: (_userName: string, record: TunesResponse<TunesResponseExpand>) => (
|
||||
render: (_userName: string, record) => (
|
||||
<Link to={generatePath(Routes.USER_ROOT, { userId: record.author })}>
|
||||
<AuthorName author={record.expand!.author} />
|
||||
</Link>
|
||||
|
@ -198,20 +202,30 @@ const Hub = () => {
|
|||
return (
|
||||
<Space>
|
||||
{isOwner && (
|
||||
<Button size={size} icon={<EditOutlined />} onClick={() => goToEdit(tuneId)} />
|
||||
<Button
|
||||
size={size}
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
goToEdit(tuneId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isClipboardSupported && (
|
||||
<Button
|
||||
size={size}
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyToClipboard(buildFullUrl([tunePath(tuneId)]))}
|
||||
onClick={() => {
|
||||
copyToClipboard(buildFullUrl([tunePath(tuneId)]));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size={size}
|
||||
type="primary"
|
||||
icon={<ArrowRightOutlined />}
|
||||
onClick={() => navigate(tunePath(tuneId))}
|
||||
onClick={() => {
|
||||
navigate(tunePath(tuneId));
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
|
@ -223,18 +237,20 @@ const Hub = () => {
|
|||
return (
|
||||
<div className="large-container">
|
||||
<Input
|
||||
// biome-ignore lint: make search input first in tab order
|
||||
tabIndex={1}
|
||||
ref={searchRef}
|
||||
style={{ marginBottom: 10, height: 40 }}
|
||||
value={searchQuery}
|
||||
placeholder="Search by anything..."
|
||||
onChange={({ target }) => debounceLoadData(target.value)}
|
||||
onChange={({ target }) => {
|
||||
debounceLoadData(target.value);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
columns={columns as any}
|
||||
loading={isLoading}
|
||||
scroll={xs ? undefined : { x: 1360 }}
|
||||
pagination={false}
|
||||
|
|
|
@ -19,16 +19,19 @@ const mapStateToProps = (state: AppState) => ({
|
|||
tuneData: state.tuneData,
|
||||
});
|
||||
|
||||
const Info = ({ tuneData }: { tuneData: TuneDataState }) => {
|
||||
const Info = ({ tuneData }: { tuneData: TuneDataState | null }) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentUser } = useAuth();
|
||||
|
||||
const goToEdit = () =>
|
||||
navigate(
|
||||
generatePath(Routes.UPLOAD_WITH_TUNE_ID, {
|
||||
tuneId: tuneData.tuneId,
|
||||
}),
|
||||
);
|
||||
const goToEdit = () => {
|
||||
if (tuneData) {
|
||||
navigate(
|
||||
generatePath(Routes.UPLOAD_WITH_TUNE_ID, {
|
||||
tuneId: tuneData.tuneId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const canManage = currentUser && tuneData && currentUser.id === tuneData.author;
|
||||
|
||||
|
@ -76,32 +79,32 @@ const Info = ({ tuneData }: { tuneData: TuneDataState }) => {
|
|||
<Row {...rowProps}>
|
||||
<Col span={24} sm={24}>
|
||||
<Item>
|
||||
<Input value={tuneData.vehicleName!} addonBefore="Vehicle name" />
|
||||
<Input value={tuneData.vehicleName} addonBefore="Vehicle name" />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row {...rowProps}>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.engineMake!} addonBefore="Engine make" />
|
||||
<Input value={tuneData.engineMake} addonBefore="Engine make" />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.engineCode!} addonBefore="Engine code" />
|
||||
<Input value={tuneData.engineCode} addonBefore="Engine code" />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row {...rowProps}>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.displacement!} addonBefore="Displacement" addonAfter="l" />
|
||||
<Input value={tuneData.displacement} addonBefore="Displacement" addonAfter="l" />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input
|
||||
value={tuneData.cylindersCount!}
|
||||
value={tuneData.cylindersCount}
|
||||
addonBefore="Cylinders"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
@ -129,7 +132,7 @@ const Info = ({ tuneData }: { tuneData: TuneDataState }) => {
|
|||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input
|
||||
value={tuneData.compression!}
|
||||
value={tuneData.compression}
|
||||
addonBefore="Compression"
|
||||
style={{ width: '100%' }}
|
||||
addonAfter=":1"
|
||||
|
@ -140,36 +143,36 @@ const Info = ({ tuneData }: { tuneData: TuneDataState }) => {
|
|||
<Row {...rowProps}>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.fuel!} addonBefore="Fuel" />
|
||||
<Input value={tuneData.fuel} addonBefore="Fuel" />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.ignition!} addonBefore="Ignition" />
|
||||
<Input value={tuneData.ignition} addonBefore="Ignition" />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row {...rowProps}>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.injectorsSize!} addonBefore="Injectors size" addonAfter="cc" />
|
||||
<Input value={tuneData.injectorsSize} addonBefore="Injectors size" addonAfter="cc" />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.year!} addonBefore="Year" />
|
||||
<Input value={tuneData.year} addonBefore="Year" />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row {...rowProps}>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.hp!} addonBefore="HP" style={{ width: '100%' }} />
|
||||
<Input value={tuneData.hp} addonBefore="HP" style={{ width: '100%' }} />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col {...colProps}>
|
||||
<Item>
|
||||
<Input value={tuneData.stockHp!} addonBefore="Stock HP" style={{ width: '100%' }} />
|
||||
<Input value={tuneData.stockHp} addonBefore="Stock HP" style={{ width: '100%' }} />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -73,8 +73,8 @@ const Logs = ({
|
|||
tuneData,
|
||||
}: {
|
||||
ui: UIState;
|
||||
config: ConfigState;
|
||||
loadedLogs: LogsState;
|
||||
config: ConfigState | null;
|
||||
loadedLogs: LogsState | null;
|
||||
tuneData: TuneDataState | null;
|
||||
}) => {
|
||||
const { lg } = Grid.useBreakpoint();
|
||||
|
@ -148,7 +148,7 @@ const Logs = ({
|
|||
return [];
|
||||
}
|
||||
|
||||
return Object.values(config.datalog)
|
||||
return Object.values(config?.datalog || {})
|
||||
.map((entry: DatalogEntry) => {
|
||||
const { units, scale, transform } = findOutputChannel(entry.name) as OutputChannel;
|
||||
const { name, label, format } = entry;
|
||||
|
@ -175,11 +175,11 @@ const Logs = ({
|
|||
const [foundFields2, setFoundFields2] = useState<DatalogEntry[]>([]);
|
||||
const fuse = new Fuse<DatalogEntry>(fields, fuseOptions);
|
||||
|
||||
const debounceSearch1 = debounce(async (searchText: string) => {
|
||||
const debounceSearch1 = debounce((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 debounceSearch2 = debounce((searchText: string) => {
|
||||
const result = fuse.search(searchText);
|
||||
setFoundFields2(result.length > 0 ? result.map((item) => item.item) : fields);
|
||||
}, 300);
|
||||
|
@ -190,10 +190,10 @@ const Logs = ({
|
|||
const { signal } = controller;
|
||||
|
||||
const loadData = async () => {
|
||||
const logFileName = routeMatch?.params.fileName!;
|
||||
const logFileName = routeMatch?.params.fileName ?? '';
|
||||
|
||||
// user didn't upload any logs
|
||||
if (tuneData && (tuneData.logFiles || []).length === 0) {
|
||||
if (tuneData && tuneData.logFiles.length === 0) {
|
||||
navigate(Routes.HUB);
|
||||
|
||||
return;
|
||||
|
@ -236,7 +236,7 @@ const Logs = ({
|
|||
}
|
||||
case 'metrics': {
|
||||
setParseElapsed(msToTime(data.elapsed!));
|
||||
setSamplesCount(data.records!);
|
||||
setSamplesCount(data.records);
|
||||
setStep(2);
|
||||
break;
|
||||
}
|
||||
|
@ -258,9 +258,9 @@ const Logs = ({
|
|||
};
|
||||
|
||||
// user navigated to logs root page
|
||||
if (!routeMatch?.params.fileName && tuneData && tuneData.logFiles?.length) {
|
||||
if (!routeMatch?.params.fileName && tuneData && tuneData.logFiles.length) {
|
||||
// either redirect to the first log or to the latest selected
|
||||
if (loadedLogs.fileName) {
|
||||
if (loadedLogs?.fileName) {
|
||||
navigate(
|
||||
generatePath(Routes.TUNE_LOGS_FILE, {
|
||||
tuneId: tuneData.tuneId,
|
||||
|
@ -268,7 +268,7 @@ const Logs = ({
|
|||
}),
|
||||
);
|
||||
} else {
|
||||
const firstLogFile = (tuneData.logFiles || [])[0];
|
||||
const firstLogFile = tuneData.logFiles[0];
|
||||
navigate(
|
||||
generatePath(Routes.TUNE_LOGS_FILE, {
|
||||
tuneId: tuneData.tuneId,
|
||||
|
@ -281,12 +281,12 @@ const Logs = ({
|
|||
}
|
||||
|
||||
// first visit, logs are not loaded yet
|
||||
if (!(loadedLogs.logs || []).length && tuneData?.tuneId) {
|
||||
if (!(loadedLogs?.logs || []).length && tuneData?.tuneId) {
|
||||
loadData();
|
||||
}
|
||||
|
||||
// file changed, reload
|
||||
if ((loadedLogs.logs || []).length && loadedLogs.fileName !== routeMatch?.params.fileName) {
|
||||
if ((loadedLogs?.logs || []).length && loadedLogs?.fileName !== routeMatch?.params.fileName) {
|
||||
setLogs(undefined);
|
||||
store.dispatch({ type: 'logs/load', payload: {} });
|
||||
loadData();
|
||||
|
@ -400,7 +400,7 @@ const Logs = ({
|
|||
<Badge
|
||||
size="small"
|
||||
style={badgeStyle}
|
||||
count={tuneData?.logFiles?.length}
|
||||
count={tuneData?.logFiles.length}
|
||||
offset={[10, -3]}
|
||||
>
|
||||
<FileTextOutlined />
|
||||
|
@ -410,7 +410,7 @@ const Logs = ({
|
|||
key: 'files',
|
||||
children: (
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||
{tuneData?.logFiles?.map((fileName) => (
|
||||
{tuneData?.logFiles.map((fileName) => (
|
||||
<Typography.Paragraph key={fileName} ellipsis>
|
||||
<Link
|
||||
to={generatePath(Routes.TUNE_LOGS_FILE, {
|
||||
|
@ -438,9 +438,9 @@ const Logs = ({
|
|||
<Layout className="logs-container">
|
||||
<Content>
|
||||
<div ref={contentRef}>
|
||||
{logs || !!(loadedLogs.logs || []).length ? (
|
||||
{logs || !!(loadedLogs?.logs || []).length ? (
|
||||
<LogCanvas
|
||||
data={loadedLogs.logs || (logs!.records as LogsType)}
|
||||
data={loadedLogs?.logs || (logs!.records as LogsType)}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
selectedFields1={prepareSelectedFields(selectedFields1)}
|
||||
|
@ -464,7 +464,7 @@ const Logs = ({
|
|||
title: 'Downloading',
|
||||
subTitle: fileSize,
|
||||
description: fetchError ? (
|
||||
fetchError!.message
|
||||
fetchError.message
|
||||
) : (
|
||||
<Space>
|
||||
<GlobalOutlined />
|
||||
|
@ -475,7 +475,7 @@ const Logs = ({
|
|||
},
|
||||
{
|
||||
title: 'Decoding',
|
||||
description: parseError ? parseError!.message : 'Reading ones and zeros',
|
||||
description: parseError ? parseError.message : 'Reading ones and zeros',
|
||||
subTitle: parseElapsed,
|
||||
status: parseError && 'error',
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ const mapStateToProps = (state: AppState) => ({
|
|||
tune: state.tune,
|
||||
});
|
||||
|
||||
const Tune = ({ config, tune }: { config: ConfigType | null; tune: TuneState }) => {
|
||||
const Tune = ({ config, tune }: { config: ConfigType | null; tune: TuneState | null }) => {
|
||||
const dialogMatch = useMatch(Routes.TUNE_DIALOG);
|
||||
const tuneRootMatch = useMatch(Routes.TUNE_TUNE);
|
||||
const groupMenuDialogMatch = useMatch(Routes.TUNE_GROUP_MENU_DIALOG);
|
||||
|
@ -28,8 +28,8 @@ const Tune = ({ config, tune }: { config: ConfigType | null; tune: TuneState })
|
|||
|
||||
useEffect(() => {
|
||||
if (tune && config && tuneRootMatch && tuneId) {
|
||||
const firstCategory = Object.keys(config!.menus)[0];
|
||||
const firstDialog = Object.keys(config!.menus[firstCategory].subMenus)[0];
|
||||
const firstCategory = Object.keys(config.menus)[0];
|
||||
const firstDialog = Object.keys(config.menus[firstCategory].subMenus)[0];
|
||||
|
||||
const firstDialogPath = generatePath(Routes.TUNE_DIALOG, {
|
||||
tuneId,
|
||||
|
@ -51,16 +51,18 @@ const Tune = ({ config, tune }: { config: ConfigType | null; tune: TuneState })
|
|||
groupMenuDialogMatch,
|
||||
]);
|
||||
|
||||
if (!(tune && config && (dialogMatch || groupMenuDialogMatch))) {
|
||||
if (!(config && (dialogMatch || groupMenuDialogMatch))) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideBar matchedPath={dialogMatch!} matchedGroupMenuDialogPath={groupMenuDialogMatch} />
|
||||
<SideBar matchedPath={dialogMatch} matchedGroupMenuDialogPath={groupMenuDialogMatch} />
|
||||
<Dialog
|
||||
name={
|
||||
groupMenuDialogMatch ? groupMenuDialogMatch.params.dialog! : dialogMatch?.params.dialog!
|
||||
groupMenuDialogMatch
|
||||
? groupMenuDialogMatch.params.dialog!
|
||||
: dialogMatch?.params.dialog ?? ''
|
||||
}
|
||||
url={groupMenuDialogMatch ? groupMenuDialogMatch.pathname : dialogMatch?.pathname || ''}
|
||||
/>
|
||||
|
|
|
@ -69,12 +69,12 @@ import {
|
|||
|
||||
const { Item, useForm } = Form;
|
||||
|
||||
enum MaxFiles {
|
||||
TUNE_FILES = 1,
|
||||
LOG_FILES = 5,
|
||||
TOOTH_LOG_FILES = 5,
|
||||
CUSTOM_INI_FILES = 1,
|
||||
}
|
||||
const MaxFiles = {
|
||||
TUNE_FILES: 1,
|
||||
LOG_FILES: 5,
|
||||
TOOTH_LOG_FILES: 5,
|
||||
CUSTOM_INI_FILES: 1,
|
||||
} as const;
|
||||
|
||||
interface ValidationResult {
|
||||
result: boolean;
|
||||
|
@ -160,12 +160,13 @@ const UploadPage = () => {
|
|||
|
||||
const noop = () => {};
|
||||
|
||||
const goToNewTune = () =>
|
||||
const goToNewTune = () => {
|
||||
navigate(
|
||||
generatePath(Routes.TUNE_ROOT, {
|
||||
tuneId: newTuneId!,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const publishTune = async (values: TunesRecord) => {
|
||||
setIsLoading(true);
|
||||
|
@ -193,10 +194,10 @@ const UploadPage = () => {
|
|||
|
||||
const compressedCustomIniFile = customIniFile
|
||||
? bufferToFile(
|
||||
await compress(customIniFile!),
|
||||
await compress(customIniFile),
|
||||
(customIniFile as UploadedFile).uid
|
||||
? customIniFile!.name
|
||||
: removeFilenameSuffix(customIniFile!.name),
|
||||
? customIniFile.name
|
||||
: removeFilenameSuffix(customIniFile.name),
|
||||
)
|
||||
: null;
|
||||
|
||||
|
@ -238,7 +239,7 @@ const UploadPage = () => {
|
|||
year,
|
||||
hp,
|
||||
stockHp,
|
||||
readme: readme?.trim(),
|
||||
readme: readme.trim(),
|
||||
tags,
|
||||
visibility,
|
||||
tuneFile: compressedTuneFile as unknown as string,
|
||||
|
@ -333,10 +334,10 @@ const UploadPage = () => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const uploadTune = async (options: UploadRequestOption) => {
|
||||
const uploadTune = (options: UploadRequestOption) => {
|
||||
upload(
|
||||
options,
|
||||
async (file) => {
|
||||
(file) => {
|
||||
setTuneFile(file);
|
||||
},
|
||||
async (file) => {
|
||||
|
@ -377,10 +378,10 @@ const UploadPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const uploadLogs = async (options: UploadRequestOption) => {
|
||||
const uploadLogs = (options: UploadRequestOption) => {
|
||||
upload(
|
||||
options,
|
||||
async (file) => {
|
||||
(file) => {
|
||||
setLogFiles((prev) => [...prev, file]);
|
||||
},
|
||||
async (file) => {
|
||||
|
@ -415,10 +416,10 @@ const UploadPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const uploadToothLogs = async (options: UploadRequestOption) => {
|
||||
const uploadToothLogs = (options: UploadRequestOption) => {
|
||||
upload(
|
||||
options,
|
||||
async (file) => {
|
||||
(file) => {
|
||||
setToothLogFiles((prev) => [...prev, file]);
|
||||
},
|
||||
async (file) => {
|
||||
|
@ -437,10 +438,10 @@ const UploadPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const uploadCustomIni = async (options: UploadRequestOption) => {
|
||||
const uploadCustomIni = (options: UploadRequestOption) => {
|
||||
upload(
|
||||
options,
|
||||
async (file) => {
|
||||
(file) => {
|
||||
setCustomIniFile(file);
|
||||
},
|
||||
async (file) => {
|
||||
|
@ -471,19 +472,19 @@ const UploadPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const removeTuneFile = async () => {
|
||||
const removeTuneFile = () => {
|
||||
setTuneFile(undefined);
|
||||
};
|
||||
|
||||
const removeLogFile = async (file: UploadFile) => {
|
||||
const removeLogFile = (file: UploadFile) => {
|
||||
setLogFiles((prev) => prev.filter((f) => removeFilenameSuffix(f.name) !== file.name));
|
||||
};
|
||||
|
||||
const removeToothLogFile = async (file: UploadFile) => {
|
||||
const removeToothLogFile = (file: UploadFile) => {
|
||||
setToothLogFiles((prev) => prev.filter((f) => removeFilenameSuffix(f.name) !== file.name));
|
||||
};
|
||||
|
||||
const removeCustomIniFile = async (_file: UploadFile) => {
|
||||
const removeCustomIniFile = (_file: UploadFile) => {
|
||||
setCustomIniFile(undefined);
|
||||
};
|
||||
|
||||
|
@ -502,7 +503,7 @@ const UploadPage = () => {
|
|||
setExistingTune(oldTune);
|
||||
form.setFieldsValue(oldTune);
|
||||
setIsEditMode(true);
|
||||
setReadme(oldTune.readme!);
|
||||
setReadme(oldTune.readme);
|
||||
|
||||
if (oldTune.tuneFile) {
|
||||
setTuneFile(await fetchFile(oldTune.id, oldTune.tuneFile));
|
||||
|
@ -527,7 +528,7 @@ const UploadPage = () => {
|
|||
}
|
||||
|
||||
const tempLogFiles: File[] = [];
|
||||
oldTune.logFiles?.forEach(async (fileName: string) => {
|
||||
oldTune.logFiles.forEach(async (fileName: string) => {
|
||||
tempLogFiles.push(await fetchFile(oldTune.id, fileName));
|
||||
setDefaultLogFilesList((prev) => [
|
||||
...prev,
|
||||
|
@ -541,7 +542,7 @@ const UploadPage = () => {
|
|||
setLogFiles(tempLogFiles);
|
||||
|
||||
const tempToothLogFiles: File[] = [];
|
||||
oldTune.toothLogFiles?.forEach(async (fileName: string) => {
|
||||
oldTune.toothLogFiles.forEach(async (fileName: string) => {
|
||||
tempToothLogFiles.push(await fetchFile(oldTune.id, fileName));
|
||||
setDefaultToothLogFilesList((prev) => [
|
||||
...prev,
|
||||
|
@ -570,7 +571,7 @@ const UploadPage = () => {
|
|||
[currentUser, form, navigateToNewTuneId],
|
||||
);
|
||||
|
||||
const prepareData = useCallback(async () => {
|
||||
const prepareData = useCallback(() => {
|
||||
const currentTuneId = routeMatch?.params.tuneId;
|
||||
if (currentTuneId) {
|
||||
loadExistingTune(currentTuneId);
|
||||
|
@ -582,13 +583,6 @@ const UploadPage = () => {
|
|||
|
||||
useEffect(() => {
|
||||
refreshUser().then((user) => {
|
||||
if (user === null) {
|
||||
restrictedPage();
|
||||
navigate(Routes.LOGIN);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
restrictedPage();
|
||||
navigate(Routes.LOGIN);
|
||||
|
@ -663,9 +657,14 @@ const UploadPage = () => {
|
|||
const OpenButton = () => (
|
||||
<>
|
||||
<Row>
|
||||
<Input style={{ width: `calc(100% - ${shareSupported ? 65 : 35}px)` }} value={shareUrl!} />
|
||||
<Input style={{ width: `calc(100% - ${shareSupported ? 65 : 35}px)` }} value={shareUrl} />
|
||||
<Tooltip title="Copy URL">
|
||||
<Button icon={<CopyOutlined />} onClick={() => copyToClipboard(shareUrl!)} />
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
copyToClipboard(shareUrl!);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
{shareSupported && (
|
||||
<Tooltip title="Share">
|
||||
|
@ -858,7 +857,9 @@ const UploadPage = () => {
|
|||
rows={10}
|
||||
showCount
|
||||
value={readme}
|
||||
onChange={(e) => setReadme(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setReadme(e.target.value);
|
||||
}}
|
||||
maxLength={3_000}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -26,7 +26,7 @@ const Profile = () => {
|
|||
const loadData = async () => {
|
||||
setIsTunesLoading(true);
|
||||
try {
|
||||
const { items, totalItems } = await getUserTunes(route?.params.userId!, page, pageSize);
|
||||
const { items, totalItems } = await getUserTunes(route?.params.userId ?? '', page, pageSize);
|
||||
setTotal(totalItems);
|
||||
setAuthor(items[0]!.expand!.author);
|
||||
setTunesDataSource(items);
|
||||
|
@ -60,8 +60,11 @@ const Profile = () => {
|
|||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
key="show"
|
||||
icon={<ArrowRightOutlined />}
|
||||
onClick={() => navigate(tunePath(tune.tuneId))}
|
||||
onClick={() => {
|
||||
navigate(tunePath(tune.tuneId));
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
className={tune.visibility}
|
||||
|
|
|
@ -12,9 +12,11 @@ const EmailVerification = () => {
|
|||
|
||||
useEffect(() => {
|
||||
confirmEmailVerification(routeMatch!.params.token!)
|
||||
.then(() => emailVerificationSuccess())
|
||||
.then(() => {
|
||||
emailVerificationSuccess();
|
||||
})
|
||||
.catch((error) => {
|
||||
emailVerificationFailed(error);
|
||||
emailVerificationFailed(error as Error);
|
||||
});
|
||||
|
||||
navigate(Routes.HUB);
|
||||
|
|
|
@ -40,9 +40,7 @@ const Login = ({ formRole }: { formRole: FormRoles }) => {
|
|||
const [googleUrl, setGoogleUrl] = useState<string | null>(null);
|
||||
const [githubUrl, setGithubUrl] = useState<string | null>(null);
|
||||
const [facebookUrl, setFacebookUrl] = useState<string | null>(null);
|
||||
const [providersStatuses, setProvidersStatuses] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({
|
||||
const [providersStatuses, setProvidersStatuses] = useState<Record<string, boolean>>({
|
||||
google: false,
|
||||
github: false,
|
||||
facebook: false,
|
||||
|
@ -51,7 +49,9 @@ const Login = ({ formRole }: { formRole: FormRoles }) => {
|
|||
const { login, signUp, listAuthMethods } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const isAnythingLoading = isEmailLoading || isOAuthLoading;
|
||||
const redirectAfterLogin = useCallback(() => navigate(Routes.HUB), [navigate]);
|
||||
const redirectAfterLogin = useCallback(() => {
|
||||
navigate(Routes.HUB);
|
||||
}, [navigate]);
|
||||
const isOauthEnabled = Object.values(providersStatuses).includes(true);
|
||||
|
||||
const emailLogin = async ({ email, password }: { email: string; password: string }) => {
|
||||
|
@ -76,7 +76,11 @@ const Login = ({ formRole }: { formRole: FormRoles }) => {
|
|||
email,
|
||||
password,
|
||||
username,
|
||||
}: { email: string; password: string; username: string }) => {
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
username: string;
|
||||
}) => {
|
||||
setIsEmailLoading(true);
|
||||
try {
|
||||
const user = await signUp(email, password, username);
|
||||
|
@ -94,13 +98,14 @@ const Login = ({ formRole }: { formRole: FormRoles }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const oauthMethods: {
|
||||
[provider: string]: {
|
||||
const oauthMethods: Record<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
onClick: () => void;
|
||||
};
|
||||
} = {
|
||||
}
|
||||
> = {
|
||||
google: {
|
||||
label: 'Google',
|
||||
icon: <GoogleOutlined />,
|
||||
|
|
|
@ -15,17 +15,16 @@ const OauthCallback = () => {
|
|||
useEffect(() => {
|
||||
const authProviders = JSON.parse(
|
||||
window.localStorage.getItem('authProviders') || '',
|
||||
) as unknown as AuthProviderInfo[];
|
||||
) as AuthProviderInfo[];
|
||||
|
||||
oAuth(
|
||||
routeMatch?.params.provider as OAuthProviders,
|
||||
searchParams.get('code')!,
|
||||
authProviders.find((provider) => provider.name === routeMatch?.params.provider)
|
||||
?.codeVerifier!,
|
||||
).then(() => {
|
||||
logInSuccessful();
|
||||
navigate(Routes.HUB, { replace: true });
|
||||
});
|
||||
?.codeVerifier ?? '',
|
||||
);
|
||||
logInSuccessful();
|
||||
navigate(Routes.HUB, { replace: true });
|
||||
}, [navigate, oAuth, routeMatch, searchParams]);
|
||||
|
||||
return <Loader />;
|
||||
|
|
|
@ -44,12 +44,13 @@ const Profile = () => {
|
|||
const [isTunesLoading, setIsTunesLoading] = useState(false);
|
||||
const [tunesDataSource, setTunesDataSource] = useState<TunesResponse[]>([]);
|
||||
|
||||
const goToEdit = (tuneId: string) =>
|
||||
const goToEdit = (tuneId: string) => {
|
||||
navigate(
|
||||
generatePath(Routes.UPLOAD_WITH_TUNE_ID, {
|
||||
tuneId,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const resendEmailVerification = async () => {
|
||||
setIsSendingVerification(true);
|
||||
|
@ -100,7 +101,7 @@ const Profile = () => {
|
|||
}
|
||||
|
||||
refreshUser().then((user) => {
|
||||
if (currentUser === null || user === null) {
|
||||
if (user === null) {
|
||||
restrictedPage();
|
||||
navigate(Routes.LOGIN);
|
||||
}
|
||||
|
@ -180,10 +181,19 @@ const Profile = () => {
|
|||
) : (
|
||||
<EyeOutlined />
|
||||
),
|
||||
<Button icon={<EditOutlined />} onClick={() => goToEdit(tune.tuneId)} />,
|
||||
<Button
|
||||
key="edit"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
goToEdit(tune.tuneId);
|
||||
}}
|
||||
/>,
|
||||
<Button
|
||||
key="view"
|
||||
icon={<ArrowRightOutlined />}
|
||||
onClick={() => navigate(tunePath(tune.tuneId))}
|
||||
onClick={() => {
|
||||
navigate(tunePath(tune.tuneId));
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
|
|
|
@ -19,6 +19,7 @@ const ResetPasswordConfirmation = () => {
|
|||
|
||||
const changePassword = async ({ password }: { password: string }) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await confirmResetPassword(routeMatch!.params.token!, password);
|
||||
passwordUpdateSuccess();
|
||||
|
|
|
@ -7,175 +7,200 @@ const baseOptions = {
|
|||
placement: 'bottomRight' as const,
|
||||
};
|
||||
|
||||
const error = (message: string, description: string) =>
|
||||
const error = (message: string, description: string) => {
|
||||
notification.warning({
|
||||
message,
|
||||
description,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const emailNotVerified = () =>
|
||||
const emailNotVerified = () => {
|
||||
notification.warning({
|
||||
message: 'Check your email',
|
||||
description: 'Your email address has to be verified before you can upload files!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const signUpSuccessful = () =>
|
||||
const signUpSuccessful = () => {
|
||||
notification.success({
|
||||
message: 'Sign Up successful',
|
||||
description: 'Welcome on board!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const signUpFailed = (err: Error) =>
|
||||
const signUpFailed = (err: Error) => {
|
||||
notification.error({
|
||||
message: 'Failed to create an account',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const logInSuccessful = () =>
|
||||
const logInSuccessful = () => {
|
||||
notification.success({
|
||||
message: 'Log in successful',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const logInFailed = (err: Error) =>
|
||||
const logInFailed = (err: Error) => {
|
||||
notification.error({
|
||||
message: 'Failed to log in',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const restrictedPage = () =>
|
||||
const restrictedPage = () => {
|
||||
notification.warning({
|
||||
message: 'Restricted page',
|
||||
description: 'You have to be logged in to access this page!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const logOutSuccessful = () =>
|
||||
const logOutSuccessful = () => {
|
||||
notification.success({
|
||||
message: 'Log out successful',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const resetSuccessful = () =>
|
||||
const resetSuccessful = () => {
|
||||
notification.success({
|
||||
message: 'Password reset initiated',
|
||||
description: 'Check your email!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const resetFailed = (err: Error) =>
|
||||
const resetFailed = (err: Error) => {
|
||||
notification.error({
|
||||
message: 'Password reset failed',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const sendingEmailVerificationFailed = (err: Error) =>
|
||||
const sendingEmailVerificationFailed = (err: Error) => {
|
||||
notification.success({
|
||||
message: 'Sending verification email failed',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const emailVerificationSent = () =>
|
||||
const emailVerificationSent = () => {
|
||||
notification.success({
|
||||
message: 'Check your email',
|
||||
description: 'Email verification sent!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const emailVerificationFailed = (err: Error) =>
|
||||
const emailVerificationFailed = (err: Error) => {
|
||||
notification.error({
|
||||
message: 'Email verification failed',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const emailVerificationSuccess = () =>
|
||||
const emailVerificationSuccess = () => {
|
||||
notification.success({
|
||||
message: 'Email verified',
|
||||
description: 'Your email has been verified!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const profileUpdateSuccess = () =>
|
||||
const profileUpdateSuccess = () => {
|
||||
notification.success({
|
||||
message: 'Profile updated',
|
||||
description: 'Your profile has been updated!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const profileUpdateFailed = (err: Error) =>
|
||||
const profileUpdateFailed = (err: Error) => {
|
||||
notification.error({
|
||||
message: 'Unable to update your profile',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const passwordUpdateSuccess = () =>
|
||||
const passwordUpdateSuccess = () => {
|
||||
notification.success({
|
||||
message: 'Password changed',
|
||||
description: 'Your password has been changed!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const passwordUpdateFailed = (err: Error) =>
|
||||
const passwordUpdateFailed = (err: Error) => {
|
||||
notification.error({
|
||||
message: 'Unable to change your password',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const databaseGenericError = (err: Error) =>
|
||||
const databaseGenericError = (err: Error) => {
|
||||
notification.error({
|
||||
message: 'Database Error',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const iniLoadingError = (err: Error) =>
|
||||
const iniLoadingError = (err: Error) => {
|
||||
notification.error({
|
||||
message: 'INI not found',
|
||||
description: err.message,
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const tuneNotFound = () =>
|
||||
const tuneNotFound = () => {
|
||||
notification.warning({
|
||||
message: 'Tune not found',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const tuneParsingError = () =>
|
||||
const tuneParsingError = () => {
|
||||
notification.error({
|
||||
message: 'Tune file is not valid',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const copiedToClipboard = () =>
|
||||
const copiedToClipboard = () => {
|
||||
notification.success({
|
||||
message: 'Copied to clipboard',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const signatureNotSupportedWarning = (message: string) =>
|
||||
const signatureNotSupportedWarning = (message: string) => {
|
||||
notification.warning({
|
||||
message,
|
||||
description: 'You need to upload custom INI file with your tune!',
|
||||
...baseOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const downloading = () =>
|
||||
const downloading = () => {
|
||||
notification.success({
|
||||
message: 'Downloading...',
|
||||
...baseOptions,
|
||||
duration: 1,
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
error,
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import PocketBase, { AuthMethodsList, AuthProviderInfo, ClientResponseError } from 'pocketbase';
|
||||
import { TypedPocketBase } from './@types/pocketbase-types';
|
||||
import { fetchEnv } from './utils/env';
|
||||
|
||||
const API_URL = fetchEnv('VITE_POCKETBASE_API_URL');
|
||||
const client = new PocketBase(API_URL);
|
||||
const client = new PocketBase(API_URL) as TypedPocketBase;
|
||||
|
||||
const formatError = (error: ClientResponseError) => {
|
||||
const { data, message } = error;
|
||||
|
||||
if (data.data) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const errors = Object.keys(data.data).map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(key) => `${key.toUpperCase()}: ${data.data[key].message}`,
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export enum Routes {
|
||||
ROOT = '/',
|
||||
HUB = '/',
|
||||
|
||||
TUNE_ROOT = '/t/:tuneId',
|
||||
|
|
|
@ -29,11 +29,16 @@ const toggleSidebar = createAction('ui/toggleSidebar');
|
|||
const initialState: AppState = {
|
||||
tune: {
|
||||
constants: {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
details: {} as any,
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
tuneData: {} as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
logs: {} as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
toothLogs: {} as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
config: {} as any,
|
||||
ui: {
|
||||
sidebarCollapsed: false,
|
||||
|
|
|
@ -2,6 +2,4 @@ export interface ParserInterface {
|
|||
parse(onProgress: (percent: number) => void): this;
|
||||
}
|
||||
|
||||
export interface ParserConstructor {
|
||||
new (buffer: ArrayBuffer): ParserInterface;
|
||||
}
|
||||
export type ParserConstructor = new (buffer: ArrayBuffer) => ParserInterface;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
export const isMac = `${window.navigator.platform}`.includes('Mac');
|
||||
export const environment = import.meta.env.VITE_ENVIRONMENT || 'development';
|
||||
export const isProduction = environment === 'production';
|
||||
|
@ -10,5 +12,5 @@ export const fetchEnv = (envName: string): string => {
|
|||
throw new Error(`Missing ENV: ${envName}`);
|
||||
}
|
||||
|
||||
return envValue;
|
||||
return envValue as string;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import locations from '../data/edge-locations.json';
|
||||
|
||||
type LocationsType = { [index: string]: string };
|
||||
type LocationsType = Record<string, string>;
|
||||
|
||||
export type OnProgress = (percent: number, total: number, edgeLocation: string | null) => void;
|
||||
|
||||
|
@ -41,8 +41,8 @@ export const fetchWithProgress = async (
|
|||
break;
|
||||
}
|
||||
|
||||
array.set(value as Uint8Array, at);
|
||||
at += (value as Uint8Array).length;
|
||||
array.set(value, at);
|
||||
at += value.length;
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(~~((at / length) * 100), length, edgeLocation);
|
||||
|
|
|
@ -7,5 +7,6 @@ enum Keys {
|
|||
ESCAPE = 'Escape',
|
||||
}
|
||||
|
||||
export const isToggleSidebar = (e: KeyEvent) => (e.metaKey || e.ctrlKey) && e.key === Keys.SIDEBAR;
|
||||
export const isEscape = (e: KeyEvent) => e.key === Keys.ESCAPE;
|
||||
export const isToggleSidebar = (e: KeyEvent) =>
|
||||
(e.metaKey || e.ctrlKey) && e.key === (Keys.SIDEBAR as string);
|
||||
export const isEscape = (e: KeyEvent) => e.key === (Keys.ESCAPE as string);
|
||||
|
|
|
@ -34,7 +34,6 @@ class LogValidator implements ParserInterface {
|
|||
private checkMLG() {
|
||||
const fileFormat = new TextDecoder('utf8')
|
||||
.decode(this.buffer.slice(0, this.mlgFormatLength))
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: false positive
|
||||
.replace(/\x00/gu, '');
|
||||
|
||||
if (fileFormat === 'MLVLG') {
|
||||
|
|
|
@ -86,9 +86,13 @@ class TriggerLogsParser implements ParserInterface {
|
|||
}
|
||||
|
||||
private detectType(): void {
|
||||
this.raw.split('\n').some((line, index) => {
|
||||
this.raw.split('\n').forEach((line, index) => {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith(this.commentPrefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// give up
|
||||
if (index > 10) {
|
||||
return true;
|
||||
|
@ -102,7 +106,7 @@ class TriggerLogsParser implements ParserInterface {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (parts.length >= 7) {
|
||||
if (parts.length >= 6) {
|
||||
this.isCompositeLogs = true;
|
||||
|
||||
return true;
|
||||
|
@ -227,6 +231,16 @@ class TriggerLogsParser implements ParserInterface {
|
|||
time: Number(parts[6]),
|
||||
});
|
||||
}
|
||||
|
||||
if (parts.length === 6) {
|
||||
// PriLevel,SecLevel,Trigger,Sync,ToothTime,Time
|
||||
this.resultComposite.push({
|
||||
...base,
|
||||
maxTime: 0,
|
||||
toothTime: Number(parts[4]),
|
||||
time: Number(parts[5]),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { TuneWithDetails } from '@hyper-tuner/types';
|
||||
|
||||
class TuneParser {
|
||||
|
@ -16,14 +20,14 @@ class TuneParser {
|
|||
},
|
||||
};
|
||||
|
||||
parse(buffer: ArrayBuffer): TuneParser {
|
||||
parse(buffer: ArrayBuffer): this {
|
||||
const raw = new TextDecoder().decode(buffer);
|
||||
const xml = new DOMParser().parseFromString(raw, 'text/xml');
|
||||
const xmlPages = xml.getElementsByTagName('page');
|
||||
const bibliography = xml.getElementsByTagName('bibliography')[0]?.attributes as any;
|
||||
const versionInfo = xml.getElementsByTagName('versionInfo')[0]?.attributes as any;
|
||||
const bibliography = (xml.getElementsByTagName('bibliography')[0] as any)?.attributes;
|
||||
const versionInfo = (xml.getElementsByTagName('versionInfo')[0] as any)?.attributes;
|
||||
|
||||
if (!(xmlPages && versionInfo)) {
|
||||
if (!versionInfo) {
|
||||
this.isTuneValid = false;
|
||||
|
||||
return this;
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
Page as ConfigPageType,
|
||||
SimpleConstant as SimpleConstantType,
|
||||
TuneConstants as TuneConstantsType,
|
||||
ConstantTypes,
|
||||
} from '@hyper-tuner/types';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
|
@ -35,8 +36,8 @@ export const prepareConstDeclarations = (
|
|||
];
|
||||
|
||||
// we need array index instead of a display value
|
||||
if (constant?.type === 'bits') {
|
||||
val = (constant.values as string[]).indexOf(`${val}`);
|
||||
if (constant?.type === ConstantTypes.BITS) {
|
||||
val = constant.values.indexOf(`${val}`);
|
||||
}
|
||||
|
||||
// escape string values
|
||||
|
@ -82,6 +83,7 @@ export const evaluateExpression = (
|
|||
try {
|
||||
// TODO: strip eval from `command` etc
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return eval(`
|
||||
'use strict';
|
||||
const arrayValue = (number, layout) => number;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const aspirationMapper: { [key: string]: string } = {
|
||||
export const aspirationMapper: Record<string, string> = {
|
||||
na: 'N/A',
|
||||
turbocharged: 'Turbocharged',
|
||||
supercharged: 'Supercharged',
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { Plugin as uPlotPlugin } from 'uplot';
|
||||
import { ZoomPluginOptions } from './zoomPluginOptions';
|
||||
|
||||
const ARROW_UP = 'ArrowUp';
|
||||
const ARROW_DOWN = 'ArrowDown';
|
||||
const ARROW_LEFT = 'ArrowLeft';
|
||||
const ARROW_RIGHT = 'ArrowRight';
|
||||
|
||||
function keyboardZoomPlugin(options: ZoomPluginOptions = {}): uPlotPlugin {
|
||||
const { zoomFactor = 0.9, panFactor = 0.3 } = options;
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
ready(u) {
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = u.cursor;
|
||||
const { left, top } = cursor;
|
||||
const xVal = u.posToVal(left || 0, 'x');
|
||||
const yVal = u.posToVal(top || 0, 'y');
|
||||
|
||||
const xRange = (u.scales.x.max ?? 0) - (u.scales.x.min ?? 0);
|
||||
const yRange = (u.scales.y.max ?? 0) - (u.scales.y.min ?? 0);
|
||||
|
||||
if (e.key === ARROW_UP || e.key === ARROW_DOWN) {
|
||||
const zoomOut = e.key === ARROW_DOWN;
|
||||
const newZoomFactor = zoomOut ? 1 / zoomFactor : zoomFactor;
|
||||
|
||||
const nxRange = xRange * newZoomFactor;
|
||||
const nxMin = xVal - (xVal - (u.scales.x.min ?? 0)) * newZoomFactor;
|
||||
|
||||
const nyRange = yRange * newZoomFactor;
|
||||
const nyMin = yVal - (yVal - (u.scales.y.min ?? 0)) * newZoomFactor;
|
||||
|
||||
u.batch(() => {
|
||||
u.setScale('x', { min: nxMin, max: nxMin + nxRange });
|
||||
u.setScale('y', { min: nyMin, max: nyMin + nyRange });
|
||||
});
|
||||
} else if (e.key === ARROW_LEFT || e.key === ARROW_RIGHT) {
|
||||
const scrollDirection = e.key === ARROW_LEFT ? -1 : 1;
|
||||
const scrollAmount = (xRange / 10) * scrollDirection * panFactor;
|
||||
const nxMin = (u.scales.x.min ?? 0) + scrollAmount;
|
||||
|
||||
u.batch(() => {
|
||||
u.setScale('x', { min: nxMin, max: nxMin + xRange });
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default keyboardZoomPlugin;
|
|
@ -0,0 +1,133 @@
|
|||
import uPlot, { Plugin as uPlotPlugin } from 'uplot';
|
||||
import { ZoomPluginOptions } from './zoomPluginOptions';
|
||||
|
||||
const chartInstances: uPlot[] = [];
|
||||
|
||||
const mouseZoomPlugin = (options: ZoomPluginOptions = {}): uPlotPlugin => {
|
||||
const { zoomFactor = 0.9 } = options;
|
||||
|
||||
let xMin: number;
|
||||
let xMax: number;
|
||||
let yMin: number;
|
||||
let yMax: number;
|
||||
let xRange: number;
|
||||
let yRange: number;
|
||||
let over: HTMLElement | null = null;
|
||||
let rect: DOMRect | null = null;
|
||||
|
||||
function isCtrlPressed(e: MouseEvent): boolean {
|
||||
return e.ctrlKey || e.metaKey;
|
||||
}
|
||||
|
||||
function clamp(
|
||||
nRange: number,
|
||||
nMin: number,
|
||||
nMax: number,
|
||||
fRange: number,
|
||||
fMin: number,
|
||||
fMax: number,
|
||||
): [number, number] {
|
||||
let newNMin = nMin;
|
||||
let newNMax = nMax;
|
||||
|
||||
if (nRange > fRange) {
|
||||
newNMin = fMin;
|
||||
newNMax = fMax;
|
||||
} else if (nMin < fMin) {
|
||||
newNMin = fMin;
|
||||
newNMax = fMin + nRange;
|
||||
} else if (nMax > fMax) {
|
||||
newNMax = fMax;
|
||||
newNMin = fMax - nRange;
|
||||
}
|
||||
|
||||
return [newNMin, newNMax];
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
ready(u) {
|
||||
chartInstances.push(u); // Add the current chart instance to the list
|
||||
|
||||
xMin = u.scales.x.min ?? 0;
|
||||
xMax = u.scales.x.max ?? 0;
|
||||
yMin = u.scales.y.min ?? 0;
|
||||
yMax = u.scales.y.max ?? 0;
|
||||
xRange = xMax - xMin;
|
||||
yRange = yMax - yMin;
|
||||
over = u.over;
|
||||
rect = over.getBoundingClientRect();
|
||||
|
||||
over.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
if (e.button === 1) {
|
||||
e.preventDefault();
|
||||
|
||||
const left0 = e.clientX;
|
||||
const scXMin0 = u.scales.x.min;
|
||||
const scXMax0 = u.scales.x.max;
|
||||
const xUnitsPerPx = u.valToPos(1, 'x') - u.valToPos(0, 'x');
|
||||
|
||||
function onMove(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
const left1 = e.clientX;
|
||||
const dx = xUnitsPerPx * (left1 - left0);
|
||||
|
||||
u.setScale('x', {
|
||||
min: (scXMin0 ?? 0) - dx,
|
||||
max: (scXMax0 ?? 0) - dx,
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
}
|
||||
});
|
||||
|
||||
over.addEventListener('wheel', (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isCtrlPressed(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = u.cursor;
|
||||
|
||||
const { left, top } = cursor;
|
||||
const leftPct = (left || 0) / (rect?.width || 1);
|
||||
const btmPct = 1 - (top || 0) / (rect?.height || 1);
|
||||
const xVal = u.posToVal(left || 0, 'x');
|
||||
const yVal = u.posToVal(top || 0, 'y');
|
||||
const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
|
||||
const oyRange = (u.scales.y.max || 0) - (u.scales.y.min || 0);
|
||||
|
||||
const nxRange = e.deltaY < 0 ? oxRange * zoomFactor : oxRange / zoomFactor;
|
||||
let nxMin = xVal - leftPct * nxRange;
|
||||
let nxMax = nxMin + nxRange;
|
||||
[nxMin, nxMax] = clamp(nxRange, nxMin, nxMax, xRange, xMin, xMax);
|
||||
|
||||
const nyRange = e.deltaY < 0 ? oyRange * zoomFactor : oyRange / zoomFactor;
|
||||
let nyMin = yVal - btmPct * nyRange;
|
||||
let nyMax = nyMin + nyRange;
|
||||
[nyMin, nyMax] = clamp(nyRange, nyMin, nyMax, yRange, yMin, yMax);
|
||||
|
||||
// Loop through all chart instances and apply the same zoom to each of them
|
||||
chartInstances.forEach((chartInstance) => {
|
||||
chartInstance.batch(() => {
|
||||
chartInstance.setScale('x', {
|
||||
min: nxMin,
|
||||
max: nxMax,
|
||||
});
|
||||
|
||||
chartInstance.setScale('y', {
|
||||
min: nyMin,
|
||||
max: nyMax,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default mouseZoomPlugin;
|
|
@ -1,138 +1,138 @@
|
|||
import uPlot from 'uplot';
|
||||
import uPlot, { Plugin as uPlotPlugin } from 'uplot';
|
||||
|
||||
interface Point {
|
||||
type Point = {
|
||||
x: number;
|
||||
y: number;
|
||||
d?: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
}
|
||||
};
|
||||
|
||||
const touchZoomPlugin = () => {
|
||||
const init = (u: uPlot, _opts: any, _data: any) => {
|
||||
const { over } = u;
|
||||
|
||||
let rect: DOMRect;
|
||||
let oxRange: number;
|
||||
let oyRange: number;
|
||||
let xVal: number;
|
||||
let yVal: number;
|
||||
|
||||
const fr: Point = { x: 0, y: 0, dx: 0, dy: 0 };
|
||||
const to: Point = { x: 0, y: 0, dx: 0, dy: 0 };
|
||||
|
||||
const storePos = (t: Point, e: TouchEvent) => {
|
||||
const ts = e.touches;
|
||||
|
||||
const t0 = ts[0];
|
||||
const t0x = t0.clientX - rect.left;
|
||||
const t0y = t0.clientY - rect.top;
|
||||
|
||||
if (ts.length === 1) {
|
||||
t.x = t0x;
|
||||
t.y = t0y;
|
||||
t.d = t.dx = t.dy = 1;
|
||||
} else {
|
||||
const t1 = e.touches[1];
|
||||
const t1x = t1.clientX - rect.left;
|
||||
const t1y = t1.clientY - rect.top;
|
||||
|
||||
const xMin = Math.min(t0x, t1x);
|
||||
const yMin = Math.min(t0y, t1y);
|
||||
const xMax = Math.max(t0x, t1x);
|
||||
const yMax = Math.max(t0y, t1y);
|
||||
|
||||
// mid points
|
||||
t.y = (yMin + yMax) / 2;
|
||||
t.x = (xMin + xMax) / 2;
|
||||
|
||||
t.dx = xMax - xMin;
|
||||
t.dy = yMax - yMin;
|
||||
|
||||
// dist
|
||||
t.d = Math.sqrt(t.dx * t.dx + t.dy * t.dy);
|
||||
}
|
||||
};
|
||||
|
||||
let rafPending = false;
|
||||
|
||||
const zoom = () => {
|
||||
rafPending = false;
|
||||
|
||||
const left = to.x;
|
||||
const top = to.y;
|
||||
|
||||
// non-uniform scaling
|
||||
// let xFactor = fr.dx / to.dx;
|
||||
// let yFactor = fr.dy / to.dy;
|
||||
|
||||
// uniform x/y scaling
|
||||
const xFactor = fr.d! / to.d!;
|
||||
const yFactor = fr.d! / to.d!;
|
||||
|
||||
const leftPct = left / rect.width;
|
||||
const btmPct = 1 - top / rect.height;
|
||||
|
||||
const nxRange = oxRange * xFactor;
|
||||
const nxMin = xVal - leftPct * nxRange;
|
||||
const nxMax = nxMin + nxRange;
|
||||
|
||||
const nyRange = oyRange * yFactor;
|
||||
const nyMin = yVal - btmPct * nyRange;
|
||||
const nyMax = nyMin + nyRange;
|
||||
|
||||
u.batch(() => {
|
||||
u.setScale('x', {
|
||||
min: nxMin,
|
||||
max: nxMax,
|
||||
});
|
||||
|
||||
u.setScale('y', {
|
||||
min: nyMin,
|
||||
max: nyMax,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const touchmove = (e: TouchEvent) => {
|
||||
storePos(to, e);
|
||||
|
||||
if (!rafPending) {
|
||||
rafPending = true;
|
||||
requestAnimationFrame(zoom);
|
||||
}
|
||||
};
|
||||
|
||||
over.addEventListener('touchstart', (e: TouchEvent) => {
|
||||
if (e.touches.length > 1) {
|
||||
// prevent default pinch zoom
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
rect = over.getBoundingClientRect();
|
||||
|
||||
storePos(fr, e);
|
||||
|
||||
oxRange = u.scales.x.max! - u.scales.x.min!;
|
||||
oyRange = u.scales.y.max! - u.scales.y.min!;
|
||||
|
||||
const left = fr.x;
|
||||
const top = fr.y;
|
||||
|
||||
xVal = u.posToVal(left, 'x');
|
||||
yVal = u.posToVal(top, 'y');
|
||||
|
||||
document.addEventListener('touchmove', touchmove, { passive: true });
|
||||
});
|
||||
|
||||
over.addEventListener('touchend', (_e: TouchEvent) => {
|
||||
document.removeEventListener('touchmove', touchmove);
|
||||
});
|
||||
};
|
||||
const chartInstances: uPlot[] = [];
|
||||
|
||||
const touchZoomPlugin = (): uPlotPlugin => {
|
||||
return {
|
||||
hooks: {
|
||||
init,
|
||||
init(u, _opts, _data) {
|
||||
const { over } = u;
|
||||
|
||||
let rect: DOMRect;
|
||||
let oxRange: number;
|
||||
let oyRange: number;
|
||||
let xVal: number;
|
||||
let yVal: number;
|
||||
|
||||
const fr: Point = { x: 0, y: 0, dx: 0, dy: 0 };
|
||||
const to: Point = { x: 0, y: 0, dx: 0, dy: 0 };
|
||||
|
||||
const storePos = (t: Point, e: TouchEvent) => {
|
||||
const ts = e.touches;
|
||||
|
||||
const t0 = ts[0];
|
||||
const t0x = t0.clientX - rect.left;
|
||||
const t0y = t0.clientY - rect.top;
|
||||
|
||||
if (ts.length === 1) {
|
||||
t.x = t0x;
|
||||
t.y = t0y;
|
||||
t.d = t.dx = t.dy = 1;
|
||||
} else {
|
||||
const t1 = e.touches[1];
|
||||
const t1x = t1.clientX - rect.left;
|
||||
const t1y = t1.clientY - rect.top;
|
||||
|
||||
const xMin = Math.min(t0x, t1x);
|
||||
const yMin = Math.min(t0y, t1y);
|
||||
const xMax = Math.max(t0x, t1x);
|
||||
const yMax = Math.max(t0y, t1y);
|
||||
|
||||
// mid points
|
||||
t.y = (yMin + yMax) / 2;
|
||||
t.x = (xMin + xMax) / 2;
|
||||
|
||||
t.dx = xMax - xMin;
|
||||
t.dy = yMax - yMin;
|
||||
|
||||
// dist
|
||||
t.d = Math.sqrt(t.dx * t.dx + t.dy * t.dy);
|
||||
}
|
||||
};
|
||||
|
||||
let rafPending = false;
|
||||
|
||||
const zoomCharts = () => {
|
||||
rafPending = false;
|
||||
|
||||
const left = to.x;
|
||||
const top = to.y;
|
||||
|
||||
const xFactor = fr.d! / to.d!;
|
||||
const yFactor = fr.d! / to.d!;
|
||||
|
||||
const leftPct = left / rect.width;
|
||||
const btmPct = 1 - top / rect.height;
|
||||
|
||||
const nxRange = oxRange * xFactor;
|
||||
const nxMin = xVal - leftPct * nxRange;
|
||||
const nxMax = nxMin + nxRange;
|
||||
|
||||
const nyRange = oyRange * yFactor;
|
||||
const nyMin = yVal - btmPct * nyRange;
|
||||
const nyMax = nyMin + nyRange;
|
||||
|
||||
// Loop through all chart instances and apply the same zoom to each of them
|
||||
chartInstances.forEach((chartInstance) => {
|
||||
chartInstance.batch(() => {
|
||||
chartInstance.setScale('x', {
|
||||
min: nxMin,
|
||||
max: nxMax,
|
||||
});
|
||||
|
||||
chartInstance.setScale('y', {
|
||||
min: nyMin,
|
||||
max: nyMax,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const touchmove = (e: TouchEvent) => {
|
||||
storePos(to, e);
|
||||
|
||||
if (!rafPending) {
|
||||
rafPending = true;
|
||||
requestAnimationFrame(zoomCharts);
|
||||
}
|
||||
};
|
||||
|
||||
over.addEventListener('touchstart', (e: TouchEvent) => {
|
||||
if (e.touches.length > 1) {
|
||||
// prevent default pinch zoom
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
rect = over.getBoundingClientRect();
|
||||
|
||||
storePos(fr, e);
|
||||
|
||||
oxRange = u.scales.x.max! - u.scales.x.min!;
|
||||
oyRange = u.scales.y.max! - u.scales.y.min!;
|
||||
|
||||
const left = fr.x;
|
||||
const top = fr.y;
|
||||
|
||||
xVal = u.posToVal(left, 'x');
|
||||
yVal = u.posToVal(top, 'y');
|
||||
|
||||
document.addEventListener('touchmove', touchmove, { passive: true });
|
||||
});
|
||||
|
||||
over.addEventListener('touchend', (_e: TouchEvent) => {
|
||||
document.removeEventListener('touchmove', touchmove);
|
||||
});
|
||||
|
||||
chartInstances.push(u); // Add the current chart instance to the list
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export type ZoomPluginOptions = {
|
||||
zoomFactor?: number;
|
||||
panFactor?: number;
|
||||
};
|
|
@ -3,7 +3,7 @@ import { fetchEnv } from './env';
|
|||
export const buildFullUrl = (parts = [] as string[]) =>
|
||||
`${fetchEnv('VITE_WEB_URL')}/#${parts.join('/')}`;
|
||||
|
||||
export const buildRedirectUrl = (redirectPage: string, params: { [param: string]: string }) => {
|
||||
export const buildRedirectUrl = (redirectPage: string, params: Record<string, string>) => {
|
||||
const url = new URL(fetchEnv('VITE_WEB_URL'));
|
||||
url.search = new URLSearchParams({
|
||||
redirect: redirectPage,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { decompress } from '../utils/compression';
|
|||
import LogValidator from '../utils/logs/LogValidator';
|
||||
import MslLogParser from '../utils/logs/MslLogParser';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
const ctx: Worker = self as any;
|
||||
|
||||
export interface WorkerOutput {
|
||||
|
|
Loading…
Reference in New Issue