Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b5b24b17 | ||
|
|
cde715894e | ||
|
|
adaadeabff | ||
|
|
35864b4de7 | ||
|
|
621cb1913f | ||
|
|
3e3e3f1c0b | ||
|
|
cdf0374020 | ||
|
|
ec406889f7 | ||
|
|
b731a3bc36 | ||
|
|
27e15820f7 | ||
|
|
b66214678d |
No files matched your search
@@ -0,0 +1,14 @@
|
||||
## What does this PR do / add / change?
|
||||
|
||||
<!-- Describe the purpose of this PR. What problem does it solve or what feature does it add? -->
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- Include screenshots for any UI changes. Remove this section if not applicable. -->
|
||||
|
||||
## Checklist before opening the pr
|
||||
|
||||
- [ ] Tested locally
|
||||
- [ ] Tested on the machine (if hardware interaction is involved)
|
||||
- [ ] No format errors (`cargo fmt` / `npm run format`)
|
||||
- [ ] No build errors (`cargo build` / `npm run build`)
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NIX_PATH: nixpkgs=channel:nixos-25.05
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_VERSION: "1.86"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
Generated
+87
-37
@@ -71,7 +71,7 @@
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"electron": "^37.3.1",
|
||||
"electron": "^40.6.1",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
@@ -4698,8 +4698,15 @@
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
"deep-eql": "^5.0.1",
|
||||
"loupe": "^3.1.0",
|
||||
"pathval": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
@@ -4759,6 +4766,16 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
|
||||
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
@@ -5040,6 +5057,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -5196,15 +5223,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "37.10.3",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz",
|
||||
"integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==",
|
||||
"version": "40.6.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-40.6.1.tgz",
|
||||
"integrity": "sha512-u9YfoixttdauciHV9Ut9Zf3YipJoU093kR1GSYTTXTAXqhiXI0G1A0NnL/f0O2m2UULCXaXMf2W71PloR6V9pQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^22.7.7",
|
||||
"@types/node": "^24.9.0",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -5285,16 +5312,6 @@
|
||||
"global-agent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "22.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
|
||||
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
@@ -5330,13 +5347,6 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron/node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
@@ -7960,6 +7970,13 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
@@ -8937,6 +8954,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -9316,6 +9345,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
||||
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
@@ -10456,8 +10495,31 @@
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "~1.5.0",
|
||||
"seroval-plugins": "~1.5.0"
|
||||
"seroval": "~1.3.0",
|
||||
"seroval-plugins": "~1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-js/node_modules/seroval": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-js/node_modules/seroval-plugins": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz",
|
||||
"integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"seroval": "^1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
@@ -11258,18 +11320,6 @@
|
||||
"yaku": "^0.16.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unzip-crx-3/node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"electron": "^37.3.1",
|
||||
"electron": "^40.6.1",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
|
||||
@@ -20,6 +20,7 @@ import { TouchButton } from "../touch/TouchButton";
|
||||
import { seriesToUPlotData } from "@/lib/timeseries";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { getAllTimeSeries } from "./createChart";
|
||||
import { TargetDashOverlay } from "./TargetDashOverlay";
|
||||
|
||||
// Collects lines connected to visible series for rendering
|
||||
const getVisibleLines = (data: any, visibleSeries: boolean[]) => {
|
||||
@@ -110,10 +111,26 @@ export function BigGraph({
|
||||
|
||||
// State for series visibility
|
||||
const normalizedSeries = normalizeDataSeries(newData);
|
||||
const latestExportContextRef = useRef({
|
||||
normalizedSeries,
|
||||
config,
|
||||
unit,
|
||||
renderValue,
|
||||
});
|
||||
const [visibleSeries, setVisibleSeries] = useState<boolean[]>(
|
||||
new Array(normalizedSeries.length).fill(true),
|
||||
);
|
||||
|
||||
// Keep latest export context in a ref so export callbacks always read fresh data.
|
||||
useEffect(() => {
|
||||
latestExportContextRef.current = {
|
||||
normalizedSeries,
|
||||
config,
|
||||
unit,
|
||||
renderValue,
|
||||
};
|
||||
}, [normalizedSeries, config, unit, renderValue]);
|
||||
|
||||
// Enhanced configuration with visible series lines
|
||||
const enhancedConfig = React.useMemo(() => {
|
||||
return mergeConfigWithVisibleLines(config, newData, visibleSeries);
|
||||
@@ -206,35 +223,27 @@ export function BigGraph({
|
||||
// Register export functionality for the graph
|
||||
useEffect(() => {
|
||||
if (onRegisterForExport) {
|
||||
// Unregister any previous registrations for this graph
|
||||
if (onUnregisterFromExport) {
|
||||
// Clear any existing registrations for this graph's series
|
||||
normalizedSeries.forEach((_, index) => {
|
||||
const seriesId = `${graphId}-series-${index}`;
|
||||
onUnregisterFromExport(seriesId);
|
||||
});
|
||||
}
|
||||
|
||||
// Register each series as a separate export entry
|
||||
normalizedSeries.forEach((series, index) => {
|
||||
if (!series.newData) return;
|
||||
|
||||
normalizedSeries.forEach((_, index) => {
|
||||
const seriesId = `${graphId}-series-${index}`;
|
||||
const seriesTitle = series.title || `Series ${index + 1}`;
|
||||
|
||||
const getSeriesExportData = (): GraphExportData | null => {
|
||||
const current = latestExportContextRef.current;
|
||||
const series = current.normalizedSeries[index];
|
||||
if (!series?.newData) return null;
|
||||
|
||||
// Create export data for this specific series only
|
||||
const seriesLines = series.lines || [];
|
||||
const exportConfig = {
|
||||
...config,
|
||||
title: config.title, // Keep original graph title
|
||||
lines: [...(config.lines || []), ...seriesLines],
|
||||
...current.config,
|
||||
title: current.config.title, // Keep original graph title
|
||||
lines: [...(current.config.lines || []), ...seriesLines],
|
||||
};
|
||||
|
||||
// Create a single-series data structure for this series
|
||||
const singleSeriesData: SeriesData = {
|
||||
newData: series.newData,
|
||||
title: seriesTitle,
|
||||
title: series.title || `Series ${index + 1}`,
|
||||
color: series.color,
|
||||
lines: series.lines,
|
||||
};
|
||||
@@ -242,8 +251,8 @@ export function BigGraph({
|
||||
return {
|
||||
config: exportConfig,
|
||||
data: singleSeriesData, // Return ONLY this specific series
|
||||
unit,
|
||||
renderValue,
|
||||
unit: current.unit,
|
||||
renderValue: current.renderValue,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -264,19 +273,7 @@ export function BigGraph({
|
||||
onRegisterForExport,
|
||||
onUnregisterFromExport,
|
||||
graphId,
|
||||
config,
|
||||
normalizedSeries.length,
|
||||
unit,
|
||||
renderValue,
|
||||
// Add dependencies for series data
|
||||
JSON.stringify(
|
||||
normalizedSeries.map((s) => ({
|
||||
title: s.title,
|
||||
color: s.color,
|
||||
hasData: !!s.newData,
|
||||
linesCount: s.lines?.length || 0,
|
||||
})),
|
||||
),
|
||||
]);
|
||||
|
||||
// Updates Y-axis scale dynamically based on visible data
|
||||
@@ -595,10 +592,18 @@ export function BigGraph({
|
||||
|
||||
<div className="flex-1 overflow-hidden rounded-b-3xl pt-4">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`h-full w-full overflow-hidden transition-opacity duration-100`}
|
||||
className={`relative h-full w-full overflow-hidden transition-opacity duration-100`}
|
||||
style={{ backgroundColor: colors.background }}
|
||||
/>
|
||||
>
|
||||
<div ref={containerRef} className="h-full w-full overflow-hidden" />
|
||||
<TargetDashOverlay
|
||||
uplotRef={uplotRef}
|
||||
newData={newData}
|
||||
config={enhancedConfig}
|
||||
selectedTimeWindow={selectedTimeWindow}
|
||||
isLiveMode={isLiveMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
import React, { useEffect, useMemo, useState, useId } from "react";
|
||||
import uPlot from "uplot";
|
||||
import { BigGraphProps, GraphConfig } from "./types";
|
||||
import { getAllTimeSeries } from "./createChart";
|
||||
|
||||
type OverlayLine = {
|
||||
key: string;
|
||||
color: string;
|
||||
width: number;
|
||||
dash: number[];
|
||||
d: string;
|
||||
};
|
||||
|
||||
type ClipRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
function findFirstGe(arr: number[], value: number): number {
|
||||
let lo = 0;
|
||||
let hi = arr.length - 1;
|
||||
let ans = arr.length;
|
||||
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (arr[mid] >= value) {
|
||||
ans = mid;
|
||||
hi = mid - 1;
|
||||
} else {
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return ans;
|
||||
}
|
||||
|
||||
function findLastLe(arr: number[], value: number): number {
|
||||
let lo = 0;
|
||||
let hi = arr.length - 1;
|
||||
let ans = -1;
|
||||
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (arr[mid] <= value) {
|
||||
ans = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return ans;
|
||||
}
|
||||
|
||||
function buildSteppedPath(
|
||||
u: uPlot,
|
||||
xData: number[],
|
||||
yData: Array<number | null>,
|
||||
): string {
|
||||
const xMin = u.scales.x?.min;
|
||||
const xMax = u.scales.x?.max;
|
||||
|
||||
if (xData.length < 2 || xMin === undefined || xMax === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const firstVisibleIdx = Math.max(0, findFirstGe(xData, xMin) - 1);
|
||||
const lastVisibleIdx = Math.min(
|
||||
xData.length - 1,
|
||||
findLastLe(xData, xMax) + 1,
|
||||
);
|
||||
|
||||
if (lastVisibleIdx < firstVisibleIdx) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
let started = false;
|
||||
let prevX = 0;
|
||||
let prevY = 0;
|
||||
|
||||
for (let i = firstVisibleIdx; i <= lastVisibleIdx; i++) {
|
||||
const value = yData[i];
|
||||
if (value === null || value === undefined) continue;
|
||||
|
||||
const x = u.valToPos(xData[i], "x", true);
|
||||
const y = u.valToPos(value, "y", true);
|
||||
|
||||
if (!started) {
|
||||
parts.push(`M ${x} ${y}`);
|
||||
started = true;
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (x !== prevX) {
|
||||
parts.push(`L ${x} ${prevY}`);
|
||||
}
|
||||
if (y !== prevY) {
|
||||
parts.push(`L ${x} ${y}`);
|
||||
}
|
||||
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function buildFlatLatestValuePath(
|
||||
u: uPlot,
|
||||
yData: Array<number | null>,
|
||||
): string {
|
||||
let latestValue: number | null = null;
|
||||
|
||||
for (let i = yData.length - 1; i >= 0; i--) {
|
||||
const value = yData[i];
|
||||
if (value !== null && value !== undefined) {
|
||||
latestValue = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (latestValue === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const y = u.valToPos(latestValue, "y", true);
|
||||
const left = u.bbox.left;
|
||||
const right = u.bbox.left + u.bbox.width;
|
||||
|
||||
return `M ${left} ${y} L ${right} ${y}`;
|
||||
}
|
||||
|
||||
function getHistoricalDashTargets(
|
||||
data: BigGraphProps["newData"],
|
||||
config: GraphConfig,
|
||||
): Array<{
|
||||
dataIndex: number;
|
||||
dash: number[];
|
||||
color: string;
|
||||
width: number;
|
||||
}> {
|
||||
const allOriginalSeries = getAllTimeSeries(data);
|
||||
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
|
||||
let visibleLineIndex = 0;
|
||||
|
||||
const targets: Array<{
|
||||
dataIndex: number;
|
||||
dash: number[];
|
||||
color: string;
|
||||
width: number;
|
||||
}> = [];
|
||||
|
||||
config.lines?.forEach((line) => {
|
||||
if (line.show === false) return;
|
||||
|
||||
const dash = line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
|
||||
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
|
||||
|
||||
if (isHistoricalDashedTarget) {
|
||||
targets.push({
|
||||
dataIndex: firstConfigLineDataIndex + visibleLineIndex,
|
||||
dash: dash!,
|
||||
color: line.color,
|
||||
width: line.width ?? 1,
|
||||
});
|
||||
}
|
||||
|
||||
visibleLineIndex++;
|
||||
});
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
function areDashArraysEqual(a: number[], b: number[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function areOverlayLinesEqual(a: OverlayLine[], b: OverlayLine[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (
|
||||
a[i].key !== b[i].key ||
|
||||
a[i].color !== b[i].color ||
|
||||
a[i].width !== b[i].width ||
|
||||
a[i].d !== b[i].d ||
|
||||
!areDashArraysEqual(a[i].dash, b[i].dash)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isSameClipRect(a: ClipRect | null, b: ClipRect): boolean {
|
||||
return (
|
||||
!!a &&
|
||||
a.x === b.x &&
|
||||
a.y === b.y &&
|
||||
a.width === b.width &&
|
||||
a.height === b.height
|
||||
);
|
||||
}
|
||||
|
||||
export function TargetDashOverlay({
|
||||
uplotRef,
|
||||
newData,
|
||||
config,
|
||||
selectedTimeWindow,
|
||||
isLiveMode,
|
||||
}: {
|
||||
uplotRef: React.RefObject<uPlot | null>;
|
||||
newData: BigGraphProps["newData"];
|
||||
config: GraphConfig;
|
||||
selectedTimeWindow: number | "all";
|
||||
isLiveMode: boolean;
|
||||
}) {
|
||||
const targetMeta = useMemo(
|
||||
() => getHistoricalDashTargets(newData, config),
|
||||
[newData, config],
|
||||
);
|
||||
const clipPathId = useId().replace(/:/g, "");
|
||||
|
||||
const [lines, setLines] = useState<OverlayLine[]>([]);
|
||||
const [clipRect, setClipRect] = useState<ClipRect | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetMeta.length === 0) {
|
||||
setLines([]);
|
||||
setClipRect(null);
|
||||
}
|
||||
}, [targetMeta]);
|
||||
|
||||
useEffect(() => {
|
||||
let rafId: number | null = null;
|
||||
let bootstrapRafId: number | null = null;
|
||||
let recalcScheduled = false;
|
||||
let hooksAttached = false;
|
||||
const removeHookFns: Array<() => void> = [];
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const recalc = () => {
|
||||
recalcScheduled = false;
|
||||
const u = uplotRef.current;
|
||||
if (!u || targetMeta.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const xData = u.data[0] as number[] | undefined;
|
||||
if (!xData || xData.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextLines = targetMeta
|
||||
.map((meta, index) => {
|
||||
const yData = u.data[meta.dataIndex] as
|
||||
| Array<number | null>
|
||||
| undefined;
|
||||
if (!yData || yData.length < 2) return null;
|
||||
|
||||
const useFlatShortWindowPath =
|
||||
isLiveMode &&
|
||||
selectedTimeWindow !== "all" &&
|
||||
selectedTimeWindow <= 60_000;
|
||||
|
||||
const d = useFlatShortWindowPath
|
||||
? buildFlatLatestValuePath(u, yData)
|
||||
: buildSteppedPath(u, xData, yData);
|
||||
if (!d) return null;
|
||||
|
||||
return {
|
||||
key: `${meta.dataIndex}-${index}`,
|
||||
color: meta.color,
|
||||
width: meta.width,
|
||||
dash: meta.dash,
|
||||
d,
|
||||
} as OverlayLine;
|
||||
})
|
||||
.filter((line): line is OverlayLine => !!line);
|
||||
|
||||
if (nextLines.length > 0) {
|
||||
setLines((prev) =>
|
||||
areOverlayLinesEqual(prev, nextLines) ? prev : nextLines,
|
||||
);
|
||||
const nextClipRect: ClipRect = {
|
||||
x: u.bbox.left,
|
||||
y: u.bbox.top,
|
||||
width: u.bbox.width,
|
||||
height: u.bbox.height,
|
||||
};
|
||||
setClipRect((prev) =>
|
||||
isSameClipRect(prev, nextClipRect) ? prev : nextClipRect,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleRecalc = () => {
|
||||
if (recalcScheduled) return;
|
||||
recalcScheduled = true;
|
||||
rafId = window.requestAnimationFrame(recalc);
|
||||
};
|
||||
|
||||
scheduleRecalc();
|
||||
|
||||
if (targetMeta.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addHook = (u: any, hookName: string, fn: () => void) => {
|
||||
const hooks = u.hooks?.[hookName];
|
||||
if (!Array.isArray(hooks)) return;
|
||||
hooks.push(fn);
|
||||
removeHookFns.push(() => {
|
||||
const idx = hooks.indexOf(fn);
|
||||
if (idx >= 0) hooks.splice(idx, 1);
|
||||
});
|
||||
};
|
||||
|
||||
const onWindowResize = () => scheduleRecalc();
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
|
||||
const attachHooks = (): boolean => {
|
||||
if (hooksAttached) return true;
|
||||
const u = uplotRef.current as any;
|
||||
if (!u) return false;
|
||||
|
||||
addHook(u, "draw", scheduleRecalc);
|
||||
addHook(u, "setScale", scheduleRecalc);
|
||||
addHook(u, "setData", scheduleRecalc);
|
||||
|
||||
const rootElement = u.root as HTMLElement | undefined;
|
||||
if (rootElement && typeof ResizeObserver !== "undefined") {
|
||||
resizeObserver = new ResizeObserver(() => scheduleRecalc());
|
||||
resizeObserver.observe(rootElement);
|
||||
}
|
||||
|
||||
hooksAttached = true;
|
||||
scheduleRecalc();
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!attachHooks()) {
|
||||
const tryAttach = () => {
|
||||
if (attachHooks()) return;
|
||||
bootstrapRafId = window.requestAnimationFrame(tryAttach);
|
||||
};
|
||||
bootstrapRafId = window.requestAnimationFrame(tryAttach);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rafId !== null) {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
}
|
||||
if (bootstrapRafId !== null) {
|
||||
window.cancelAnimationFrame(bootstrapRafId);
|
||||
}
|
||||
removeHookFns.forEach((fn) => fn());
|
||||
window.removeEventListener("resize", onWindowResize);
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, [uplotRef, targetMeta, isLiveMode, selectedTimeWindow]);
|
||||
|
||||
if (lines.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
className="pointer-events-none absolute inset-0 h-full w-full"
|
||||
aria-hidden
|
||||
>
|
||||
{clipRect && (
|
||||
<defs>
|
||||
<clipPath id={clipPathId}>
|
||||
<rect
|
||||
x={clipRect.x}
|
||||
y={clipRect.y}
|
||||
width={clipRect.width}
|
||||
height={clipRect.height}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
)}
|
||||
|
||||
<g clipPath={clipRect ? `url(#${clipPathId})` : undefined}>
|
||||
{lines.map((line) => {
|
||||
return (
|
||||
<path
|
||||
key={line.key}
|
||||
d={line.d}
|
||||
fill="none"
|
||||
stroke={line.color}
|
||||
strokeWidth={line.width}
|
||||
strokeLinecap="butt"
|
||||
strokeDasharray={line.dash.join(" ")}
|
||||
strokeDashoffset={0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -12,15 +12,6 @@ import {
|
||||
import { BigGraphProps, CreateChartParams } from "./types";
|
||||
import { normalizeDataSeries } from "./animation";
|
||||
|
||||
type CustomDashedTargetLine = {
|
||||
dataIndex: number;
|
||||
dash: number[];
|
||||
color: string;
|
||||
width: number;
|
||||
};
|
||||
|
||||
const DASH_FLOW_PX_PER_SEC = 180;
|
||||
|
||||
export function createChart({
|
||||
containerRef,
|
||||
uplotRef,
|
||||
@@ -161,8 +152,6 @@ export function createChart({
|
||||
|
||||
// Build series configuration for ALL series (but control visibility)
|
||||
const seriesConfig: uPlot.Series[] = [{ label: "Time" }];
|
||||
const customDashedTargetLines: CustomDashedTargetLine[] = [];
|
||||
const dashFlowStartTime = performance.now();
|
||||
|
||||
// Add ALL data series with visibility control
|
||||
const defaultColors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"];
|
||||
@@ -188,35 +177,21 @@ export function createChart({
|
||||
});
|
||||
|
||||
// Add config lines
|
||||
let visibleLineIndex = 0;
|
||||
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
|
||||
config.lines?.forEach((line) => {
|
||||
if (line.show !== false) {
|
||||
const dash =
|
||||
line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
|
||||
const lineDataIndex = firstConfigLineDataIndex + visibleLineIndex;
|
||||
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
|
||||
|
||||
if (isHistoricalDashedTarget) {
|
||||
customDashedTargetLines.push({
|
||||
dataIndex: lineDataIndex,
|
||||
dash: dash!,
|
||||
color: line.color,
|
||||
width: line.width ?? 1,
|
||||
});
|
||||
}
|
||||
|
||||
seriesConfig.push({
|
||||
label: line.label,
|
||||
// Historical dashed targets are rendered manually in draw hook to
|
||||
// avoid dash shimmer while the live x-axis slides.
|
||||
// Historical dashed targets are rendered in an SVG overlay.
|
||||
stroke: isHistoricalDashedTarget ? "rgba(0,0,0,0)" : line.color,
|
||||
width: line.width ?? 1,
|
||||
dash: isHistoricalDashedTarget ? undefined : dash,
|
||||
show: true,
|
||||
points: { show: false },
|
||||
});
|
||||
visibleLineIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -346,119 +321,6 @@ export function createChart({
|
||||
}
|
||||
},
|
||||
],
|
||||
draw: [
|
||||
(u) => {
|
||||
if (customDashedTargetLines.length === 0) return;
|
||||
const xData = u.data[0] as number[] | undefined;
|
||||
if (!xData || xData.length < 2) return;
|
||||
|
||||
const xMin = u.scales.x?.min;
|
||||
const xMax = u.scales.x?.max;
|
||||
if (xMin === undefined || xMax === undefined) return;
|
||||
|
||||
const ctx = u.ctx;
|
||||
const left = u.bbox.left;
|
||||
const top = u.bbox.top;
|
||||
const snap = (v: number) => Math.round(v) + 0.5;
|
||||
const findFirstGe = (arr: number[], value: number): number => {
|
||||
let lo = 0;
|
||||
let hi = arr.length - 1;
|
||||
let ans = arr.length;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (arr[mid] >= value) {
|
||||
ans = mid;
|
||||
hi = mid - 1;
|
||||
} else {
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
};
|
||||
const findLastLe = (arr: number[], value: number): number => {
|
||||
let lo = 0;
|
||||
let hi = arr.length - 1;
|
||||
let ans = -1;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (arr[mid] <= value) {
|
||||
ans = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
};
|
||||
const firstVisibleIdx = Math.max(0, findFirstGe(xData, xMin) - 1);
|
||||
const lastVisibleIdx = Math.min(
|
||||
xData.length - 1,
|
||||
findLastLe(xData, xMax) + 1,
|
||||
);
|
||||
if (lastVisibleIdx < firstVisibleIdx) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(left, top, u.bbox.width, u.bbox.height);
|
||||
ctx.clip();
|
||||
ctx.lineCap = "butt";
|
||||
|
||||
customDashedTargetLines.forEach(
|
||||
({ dataIndex, dash, color, width }) => {
|
||||
const yData = u.data[dataIndex] as
|
||||
| Array<number | null>
|
||||
| undefined;
|
||||
if (!yData || yData.length < 2) return;
|
||||
|
||||
const dashPeriod = dash.reduce((acc, curr) => acc + curr, 0);
|
||||
ctx.setLineDash(dash);
|
||||
const elapsedMs = performance.now() - dashFlowStartTime;
|
||||
const dashTravelPx = (elapsedMs * DASH_FLOW_PX_PER_SEC) / 1000;
|
||||
ctx.lineDashOffset =
|
||||
dashPeriod > 0 ? -(dashTravelPx % dashPeriod) : 0;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.beginPath();
|
||||
|
||||
let started = false;
|
||||
let prevX = 0;
|
||||
let prevY = 0;
|
||||
|
||||
for (let i = firstVisibleIdx; i <= lastVisibleIdx; i++) {
|
||||
const value = yData[i];
|
||||
if (value === null || value === undefined) continue;
|
||||
|
||||
const x = snap(u.valToPos(xData[i], "x", true));
|
||||
const y = snap(u.valToPos(value, "y", true));
|
||||
|
||||
if (!started) {
|
||||
ctx.moveTo(x, y);
|
||||
started = true;
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (x !== prevX) {
|
||||
ctx.lineTo(x, prevY);
|
||||
}
|
||||
if (y !== prevY) {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
}
|
||||
|
||||
if (started) {
|
||||
ctx.stroke();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
],
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
@@ -660,25 +522,12 @@ export function createChart({
|
||||
|
||||
const cleanup = attachEventHandlers(containerRef.current, handlers);
|
||||
|
||||
let dashFlowAnimationFrame: number | null = null;
|
||||
if (customDashedTargetLines.length > 0) {
|
||||
const animateDashFlow = () => {
|
||||
if (!uplotRef.current) return;
|
||||
uplotRef.current.redraw();
|
||||
dashFlowAnimationFrame = requestAnimationFrame(animateDashFlow);
|
||||
};
|
||||
dashFlowAnimationFrame = requestAnimationFrame(animateDashFlow);
|
||||
}
|
||||
|
||||
animationRefs.lastRenderedData.current = {
|
||||
timestamps: [...timestamps],
|
||||
values: [...primaryValues],
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (dashFlowAnimationFrame !== null) {
|
||||
cancelAnimationFrame(dashFlowAnimationFrame);
|
||||
}
|
||||
cleanup();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { TimeSeries, seriesToUPlotData } from "@/lib/timeseries";
|
||||
import { renderUnitSymbol, Unit } from "@/control/units";
|
||||
import { GraphConfig, SeriesData, GraphLine } from "./types";
|
||||
|
||||
export type GraphExportData = {
|
||||
config: GraphConfig;
|
||||
data: SeriesData; // Always a single series
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
};
|
||||
|
||||
export function exportGraphsToExcel(
|
||||
graphDataMap: Map<string, () => GraphExportData | null>,
|
||||
groupId: string,
|
||||
): void {
|
||||
try {
|
||||
// Filter out invalid series IDs (those without "-series-")
|
||||
const filteredMap = new Map<string, () => GraphExportData | null>();
|
||||
graphDataMap.forEach((getDataFn, seriesId) => {
|
||||
if (seriesId.includes("-series-")) {
|
||||
filteredMap.set(seriesId, getDataFn);
|
||||
}
|
||||
});
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const exportTimestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:.]/g, "-")
|
||||
.slice(0, 19);
|
||||
|
||||
const usedSheetNames = new Set<string>(); // Track unique sheet names
|
||||
let processedCount = 0;
|
||||
|
||||
// Process each valid series
|
||||
filteredMap.forEach((getDataFn, seriesId) => {
|
||||
const exportData = getDataFn();
|
||||
if (!exportData?.data?.newData) {
|
||||
console.warn(`No data for series: ${seriesId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const series = exportData.data;
|
||||
const seriesTitle = series.title || `Series ${processedCount + 1}`;
|
||||
|
||||
if (!series.newData) {
|
||||
console.warn(`Series ${seriesTitle} has null data`);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetLines: GraphLine[] = [
|
||||
...(exportData.config.lines || []),
|
||||
...(series.lines || []),
|
||||
];
|
||||
|
||||
const graphLineData = {
|
||||
graphTitle: exportData.config.title,
|
||||
lineTitle: seriesTitle,
|
||||
series: series.newData,
|
||||
color: series.color,
|
||||
unit: exportData.unit,
|
||||
renderValue: exportData.renderValue,
|
||||
config: exportData.config,
|
||||
targetLines: targetLines,
|
||||
};
|
||||
|
||||
// Create and append statistics sheet
|
||||
const statsData = createGraphLineStatsSheet(graphLineData);
|
||||
const statsWorksheet = XLSX.utils.aoa_to_sheet(statsData);
|
||||
const statsSheetName = generateUniqueSheetName(
|
||||
`${seriesTitle} Stats`,
|
||||
usedSheetNames,
|
||||
);
|
||||
XLSX.utils.book_append_sheet(workbook, statsWorksheet, statsSheetName);
|
||||
|
||||
// Create and append data sheet
|
||||
const dataRows = createGraphLineDataSheet(graphLineData);
|
||||
if (dataRows.length > 0) {
|
||||
const dataWorksheet = XLSX.utils.json_to_sheet(dataRows);
|
||||
const dataSheetName = generateUniqueSheetName(
|
||||
`${seriesTitle} Data`,
|
||||
usedSheetNames,
|
||||
);
|
||||
XLSX.utils.book_append_sheet(workbook, dataWorksheet, dataSheetName);
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
});
|
||||
|
||||
if (processedCount === 0) {
|
||||
alert("No data available to export from any graphs in this group");
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = `${groupId.toLowerCase().replace(/\s+/g, "_")}_export_${exportTimestamp}.xlsx`;
|
||||
XLSX.writeFile(workbook, filename);
|
||||
} catch (error) {
|
||||
alert(
|
||||
`Error exporting data to Excel: ${error instanceof Error ? error.message : "Unknown error"}. Please try again.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate statistics sheet for a graph line
|
||||
function createGraphLineStatsSheet(graphLine: {
|
||||
graphTitle: string;
|
||||
lineTitle: string;
|
||||
series: TimeSeries;
|
||||
color?: string;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
config: GraphConfig;
|
||||
targetLines: GraphLine[];
|
||||
}): any[][] {
|
||||
const [timestamps, values] = seriesToUPlotData(graphLine.series.long);
|
||||
const unitSymbol = renderUnitSymbol(graphLine.unit) || "";
|
||||
|
||||
const statsData = [
|
||||
[`Graph Line Statistics: ${graphLine.lineTitle}`, ""],
|
||||
["Graph", graphLine.graphTitle],
|
||||
["Line Name", graphLine.lineTitle],
|
||||
["Line Color", graphLine.color || "Default"],
|
||||
["Generated", new Date()],
|
||||
["", ""],
|
||||
["Data Points Information", ""],
|
||||
["Total Data Points", timestamps.length.toString()],
|
||||
];
|
||||
|
||||
if (timestamps.length > 0) {
|
||||
statsData.push(["Time Range Start", new Date(timestamps[0])]);
|
||||
statsData.push([
|
||||
"Time Range End",
|
||||
new Date(timestamps[timestamps.length - 1]),
|
||||
]);
|
||||
|
||||
const duration = timestamps[timestamps.length - 1] - timestamps[0];
|
||||
const durationHours = (duration / (1000 * 60 * 60)).toFixed(2);
|
||||
statsData.push(["Duration (hours)", durationHours]);
|
||||
|
||||
if (values.length > 0) {
|
||||
const minValue = Math.min(...values);
|
||||
const maxValue = Math.max(...values);
|
||||
const avgValue = values.reduce((a, b) => a + b, 0) / values.length;
|
||||
const stdDev = Math.sqrt(
|
||||
values.reduce((sum, val) => sum + Math.pow(val - avgValue, 2), 0) /
|
||||
values.length,
|
||||
);
|
||||
|
||||
statsData.push(["", ""], ["Value Statistics", ""]);
|
||||
statsData.push([
|
||||
`Minimum Value (${unitSymbol})`,
|
||||
graphLine.renderValue
|
||||
? graphLine.renderValue(minValue)
|
||||
: minValue.toFixed(3),
|
||||
]);
|
||||
statsData.push([
|
||||
`Maximum Value (${unitSymbol})`,
|
||||
graphLine.renderValue
|
||||
? graphLine.renderValue(maxValue)
|
||||
: maxValue.toFixed(3),
|
||||
]);
|
||||
statsData.push([
|
||||
`Average Value (${unitSymbol})`,
|
||||
graphLine.renderValue
|
||||
? graphLine.renderValue(avgValue)
|
||||
: avgValue.toFixed(3),
|
||||
]);
|
||||
statsData.push([
|
||||
`Standard Deviation (${unitSymbol})`,
|
||||
graphLine.renderValue
|
||||
? graphLine.renderValue(stdDev)
|
||||
: stdDev.toFixed(3),
|
||||
]);
|
||||
statsData.push([
|
||||
`Range (${unitSymbol})`,
|
||||
graphLine.renderValue
|
||||
? graphLine.renderValue(maxValue - minValue)
|
||||
: (maxValue - minValue).toFixed(3),
|
||||
]);
|
||||
|
||||
// Percentiles
|
||||
const sortedValues = [...values].sort((a, b) => a - b);
|
||||
const p25 = sortedValues[Math.floor(sortedValues.length * 0.25)];
|
||||
const p50 = sortedValues[Math.floor(sortedValues.length * 0.5)];
|
||||
const p75 = sortedValues[Math.floor(sortedValues.length * 0.75)];
|
||||
|
||||
statsData.push(["", ""], ["Percentiles", ""]);
|
||||
statsData.push([
|
||||
`25th Percentile (${unitSymbol})`,
|
||||
graphLine.renderValue ? graphLine.renderValue(p25) : p25.toFixed(3),
|
||||
]);
|
||||
statsData.push([
|
||||
`50th Percentile/Median (${unitSymbol})`,
|
||||
graphLine.renderValue ? graphLine.renderValue(p50) : p50.toFixed(3),
|
||||
]);
|
||||
statsData.push([
|
||||
`75th Percentile (${unitSymbol})`,
|
||||
graphLine.renderValue ? graphLine.renderValue(p75) : p75.toFixed(3),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add target line information
|
||||
if (graphLine.targetLines.length > 0) {
|
||||
statsData.push(["", ""], ["Target Lines", ""]);
|
||||
graphLine.targetLines.forEach((line, index) => {
|
||||
statsData.push([
|
||||
`Target Line ${index + 1}`,
|
||||
line.label || `Line ${line.value}`,
|
||||
]);
|
||||
statsData.push([
|
||||
` Value (${unitSymbol})`,
|
||||
graphLine.renderValue
|
||||
? graphLine.renderValue(line.value)
|
||||
: line.value.toFixed(3),
|
||||
]);
|
||||
statsData.push([` Type`, line.type || "reference"]);
|
||||
statsData.push([` Color`, line.color || "default"]);
|
||||
statsData.push([` Show`, line.show !== false ? "Yes" : "No"]);
|
||||
|
||||
if (line.type === "threshold" && values.length > 0) {
|
||||
const withinThreshold = values.filter(
|
||||
(val) => Math.abs(val - line.value) <= line.value * 0.05,
|
||||
).length;
|
||||
const percentageWithin = (
|
||||
(withinThreshold / values.length) *
|
||||
100
|
||||
).toFixed(1);
|
||||
|
||||
statsData.push([
|
||||
` Points Within Threshold (5%)`,
|
||||
`${withinThreshold} (${percentageWithin}%)`,
|
||||
]);
|
||||
|
||||
const differences = values.map((val) => Math.abs(val - line.value));
|
||||
const minDifference = Math.min(...differences);
|
||||
const maxDifference = Math.max(...differences);
|
||||
|
||||
statsData.push([
|
||||
` Closest Approach (${unitSymbol})`,
|
||||
graphLine.renderValue
|
||||
? graphLine.renderValue(minDifference)
|
||||
: minDifference.toFixed(3),
|
||||
]);
|
||||
statsData.push([
|
||||
` Furthest Distance (${unitSymbol})`,
|
||||
graphLine.renderValue
|
||||
? graphLine.renderValue(maxDifference)
|
||||
: maxDifference.toFixed(3),
|
||||
]);
|
||||
}
|
||||
|
||||
if (index < graphLine.targetLines.length - 1) {
|
||||
statsData.push([""]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
statsData.push(["", ""], ["Target Lines", ""]);
|
||||
statsData.push(["No target lines defined", ""]);
|
||||
}
|
||||
|
||||
return statsData;
|
||||
}
|
||||
|
||||
// Generate data sheet for a graph line
|
||||
function createGraphLineDataSheet(graphLine: {
|
||||
graphTitle: string;
|
||||
lineTitle: string;
|
||||
series: TimeSeries;
|
||||
color?: string;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
config: GraphConfig;
|
||||
targetLines: GraphLine[];
|
||||
}): any[] {
|
||||
const [timestamps, values] = seriesToUPlotData(graphLine.series.long);
|
||||
|
||||
if (timestamps.length === 0) return [];
|
||||
|
||||
const unitSymbol = renderUnitSymbol(graphLine.unit) || "";
|
||||
|
||||
return timestamps.map((timestamp, index) => {
|
||||
const value = values[index];
|
||||
return {
|
||||
Timestamp: new Date(timestamp),
|
||||
[`Value (${unitSymbol})`]: graphLine.renderValue
|
||||
? graphLine.renderValue(value)
|
||||
: value?.toFixed(3) || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure sheet names are unique and valid for Excel
|
||||
function generateUniqueSheetName(
|
||||
name: string,
|
||||
usedSheetNames: Set<string>,
|
||||
): string {
|
||||
let baseSheetName = name
|
||||
.replace(/[\\/?*$:[\]]/g, "_") // Remove invalid characters
|
||||
.substring(0, 31); // Excel sheet name limit
|
||||
|
||||
if (!baseSheetName || baseSheetName.trim().length === 0) {
|
||||
baseSheetName = "Sheet";
|
||||
}
|
||||
|
||||
let sheetName = baseSheetName;
|
||||
let counter = 1;
|
||||
|
||||
while (usedSheetNames.has(sheetName)) {
|
||||
const suffix = `_${counter}`;
|
||||
const maxBaseLength = 31 - suffix.length;
|
||||
sheetName = `${baseSheetName.substring(0, maxBaseLength)}${suffix}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
usedSheetNames.add(sheetName);
|
||||
return sheetName;
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { LogEntry } from "@/stores/logsStore";
|
||||
import { ExportConfig, IExportConfig } from "./excelExportConfig";
|
||||
import {
|
||||
IValueFormatter,
|
||||
ValueFormatter,
|
||||
TimestampConverter,
|
||||
ArrayUtils,
|
||||
} from "./excelFormatters";
|
||||
import { MetadataProviderFactory } from "./excelMetadata";
|
||||
import { ChartAxisCalculator, ExcelCellSanitizer } from "./excelUtils";
|
||||
import { CommentManager } from "./excelCommentManager";
|
||||
import { CombinedSheetData, PidData } from "./excelExportTypes";
|
||||
|
||||
/**
|
||||
* Creates the combined analysis sheet with all series data
|
||||
*/
|
||||
export class AnalysisSheetBuilder {
|
||||
private config: IExportConfig;
|
||||
private formatter: IValueFormatter;
|
||||
|
||||
constructor(
|
||||
private allSheetData: CombinedSheetData[],
|
||||
private groupId: string,
|
||||
private logs: LogEntry[],
|
||||
private pidData?: PidData,
|
||||
config?: IExportConfig,
|
||||
formatter?: IValueFormatter,
|
||||
) {
|
||||
this.config = config || new ExportConfig();
|
||||
this.formatter = formatter || new ValueFormatter();
|
||||
}
|
||||
|
||||
async build(): Promise<XLSX.WorkSheet> {
|
||||
// Get sorted timestamps
|
||||
const sortedTimestamps = this.getSortedTimestamps();
|
||||
const startTime = sortedTimestamps[0];
|
||||
const endTime = sortedTimestamps[sortedTimestamps.length - 1];
|
||||
|
||||
// Filter relevant comments
|
||||
const relevantComments = CommentManager.filterRelevant(
|
||||
this.logs,
|
||||
startTime,
|
||||
endTime,
|
||||
);
|
||||
|
||||
// Build data by timestamp map
|
||||
const dataByTimestamp = this.buildDataByTimestampMap();
|
||||
|
||||
// Build columns
|
||||
const columns = this.buildColumns();
|
||||
|
||||
// Create sheet data array
|
||||
const sheetData: any[][] = [];
|
||||
|
||||
// Add title row
|
||||
const timeRangeTitle = this.formatter.formatTimeRange(startTime, endTime);
|
||||
sheetData.push(
|
||||
ArrayUtils.createRow(
|
||||
[`${this.groupId} - ${timeRangeTitle}`],
|
||||
columns.length,
|
||||
),
|
||||
);
|
||||
sheetData.push(ArrayUtils.createEmptyArray(columns.length));
|
||||
|
||||
// Add target values row if applicable
|
||||
this.addTargetValuesRow(sheetData, columns.length);
|
||||
|
||||
// Add header row
|
||||
sheetData.push(columns);
|
||||
|
||||
// Add data rows
|
||||
const dataStartRow = sheetData.length;
|
||||
let maxSeconds = 0;
|
||||
|
||||
sortedTimestamps.forEach((timestamp) => {
|
||||
const row = this.buildDataRow(
|
||||
timestamp,
|
||||
startTime,
|
||||
dataByTimestamp,
|
||||
relevantComments,
|
||||
);
|
||||
|
||||
const secondsFromStart = TimestampConverter.toSecondsFromStart(
|
||||
timestamp,
|
||||
startTime,
|
||||
);
|
||||
maxSeconds = Math.max(maxSeconds, secondsFromStart);
|
||||
|
||||
sheetData.push(row);
|
||||
});
|
||||
|
||||
// Add metadata
|
||||
await this.addMetadata(sheetData, columns.length, relevantComments);
|
||||
|
||||
// Add chart instructions
|
||||
this.addChartInstructions(
|
||||
sheetData,
|
||||
columns.length,
|
||||
dataStartRow,
|
||||
maxSeconds,
|
||||
timeRangeTitle,
|
||||
);
|
||||
|
||||
const sanitizedSheetData = sheetData.map((row) =>
|
||||
ExcelCellSanitizer.sanitizeRow(row),
|
||||
);
|
||||
|
||||
// Convert to worksheet
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(sanitizedSheetData);
|
||||
|
||||
// Configure worksheet
|
||||
this.configureWorksheet(worksheet, columns.length);
|
||||
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
private getSortedTimestamps(): number[] {
|
||||
const allTimestamps = new Set<number>();
|
||||
this.allSheetData.forEach((data) => {
|
||||
data.timestamps.forEach((ts) => allTimestamps.add(ts));
|
||||
});
|
||||
return Array.from(allTimestamps).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
private buildDataByTimestampMap(): Map<number, Map<string, number>> {
|
||||
const dataByTimestamp = new Map<number, Map<string, number>>();
|
||||
|
||||
this.allSheetData.forEach((sheetData) => {
|
||||
sheetData.timestamps.forEach((ts, idx) => {
|
||||
if (!dataByTimestamp.has(ts)) {
|
||||
dataByTimestamp.set(ts, new Map());
|
||||
}
|
||||
dataByTimestamp
|
||||
.get(ts)!
|
||||
.set(sheetData.sheetName, sheetData.values[idx]);
|
||||
});
|
||||
});
|
||||
|
||||
return dataByTimestamp;
|
||||
}
|
||||
|
||||
private buildColumns(): string[] {
|
||||
const columns: string[] = ["Timestamp"];
|
||||
const availableColumns = this.allSheetData.map((d) => d.sheetName);
|
||||
columns.push(...availableColumns);
|
||||
columns.push("User Comments");
|
||||
return columns;
|
||||
}
|
||||
|
||||
private addTargetValuesRow(sheetData: any[][], columnCount: number): void {
|
||||
const targetValues: any[] = ["Target Values"];
|
||||
let hasTargets = false;
|
||||
|
||||
this.allSheetData.forEach((sheetDataEntry) => {
|
||||
if (sheetDataEntry.targetLines.length > 0) {
|
||||
const targetLine = sheetDataEntry.targetLines.find(
|
||||
(line) => line.type === "target",
|
||||
);
|
||||
if (targetLine) {
|
||||
targetValues.push(this.formatter.formatNumber(targetLine.value));
|
||||
hasTargets = true;
|
||||
} else {
|
||||
targetValues.push("");
|
||||
}
|
||||
} else {
|
||||
targetValues.push("");
|
||||
}
|
||||
});
|
||||
|
||||
targetValues.push(""); // Empty for comments column
|
||||
|
||||
if (hasTargets) {
|
||||
sheetData.push(targetValues);
|
||||
sheetData.push(ArrayUtils.createEmptyArray(columnCount));
|
||||
}
|
||||
}
|
||||
|
||||
private buildDataRow(
|
||||
timestamp: number,
|
||||
startTime: number,
|
||||
dataByTimestamp: Map<number, Map<string, number>>,
|
||||
relevantComments: LogEntry[],
|
||||
): any[] {
|
||||
const row: any[] = [];
|
||||
|
||||
// Calculate seconds from start using TimestampConverter
|
||||
const secondsFromStart = TimestampConverter.toSecondsFromStart(
|
||||
timestamp,
|
||||
startTime,
|
||||
);
|
||||
row.push(secondsFromStart);
|
||||
|
||||
// Add data for each column
|
||||
this.allSheetData.forEach((sheetDataEntry) => {
|
||||
const tsData = dataByTimestamp.get(timestamp);
|
||||
if (tsData && tsData.has(sheetDataEntry.sheetName)) {
|
||||
row.push(
|
||||
this.sanitizeNumber(tsData.get(sheetDataEntry.sheetName) ?? NaN),
|
||||
);
|
||||
} else {
|
||||
row.push("");
|
||||
}
|
||||
});
|
||||
|
||||
// Check for comments at this timestamp
|
||||
const comment = CommentManager.findAtTimestamp(relevantComments, timestamp);
|
||||
row.push(comment ? comment.message : "");
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private sanitizeNumber(value: number): number | "" {
|
||||
return Number.isFinite(value) ? value : "";
|
||||
}
|
||||
|
||||
private async addMetadata(
|
||||
sheetData: any[][],
|
||||
columnCount: number,
|
||||
relevantComments: LogEntry[],
|
||||
): Promise<void> {
|
||||
sheetData.push(ArrayUtils.createEmptyArray(columnCount));
|
||||
sheetData.push(ArrayUtils.createEmptyArray(columnCount));
|
||||
|
||||
// Use MetadataProvider to build metadata
|
||||
const metadataProvider = MetadataProviderFactory.createForExport({
|
||||
softwareName: this.config.getSoftwareName(),
|
||||
exportDate: this.formatter.formatDate(new Date()),
|
||||
pidData: this.pidData,
|
||||
commentCount: relevantComments.length,
|
||||
});
|
||||
|
||||
sheetData.push(...metadataProvider.buildRows(columnCount));
|
||||
}
|
||||
|
||||
private addChartInstructions(
|
||||
sheetData: any[][],
|
||||
columnCount: number,
|
||||
dataStartRow: number,
|
||||
maxSeconds: number,
|
||||
timeRangeTitle: string,
|
||||
): void {
|
||||
// Calculate optimal Y-axis range from all data
|
||||
const allValues = this.allSheetData.flatMap((sheet) => sheet.values);
|
||||
const yAxisRange = ChartAxisCalculator.calculateOptimalRange(allValues);
|
||||
const yAxisInstruction = ChartAxisCalculator.formatRangeInstruction(
|
||||
yAxisRange.min,
|
||||
yAxisRange.max,
|
||||
);
|
||||
|
||||
sheetData.push(ArrayUtils.createEmptyArray(columnCount));
|
||||
sheetData.push(ArrayUtils.createEmptyArray(columnCount));
|
||||
sheetData.push(ArrayUtils.createRow(["Chart Instructions"], columnCount));
|
||||
sheetData.push(
|
||||
ArrayUtils.createRow(
|
||||
[`1. Select all data from row ${dataStartRow} to the last data row`],
|
||||
columnCount,
|
||||
),
|
||||
);
|
||||
sheetData.push(
|
||||
ArrayUtils.createRow(
|
||||
["2. Insert > Chart > Scatter Chart with Straight Lines and Markers"],
|
||||
columnCount,
|
||||
),
|
||||
);
|
||||
sheetData.push(
|
||||
ArrayUtils.createRow(
|
||||
["3. X-axis: Time (seconds), Y-axis: All measurement columns"],
|
||||
columnCount,
|
||||
),
|
||||
);
|
||||
sheetData.push(
|
||||
ArrayUtils.createRow(
|
||||
[`4. Set X-axis range: 0 to ${maxSeconds}`],
|
||||
columnCount,
|
||||
),
|
||||
);
|
||||
sheetData.push(ArrayUtils.createRow([yAxisInstruction], columnCount));
|
||||
sheetData.push(
|
||||
ArrayUtils.createRow(["6. Position legend at bottom"], columnCount),
|
||||
);
|
||||
sheetData.push(
|
||||
ArrayUtils.createRow(
|
||||
[`7. Chart Title: ${this.groupId} - ${timeRangeTitle}`],
|
||||
columnCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private configureWorksheet(
|
||||
worksheet: XLSX.WorkSheet,
|
||||
columnCount: number,
|
||||
): void {
|
||||
// Merge title cells
|
||||
if (!worksheet["!merges"]) worksheet["!merges"] = [];
|
||||
worksheet["!merges"].push({
|
||||
s: { r: 0, c: 0 },
|
||||
e: { r: 0, c: columnCount - 1 },
|
||||
});
|
||||
|
||||
// Set column widths
|
||||
const colWidths = [
|
||||
{ wch: 12 }, // Timestamp
|
||||
...this.allSheetData.map(() => ({ wch: 12 })),
|
||||
{ wch: 40 }, // Comments
|
||||
];
|
||||
worksheet["!cols"] = colWidths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { LogEntry } from "@/stores/logsStore";
|
||||
|
||||
/**
|
||||
* Filters and manages log comments for export
|
||||
*/
|
||||
export class CommentManager {
|
||||
static filterRelevant(
|
||||
logs: LogEntry[],
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
): LogEntry[] {
|
||||
return logs.filter(
|
||||
(log) =>
|
||||
log.timestamp.getTime() >= startTime &&
|
||||
log.timestamp.getTime() <= endTime &&
|
||||
log.level === "info" &&
|
||||
log.message.toLowerCase().includes("comment"),
|
||||
);
|
||||
}
|
||||
|
||||
static findAtTimestamp(
|
||||
comments: LogEntry[],
|
||||
timestamp: number,
|
||||
tolerance: number = 1000,
|
||||
): LogEntry | undefined {
|
||||
return comments.find(
|
||||
(log) => Math.abs(log.timestamp.getTime() - timestamp) < tolerance,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { TimeSeries, seriesToUPlotData } from "@/lib/timeseries";
|
||||
import { renderUnitSymbol, Unit } from "@/control/units";
|
||||
import { GraphConfig, GraphLine } from "../types";
|
||||
import { IValueFormatter, ValueFormatter } from "./excelFormatters";
|
||||
import { StatisticsCalculator } from "./excelStatisticsCalculator";
|
||||
import { ExcelCellSanitizer } from "./excelUtils";
|
||||
|
||||
/**
|
||||
* Creates individual data sheets for each series
|
||||
*/
|
||||
export class DataSheetBuilder {
|
||||
private formatter: IValueFormatter;
|
||||
|
||||
constructor(
|
||||
private graphLine: {
|
||||
graphTitle: string;
|
||||
lineTitle: string;
|
||||
series: TimeSeries;
|
||||
color?: string;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
config: GraphConfig;
|
||||
targetLines: GraphLine[];
|
||||
},
|
||||
private seriesTitle: string,
|
||||
private unit: Unit | undefined,
|
||||
formatter?: IValueFormatter,
|
||||
) {
|
||||
this.formatter = formatter || new ValueFormatter();
|
||||
}
|
||||
|
||||
build(): XLSX.WorkSheet {
|
||||
const [timestamps, values] = seriesToUPlotData(this.graphLine.series.long);
|
||||
const unitSymbol = renderUnitSymbol(this.unit) || "";
|
||||
|
||||
const sheetData: any[][] = [];
|
||||
|
||||
// Build header
|
||||
const col1Header = unitSymbol
|
||||
? `${unitSymbol} ${this.seriesTitle}`
|
||||
: this.seriesTitle;
|
||||
|
||||
sheetData.push(["Timestamp", col1Header, "", "", "Statistic", "Value"]);
|
||||
|
||||
// Build stats section
|
||||
const statsRows = this.buildStatsRows(timestamps, values, unitSymbol);
|
||||
|
||||
// Combine data and stats rows
|
||||
const maxRows = Math.max(timestamps.length, statsRows.length);
|
||||
for (let i = 0; i < maxRows; i++) {
|
||||
const row = this.buildDataRow(i, timestamps, values, statsRows);
|
||||
sheetData.push(row);
|
||||
}
|
||||
|
||||
const sanitizedSheetData = sheetData.map((row) =>
|
||||
ExcelCellSanitizer.sanitizeRow(row),
|
||||
);
|
||||
|
||||
// Convert to worksheet
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(sanitizedSheetData);
|
||||
worksheet["!cols"] = [
|
||||
{ wch: 20 }, // Timestamp
|
||||
{ wch: 15 }, // Value
|
||||
{ wch: 5 }, // Empty
|
||||
{ wch: 5 }, // Empty
|
||||
{ wch: 30 }, // Statistic name
|
||||
{ wch: 20 }, // Statistic value
|
||||
];
|
||||
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
private buildStatsRows(
|
||||
timestamps: number[],
|
||||
values: number[],
|
||||
unitSymbol: string,
|
||||
): string[][] {
|
||||
const statsRows: string[][] = [];
|
||||
|
||||
statsRows.push(["Graph", this.graphLine.graphTitle]);
|
||||
statsRows.push(["Line Name", this.graphLine.lineTitle]);
|
||||
statsRows.push(["Line Color", this.graphLine.color || "Default"]);
|
||||
statsRows.push(["Generated", this.formatter.formatDate(new Date())]);
|
||||
statsRows.push(["", ""]);
|
||||
statsRows.push(["Total Data Points", timestamps.length.toString()]);
|
||||
|
||||
if (timestamps.length > 0) {
|
||||
const firstDate = new Date(timestamps[0]);
|
||||
const lastDate = new Date(timestamps[timestamps.length - 1]);
|
||||
|
||||
statsRows.push([
|
||||
"Time Range Start",
|
||||
this.formatter.formatDate(firstDate),
|
||||
]);
|
||||
statsRows.push(["Time Range End", this.formatter.formatDate(lastDate)]);
|
||||
|
||||
const duration = timestamps[timestamps.length - 1] - timestamps[0];
|
||||
const durationHours = this.formatter.formatDuration(duration);
|
||||
statsRows.push(["Duration (hours)", durationHours]);
|
||||
|
||||
if (values.length > 0) {
|
||||
const stats = StatisticsCalculator.calculate(values);
|
||||
|
||||
statsRows.push(["", ""]);
|
||||
statsRows.push([
|
||||
`Minimum Value (${unitSymbol})`,
|
||||
this.formatValue(stats.min),
|
||||
]);
|
||||
statsRows.push([
|
||||
`Maximum Value (${unitSymbol})`,
|
||||
this.formatValue(stats.max),
|
||||
]);
|
||||
statsRows.push([
|
||||
`Average Value (${unitSymbol})`,
|
||||
this.formatValue(stats.avg),
|
||||
]);
|
||||
statsRows.push([
|
||||
`Standard Deviation (${unitSymbol})`,
|
||||
this.formatValue(stats.stdDev),
|
||||
]);
|
||||
statsRows.push([
|
||||
`Range (${unitSymbol})`,
|
||||
this.formatValue(stats.range),
|
||||
]);
|
||||
|
||||
statsRows.push(["", ""]);
|
||||
statsRows.push([
|
||||
`25th Percentile (${unitSymbol})`,
|
||||
this.formatValue(stats.p25),
|
||||
]);
|
||||
statsRows.push([
|
||||
`50th Percentile (${unitSymbol})`,
|
||||
this.formatValue(stats.p50),
|
||||
]);
|
||||
statsRows.push([
|
||||
`75th Percentile (${unitSymbol})`,
|
||||
this.formatValue(stats.p75),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return statsRows;
|
||||
}
|
||||
|
||||
private buildDataRow(
|
||||
index: number,
|
||||
timestamps: number[],
|
||||
values: number[],
|
||||
statsRows: string[][],
|
||||
): any[] {
|
||||
const row: any[] = ["", "", "", ""];
|
||||
|
||||
// Add timestamp and value data
|
||||
if (index < timestamps.length) {
|
||||
const date = new Date(timestamps[index]);
|
||||
row[0] = this.formatter.formatDate(date);
|
||||
row[1] = this.formatValue(values[index]);
|
||||
}
|
||||
|
||||
// Add stats
|
||||
if (index < statsRows.length) {
|
||||
row[4] = statsRows[index][0];
|
||||
row[5] = statsRows[index][1];
|
||||
} else {
|
||||
row[4] = "";
|
||||
row[5] = "";
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private formatValue(value: number): string {
|
||||
return this.graphLine.renderValue
|
||||
? this.graphLine.renderValue(value)
|
||||
: this.formatter.formatNumber(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ValueFormatter } from "./excelFormatters";
|
||||
|
||||
/**
|
||||
* Utility class for date/time formatting and manipulation
|
||||
* Now uses ValueFormatter internally
|
||||
*/
|
||||
export class DateFormatter {
|
||||
private static formatter = new ValueFormatter();
|
||||
|
||||
static format(date: Date): string {
|
||||
return this.formatter.formatDate(date);
|
||||
}
|
||||
|
||||
static getExportTimestamp(): string {
|
||||
return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||
}
|
||||
|
||||
static formatTimeRange(startTime: number, endTime: number): string {
|
||||
return this.formatter.formatTimeRange(startTime, endTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { seriesToUPlotData } from "@/lib/timeseries";
|
||||
import { renderUnitSymbol } from "@/control/units";
|
||||
import { GraphLine } from "../types";
|
||||
import { LogEntry } from "@/stores/logsStore";
|
||||
import { ExportConfig, IExportConfig } from "./excelExportConfig";
|
||||
import { IValueFormatter, ValueFormatter } from "./excelFormatters";
|
||||
import { ExcelCellSanitizer, IPidDataProvider } from "./excelUtils";
|
||||
import {
|
||||
CombinedSheetData,
|
||||
GraphExportData,
|
||||
PidData,
|
||||
} from "./excelExportTypes";
|
||||
import { DateFormatter } from "./excelDateFormatter";
|
||||
import { SheetNameManager } from "./excelSheetNameManager";
|
||||
import { DataSheetBuilder } from "./excelDataSheetBuilder";
|
||||
import { AnalysisSheetBuilder } from "./excelAnalysisSheetBuilder";
|
||||
|
||||
export type { GraphExportData, PidSettings, PidData } from "./excelExportTypes";
|
||||
|
||||
/**
|
||||
* Main orchestrator for Excel export functionality
|
||||
*/
|
||||
export class ExcelExporter {
|
||||
private config: IExportConfig;
|
||||
private formatter: IValueFormatter;
|
||||
private sheetNameManager: SheetNameManager;
|
||||
private pidDataProvider?: IPidDataProvider;
|
||||
|
||||
constructor(
|
||||
config?: IExportConfig,
|
||||
formatter?: IValueFormatter,
|
||||
pidDataProvider?: IPidDataProvider,
|
||||
) {
|
||||
this.config = config || new ExportConfig();
|
||||
this.formatter = formatter || new ValueFormatter();
|
||||
this.sheetNameManager = new SheetNameManager(this.config);
|
||||
this.pidDataProvider = pidDataProvider;
|
||||
}
|
||||
|
||||
async export(
|
||||
graphDataMap: Map<string, () => GraphExportData | null>,
|
||||
groupId: string,
|
||||
logs: LogEntry[] = [],
|
||||
pidData?: PidData,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// If PID data provider is available and no PID data provided, fetch it
|
||||
if (!pidData && this.pidDataProvider) {
|
||||
pidData = (await this.pidDataProvider.fetchPidSettings()) || undefined;
|
||||
}
|
||||
|
||||
const filteredMap = this.filterValidSeries(graphDataMap);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const exportTimestamp = DateFormatter.getExportTimestamp();
|
||||
|
||||
const allSheetData: CombinedSheetData[] = [];
|
||||
|
||||
// Process each series
|
||||
filteredMap.forEach((getDataFn) => {
|
||||
const exportData = getDataFn();
|
||||
if (!exportData?.data?.newData) return;
|
||||
|
||||
const series = exportData.data;
|
||||
const seriesTitle = series.title || "Series";
|
||||
|
||||
// Ensure newData is not null before proceeding
|
||||
if (!series.newData) return;
|
||||
|
||||
const sheetName = this.sheetNameManager.generate(
|
||||
exportData.config.title,
|
||||
seriesTitle,
|
||||
exportData.unit,
|
||||
);
|
||||
|
||||
const targetLines: GraphLine[] = [
|
||||
...(exportData.config.lines || []),
|
||||
...(series.lines || []),
|
||||
];
|
||||
|
||||
// Create data sheet
|
||||
const dataSheetBuilder = new DataSheetBuilder(
|
||||
{
|
||||
graphTitle: exportData.config.title,
|
||||
lineTitle: seriesTitle,
|
||||
series: series.newData,
|
||||
color: series.color,
|
||||
unit: exportData.unit,
|
||||
renderValue: exportData.renderValue,
|
||||
config: exportData.config,
|
||||
targetLines,
|
||||
},
|
||||
seriesTitle,
|
||||
exportData.unit,
|
||||
this.formatter,
|
||||
);
|
||||
|
||||
const worksheet = dataSheetBuilder.build();
|
||||
ExcelCellSanitizer.sanitizeWorksheet(worksheet);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||
|
||||
// Collect data for analysis sheet
|
||||
const [timestamps, values] = seriesToUPlotData(series.newData.long);
|
||||
allSheetData.push({
|
||||
sheetName,
|
||||
timestamps,
|
||||
values,
|
||||
unit: renderUnitSymbol(exportData.unit) || "",
|
||||
seriesTitle,
|
||||
graphTitle: exportData.config.title,
|
||||
targetLines,
|
||||
color: series.color,
|
||||
});
|
||||
});
|
||||
|
||||
if (allSheetData.length === 0) {
|
||||
alert("No data available to export from any graphs in this group");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create analysis sheet
|
||||
const analysisSheetBuilder = new AnalysisSheetBuilder(
|
||||
allSheetData,
|
||||
groupId,
|
||||
logs,
|
||||
pidData,
|
||||
this.config,
|
||||
this.formatter,
|
||||
);
|
||||
const analysisSheet = await analysisSheetBuilder.build();
|
||||
ExcelCellSanitizer.sanitizeWorksheet(analysisSheet);
|
||||
XLSX.utils.book_append_sheet(workbook, analysisSheet, "Analysis");
|
||||
|
||||
const xlsxBuffer = XLSX.write(workbook, {
|
||||
type: "buffer",
|
||||
bookType: "xlsx",
|
||||
});
|
||||
|
||||
this.triggerDownload(
|
||||
xlsxBuffer,
|
||||
groupId,
|
||||
DateFormatter.getExportTimestamp(),
|
||||
);
|
||||
} catch (error) {
|
||||
alert(
|
||||
`Error exporting data to Excel: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}. Please try again.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private filterValidSeries(
|
||||
graphDataMap: Map<string, () => GraphExportData | null>,
|
||||
): Map<string, () => GraphExportData | null> {
|
||||
const filteredMap = new Map<string, () => GraphExportData | null>();
|
||||
graphDataMap.forEach((getDataFn, seriesId) => {
|
||||
if (seriesId.includes("-series-")) {
|
||||
filteredMap.set(seriesId, getDataFn);
|
||||
}
|
||||
});
|
||||
return filteredMap;
|
||||
}
|
||||
|
||||
private triggerDownload(
|
||||
buffer: ArrayBuffer,
|
||||
groupId: string,
|
||||
exportTimestamp: string,
|
||||
): void {
|
||||
const filename = `${groupId
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "_")}_export_${exportTimestamp}.xlsx`;
|
||||
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to maintain backward compatibility with existing code
|
||||
*/
|
||||
export async function exportGraphsToExcel(
|
||||
graphDataMap: Map<string, () => GraphExportData | null>,
|
||||
groupId: string,
|
||||
logs: LogEntry[] = [],
|
||||
pidData?: PidData,
|
||||
): Promise<void> {
|
||||
const exporter = new ExcelExporter();
|
||||
await exporter.export(graphDataMap, groupId, logs, pidData);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Configuration system for Excel Export
|
||||
* Centralizes all hardcoded values and provides type-safe access
|
||||
* Follows Dependency Inversion Principle - depends on abstractions
|
||||
*/
|
||||
|
||||
export interface IExportConfig {
|
||||
getSoftwareName(): string;
|
||||
getDefaultPrecision(): number;
|
||||
getUnitFriendlyName(unit: string): string | undefined;
|
||||
getDefaultChartColor(): string;
|
||||
getDateLocale(): string;
|
||||
getYAxisRange(): { min: number; max: number } | null; // null means auto-scale
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation that can be extended or replaced
|
||||
*/
|
||||
export class ExportConfig implements IExportConfig {
|
||||
private readonly config = {
|
||||
softwareName: "QiTech Control",
|
||||
defaultPrecision: 3,
|
||||
unitFriendlyNames: {
|
||||
"°C": "Temp",
|
||||
W: "Watt",
|
||||
A: "Ampere",
|
||||
bar: "Bar",
|
||||
rpm: "Rpm",
|
||||
"1/min": "Rpm",
|
||||
mm: "mm",
|
||||
"%": "Percent",
|
||||
} as Record<string, string>,
|
||||
defaultChartColor: "#9b59b6",
|
||||
dateLocale: "de-DE",
|
||||
// null means auto-scale based on data
|
||||
yAxisRange: null as { min: number; max: number } | null,
|
||||
};
|
||||
|
||||
getSoftwareName(): string {
|
||||
return this.config.softwareName;
|
||||
}
|
||||
|
||||
getDefaultPrecision(): number {
|
||||
return this.config.defaultPrecision;
|
||||
}
|
||||
|
||||
getUnitFriendlyName(unit: string): string | undefined {
|
||||
return this.config.unitFriendlyNames[unit];
|
||||
}
|
||||
|
||||
getDefaultChartColor(): string {
|
||||
return this.config.defaultChartColor;
|
||||
}
|
||||
|
||||
getDateLocale(): string {
|
||||
return this.config.dateLocale;
|
||||
}
|
||||
|
||||
getYAxisRange(): { min: number; max: number } | null {
|
||||
return this.config.yAxisRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow runtime configuration updates
|
||||
*/
|
||||
setYAxisRange(min: number, max: number): void {
|
||||
this.config.yAxisRange = { min, max };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new unit mapping at runtime
|
||||
*/
|
||||
addUnitFriendlyName(unit: string, friendlyName: string): void {
|
||||
this.config.unitFriendlyNames[unit] = friendlyName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Machine-aware configuration that fetches data from machine context
|
||||
* This can be extended to fetch PID settings, machine-specific units, etc.
|
||||
*/
|
||||
export class MachineAwareExportConfig extends ExportConfig {
|
||||
constructor(private machineContext?: any) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to get software name from machine context if available
|
||||
*/
|
||||
override getSoftwareName(): string {
|
||||
// Could fetch from machine context in future
|
||||
return this.machineContext?.softwareName ?? super.getSoftwareName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating configuration instances
|
||||
* Follows Abstract Factory Pattern
|
||||
*/
|
||||
export class ExportConfigFactory {
|
||||
static create(machineContext?: any): IExportConfig {
|
||||
if (machineContext) {
|
||||
return new MachineAwareExportConfig(machineContext);
|
||||
}
|
||||
return new ExportConfig();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Unit } from "@/control/units";
|
||||
import { GraphConfig, SeriesData, GraphLine } from "../types";
|
||||
|
||||
/**
|
||||
* Type definitions for export data structures
|
||||
*/
|
||||
export type GraphExportData = {
|
||||
config: GraphConfig;
|
||||
data: SeriesData;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
};
|
||||
|
||||
export type PidSettings = {
|
||||
kp: number;
|
||||
ki: number;
|
||||
kd: number;
|
||||
zone?: string; // For temperature zones (front, middle, back, nozzle)
|
||||
};
|
||||
|
||||
export type PidData = {
|
||||
temperature?: Record<string, PidSettings>; // keyed by zone
|
||||
pressure?: PidSettings;
|
||||
};
|
||||
|
||||
export type CombinedSheetData = {
|
||||
sheetName: string;
|
||||
timestamps: number[];
|
||||
values: number[];
|
||||
unit: string;
|
||||
seriesTitle: string;
|
||||
graphTitle: string;
|
||||
targetLines: GraphLine[];
|
||||
color?: string;
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Value formatting utilities following Strategy Pattern
|
||||
* Centralizes all formatting logic to eliminate duplication
|
||||
*/
|
||||
|
||||
export interface IValueFormatter {
|
||||
formatNumber(value: number): string;
|
||||
formatDate(date: Date): string;
|
||||
formatTimeRange(startTime: number, endTime: number): string;
|
||||
formatDuration(milliseconds: number): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default formatter implementation
|
||||
*/
|
||||
export class ValueFormatter implements IValueFormatter {
|
||||
constructor(
|
||||
private precision: number = 3,
|
||||
private locale: string = "de-DE",
|
||||
) {}
|
||||
|
||||
formatNumber(value: number): string {
|
||||
if (value == null || isNaN(value)) {
|
||||
return "";
|
||||
}
|
||||
return value.toFixed(this.precision);
|
||||
}
|
||||
|
||||
formatDate(date: Date): string {
|
||||
return date.toLocaleString(this.locale, {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
formatTimeRange(startTime: number, endTime: number): string {
|
||||
const startDate = new Date(startTime);
|
||||
const endDate = new Date(endTime);
|
||||
return `${this.formatDate(startDate)} bis ${this.formatDate(endDate)}`;
|
||||
}
|
||||
|
||||
formatDuration(milliseconds: number): string {
|
||||
const hours = (milliseconds / (1000 * 60 * 60)).toFixed(2);
|
||||
return hours;
|
||||
}
|
||||
|
||||
setPrecision(precision: number): void {
|
||||
this.precision = precision;
|
||||
}
|
||||
|
||||
setLocale(locale: string): void {
|
||||
this.locale = locale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom formatter that can use a render function
|
||||
*/
|
||||
export class CustomValueFormatter extends ValueFormatter {
|
||||
constructor(
|
||||
private customRenderFn?: (value: number) => string,
|
||||
precision?: number,
|
||||
locale?: string,
|
||||
) {
|
||||
super(precision, locale);
|
||||
}
|
||||
|
||||
override formatNumber(value: number): string {
|
||||
if (this.customRenderFn) {
|
||||
return this.customRenderFn(value);
|
||||
}
|
||||
return super.formatNumber(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp utilities to eliminate duplicate conversion logic
|
||||
*/
|
||||
export class TimestampConverter {
|
||||
/**
|
||||
* Convert timestamp to seconds from start
|
||||
*/
|
||||
static toSecondsFromStart(timestamp: number, startTime: number): number {
|
||||
return Math.floor((timestamp - startTime) / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert multiple timestamps to seconds from start
|
||||
*/
|
||||
static arrayToSecondsFromStart(
|
||||
timestamps: number[],
|
||||
startTime: number,
|
||||
): number[] {
|
||||
return timestamps.map((ts) => this.toSecondsFromStart(ts, startTime));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Array utilities to eliminate duplicate fill patterns
|
||||
*/
|
||||
export class ArrayUtils {
|
||||
/**
|
||||
* Create an array filled with empty strings
|
||||
*/
|
||||
static createEmptyArray(count: number): string[] {
|
||||
return Array(count).fill("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a row with initial values and fill rest with empty strings
|
||||
*/
|
||||
static createRow(initialValues: any[], totalColumns: number): any[] {
|
||||
const emptyCount = Math.max(0, totalColumns - initialValues.length);
|
||||
return [...initialValues, ...this.createEmptyArray(emptyCount)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Metadata provider system following Open/Closed Principle
|
||||
* New metadata sections can be added without modifying existing code
|
||||
*/
|
||||
|
||||
export interface IMetadataSection {
|
||||
getTitle(): string;
|
||||
getRows(): Array<{ key: string; value: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base metadata section implementation
|
||||
*/
|
||||
export abstract class MetadataSection implements IMetadataSection {
|
||||
abstract getTitle(): string;
|
||||
abstract getRows(): Array<{ key: string; value: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export information section
|
||||
*/
|
||||
export class ExportInfoSection extends MetadataSection {
|
||||
constructor(
|
||||
private softwareName: string,
|
||||
private exportDate: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
return "Export Information";
|
||||
}
|
||||
|
||||
getRows(): Array<{ key: string; value: string }> {
|
||||
return [
|
||||
{ key: "Software", value: this.softwareName },
|
||||
{ key: "Export Date", value: this.exportDate },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PID settings section for temperature controllers
|
||||
*/
|
||||
export class TemperaturePidSection extends MetadataSection {
|
||||
constructor(
|
||||
private pidSettings: Record<string, { kp: number; ki: number; kd: number }>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
return "Temperature Controllers";
|
||||
}
|
||||
|
||||
getRows(): Array<{ key: string; value: string }> {
|
||||
const rows: Array<{ key: string; value: string }> = [];
|
||||
|
||||
Object.entries(this.pidSettings).forEach(([zone, settings]) => {
|
||||
rows.push(
|
||||
{ key: ` ${zone} - Kp`, value: settings.kp.toFixed(3) },
|
||||
{ key: ` ${zone} - Ki`, value: settings.ki.toFixed(3) },
|
||||
{ key: ` ${zone} - Kd`, value: settings.kd.toFixed(3) },
|
||||
);
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PID settings section for pressure controller
|
||||
*/
|
||||
export class PressurePidSection extends MetadataSection {
|
||||
constructor(private pidSettings: { kp: number; ki: number; kd: number }) {
|
||||
super();
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
return "Pressure Controller";
|
||||
}
|
||||
|
||||
getRows(): Array<{ key: string; value: string }> {
|
||||
return [
|
||||
{ key: " Kp", value: this.pidSettings.kp.toFixed(3) },
|
||||
{ key: " Ki", value: this.pidSettings.ki.toFixed(3) },
|
||||
{ key: " Kd", value: this.pidSettings.kd.toFixed(3) },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment statistics section
|
||||
*/
|
||||
export class CommentStatsSection extends MetadataSection {
|
||||
constructor(private commentCount: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
return "Comment Statistics";
|
||||
}
|
||||
|
||||
getRows(): Array<{ key: string; value: string }> {
|
||||
return [{ key: "Total Comments", value: this.commentCount.toString() }];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata provider that aggregates multiple sections
|
||||
* Follows Composite Pattern
|
||||
*/
|
||||
export class MetadataProvider {
|
||||
private sections: IMetadataSection[] = [];
|
||||
|
||||
addSection(section: IMetadataSection): this {
|
||||
this.sections.push(section);
|
||||
return this;
|
||||
}
|
||||
|
||||
getSections(): IMetadataSection[] {
|
||||
return this.sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build metadata rows for Excel sheet
|
||||
*/
|
||||
buildRows(columnCount: number): string[][] {
|
||||
const rows: string[][] = [];
|
||||
|
||||
this.sections.forEach((section, index) => {
|
||||
// Add empty row before each section except the first
|
||||
if (index > 0) {
|
||||
rows.push(Array(columnCount).fill(""));
|
||||
}
|
||||
|
||||
// Add section title
|
||||
rows.push([section.getTitle(), ...Array(columnCount - 1).fill("")]);
|
||||
|
||||
// Add section rows
|
||||
section.getRows().forEach((row) => {
|
||||
rows.push([row.key, row.value, ...Array(columnCount - 2).fill("")]);
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating metadata providers
|
||||
*/
|
||||
export class MetadataProviderFactory {
|
||||
static createForExport(params: {
|
||||
softwareName: string;
|
||||
exportDate: string;
|
||||
pidData?: {
|
||||
temperature?: Record<string, { kp: number; ki: number; kd: number }>;
|
||||
pressure?: { kp: number; ki: number; kd: number };
|
||||
};
|
||||
commentCount?: number;
|
||||
}): MetadataProvider {
|
||||
const provider = new MetadataProvider();
|
||||
|
||||
// Always add export info
|
||||
provider.addSection(
|
||||
new ExportInfoSection(params.softwareName, params.exportDate),
|
||||
);
|
||||
|
||||
// Add PID sections if available
|
||||
if (params.pidData?.temperature) {
|
||||
provider.addSection(
|
||||
new TemperaturePidSection(params.pidData.temperature),
|
||||
);
|
||||
}
|
||||
|
||||
if (params.pidData?.pressure) {
|
||||
provider.addSection(new PressurePidSection(params.pidData.pressure));
|
||||
}
|
||||
|
||||
// Add comment stats if provided
|
||||
if (params.commentCount !== undefined) {
|
||||
provider.addSection(new CommentStatsSection(params.commentCount));
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { IExportConfig } from "./excelExportConfig";
|
||||
import { IValueFormatter } from "./excelFormatters";
|
||||
import { MetadataProvider, MetadataProviderFactory } from "./excelMetadata";
|
||||
import { PidData } from "./excelExportTypes";
|
||||
|
||||
/**
|
||||
* Builds metadata sections for Excel sheets
|
||||
* Now uses MetadataProvider for better separation of concerns
|
||||
* @deprecated Use MetadataProvider directly for new code
|
||||
*/
|
||||
export class MetadataBuilder {
|
||||
private metadataProvider: MetadataProvider;
|
||||
private config: IExportConfig;
|
||||
private formatter: IValueFormatter;
|
||||
|
||||
constructor(config: IExportConfig, formatter: IValueFormatter) {
|
||||
this.config = config;
|
||||
this.formatter = formatter;
|
||||
this.metadataProvider = new MetadataProvider();
|
||||
}
|
||||
|
||||
addExportInfo(columnCount: number): this {
|
||||
// This method is kept for backward compatibility
|
||||
// Actual data is added when building final rows
|
||||
return this;
|
||||
}
|
||||
|
||||
addPidSettings(pidData: PidData | undefined, columnCount: number): this {
|
||||
// This method is kept for backward compatibility
|
||||
// Actual data is added when building final rows
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build metadata rows using MetadataProvider
|
||||
*/
|
||||
getRows(
|
||||
columnCount: number,
|
||||
pidData?: PidData,
|
||||
commentCount?: number,
|
||||
): string[][] {
|
||||
// Create metadata provider with all sections
|
||||
const provider = MetadataProviderFactory.createForExport({
|
||||
softwareName: this.config.getSoftwareName(),
|
||||
exportDate: this.formatter.formatDate(new Date()),
|
||||
pidData: pidData,
|
||||
commentCount: commentCount,
|
||||
});
|
||||
|
||||
return provider.buildRows(columnCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { renderUnitSymbol, Unit } from "@/control/units";
|
||||
import { IExportConfig } from "./excelExportConfig";
|
||||
|
||||
/**
|
||||
* Manages unique sheet name generation for Excel workbooks
|
||||
* Now uses IExportConfig for unit mappings
|
||||
*/
|
||||
export class SheetNameManager {
|
||||
private usedNames = new Set<string>();
|
||||
|
||||
constructor(private config: IExportConfig) {}
|
||||
|
||||
generate(
|
||||
graphTitle: string,
|
||||
seriesTitle: string,
|
||||
unit: Unit | undefined,
|
||||
): string {
|
||||
const unitSymbol = renderUnitSymbol(unit) || "";
|
||||
let sheetName = "";
|
||||
|
||||
// Use unit-based name for generic series, otherwise use series title
|
||||
if (/^Series \d+$/i.test(seriesTitle)) {
|
||||
const friendlyUnitName = this.config.getUnitFriendlyName(unitSymbol);
|
||||
sheetName = friendlyUnitName || seriesTitle;
|
||||
} else {
|
||||
const friendlyUnitName = this.config.getUnitFriendlyName(unitSymbol);
|
||||
if (
|
||||
friendlyUnitName &&
|
||||
!seriesTitle.toLowerCase().includes(friendlyUnitName.toLowerCase())
|
||||
) {
|
||||
sheetName = `${seriesTitle} ${friendlyUnitName}`;
|
||||
} else {
|
||||
sheetName = seriesTitle;
|
||||
}
|
||||
}
|
||||
|
||||
return this.makeUnique(this.sanitize(sheetName));
|
||||
}
|
||||
|
||||
private sanitize(name: string): string {
|
||||
return (
|
||||
name
|
||||
.replace(/[\\/?*$:[\]]/g, "_")
|
||||
.substring(0, 31)
|
||||
.trim() || "Sheet"
|
||||
);
|
||||
}
|
||||
|
||||
private makeUnique(name: string): string {
|
||||
let finalName = name;
|
||||
let counter = 1;
|
||||
|
||||
while (this.usedNames.has(finalName)) {
|
||||
const suffix = `_${counter}`;
|
||||
const maxBaseLength = 31 - suffix.length;
|
||||
finalName = `${name.substring(0, maxBaseLength)}${suffix}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
this.usedNames.add(finalName);
|
||||
return finalName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Handles statistical calculations for time series data
|
||||
*/
|
||||
export class StatisticsCalculator {
|
||||
static calculate(values: number[]): {
|
||||
min: number;
|
||||
max: number;
|
||||
avg: number;
|
||||
stdDev: number;
|
||||
range: number;
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
} {
|
||||
if (values.length === 0) {
|
||||
throw new Error("Cannot calculate statistics for empty array");
|
||||
}
|
||||
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
||||
const stdDev = Math.sqrt(
|
||||
values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) /
|
||||
values.length,
|
||||
);
|
||||
const range = max - min;
|
||||
|
||||
const sortedValues = [...values].sort((a, b) => a - b);
|
||||
const p25 = sortedValues[Math.floor(sortedValues.length * 0.25)];
|
||||
const p50 = sortedValues[Math.floor(sortedValues.length * 0.5)];
|
||||
const p75 = sortedValues[Math.floor(sortedValues.length * 0.75)];
|
||||
|
||||
return { min, max, avg, stdDev, range, p25, p50, p75 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Additional utility classes for Excel export
|
||||
* Following SOLID principles and DRY
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utility for calculating optimal Y-axis range based on data
|
||||
* Can be extended for more sophisticated scaling algorithms
|
||||
*/
|
||||
export class ChartAxisCalculator {
|
||||
/**
|
||||
* Calculate optimal Y-axis range with padding
|
||||
*/
|
||||
static calculateOptimalRange(
|
||||
values: number[],
|
||||
paddingPercent: number = 10,
|
||||
): { min: number; max: number } {
|
||||
if (values.length === 0) {
|
||||
return { min: 0, max: 1000 }; // fallback
|
||||
}
|
||||
|
||||
const dataMin = Math.min(...values);
|
||||
const dataMax = Math.max(...values);
|
||||
const range = dataMax - dataMin;
|
||||
|
||||
// Add padding
|
||||
const padding = range * (paddingPercent / 100);
|
||||
|
||||
return {
|
||||
min: Math.floor(dataMin - padding),
|
||||
max: Math.ceil(dataMax + padding),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Y-axis range instruction for Excel
|
||||
*/
|
||||
static formatRangeInstruction(min: number, max: number): string {
|
||||
return `5. Set Y-axis range: ${min} to ${max}`;
|
||||
}
|
||||
}
|
||||
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
/**
|
||||
* Sanitizes cell values to avoid invalid XML in XLSX exports.
|
||||
*/
|
||||
export class ExcelCellSanitizer {
|
||||
/* eslint-disable no-control-regex */
|
||||
private static invalidXmlChars =
|
||||
/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\uD800-\uDFFF\uFFFE\uFFFF]/g;
|
||||
/* eslint-enable no-control-regex */
|
||||
|
||||
static sanitizeCell(value: unknown): string | number {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : "";
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.replace(this.invalidXmlChars, "");
|
||||
}
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return String(value).replace(this.invalidXmlChars, "");
|
||||
}
|
||||
|
||||
static sanitizeRow(row: unknown[]): Array<string | number> {
|
||||
return row.map((value) => this.sanitizeCell(value));
|
||||
}
|
||||
|
||||
static sanitizeWorksheet(worksheet: XLSX.WorkSheet): void {
|
||||
Object.keys(worksheet).forEach((key) => {
|
||||
if (key.startsWith("!")) return;
|
||||
const cell = worksheet[key] as XLSX.CellObject | undefined;
|
||||
if (!cell) return;
|
||||
|
||||
const sanitized = this.sanitizeCell(cell.v);
|
||||
if (sanitized === "") {
|
||||
delete worksheet[key];
|
||||
return;
|
||||
}
|
||||
|
||||
cell.v = sanitized as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for fetching machine PID settings
|
||||
*/
|
||||
export interface IPidDataProvider {
|
||||
fetchPidSettings(): Promise<{
|
||||
temperature?: Record<string, { kp: number; ki: number; kd: number }>;
|
||||
pressure?: { kp: number; ki: number; kd: number };
|
||||
} | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider that fetches PID settings from machine API
|
||||
* Implements Dependency Inversion Principle
|
||||
*/
|
||||
export class MachinePidDataProvider implements IPidDataProvider {
|
||||
constructor(
|
||||
private baseUrl: string = "http://10.10.10.1:3001",
|
||||
private machineSlug?: string,
|
||||
private machineSerial?: number,
|
||||
) {}
|
||||
|
||||
async fetchPidSettings(): Promise<{
|
||||
temperature?: Record<string, { kp: number; ki: number; kd: number }>;
|
||||
pressure?: { kp: number; ki: number; kd: number };
|
||||
} | null> {
|
||||
try {
|
||||
// If machine slug/serial provided, fetch from specific machine
|
||||
if (this.machineSlug && this.machineSerial !== undefined) {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/v2/machine/${this.machineSlug}/${this.machineSerial}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn("Failed to fetch machine data for PID settings");
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Extract PID settings from machine data
|
||||
// This is a placeholder - actual implementation depends on machine API structure
|
||||
return this.extractPidFromMachineData(data);
|
||||
}
|
||||
|
||||
// Otherwise, return null (PID data should be passed in)
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching PID settings from machine:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private extractPidFromMachineData(machineData: any): {
|
||||
temperature?: Record<string, { kp: number; ki: number; kd: number }>;
|
||||
pressure?: { kp: number; ki: number; kd: number };
|
||||
} | null {
|
||||
// This is a placeholder implementation
|
||||
// Actual extraction depends on machine API structure
|
||||
// TODO: Implement based on actual machine data structure
|
||||
|
||||
const pidSettings: any = {};
|
||||
|
||||
// Example: Look for PID-related fields in machine data
|
||||
if (machineData.temperature_controllers) {
|
||||
pidSettings.temperature = machineData.temperature_controllers;
|
||||
}
|
||||
|
||||
if (machineData.pressure_controller) {
|
||||
pidSettings.pressure = machineData.pressure_controller;
|
||||
}
|
||||
|
||||
return Object.keys(pidSettings).length > 0 ? pidSettings : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock provider for testing
|
||||
*/
|
||||
export class MockPidDataProvider implements IPidDataProvider {
|
||||
constructor(
|
||||
private mockData: {
|
||||
temperature?: Record<string, { kp: number; ki: number; kd: number }>;
|
||||
pressure?: { kp: number; ki: number; kd: number };
|
||||
} | null,
|
||||
) {}
|
||||
|
||||
async fetchPidSettings(): Promise<{
|
||||
temperature?: Record<string, { kp: number; ki: number; kd: number }>;
|
||||
pressure?: { kp: number; ki: number; kd: number };
|
||||
} | null> {
|
||||
return Promise.resolve(this.mockData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Version info rendering removed (YAGNI).
|
||||
export {};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { exportGraphsToExcel } from "./excelExport";
|
||||
export type { GraphExportData } from "./excelExport";
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { PropGraphSync, SwitchOrigin } from "./types";
|
||||
import { GraphExportData, exportGraphsToExcel } from "./excelExport";
|
||||
import { useLogsStore } from "@/stores/logsStore";
|
||||
import { useGraphSettingsStore } from "@/stores/graphSettingsStore";
|
||||
|
||||
export function useGraphSync(exportGroupId?: string) {
|
||||
@@ -132,7 +133,14 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
console.warn("No graphs registered for export");
|
||||
return;
|
||||
}
|
||||
exportGraphsToExcel(graphDataRef.current, exportGroupId || "synced-graphs");
|
||||
const logs = useLogsStore.getState().entries;
|
||||
exportGraphsToExcel(
|
||||
graphDataRef.current,
|
||||
exportGroupId || "synced-graphs",
|
||||
logs,
|
||||
).catch((error) => {
|
||||
console.error("Failed to export graphs:", error);
|
||||
});
|
||||
}, [exportGroupId]);
|
||||
|
||||
const handleTimeWindowChange = useCallback(
|
||||
|
||||
@@ -132,7 +132,7 @@ export function usePresets<T>({
|
||||
if (latestPresetId === undefined) {
|
||||
const preset = store.insert({
|
||||
id: undefined,
|
||||
name: "Latest Machine Stettings",
|
||||
name: "Latest Machine Settings",
|
||||
machineIdentification: machine_identification,
|
||||
lastModified: new Date(),
|
||||
schemaVersion,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { SectionTitle } from "@/components/SectionTitle";
|
||||
import { Terminal } from "@/components/Terminal";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { UpdateProgressBar } from "@/components/UpdateProgressBar";
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { updateExecute, updateCancelWithStore } from "@/helpers/update_helpers";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
@@ -89,7 +88,7 @@ export function UpdateExecutePage() {
|
||||
<Page>
|
||||
<SectionTitle title="Apply Update" />
|
||||
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div className="flex flex-row gap-4">
|
||||
<TouchButton
|
||||
className="w-max"
|
||||
icon="lu:CircleFadingArrowUp"
|
||||
@@ -109,14 +108,6 @@ export function UpdateExecutePage() {
|
||||
Cancel Update
|
||||
</TouchButton>
|
||||
)}
|
||||
{isUpdating && (
|
||||
<div className="ml-auto flex items-center gap-2 rounded-xl border border-blue-400 bg-blue-600 px-4 py-2.5 text-white shadow-xl backdrop-blur-sm transition-all duration-300">
|
||||
<Icon name="lu:Info" className="h-5 w-5 text-blue-100" />
|
||||
<span className="text-base leading-snug text-blue-50">
|
||||
Updates typically take approximately <strong>5 minutes</strong>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{currentUpdateInfo && (
|
||||
<Alert title="Update Information" variant="info">
|
||||
@@ -145,7 +136,8 @@ export function UpdateExecutePage() {
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
Please stay connected to the internet and leave the power on. The
|
||||
machine will reboot after the update completes.
|
||||
update procedure takes a couple of minutes and reboots the machine
|
||||
afterwards.
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
Generated
+111
-12
@@ -1,6 +1,21 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1771796463,
|
||||
"narHash": "sha256-9bCDuUzpwJXcHMQYMS1yNuzYMmKO/CCwCexpjWOl62I=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "3d3de3313e263e04894f284ac18177bd26169bad",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane_2": {
|
||||
"locked": {
|
||||
"lastModified": 1750266157,
|
||||
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
|
||||
@@ -15,7 +30,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane_2": {
|
||||
"crane_3": {
|
||||
"locked": {
|
||||
"lastModified": 1750266157,
|
||||
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
|
||||
@@ -102,12 +117,51 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_5": {
|
||||
"inputs": {
|
||||
"systems": "systems_5"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771851181,
|
||||
"narHash": "sha256-gFgE6mGUftwseV3DUENMb0k0EiHd739lZexPo5O/sdQ=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "9a4b494b1aa1b93d8edf167f46dc8e0c0011280c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"qitech-control",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1750973805,
|
||||
"narHash": "sha256-BZXgag7I0rnL/HMHAsBz3tQrfKAibpY2vovexl2lS+Y=",
|
||||
@@ -122,9 +176,10 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager_2": {
|
||||
"home-manager_3": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"qitech-control",
|
||||
"qitech-control",
|
||||
"nixpkgs"
|
||||
]
|
||||
@@ -143,9 +198,10 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager_3": {
|
||||
"home-manager_4": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"qitech-control",
|
||||
"qitech-control",
|
||||
"qitech-control",
|
||||
"nixpkgs"
|
||||
@@ -167,11 +223,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1750969886,
|
||||
"narHash": "sha256-zW/OFnotiz/ndPFdebpo3X0CrbVNf22n4DjN2vxlb58=",
|
||||
"lastModified": 1767313136,
|
||||
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a676066377a2fe7457369dd37c31fd2263b662f4",
|
||||
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -207,6 +263,31 @@
|
||||
],
|
||||
"qitech-control": "qitech-control_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772023797,
|
||||
"narHash": "sha256-RipiUlH5UKiofTRquREluUFOwEfNjLFEvyDol/VWDRg=",
|
||||
"owner": "qitechgmbh",
|
||||
"repo": "control",
|
||||
"rev": "3e3e3f1c0bc2b26fe0fc1e7d49b65e3fd3d7f368",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "qitechgmbh",
|
||||
"repo": "control",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"qitech-control_2": {
|
||||
"inputs": {
|
||||
"crane": "crane_3",
|
||||
"flake-utils": "flake-utils_3",
|
||||
"home-manager": "home-manager_3",
|
||||
"nixpkgs": [
|
||||
"qitech-control",
|
||||
"nixpkgs"
|
||||
],
|
||||
"qitech-control": "qitech-control_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1751047510,
|
||||
"narHash": "sha256-SZ+DxQvFJKJOro1AVUwWltLQO1L9TIc6AI4tnM9V4tk=",
|
||||
@@ -221,15 +302,16 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"qitech-control_2": {
|
||||
"qitech-control_3": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_3",
|
||||
"home-manager": "home-manager_3",
|
||||
"flake-utils": "flake-utils_4",
|
||||
"home-manager": "home-manager_4",
|
||||
"nixpkgs": [
|
||||
"qitech-control",
|
||||
"qitech-control",
|
||||
"nixpkgs"
|
||||
],
|
||||
"qitech-control": "qitech-control_3",
|
||||
"qitech-control": "qitech-control_4",
|
||||
"rust-overlay": "rust-overlay_2"
|
||||
},
|
||||
"locked": {
|
||||
@@ -246,10 +328,11 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"qitech-control_3": {
|
||||
"qitech-control_4": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_4",
|
||||
"flake-utils": "flake-utils_5",
|
||||
"nixpkgs": [
|
||||
"qitech-control",
|
||||
"qitech-control",
|
||||
"qitech-control",
|
||||
"nixpkgs"
|
||||
@@ -300,6 +383,7 @@
|
||||
"rust-overlay_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"qitech-control",
|
||||
"qitech-control",
|
||||
"qitech-control",
|
||||
"nixpkgs"
|
||||
@@ -378,6 +462,21 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_5": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "QiTech Control";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
|
||||
|
||||
# Crane for Rust builds with dependency caching
|
||||
crane = { url = "github:ipetkov/crane"; };
|
||||
|
||||
+16
-10
@@ -15,7 +15,7 @@ in {
|
||||
consoleMode = "max"; # Use the highest available resolution
|
||||
};
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_6_13;
|
||||
boot.kernelPackages = pkgs.linuxPackages_6_18;
|
||||
boot.kernelModules = [ "i915" ];
|
||||
|
||||
boot.kernelParams = [
|
||||
@@ -134,7 +134,6 @@ in {
|
||||
|
||||
# Enable the X11 windowing system.
|
||||
services.xserver.enable = true;
|
||||
#services.xserver.videoDrivers = [ "intel" ];
|
||||
services.xserver.displayManager.gdm = {
|
||||
enable = true;
|
||||
autoSuspend = false;
|
||||
@@ -186,18 +185,25 @@ in {
|
||||
powertop.enable = false;
|
||||
};
|
||||
|
||||
# Ensure all power management is disabled
|
||||
|
||||
services.logind = {
|
||||
# This remains for backward compatibility/high-level override
|
||||
lidSwitch = "ignore";
|
||||
extraConfig = ''
|
||||
HandlePowerKey=ignore
|
||||
HandleSuspendKey=ignore
|
||||
HandleHibernateKey=ignore
|
||||
HandleLidSwitch=ignore
|
||||
IdleAction=ignore
|
||||
'';
|
||||
|
||||
# Structured settings for logind.conf
|
||||
settings = {
|
||||
Login = {
|
||||
HandlePowerKey = "ignore";
|
||||
HandleSuspendKey = "ignore";
|
||||
HandleHibernateKey = "ignore";
|
||||
HandleLidSwitch = "ignore";
|
||||
IdleAction = "ignore";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
# Configure keymap in X11
|
||||
services.xserver.xkb = {
|
||||
layout = "de";
|
||||
|
||||
@@ -10,7 +10,7 @@ buildNpmPackage rec {
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
|
||||
|
||||
makeCacheWritable = true;
|
||||
npmDepsHash = "sha256-N2kdS9IKkEMNOPPnRNQ9GvCenR/k88fVyAFzXMJyxs4=";
|
||||
npmDepsHash = "sha256-Asa8lfMXGUvGkudj0DXQk9VRQ8ugmmP969t7vcaTE0E=";
|
||||
npmFlags = [ "--no-audit" "--no-fund" ];
|
||||
|
||||
installPhase = ''
|
||||
@@ -44,7 +44,6 @@ buildNpmPackage rec {
|
||||
--enable-gpu \
|
||||
--ignore-gpu-blocklist \
|
||||
--enable-gpu-rasterization \
|
||||
--disable-software-rasterizer \
|
||||
"$@"
|
||||
EOF
|
||||
chmod +x $out/bin/qitech-control-electron
|
||||
|
||||
@@ -30,6 +30,9 @@ docker run --rm \
|
||||
--workdir /workspace \
|
||||
nixos/nix:latest \
|
||||
sh -c "
|
||||
whoami
|
||||
ls -lh
|
||||
cd /workspace
|
||||
echo 'Enabling flakes and nix-command...'
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' > ~/.config/nix/nix.conf
|
||||
|
||||
Generated
+868
@@ -0,0 +1,868 @@
|
||||
{
|
||||
"name": "control",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"electron": "^38.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"env-paths": "^2.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"got": "^11.8.5",
|
||||
"progress": "^2.0.3",
|
||||
"semver": "^6.2.0",
|
||||
"sumchecker": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"global-agent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "*",
|
||||
"@types/keyv": "^3.1.4",
|
||||
"@types/node": "*",
|
||||
"@types/responselike": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/keyv": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
|
||||
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/boolean": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^5.1.0",
|
||||
"http-cache-semantics": "^4.0.0",
|
||||
"keyv": "^4.0.0",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"normalize-url": "^6.0.1",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "38.8.4",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-38.8.4.tgz",
|
||||
"integrity": "sha512-vYZ2SH2OhzkCmkbRaijtZLIEJRMnLUL2KQmcAM/7kamf4++3AKSNCBjE1epmQnYFY1M4oKjOUcS3G02ayxR5Bw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^22.7.7",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"electron": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20.55"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"extract-zip": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"es6-error": "^4.1.1",
|
||||
"matcher": "^3.0.0",
|
||||
"roarr": "^2.15.3",
|
||||
"semver": "^7.3.2",
|
||||
"serialize-error": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/globalthis": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-properties": "^1.2.1",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "11.8.6",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
"@types/cacheable-request": "^6.0.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^5.0.3",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-url": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-alpn": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"detect-node": "^2.0.4",
|
||||
"globalthis": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"semver-compare": "^1.0.0",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/serialize-error": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sumchecker": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"electron": "^38.8.4"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user