From f87c2f5b0a6402688f7bc9b1483d7abea2b1f53c Mon Sep 17 00:00:00 2001 From: Erkki Halinen Date: Thu, 5 Oct 2023 16:49:56 +0300 Subject: [PATCH 01/68] Update versioning to reflect the new released version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8af4da8c..4484aab7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "helmet-ui", "productName": "Helmet 4.1", - "version": "4.1.5-SNAPSHOT", + "version": "4.2.1-SNAPSHOT", "private": true, "description": "Helmet 4 Model System UI", "main": "src/main/index.js", From ee270b6db0f1358733ecc231f409052f52875c0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 18:39:22 +0000 Subject: [PATCH 02/68] Bump electron from 22.3.24 to 22.3.25 Bumps [electron](https://github.com/electron/electron) from 22.3.24 to 22.3.25. - [Release notes](https://github.com/electron/electron/releases) - [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md) - [Commits](https://github.com/electron/electron/compare/v22.3.24...v22.3.25) --- updated-dependencies: - dependency-name: electron dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e696dfa..baadfe4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "helmet-ui", - "version": "4.1.5-SNAPSHOT", + "version": "4.2.1-SNAPSHOT", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "4.1.5-SNAPSHOT", + "version": "4.2.1-SNAPSHOT", "license": "EUPL-1.2", "dependencies": { "@electron/remote": "^2.0.8", @@ -35,7 +35,7 @@ "@electron-forge/maker-squirrel": "6.0.4", "@electron-forge/maker-zip": "6.0.4", "@electron-forge/publisher-github": "6.0.4", - "electron": "22.3.24" + "electron": "22.3.25" } }, "node_modules/@electron-forge/cli": { @@ -2096,9 +2096,9 @@ } }, "node_modules/electron": { - "version": "22.3.24", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.24.tgz", - "integrity": "sha512-wnGsShoRVk1Jmgr7h/jZK9bI5UwMF88sdQ5c8z2j2N8B9elhF/jKDFjwDXUrY1Y0xzAskOP0tYIDE+UbUM4byQ==", + "version": "22.3.25", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.25.tgz", + "integrity": "sha512-AjrP7bebMs/IPsgmyowptbA7jycTkrJC7jLZTb5JoH30PkBC6pZx/7XQ0aDok82SsmSiF4UJDOg+HoLrEBiqmg==", "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", @@ -8377,9 +8377,9 @@ } }, "electron": { - "version": "22.3.24", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.24.tgz", - "integrity": "sha512-wnGsShoRVk1Jmgr7h/jZK9bI5UwMF88sdQ5c8z2j2N8B9elhF/jKDFjwDXUrY1Y0xzAskOP0tYIDE+UbUM4byQ==", + "version": "22.3.25", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.25.tgz", + "integrity": "sha512-AjrP7bebMs/IPsgmyowptbA7jycTkrJC7jLZTb5JoH30PkBC6pZx/7XQ0aDok82SsmSiF4UJDOg+HoLrEBiqmg==", "requires": { "@electron/get": "^2.0.0", "@types/node": "^16.11.26", diff --git a/package.json b/package.json index 4484aab7..9a121b7b 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,6 @@ "@electron-forge/maker-squirrel": "6.0.4", "@electron-forge/maker-zip": "6.0.4", "@electron-forge/publisher-github": "6.0.4", - "electron": "22.3.24" + "electron": "22.3.25" } } From 4e031c7951b93da2fa8684c6d490e1b1353130c9 Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:09:24 +0300 Subject: [PATCH 03/68] #203: Add tabs to UI, move CBA to separate tab, cleanup empty css rules --- src/renderer/components/App.css | 6 +- .../HelmetProject/HelmetProject.css | 41 +++++++++---- .../HelmetProject/HelmetProject.jsx | 58 ++++++++++++------- src/renderer/index.html | 2 + 4 files changed, 72 insertions(+), 35 deletions(-) diff --git a/src/renderer/components/App.css b/src/renderer/components/App.css index 55b9ba43..596d7b0c 100644 --- a/src/renderer/components/App.css +++ b/src/renderer/components/App.css @@ -7,7 +7,7 @@ body { margin: 0; padding: 0; position: relative; - background: #ffffff; + background-color: #F4F4F5; font-size: 18px; font-family: gotham_rounded_medium, sans-serif; } @@ -102,14 +102,12 @@ hr { letter-spacing: -0.02em; } -.App__header-title { -} - .App__header-version { font-size: 60%; } .App__body { + height: 920px; } .header-documentation-link { diff --git a/src/renderer/components/HelmetProject/HelmetProject.css b/src/renderer/components/HelmetProject/HelmetProject.css index 1ac6b1cf..4c9fd86d 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.css +++ b/src/renderer/components/HelmetProject/HelmetProject.css @@ -1,20 +1,41 @@ .Project { position: relative; + background: #ffffff; + height: 100%; } - /* -.Project__runtime { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100vh; -} - */ + .Project__selected-details { position: absolute; top: 130px; right: 20px; left: 635px; } -.HelmetProject__control-group-separator { + +.tab-list { + display: flex; + justify-content: flex-start; + list-style: none; + width: 100%; + height: 5rem; + background-color: #F4F4F5; +} + +.tab-list-item { + width: 12rem; + height: 2rem; + display: flex; + align-content: center; + justify-content: center; + color: lightgray; +} + +.tab-item-name { + margin-top: 2rem; } + +.selected-tab { + background-color: white; + height: 100%; + color: #333333; + border-radius: 1rem 1rem 0 0; +} \ No newline at end of file diff --git a/src/renderer/components/HelmetProject/HelmetProject.jsx b/src/renderer/components/HelmetProject/HelmetProject.jsx index 98ebaf58..6d5d43f1 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.jsx +++ b/src/renderer/components/HelmetProject/HelmetProject.jsx @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import Store from "electron-store"; import fs from "fs"; import path from "path"; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; const {ipcRenderer} = require('electron'); @@ -383,27 +384,42 @@ const HelmetProject = ({ {/* Panel for primary view and controls */}
- _loadProjectScenarios(projectPath)} - scenarios={scenarios} - scenarioIDsToRun={scenarioIDsToRun} - runningScenarioID={runningScenarioID} - openScenarioID={openScenarioID} - setOpenScenarioID={setOpenScenarioID} - deleteScenario={(scenario) => {_deleteScenario(scenario)}} - handleClickScenarioToActive={_handleClickScenarioToActive} - handleClickNewScenario={_handleClickNewScenario} - handleClickStartStop={_handleClickStartStop} - logArgs={logArgs} - duplicateScenario={duplicateScenario} - /> - + + + +

Scenarios

+
+ +

CBA

+
+
+ + + _loadProjectScenarios(projectPath)} + scenarios={scenarios} + scenarioIDsToRun={scenarioIDsToRun} + runningScenarioID={runningScenarioID} + openScenarioID={openScenarioID} + setOpenScenarioID={setOpenScenarioID} + deleteScenario={(scenario) => {_deleteScenario(scenario)}} + handleClickScenarioToActive={_handleClickScenarioToActive} + handleClickNewScenario={_handleClickNewScenario} + handleClickStartStop={_handleClickStartStop} + logArgs={logArgs} + duplicateScenario={duplicateScenario} + /> + + + + +
{/* Panel for secondary view(s) and controls */} diff --git a/src/renderer/index.html b/src/renderer/index.html index 56fb831b..65af139f 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -27,6 +27,7 @@ + + From 2c985e5adecdfa18d16a3a45f64727147a1c8ba2 Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:13:48 +0300 Subject: [PATCH 04/68] Add package files --- package-lock.json | 69 ++++++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index baadfe4a..06a38092 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "react": "^18.1.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.1.0", + "react-tabs": "^6.0.2", "react-tooltip": "^5.15.0", "uuid": "^9.0.0", "vex-js": "^4.1.0" @@ -1578,6 +1579,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -5157,6 +5166,16 @@ "node": ">=10" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -5280,6 +5299,23 @@ "react": "^18.2.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-tabs": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", + "integrity": "sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==", + "dependencies": { + "clsx": "^2.0.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/react-tooltip": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.15.0.tgz", @@ -7564,9 +7600,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "requires": { - "ajv": "^8.0.0" - } + "requires": {} }, "ansi-escapes": { "version": "4.3.2", @@ -8010,6 +8044,11 @@ "mimic-response": "^1.0.0" } }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -10694,6 +10733,16 @@ "retry": "^0.12.0" } }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -10773,6 +10822,20 @@ "scheduler": "^0.23.0" } }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-tabs": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", + "integrity": "sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==", + "requires": { + "clsx": "^2.0.0", + "prop-types": "^15.5.0" + } + }, "react-tooltip": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.15.0.tgz", diff --git a/package.json b/package.json index 9a121b7b..3c60598b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "react": "^18.1.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.1.0", + "react-tabs": "^6.0.2", "react-tooltip": "^5.15.0", "uuid": "^9.0.0", "vex-js": "^4.1.0" From 2e76e3645e680d2a1d1881f3e54b0220e2a79c2c Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Tue, 6 Aug 2024 09:40:03 +0300 Subject: [PATCH 05/68] Tweak styling, add padding to settings button --- src/renderer/components/App.css | 3 +++ .../CostBenefitAnalysis/CostBenefitAnalysis.css | 1 + .../components/HelmetProject/HelmetProject.css | 13 +++++++++---- .../components/HelmetProject/HelmetProject.jsx | 8 ++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/renderer/components/App.css b/src/renderer/components/App.css index 596d7b0c..89233c87 100644 --- a/src/renderer/components/App.css +++ b/src/renderer/components/App.css @@ -83,6 +83,9 @@ hr { background-color: #ffffff; background-image: url('Settings.png'); background-repeat: no-repeat; + background-position: center; + padding: 0.5rem; + border-radius: 1rem; } .App__header { diff --git a/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css b/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css index 050ed788..2b003809 100644 --- a/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css +++ b/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css @@ -5,6 +5,7 @@ box-sizing: border-box; padding: 20px; margin-bottom: 20px; + margin-top: 3rem; } .CBA__heading { diff --git a/src/renderer/components/HelmetProject/HelmetProject.css b/src/renderer/components/HelmetProject/HelmetProject.css index 4c9fd86d..f070c6b4 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.css +++ b/src/renderer/components/HelmetProject/HelmetProject.css @@ -15,6 +15,7 @@ display: flex; justify-content: flex-start; list-style: none; + list-style-position: inside; width: 100%; height: 5rem; background-color: #F4F4F5; @@ -22,20 +23,24 @@ .tab-list-item { width: 12rem; - height: 2rem; + height: 100%; display: flex; align-content: center; justify-content: center; - color: lightgray; + color: #333333; + border-radius: 1rem 1rem 0 0; + background-color: white; + opacity: 0.65; + margin-left: 5px; } .tab-item-name { - margin-top: 2rem; + padding-top: 2rem; } .selected-tab { background-color: white; height: 100%; color: #333333; - border-radius: 1rem 1rem 0 0; + opacity: 1; } \ No newline at end of file diff --git a/src/renderer/components/HelmetProject/HelmetProject.jsx b/src/renderer/components/HelmetProject/HelmetProject.jsx index 6d5d43f1..12ce9f14 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.jsx +++ b/src/renderer/components/HelmetProject/HelmetProject.jsx @@ -386,11 +386,11 @@ const HelmetProject = ({
- -

Scenarios

+ + Scenarios - -

CBA

+ + CBA
From 9b7ba35dd324cebb385c6a044d19b9a9ad40a42d Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:48:02 +0300 Subject: [PATCH 06/68] Tweak tab styling, change tab titles --- .../components/HelmetProject/HelmetProject.css | 14 ++++++++++++-- .../components/HelmetProject/HelmetProject.jsx | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/renderer/components/HelmetProject/HelmetProject.css b/src/renderer/components/HelmetProject/HelmetProject.css index f070c6b4..33d2f717 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.css +++ b/src/renderer/components/HelmetProject/HelmetProject.css @@ -23,7 +23,7 @@ .tab-list-item { width: 12rem; - height: 100%; + height: 60%; display: flex; align-content: center; justify-content: center; @@ -32,15 +32,25 @@ background-color: white; opacity: 0.65; margin-left: 5px; + cursor: default; } .tab-item-name { padding-top: 2rem; } +.tab-item-name:hover { + border-bottom: 2px solid #333333b5; +} + .selected-tab { background-color: white; - height: 100%; + height: 60%; color: #333333; opacity: 1; + outline: none; +} + +.selected-tab:hover { + border-bottom: 2px solid #3333337e; } \ No newline at end of file diff --git a/src/renderer/components/HelmetProject/HelmetProject.jsx b/src/renderer/components/HelmetProject/HelmetProject.jsx index 12ce9f14..5a06be50 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.jsx +++ b/src/renderer/components/HelmetProject/HelmetProject.jsx @@ -387,7 +387,7 @@ const HelmetProject = ({ - Scenarios + Skenaariot CBA From ea6b0f0c78ddad150403c5572adffd624e23e927 Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:31:59 +0300 Subject: [PATCH 07/68] Hide X overflow so we don't get a horizontal scroll bar --- src/renderer/components/App.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/components/App.css b/src/renderer/components/App.css index 89233c87..0428867c 100644 --- a/src/renderer/components/App.css +++ b/src/renderer/components/App.css @@ -10,6 +10,7 @@ body { background-color: #F4F4F5; font-size: 18px; font-family: gotham_rounded_medium, sans-serif; + overflow-x: hidden; } button { From 24e746cd0b8087bbf0c8ca40a98dbe61abfa41c4 Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:19:17 +0300 Subject: [PATCH 08/68] Tweak CBA tab styling --- .../HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css b/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css index 2b003809..39ddff53 100644 --- a/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css +++ b/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css @@ -1,7 +1,5 @@ .CBA { - background: #F4F4F5; - border-top: 1px solid #CCCCCC; - border-bottom: 1px solid #CCCCCC; + background: #FFFFFF; box-sizing: border-box; padding: 20px; margin-bottom: 20px; From 04e28bf0a5e4ff4c672a1b97850981804b5a8ef4 Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:37:34 +0300 Subject: [PATCH 09/68] Tweak styles, remove empty CSS classes --- .../CostBenefitAnalysis/CostBenefitAnalysis.css | 5 +---- .../components/HelmetProject/HelmetProject.jsx | 2 +- .../components/HelmetProject/Runtime/Runtime.css | 12 +----------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css b/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css index 39ddff53..bc569ee2 100644 --- a/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css +++ b/src/renderer/components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css @@ -3,7 +3,7 @@ box-sizing: border-box; padding: 20px; margin-bottom: 20px; - margin-top: 3rem; + margin-top: 1rem; } .CBA__heading { @@ -65,9 +65,6 @@ padding: 0; } -.CBA__run { -} - .CBA__run button { margin-top: 20px; } \ No newline at end of file diff --git a/src/renderer/components/HelmetProject/HelmetProject.jsx b/src/renderer/components/HelmetProject/HelmetProject.jsx index 5a06be50..5ebe91d0 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.jsx +++ b/src/renderer/components/HelmetProject/HelmetProject.jsx @@ -394,7 +394,7 @@ const HelmetProject = ({ - + _loadProjectScenarios(projectPath)} diff --git a/src/renderer/components/HelmetProject/Runtime/Runtime.css b/src/renderer/components/HelmetProject/Runtime/Runtime.css index 6fddc688..494f0f30 100644 --- a/src/renderer/components/HelmetProject/Runtime/Runtime.css +++ b/src/renderer/components/HelmetProject/Runtime/Runtime.css @@ -1,6 +1,3 @@ -.Runtime { -} - /* Helmet project */ .Runtime__helmet-project-controls { @@ -67,8 +64,6 @@ width: 24px !important; height: 24px !important; } -.Runtime__scenario-activate-checkbox--active { -} .Runtime__scenario-open-config { display: inline-block; @@ -126,15 +121,10 @@ .Runtime__start-stop-controls { padding: 20px; -} - -.Runtime__start-stop-description { + background-color: #FFFFFF; } .Runtime__start-stop-scenarios { color: #00985F; font-weight: bold; -} - -.Runtime__start-stop-btn { } \ No newline at end of file From 2564e63e7576eb82b6a641662157908abc346d25 Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:20:05 +0300 Subject: [PATCH 10/68] WIP: #205 better support for multiple EMME python versions --- src/renderer/components/App.jsx | 21 +++++++++- src/renderer/components/Settings/Settings.css | 28 ++++++++++++++ src/renderer/components/Settings/Settings.jsx | 33 ++++++++++++---- src/renderer/search_emme_pythonpath.js | 38 +++++++++++++++++++ 4 files changed, 111 insertions(+), 9 deletions(-) diff --git a/src/renderer/components/App.jsx b/src/renderer/components/App.jsx index 5cd09c0e..88e1ac3c 100644 --- a/src/renderer/components/App.jsx +++ b/src/renderer/components/App.jsx @@ -8,13 +8,13 @@ const {execSync} = require('child_process'); const path = require('path'); // vex-js imported globally in index.html, since we cannot access webpack config in electron-forge - const App = ({helmetUIVersion, versions, searchEMMEPython}) => { // Global settings const [isSettingsOpen, setSettingsOpen] = useState(false); // whether Settings -dialog is open const [isProjectRunning, setProjectRunning] = useState(false); // whether currently selected Project is running const [emmePythonPath, setEmmePythonPath] = useState(undefined); // file path to EMME python executable + const [emmePythonEnvs, setEmmePythonEnvs] = useState([]); // List of all discovered python executables const [helmetScriptsPath, setHelmetScriptsPath] = useState(undefined); // folder path to HELMET model system scripts const [projectPath, setProjectPath] = useState(undefined); // folder path to scenario configs, default homedir const [basedataPath, setBasedataPath] = useState(undefined); // folder path to base input data (subdirectories: 2016_zonedata, 2016_basematrices) @@ -30,6 +30,20 @@ const App = ({helmetUIVersion, versions, searchEMMEPython}) => { globalSettingsStore.current.set('emme_python_path', newPath); }; + const _setEMMEPythonEnvs = (newList) => { + setEmmePythonEnvs(newList); + globalSettingsStore.current.set('emme_python_envs', newList); + } + + const _removeFromEMMEPythonEnvs = (path) => { + const pythonEnvs = emmePythonEnvs.slice(); + const foundPath = pythonEnvs.indexOf(path); + if (foundPath > -1) { + pythonEnvs.splice(foundPath,1); + _setEMMEPythonEnvs(pythonEnvs); + } + } + const _setHelmetScriptsPath = (newPath) => { // Cannot use state variable since it'd be undefined at times const pythonPath = globalSettingsStore.current.get('emme_python_path'); @@ -121,11 +135,13 @@ const App = ({helmetUIVersion, versions, searchEMMEPython}) => { const existingProjectPath = globalSettingsStore.current.get('project_path'); const existingBasedataPath = globalSettingsStore.current.get('basedata_path'); const existingResultsPath = globalSettingsStore.current.get('resultdata_path'); + const existingPythonEnvs = globalSettingsStore.current.get('emme_python_envs'); setEmmePythonPath(existingEmmePythonPath); setHelmetScriptsPath(existingHelmetScriptsPath); setProjectPath(existingProjectPath); setBasedataPath(existingBasedataPath); setResultsPath(existingResultsPath); + setEmmePythonEnvs(existingPythonEnvs); // If project path is the initial (un-set), set it to homedir. Remember: state updates async so refer to existing. if (!existingProjectPath) { @@ -159,6 +175,7 @@ const App = ({helmetUIVersion, versions, searchEMMEPython}) => {
{ isDownloadingHelmetScripts={isDownloadingHelmetScripts} closeSettings={() => setSettingsOpen(false)} setEMMEPythonPath={_setEMMEPythonPath} + setEMMEPythonEnvs={_setEMMEPythonEnvs} + removeFromEMMEPythonEnvs={_removeFromEMMEPythonEnvs} setHelmetScriptsPath={_setHelmetScriptsPath} setProjectPath={_setProjectPath} setBasedataPath={_setBasedataPath} diff --git a/src/renderer/components/Settings/Settings.css b/src/renderer/components/Settings/Settings.css index 67aa77f2..391cb18f 100644 --- a/src/renderer/components/Settings/Settings.css +++ b/src/renderer/components/Settings/Settings.css @@ -99,3 +99,31 @@ height: 40px; font-size: 16px; } + +.Settings__environment_option { + display: flex; + width: 100%; + padding: 10px 12px; + margin-top: 4px; + background: #ffffff; + box-sizing: border-box; + border-radius: 5px; + font-size: 16px; + font-weight: 325; + color: #333333; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-family: inherit; + min-height: 2rem; + align-items: center; + justify-items: center; + flex-wrap: wrap; +} + +.Settings__env_option_btn { + width: 4rem; + height: 2.5rem; + border-radius: 5px; + margin-left: 1.5rem; +} diff --git a/src/renderer/components/Settings/Settings.jsx b/src/renderer/components/Settings/Settings.jsx index 6a92a00b..177916be 100644 --- a/src/renderer/components/Settings/Settings.jsx +++ b/src/renderer/components/Settings/Settings.jsx @@ -1,11 +1,23 @@ import React from 'react'; import path from "path"; const {dialog} = require('@electron/remote'); -import {searchEMMEPython} from './search_emme_pythonpath'; import versions from '../versions'; +import { listEMMEPythonPaths } from './search_emme_pythonpath'; + +const EnvironmentOption = ({ + envPath, isSelected, setPath, removePath, +}) => { + return ( +
+

{envPath}

+ + +
+ ) +} const Settings = ({ - emmePythonPath, setEMMEPythonPath, + emmePythonPath, emmePythonEnvs, setEMMEPythonPath, setEMMEPythonEnvs, removeFromEMMEPythonEnvs, helmetScriptsPath, setHelmetScriptsPath, dlHelmetScriptsVersion, isDownloadingHelmetScripts, projectPath, setProjectPath, basedataPath, setBasedataPath, @@ -24,7 +36,12 @@ const Settings = ({
Projektin asetukset
- Emme Python v3.7 + Valittu Python-ympäristö: {emmePythonPath} + Käytettävät Python-ympäristöt: + { emmePythonEnvs.map(env => { return }) } +
@@ -46,18 +63,18 @@ const Settings = ({ }) }} /> +
diff --git a/src/renderer/search_emme_pythonpath.js b/src/renderer/search_emme_pythonpath.js index b6c1a654..dc3ef21a 100644 --- a/src/renderer/search_emme_pythonpath.js +++ b/src/renderer/search_emme_pythonpath.js @@ -41,6 +41,43 @@ const searchEMMEPython = () => { } }; +const listEMMEPythonPaths = () => { + // Set Windows' python exe path postfix (e.g. Python27\python.exe) + const p = getVersion(versions.emme_python); + const pythonPathPostfix = `Python${p.major}${p.minor}\\python.exe`; + + // Search from environment variable "EMMEPATH" + const envEmmePath = process.env.EMMEPATH || ''; + const envEmmePythonPath = path.join(envEmmePath, pythonPathPostfix); + if (envEmmePath && fs.existsSync(envEmmePythonPath)) { + return [true, envEmmePythonPath]; + } + + // Not found based on EMMEPATH, try guessing some common locations on Windows + const e = getVersion(versions.emme_system); + const pythonVersion = getVersion(versions.emme_python); + const commonEmmePath = `INRO\\Emme\\Emme ${e.major}\\Emme-${e.semver}`; + const drives = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:', 'I:', 'J:', '/']; + const paths = [ + `\\Program Files\\${commonEmmePath}\\${pythonPathPostfix}`, + `\\Program Files (x86)\\${commonEmmePath}\\${pythonPathPostfix}`, + `\\${commonEmmePath}\\${pythonPathPostfix}`, + `usr/bin/python${pythonVersion.major}`, // mainly for developers on Mac & Linux + `Users/erkki/pyyttoni/testi/testikansio/testiloremipsum2.34.5`, + ]; + const allPathCombinations = drives.reduce( + (accumulator, d) => { + // Combine each (d)rive to all (p)aths, and merge results via reduce + return accumulator.concat(paths.map((p) => `${d}${p}`)); + }, []); + const pythonInstallations = allPathCombinations.filter(fs.existsSync); + if (pythonInstallations.length > 0) { + return [true, pythonInstallations]; + } else { + return [false, null]; + } +} + /** * Dissect given semantic version number string */ @@ -56,4 +93,5 @@ function getVersion(semver) { module.exports = { searchEMMEPython: searchEMMEPython, + listEMMEPythonPaths: listEMMEPythonPaths, }; From 0cae73a080dec52b4730ac5151869cde8a816684 Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:02:57 +0300 Subject: [PATCH 11/68] Add manual path adding to python envs list --- src/renderer/components/App.jsx | 10 ++++ src/renderer/components/Settings/Settings.css | 15 ++++- src/renderer/components/Settings/Settings.jsx | 58 ++++++++++--------- src/renderer/search_emme_pythonpath.js | 1 - 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/renderer/components/App.jsx b/src/renderer/components/App.jsx index 88e1ac3c..8d365064 100644 --- a/src/renderer/components/App.jsx +++ b/src/renderer/components/App.jsx @@ -44,6 +44,15 @@ const App = ({helmetUIVersion, versions, searchEMMEPython}) => { } } + const _addToEMMEPythonEnvs = (path) => { + const pythonEnvs = emmePythonEnvs.slice(); + const foundPath = pythonEnvs.indexOf(path); + if (foundPath === -1) { + pythonEnvs.push(path); + _setEMMEPythonEnvs(pythonEnvs); + } + } + const _setHelmetScriptsPath = (newPath) => { // Cannot use state variable since it'd be undefined at times const pythonPath = globalSettingsStore.current.get('emme_python_path'); @@ -185,6 +194,7 @@ const App = ({helmetUIVersion, versions, searchEMMEPython}) => { closeSettings={() => setSettingsOpen(false)} setEMMEPythonPath={_setEMMEPythonPath} setEMMEPythonEnvs={_setEMMEPythonEnvs} + addToEMMEPythonEnvs={_addToEMMEPythonEnvs} removeFromEMMEPythonEnvs={_removeFromEMMEPythonEnvs} setHelmetScriptsPath={_setHelmetScriptsPath} setProjectPath={_setProjectPath} diff --git a/src/renderer/components/Settings/Settings.css b/src/renderer/components/Settings/Settings.css index 391cb18f..04124d77 100644 --- a/src/renderer/components/Settings/Settings.css +++ b/src/renderer/components/Settings/Settings.css @@ -108,7 +108,7 @@ background: #ffffff; box-sizing: border-box; border-radius: 5px; - font-size: 16px; + font-size: 12px; font-weight: 325; color: #333333; white-space: nowrap; @@ -123,7 +123,18 @@ .Settings__env_option_btn { width: 4rem; - height: 2.5rem; + height: 2rem; border-radius: 5px; margin-left: 1.5rem; + font-size: 12px; +} + +.Settings__env_btn_disabled { + color: #888888; +} + +.Settings__env_option_divider { + width: 100%; + height: 1rem; + border-bottom: 1px solid #CCCCCC; } diff --git a/src/renderer/components/Settings/Settings.jsx b/src/renderer/components/Settings/Settings.jsx index 177916be..59793305 100644 --- a/src/renderer/components/Settings/Settings.jsx +++ b/src/renderer/components/Settings/Settings.jsx @@ -3,6 +3,7 @@ import path from "path"; const {dialog} = require('@electron/remote'); import versions from '../versions'; import { listEMMEPythonPaths } from './search_emme_pythonpath'; +import classNames from 'classnames'; const EnvironmentOption = ({ envPath, isSelected, setPath, removePath, @@ -10,14 +11,16 @@ const EnvironmentOption = ({ return (

{envPath}

- +
) } +const PathOptionDivider = () =>
+ const Settings = ({ - emmePythonPath, emmePythonEnvs, setEMMEPythonPath, setEMMEPythonEnvs, removeFromEMMEPythonEnvs, + emmePythonPath, emmePythonEnvs, setEMMEPythonPath, setEMMEPythonEnvs, addToEMMEPythonEnvs, removeFromEMMEPythonEnvs, helmetScriptsPath, setHelmetScriptsPath, dlHelmetScriptsVersion, isDownloadingHelmetScripts, projectPath, setProjectPath, basedataPath, setBasedataPath, @@ -36,34 +39,32 @@ const Settings = ({
Projektin asetukset
- Valittu Python-ympäristö: {emmePythonPath} Käytettävät Python-ympäristöt: - { emmePythonEnvs.map(env => { return { return ( +
+ }) } -
- - { - dialog.showOpenDialog({ - defaultPath: emmePythonPath ? emmePythonPath : path.resolve('/'), - filters: [ - { name: 'Executable', extensions: ['exe'] }, - { name: 'All Files', extensions: ['*'] } - ], - properties: ['openFile'] - }).then((e)=>{ - if (!e.canceled) { - setEMMEPythonPath(e.filePaths[0]); - } - }) - }} - /> - + removePath={removeFromEMMEPythonEnvs}/> + { index < emmePythonEnvs.length && } +
)})} +
+
Helmet-model-system {isDownloadingHelmetScripts ? diff --git a/src/renderer/search_emme_pythonpath.js b/src/renderer/search_emme_pythonpath.js index dc3ef21a..f30122ec 100644 --- a/src/renderer/search_emme_pythonpath.js +++ b/src/renderer/search_emme_pythonpath.js @@ -63,7 +63,6 @@ const listEMMEPythonPaths = () => { `\\Program Files (x86)\\${commonEmmePath}\\${pythonPathPostfix}`, `\\${commonEmmePath}\\${pythonPathPostfix}`, `usr/bin/python${pythonVersion.major}`, // mainly for developers on Mac & Linux - `Users/erkki/pyyttoni/testi/testikansio/testiloremipsum2.34.5`, ]; const allPathCombinations = drives.reduce( (accumulator, d) => { From 87853a4c26dada5474550f150fb82af5fccd8227 Mon Sep 17 00:00:00 2001 From: e-halinen <54105602+e-halinen@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:19:37 +0300 Subject: [PATCH 12/68] Add null check --- src/renderer/components/Settings/Settings.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/Settings/Settings.jsx b/src/renderer/components/Settings/Settings.jsx index 59793305..023b3ff9 100644 --- a/src/renderer/components/Settings/Settings.jsx +++ b/src/renderer/components/Settings/Settings.jsx @@ -40,13 +40,13 @@ const Settings = ({
Projektin asetukset
Käytettävät Python-ympäristöt: - { emmePythonEnvs.map((env, index) => { return ( + { emmePythonEnvs && (emmePythonEnvs.map((env, index) => { return (
{ index < emmePythonEnvs.length && } -
)})} +
)}))}
diff --git a/src/renderer/components/HelmetProject/Runtime/Runtime.jsx b/src/renderer/components/HelmetProject/Runtime/Runtime.jsx index 16e8eb0f..5f032179 100644 --- a/src/renderer/components/HelmetProject/Runtime/Runtime.jsx +++ b/src/renderer/components/HelmetProject/Runtime/Runtime.jsx @@ -51,7 +51,6 @@ const Runtime = ({ const runningScenario = activeScenarios.filter((scenario) => scenario.id === runningScenarioID); const getResultsPathFromLogfilePath = (logfilePath) => { - console.log(logfilePath.replace(/\/[^\/]+$/, '')); return logfilePath.replace(/\/[^\/]+$/, ''); } @@ -147,7 +146,6 @@ const Runtime = ({ {scenarios.map((s) => { // Component for the tooltip showing scenario settings const tooltipContent = (scenario) => { - console.log(scenario); const filteredScenarioSettings = _.pickBy(scenario, (settingValue, settingKey) => { return visibleTooltipProperties.includes(settingKey); }) diff --git a/src/renderer/components/Settings/Settings.css b/src/renderer/components/Settings/Settings.css index 04124d77..fdb6c9c8 100644 --- a/src/renderer/components/Settings/Settings.css +++ b/src/renderer/components/Settings/Settings.css @@ -100,11 +100,20 @@ font-size: 16px; } +.Settings__python-env-input-btn { + margin: 10px auto 0 auto; + height: 40px; + width: 300px; + font-size: 16px; +} + +.Settings__env_option_remove { + color: #c92c00; +} + .Settings__environment_option { display: flex; width: 100%; - padding: 10px 12px; - margin-top: 4px; background: #ffffff; box-sizing: border-box; border-radius: 5px; @@ -121,20 +130,56 @@ flex-wrap: wrap; } -.Settings__env_option_btn { +.Settings__env_option_text { + width: 100%; +} + +.Settings__emme_version_group { + display: flex; + width: 100%; +} + +.Settings__emme_edit_btn { + height: 2rem; + margin-left: .5rem; + font-size: 12px; + border: none; + margin-top: 6px; +} + +.Settings__emme_edit_save_btn { width: 4rem; height: 2rem; + font-size: 12px; + border: none; + margin-top: 6px; +} + +.Settings__emme_input { + width: 100%; + height: 5rem; border-radius: 5px; + background-color: white; +} + +.Settings__env_option_btn { + width: 4rem; + height: 1rem; margin-left: 1.5rem; + margin-bottom: 5px; font-size: 12px; + border: none; } -.Settings__env_btn_disabled { +.Settings__btn_disabled { + color: #888888; +} + +.Settings__btn_disabled:hover { color: #888888; } .Settings__env_option_divider { width: 100%; - height: 1rem; border-bottom: 1px solid #CCCCCC; } diff --git a/src/renderer/components/Settings/Settings.jsx b/src/renderer/components/Settings/Settings.jsx index 023b3ff9..338bdd76 100644 --- a/src/renderer/components/Settings/Settings.jsx +++ b/src/renderer/components/Settings/Settings.jsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import path from "path"; const {dialog} = require('@electron/remote'); -import versions from '../versions'; +const versions = require('../versions'); import { listEMMEPythonPaths } from './search_emme_pythonpath'; import classNames from 'classnames'; @@ -10,13 +10,46 @@ const EnvironmentOption = ({ }) => { return (
-

{envPath}

- - +

{envPath}

+ +
) } +const EmmeVersionEdit = ({ + emmeVersion, setEmmeVersion, closeEditing +}) => { + + const [savingDisabled, setSavingDisabled] = useState(true); + const [newEmmeVersion, setNewEmmeVersion] = useState(''); + const VERSION_NUMBER_REGEX = RegExp(/^(\d+\.)?(\d+\.)?(\*|\d+)$/); + const validateVersionNumber = (e) => { + const regExpResults = e.target.value.match(VERSION_NUMBER_REGEX); + return regExpResults !== null; + } + + return ( +
+ Syötä uusi EMME versio: + { + + setNewEmmeVersion(e.target.value); + const validationPasses = validateVersionNumber(e); + setSavingDisabled(!validationPasses); + }}> + +
+ ) +}; + const PathOptionDivider = () =>
const Settings = ({ @@ -26,8 +59,11 @@ const Settings = ({ basedataPath, setBasedataPath, resultsPath, setResultsPath, closeSettings, - promptModelSystemDownload, + promptModelSystemDownload, emmeVersion, setEmmeVersion, }) => { + + const [showEmmeDialog, setShowEmmeDialog] = useState(false); + return (
@@ -38,6 +74,20 @@ const Settings = ({
closeSettings()}>
Projektin asetukset
+ +
+ {`EMME-versio: ${emmeVersion}`} + +
+ { showEmmeDialog && setShowEmmeDialog(false)} /> }
Käytettävät Python-ympäristöt: { emmePythonEnvs && (emmePythonEnvs.map((env, index) => { return ( @@ -47,7 +97,7 @@ const Settings = ({ removePath={removeFromEMMEPythonEnvs}/> { index < emmePythonEnvs.length && }
)}))} - - -
- ) -}; - const PathOptionDivider = () =>
const Settings = ({ @@ -59,11 +26,9 @@ const Settings = ({ basedataPath, setBasedataPath, resultsPath, setResultsPath, closeSettings, - promptModelSystemDownload, emmeVersion, setEmmeVersion, + promptModelSystemDownload, }) => { - const [showEmmeDialog, setShowEmmeDialog] = useState(false); - return (
@@ -75,19 +40,6 @@ const Settings = ({
Projektin asetukset
-
- {`EMME-versio: ${emmeVersion}`} - -
- { showEmmeDialog && setShowEmmeDialog(false)} /> }
Käytettävät Python-ympäristöt: { Array.isArray(emmePythonEnvs) && emmePythonEnvs.length > 0 && (emmePythonEnvs.map((env, index) => { return ( @@ -117,12 +69,12 @@ const Settings = ({
+ + setModalOpen(false)} + onSubmit={handleModalSubmit} + title={modalOptions.title} + > + + +
) -}; +}; export default App; \ No newline at end of file diff --git a/src/renderer/components/HelmetProject/HelmetProject.jsx b/src/renderer/components/HelmetProject/HelmetProject.jsx index 434e1aed..4f008d05 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.jsx +++ b/src/renderer/components/HelmetProject/HelmetProject.jsx @@ -5,11 +5,9 @@ import Runtime from './Runtime/Runtime.jsx'; import HelmetScenario from './HelmetScenario/HelmetScenario.jsx'; import RunLog from './RunLog/RunLog.jsx'; import CostBenefitAnalysis from './CostBenefitAnalysis/CostBenefitAnalysis.jsx'; +import Modal from '../Modal/Modal'; // Import the custom modal component -const {ipcRenderer, fs, path} = window.electronAPI; - - -// vex-js imported globally in index.html, since we cannot access webpack config in electron-forge +const { ipcRenderer, fs, path } = window.electronAPI; const HelmetProject = ({ emmePythonPath, helmetScriptsPath, projectPath, basedataPath, resultsPath, @@ -46,7 +44,11 @@ const HelmetProject = ({ const [cbaOptions, setCbaOptions] = useState({}); // Scenario-specific settings under currently selected HELMET Project - const configStores = useRef({}); + const configStores = useRef(new Map()); // Use a Map to manage scenario-specific stores + + const [isModalOpen, setModalOpen] = useState(false); + const [modalError, setModalError] = useState(''); + const [newScenarioName, setNewScenarioName] = useState(''); const _handleClickScenarioToActive = (scenario) => { if(scenarioIDsToRun.includes(scenario.id)) { @@ -59,34 +61,22 @@ const HelmetProject = ({ }; const _handleClickNewScenario = () => { - ipcRenderer.send('focus-fix'); - const promptCreation = (previousError) => { - vex.dialog.prompt({ - message: (previousError ? previousError : "") + "Anna uuden skenaarion nimike:", - placeholder: '', - callback: (inputScenarioName) => { - let error = ""; - // Check for cancel button press - if (inputScenarioName === false) { - return; - } - // Check input for initial errors - if (inputScenarioName === "") { - error = "Nimike on pakollinen, tallennettavaa tiedostonime\u00e4 varten. "; - } - if (scenarios.map((s) => s.name).includes(inputScenarioName)) { - error = "Nimike on jo olemassa, valitse toinen nimi tai poista olemassa oleva ensin. "; - } - // Handle recursively any input errors (mandated by the async library since prompt isn't natively supported in Electron) - if (error) { - promptCreation(error); - } else { - _createScenario(inputScenarioName); - } - } - }); - }; - promptCreation(); + setModalOpen(true); + setModalError(''); + setNewScenarioName(''); + }; + + const handleModalSubmit = () => { + if (!newScenarioName.trim()) { + setModalError('Nimike on pakollinen, tallennettavaa tiedostonimeä varten.'); + return; + } + if (scenarios.map((s) => s.name).includes(newScenarioName.trim())) { + setModalError('Nimike on jo olemassa, valitse toinen nimi tai poista olemassa oleva ensin.'); + return; + } + _createScenario(newScenarioName.trim()); + setModalOpen(false); }; const _handleClickStartStop = () => { @@ -98,57 +88,64 @@ const HelmetProject = ({ const _loadProjectScenarios = async (projectFilepath) => { const configPath = projectFilepath; - const files = await fs.readdir(configPath); - - // Filter only .json files - const jsonFiles = files.filter(f => f.endsWith(".json")); - - // Read & parse all JSON files in parallel - const foundScenarios = ( - await Promise.all( - jsonFiles.map(async (fileName) => { - try { - const filePath = path.join(configPath, fileName); - const content = await fs.readFile(filePath); - const obj = JSON.parse(content); - - if ( - "id" in obj && - "name" in obj && - "use_fixed_transit_cost" in obj && - "iterations" in obj - ) { - // Initialize scenario-specific Store - configStores.current[obj.id] = window.electronAPI.StoreAPI.create({ - cwd: configPath, - name: fileName.slice(0, -5), - }); - - return obj; // include valid scenario + + try { + const files = await fs.readdir(configPath); + + // Filter only .json files + const jsonFiles = files.filter((f) => f.endsWith('.json')); + + // Read & parse all JSON files in parallel + const foundScenarios = ( + await Promise.all( + jsonFiles.map(async (fileName) => { + try { + const filePath = path.join(configPath, fileName); + const content = await fs.readFile(filePath); + const obj = JSON.parse(content); + + if ( + "id" in obj && + "name" in obj && + "use_fixed_transit_cost" in obj && + "iterations" in obj + ) { + // Initialize scenario-specific namespace in the shared store + const namespace = `${configPath}/${fileName.slice(0, -5)}`; + if (!configStores.current.has(namespace)) { + configStores.current.set(namespace, window.electronAPI.StoreAPI.getScenarioStore(namespace)); + } + + return obj; // include valid scenario + } else { + console.warn(`Invalid scenario structure in file: ${fileName}`); + } + } catch (err) { + console.error(`Failed to load scenario from ${fileName}:`, err); } - } catch (err) { - console.error(`Failed to load scenario from ${fileName}:`, err); - } - return null; // skip invalid or failed ones - }) - ) - ).filter(Boolean); // remove nulls - - // Add runStatus if missing (for backward compatibility with older scenarios) - const decoratedFoundScenarios = foundScenarios.map((scenario) => - scenario.runStatus === undefined - ? addRunStatusProperties(scenario) - : scenario - ); + return null; // skip invalid or failed ones + }) + ) + ).filter(Boolean); // remove nulls + + // Add runStatus if missing (for backward compatibility with older scenarios) + const decoratedFoundScenarios = foundScenarios.map((scenario) => + scenario.runStatus === undefined + ? addRunStatusProperties(scenario) + : scenario + ); - // Update UI state - setScenarios(decoratedFoundScenarios); - setOpenScenarioID(null); - setScenarioIDsToRun([]); - setRunningScenarioID(null); - setRunningScenarioIDsQueued([]); - setLogContents([]); - setLogOpened(false); + // Update UI state + setScenarios(decoratedFoundScenarios); + setOpenScenarioID(null); + setScenarioIDsToRun([]); + setRunningScenarioID(null); + setRunningScenarioIDsQueued([]); + setLogContents([]); + setLogOpened(false); + } catch (error) { + console.error(`Error loading project scenarios:`, error); + } }; const addRunStatusProperties = (scenario) => { @@ -169,14 +166,36 @@ const HelmetProject = ({ } } - const _createScenario = (newScenarioName) => { - // Generate new (unique) ID for new scenario + const _createScenario = async (newScenarioName) => { + // Generate new (unique) ID for the new scenario const newId = uuidv4(); + + // Extract the number at the beginning of the name, if it exists + const match = newScenarioName.match(/^(\d+)[ _]/); + const firstScenarioId = match ? parseInt(match[1], 10) : 1; + + // Check for .emp files in the project directory + let defaultEmmeProjectFilePath = null; + try { + const files = await fs.readdir(projectPath); + const empFiles = files.filter((file) => file.endsWith('.emp')); + if (empFiles.length === 1) { + defaultEmmeProjectFilePath = path.join(projectPath, empFiles[0]); + console.log(`Default .emp file found: ${defaultEmmeProjectFilePath}`); + } else if (empFiles.length > 1) { + console.log(`Multiple .emp files found. No default will be set.`); + } else { + console.log(`No .emp files found in the project directory.`); + } + } catch (error) { + console.error(`Error reading project directory for .emp files:`, error); + } + const newScenario = { id: newId, name: newScenarioName, - emme_project_file_path: null, - first_scenario_id: 1, + emme_project_file_path: defaultEmmeProjectFilePath, // Use the default .emp file if found + first_scenario_id: firstScenarioId, forecast_data_folder_path: null, delete_strategy_files: true, separate_emme_scenarios: false, @@ -207,29 +226,31 @@ const HelmetProject = ({ }; // Create the new scenario in "scenarios" array first setScenarios(scenarios.concat(newScenario)); - window.electronAPI.StoreAPI.set({ cwd: projectPath, name: newScenarioName }, null, newScenario); - // Then set scenario as open by id (Why id? Having open_scenario as reference causes sub-elements to be bugged because of different object reference) + const namespace = `${ projectPath }/${ newScenario.id }`; + const store = window.electronAPI.StoreAPI.getScenarioStore(namespace); + configStores.current.set(namespace, store); + store.set('scenario', newScenario); + // Then set scenario as open by id setOpenScenarioID(newId); }; - const _updateScenario = async (newValues) => { - // Update newValues to matching .id in this.state.scenarios - setScenarios(scenarios.map((s) => (s.id === newValues.id ? {...s, ...newValues} : s))); + const _updateScenario = async ( newValues ) => { + // Update newValues to matching . id in this.state.scenarios + setScenarios(scenarios.map((s) => (s.id === newValues.id ? { ... s, ... newValues } : s))); // If name changed, rename file and change reference - if (configStores.current[newValues.id].get('name') !== newValues.name) { + const namespace = `${ projectPath }/${ newValues.id }`; + const store = configStores.current.get(namespace); + if ( store.get('scenario').name !== newValues.name ) { // If name was set empty - use ID instead const newName = newValues.name ? newValues.name : newValues.id; await fs.rename( - configStores.current[newValues.id].path, + path.join(projectPath, `${store.get('scenario').name}.json`), path.join(projectPath, `${newName}.json`) ); - configStores.current[newValues.id] = window.electronAPI.StoreAPI.create({ - cwd: projectPath, - name: newName - }); + store.set('scenario', { ... newValues, name: newName }); } - // And persist all changes in file - configStores.current[newValues.id].set(newValues); + // And persist all changes in the shared store + store.set('scenario', newValues); }; const _deleteScenario = async (scenario) => { @@ -254,7 +275,9 @@ const HelmetProject = ({ } const _runAllActiveScenarios = (activeScenarioIDs) => { - const scenariosToRun = scenarios.filter((s) => activeScenarioIDs.includes(s.id)).sort((a, b) => scenarioIDsToRun.indexOf(a.id) - scenarioIDsToRun.indexOf(b.id)); + const scenariosToRun = scenarios + .filter((s) => activeScenarioIDs.includes(s.id)) + .sort((a, b) => scenarioIDsToRun.indexOf(a.id) - scenarioIDsToRun.indexOf(b.id)); // Check required global parameters are set if (!emmePythonPath) { @@ -280,7 +303,12 @@ const HelmetProject = ({ // For each active scenario, check required scenario-specific parameters are set for (let scenario of scenariosToRun) { - const store = configStores.current[scenario.id]; + const store = configStores.current.get(scenario.id); // Use the `get` method of the Map + if (!store) { + alert(`Store not found for scenario "${scenario.name}".`); + return; + } + const iterations = store.get('iterations'); if (!store.get('emme_project_file_path')) { alert(`Emme-projektia ei ole valittu skenaariossa "${scenario.name}"`); @@ -301,8 +329,8 @@ const HelmetProject = ({ setLogContents([ { level: "UI-event", - message: `Initializing run of scenarios: ${scenariosToRun.map((s) => s.name).join(', ')}` - } + message: `Initializing run of scenarios: ${scenariosToRun.map((s) => s.name).join(', ')}`, + }, ]); setLogOpened(true); // Keep log open even after run finishes (or is cancelled) setRunningScenarioID(activeScenarioIDs[0]); // Disable controls @@ -312,15 +340,30 @@ const HelmetProject = ({ 'message-from-ui-to-run-scenarios', scenariosToRun.map((s) => { // Run parameters per each run (enrich with global settings' paths to EMME python & HELMET model system - return { ...s, - emme_python_path: s.overriddenProjectSettings.emmePythonPath !== null && s.overriddenProjectSettings.emmePythonPath !== emmePythonPath ? s.overriddenProjectSettings.emmePythonPath : emmePythonPath, - helmet_scripts_path: s.overriddenProjectSettings.helmetScriptsPath !== null && s.overriddenProjectSettings.helmetScriptsPath !== helmetScriptsPath ? s.overriddenProjectSettings.helmetScriptsPath : helmetScriptsPath, - base_data_folder_path: s.overriddenProjectSettings.basedataPath !== null && s.overriddenProjectSettings.basedataPath !== basedataPath ? s.overriddenProjectSettings.basedataPath : basedataPath, - results_data_folder_path: s.overriddenProjectSettings.resultsPath !== null && s.overriddenProjectSettings.resultsPath !== resultsPath ? s.overriddenProjectSettings.resultsPath : resultsPath, + emme_python_path: + s.overriddenProjectSettings.emmePythonPath !== null && + s.overriddenProjectSettings.emmePythonPath !== emmePythonPath + ? s.overriddenProjectSettings.emmePythonPath + : emmePythonPath, + helmet_scripts_path: + s.overriddenProjectSettings.helmetScriptsPath !== null && + s.overriddenProjectSettings.helmetScriptsPath !== helmetScriptsPath + ? s.overriddenProjectSettings.helmetScriptsPath + : helmetScriptsPath, + base_data_folder_path: + s.overriddenProjectSettings.basedataPath !== null && + s.overriddenProjectSettings.basedataPath !== basedataPath + ? s.overriddenProjectSettings.basedataPath + : basedataPath, + results_data_folder_path: + s.overriddenProjectSettings.resultsPath !== null && + s.overriddenProjectSettings.resultsPath !== resultsPath + ? s.overriddenProjectSettings.resultsPath + : resultsPath, log_level: 'DEBUG', - } + }; }) ); }; @@ -480,6 +523,23 @@ const HelmetProject = ({ "" }
+ + setModalOpen(false)} + onSubmit={handleModalSubmit} + title="Uusi Helmet-skenaario" // Updated title + > + {/* Added label */} + setNewScenarioName(e.target.value)} + placeholder="Uuden skenaarion nimi" + /> + {modalError &&

{modalError}

} +
) }; diff --git a/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.jsx b/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.jsx index 505dde20..d8b5bda8 100644 --- a/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.jsx +++ b/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.jsx @@ -1,5 +1,5 @@ import React, {useState} from 'react'; -import classnames from 'classnames'; +import classNames from 'classnames'; import _ from 'lodash'; import { ArrowUp, ArrowDown, ResetIcon } from '../../../icons'; diff --git a/src/renderer/components/HelmetProject/Runtime/Runtime.jsx b/src/renderer/components/HelmetProject/Runtime/Runtime.jsx index e1845207..e71c7f90 100644 --- a/src/renderer/components/HelmetProject/Runtime/Runtime.jsx +++ b/src/renderer/components/HelmetProject/Runtime/Runtime.jsx @@ -8,42 +8,52 @@ import { SCENARIO_STATUS_STATE } from '../../../../enums.js'; const _ = window.electronAPI._; const Runtime = ({ - projectPath, scenarios, scenarioIDsToRun, runningScenarioID, openScenarioID, deleteScenario, + projectPath, + scenarios = [], // Default to an empty array + scenarioIDsToRun = [], // Default to an empty array + runningScenarioID, + openScenarioID, + deleteScenario, setOpenScenarioID, reloadScenarios, - handleClickScenarioToActive, handleClickNewScenario, - handleClickStartStop, logArgs, duplicateScenario, scenarioListHeight, setScenarioListHeight + handleClickScenarioToActive, + handleClickNewScenario, + handleClickStartStop, + logArgs, + duplicateScenario, + scenarioListHeight, + setScenarioListHeight, }) => { - const visibleTooltipProperties = [ - 'emme_project_file_path', - 'first_scenario_id', - 'first_matrix_id', - 'forecast_data_folder_path', - 'save_matrices_in_emme', - 'end_assignment_only', - 'delete_strategy_files', - 'id', - 'name', - 'iterations', - 'separate_emme_scenarios', - 'use_fixed_transit_cost', - 'overriddenProjectSettings' + 'emme_project_file_path', + 'first_scenario_id', + 'first_matrix_id', + 'forecast_data_folder_path', + 'save_matrices_in_emme', + 'end_assignment_only', + 'delete_strategy_files', + 'id', + 'name', + 'iterations', + 'separate_emme_scenarios', + 'use_fixed_transit_cost', + 'overriddenProjectSettings', ]; const areGlobalSettingsOverridden = (settings) => { - return _.filter(settings, settingValue => settingValue != null).length > 0; - } + return settings && Object.values(settings).some((value) => value != null); + }; const getPropertyForDisplayString = (settingProperty) => { - const [key, value] = settingProperty + const [key, value] = settingProperty; - if(typeof value === 'string') { - const trimmedStringValue = value.length > 30 ? "..." + value.substring(value.length-30) : value; - return `${key} : ${trimmedStringValue}` + if (typeof value === 'string') { + const trimmedStringValue = + value.length > 30 ? '...' + value.substring(value.length - 30) : value; + return `${key} : ${trimmedStringValue}`; } - return `${key} : ${value}` + return `${key} : ${value}`; }; const parseDemandConvergenceLogMessage = (message) => { @@ -51,12 +61,13 @@ const Runtime = ({ return { iteration: stringMsgArray[stringMsgArray.length - 3], value: stringMsgArray[stringMsgArray.length - 1]}; }; - const activeScenarios = scenarios.filter((scenario) => scenarioIDsToRun.includes(scenario.id)) - const runningScenario = activeScenarios.filter((scenario) => scenario.id === runningScenarioID); + const activeScenarios = Array.isArray(scenarios) + ? scenarios.filter((scenario) => scenarioIDsToRun.includes(scenario.id)) + : []; - const getResultsPathFromLogfilePath = (logfilePath) => { - return logfilePath.replace(/\/[^\/]+$/, ''); - } + const runningScenario = activeScenarios.find( + (scenario) => scenario.id === runningScenarioID + ); //Parse log contents into the currently running scenario so we can show each one individually const parseLogArgs = (runStatus, logArgs) => { @@ -88,42 +99,42 @@ const Runtime = ({ } } - if(runningScenario.length > 0) { + if( runningScenario && runningScenario.length > 0) { const runStatus = runningScenario[0].runStatus; parseLogArgs(runStatus, logArgs); } - const renderableScenarios = activeScenarios.map(activeScenario => { - if (activeScenario.id === runningScenario.id) { - return runningScenario; - } - return activeScenario; - }) + const renderableScenarios = activeScenarios.map((activeScenario) => { + if (activeScenario.id === runningScenario?.id) { + return runningScenario; + } + return activeScenario; + }); const RunStatusList = () => { - if(renderableScenarios.length > 0) { + if (renderableScenarios.length > 0) { return (
- { - renderableScenarios.map(scenarioToRender => { + {renderableScenarios.map((scenarioToRender) => { return ( ) - }) - } + statusIterationsTotal={scenarioToRender.runStatus?.statusIterationsTotal || 0} + statusIterationsCompleted={scenarioToRender.runStatus?.statusIterationsCompleted || 0} + statusReadyScenariosLogfiles={scenarioToRender.runStatus?.statusReadyScenariosLogfiles || []} + statusRunStartTime={scenarioToRender.runStatus?.statusRunStartTime || null} + statusRunFinishTime={scenarioToRender.runStatus?.statusRunFinishTime || null} + statusState={scenarioToRender.runStatus?.statusState || null} + demandConvergenceArray={scenarioToRender.runStatus?.demandConvergenceArray || []} + /> + ); + })}
- ) + ); } - return
- } + return
; + }; useEffect(() => { const resizableDiv = document.getElementById("resizableDiv"); @@ -184,86 +195,94 @@ const Runtime = ({
Ladatut skenaariot
- {/* Create table of all scenarios " " */} - {scenarios.map((s, index) => { - // Component for the tooltip showing scenario settings - const tooltipContent = (scenario) => { - const filteredScenarioSettings = _.pickBy(scenario, (settingValue, settingKey) => { - return visibleTooltipProperties.includes(settingKey); - }) - return ( -
- { - Object.entries(filteredScenarioSettings).map((property, index) => { - - if(property[0] === 'overriddenProjectSettings') { + {scenarios && scenarios.length > 0 ? ( + scenarios.map((s, index) => { + // Component for the tooltip showing scenario settings + const tooltipContent = (scenario) => { + const filteredScenarioSettings = Object.fromEntries( + Object.entries(scenario).filter(([key]) => visibleTooltipProperties.includes(key)) + ); - return areGlobalSettingsOverridden(property[1]) - ? + return ( +
+ {Object.entries(filteredScenarioSettings).map((property, index) => { + if (property[0] === 'overriddenProjectSettings') { + return areGlobalSettingsOverridden(property[1]) ? (

Overridden settings:

- { - Object.entries(property[1]).map((overrideSetting, index) => { - return overrideSetting[1] != null - ?

{getPropertyForDisplayString(overrideSetting)}

- : "" - }) - } + {Object.entries(property[1]).map((overrideSetting, index) => { + return overrideSetting[1] != null ? ( +

+ {getPropertyForDisplayString(overrideSetting)} +

+ ) : ( + '' + ); + })}
- : ""; // Return empty if global settings are all default + ) : ( + '' + ); } - return( -

{getPropertyForDisplayString(property)}

- )})} -
- ) - } + return

{getPropertyForDisplayString(property)}

; + })} +
+ ); + }; - return ( -
- - {s.name ? s.name : `Unnamed project (${s.id})`} - -   - handleClickScenarioToActive(s)} - /> -   -
runningScenarioID ? undefined : setOpenScenarioID(s.id)} - >
-   -
runningScenarioID ? undefined : deleteScenario(s)} - >
- - -   -
duplicateScenario(s)} + return ( +
- + + {s.name ? s.name : `Unnamed project (${s.id})`} + +   + handleClickScenarioToActive(s)} + /> +   +
(runningScenarioID ? undefined : setOpenScenarioID(s.id))} + >
+   +
(runningScenarioID ? undefined : deleteScenario(s))} + >
+ +   +
duplicateScenario(s)}> + +
- -
- ) - })} + ); + }) + ) : ( +

No scenarios available.

+ )}
+
+
{children}
+
+ + +
+
+
+ ); +}; + +export default Modal; diff --git a/src/renderer/components/Settings/Settings.css b/src/renderer/components/Settings/Settings.css index 6692a919..4ac44a62 100644 --- a/src/renderer/components/Settings/Settings.css +++ b/src/renderer/components/Settings/Settings.css @@ -199,3 +199,28 @@ width: 100%; border-bottom: 1px solid #CCCCCC; } + +.Settings__download-progress { + display: flex; + align-items: center; + gap: 10px; + margin-top: 10px; +} + +.Settings__download-progress progress { + width: 70%; + height: 16px; +} + +.Settings__download-progress button { + padding: 8px 16px; + background-color: #ff5f57; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.Settings__download-progress button:hover { + background-color: #e04e4b; +} diff --git a/src/renderer/components/Settings/Settings.jsx b/src/renderer/components/Settings/Settings.jsx index 8f477af7..7d1eef9b 100644 --- a/src/renderer/components/Settings/Settings.jsx +++ b/src/renderer/components/Settings/Settings.jsx @@ -34,7 +34,10 @@ const Settings = ({ resultsPath, setResultsPath, closeSettings, promptModelSystemDownload, - listEMMEPythonPaths + listEMMEPythonPaths, + getHelmetModelSystemVersion, + downloadProgress, + cancelDownload, }) => { const { setHelmetModelSystemVersion } = useHelmetModelContext(); @@ -77,7 +80,7 @@ const Settings = ({ @@ -109,34 +125,42 @@ const Settings = ({
Helmet-model-system - {isDownloadingHelmetScripts ? - - Downloading model-system {dlHelmetScriptsVersion === 'master' ? 'latest' : dlHelmetScriptsVersion}. . . - - : - - } - { - dialog.showOpenDialog({ - defaultPath: helmetScriptsPath ? helmetScriptsPath : projectPath, - properties: ['openDirectory'] - }).then((e) => { - if (!e.canceled) { - handleSetHelmetScriptsPath(e.filePaths[0]); - } - }); - }} + + { + dialog.showOpenDialog({ + defaultPath: helmetScriptsPath || projectPath, + properties: ['openDirectory'], + }).then((e) => { + if (!e.canceled) { + handleSetHelmetScriptsPath(e.filePaths[0]); + } + }); + }} /> - + {downloadProgress !== null ? ( +
+ + +
+ ) : ( + + )}
Projektin kansiopolku (oletusarvoisesti kotihakemistosi) @@ -146,15 +170,20 @@ const Settings = ({ { + onClick={() => { dialog.showOpenDialog({ defaultPath: projectPath ? projectPath : homedir, properties: ['openDirectory'] - }).then((e)=>{ + }).then((e) => { if (!e.canceled) { + console.log(`Setting projectPath to: ${e.filePaths[0]}`); setProjectPath(e.filePaths[0]); + } else { + console.log("Project path selection was canceled."); } - }) + }).catch((error) => { + console.error("Error selecting project path:", error); + }); }} />
diff --git a/src/renderer/search_emme_pythonpath.js b/src/renderer/search_emme_pythonpath.js index 78671122..78489244 100644 --- a/src/renderer/search_emme_pythonpath.js +++ b/src/renderer/search_emme_pythonpath.js @@ -1,8 +1,9 @@ import versions from '../versions'; +import _ from 'lodash'; const path = window.electronAPI.path; const fs = window.electronAPI.fs; -const _ = window.electronAPI._; + /** * Check and try to set EMME's Python location on Windows, searching from common known paths. @@ -57,20 +58,24 @@ const checkEmmePythonEnv = () => { } } -export function listEMMEPythonPaths(){ +export async function listEMMEPythonPaths() { const pythonVersion = getVersion(versions.emme_python); - const commonEmmePaths = [] - const pythonInstallations = [] + const commonEmmePaths = []; + const pythonInstallations = []; // Check if Windows python exe exists in environment variables const [hasPythonEnvInstallation, pythonEnvInstallation] = checkEmmePythonEnv(); - if(hasPythonEnvInstallation) { pythonInstallations.push(pythonEnvInstallation) }; + if (hasPythonEnvInstallation) { + pythonInstallations.push(pythonEnvInstallation); + } - versions.emme_major_versions.forEach(majorVersion => { commonEmmePaths.push(`INRO\\Emme\\Emme ${majorVersion}`) }) + versions.emme_major_versions.forEach(majorVersion => { + commonEmmePaths.push(`INRO\\Emme\\Emme ${majorVersion}`); + }); const drives = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:', 'I:', 'J:', '/']; - const paths = [] + const paths = []; commonEmmePaths.forEach(commonEmmePath => { paths.push( `Program Files\\${commonEmmePath}`, @@ -78,21 +83,25 @@ export function listEMMEPythonPaths(){ `Program Files (x86)\\Bentley\\OpenPaths`, `Program Files\\Bentley\\OpenPaths`, `${commonEmmePath}`, - ) - }) + ); + }); paths.push(`usr/bin/python${pythonVersion.major}`); // mainly for developers on Mac & Linux const allPathCombinations = drives.reduce( (accumulator, d) => { // Combine each (d)rive to all (p)aths, and merge results via reduce - return accumulator.concat(paths.map((p) => path.join(d,p))); - }, []); - allPathCombinations.forEach((pathCombination) => { - const foundPythonEnv = hasPythonEnv(pathCombination); - if (foundPythonEnv !== null) { - pythonInstallations.push(...foundPythonEnv); + return accumulator.concat(paths.map((p) => path.join(d, p))); + }, + [] + ); + + for (const pathCombination of allPathCombinations) { + const foundPythonEnv = await hasPythonEnv(pathCombination); // Await the result of hasPythonEnv + if (foundPythonEnv && Array.isArray(foundPythonEnv)) { + pythonInstallations.push(...foundPythonEnv); // Spread only if it's an array } - }); + } + if (pythonInstallations.length > 0) { // Filter out duplicates const filteredPythons = _.uniq(pythonInstallations).filter(filteredPath => { @@ -117,32 +126,31 @@ function getVersion(semver) { } } -function hasPythonEnv(basePath) { +async function hasPythonEnv(basePath) { const pathExists = fs.existsSync(basePath); let exePaths = []; if (pathExists) { try { - const subPaths = fs.readdirSync(basePath) - subPaths.forEach(subPath => { - if(subPath.startsWith("Emme") || subPath.startsWith("EMME")) { + const subPaths = await fs.readdir(basePath); + for (const subPath of subPaths) { + if (subPath.startsWith("Emme") || subPath.startsWith("EMME")) { const emmeVersionFolder = path.join(basePath, subPath); - const emmeVersionFolderContents = fs.readdirSync(emmeVersionFolder); - emmeVersionFolderContents.forEach(emmeFolderPath => { - if(emmeFolderPath.startsWith("Python")) { + const emmeVersionFolderContents = await fs.readdir(emmeVersionFolder); + for (const emmeFolderPath of emmeVersionFolderContents) { + if (emmeFolderPath.startsWith("Python")) { const pythonFolderPath = path.join(emmeVersionFolder, emmeFolderPath); - const pythonPathFiles = fs.readdirSync(pythonFolderPath); - pythonPathFiles.forEach(fileName => { - if(fileName === 'python.exe') { + const pythonPathFiles = await fs.readdir(pythonFolderPath); + for (const fileName of pythonPathFiles) { + if (fileName === 'python.exe') { const pythonExecutablePath = path.join(pythonFolderPath, fileName); exePaths.push(pythonExecutablePath); } - }) + } } - }) + } } - }) - } - catch(e) { + } + } catch (e) { console.log(`Error traversing path ${basePath}`); console.log(e); } From 13c9c7dd864e567d9c852f9fc604b6e8cbac9ddd Mon Sep 17 00:00:00 2001 From: Santeri Hiitola Date: Tue, 28 Oct 2025 14:37:10 +0200 Subject: [PATCH 59/68] Updated package requirements --- package-lock.json | 107 +++------------------------------------------- package.json | 3 +- 2 files changed, 8 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d49e024..a5c39b96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,7 @@ "react-dom": "^18.1.0", "react-tabs": "^6.0.2", "react-tooltip": "^5.15.0", - "uuid": "^9.0.0", - "vex-js": "^4.1.0" + "uuid": "^9.0.0" }, "devDependencies": { "@electron-forge/cli": "6.0.4", @@ -3867,11 +3866,6 @@ "dev": true, "optional": true }, - "node_modules/classlist-polyfill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", - "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==" - }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -4312,11 +4306,6 @@ "node": ">=0.10.0" } }, - "node_modules/deep-clone-simple": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-clone-simple/-/deep-clone-simple-1.1.1.tgz", - "integrity": "sha512-d/Knd6Cmq0hQTdTf5uggLD0ua6zpwGb1Yqorz3Y8NAmcAMKsG/UmV8+iaNqgT92WPFD7EDymwGIwwVy888T6fQ==" - }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -4420,11 +4409,6 @@ "node": ">=8" } }, - "node_modules/domify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/domify/-/domify-1.4.0.tgz", - "integrity": "sha512-CBzequt8IJiKV8fav4tn0qA9KSiZ76w3C2EkyfC8+df6UGNcZJRfvfBcRzLW39eGeETcunkOvvRBj/gKXIdULw==" - }, "node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -5070,11 +5054,6 @@ "dev": true, "optional": true }, - "node_modules/es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" - }, "node_modules/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -5535,11 +5514,6 @@ "node": ">= 4.0.0" } }, - "node_modules/form-serialize": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/form-serialize/-/form-serialize-0.7.1.tgz", - "integrity": "sha512-WGThlbRC1ELTwfR4HGLFnUBU3yzaOmiZ1ZbPgic3XsS3nGTzZdrxOLwsP4kz6QBdy1KAwvoLwN9TJk08mfqxxA==" - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -9227,31 +9201,10 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/vex-dialog": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vex-dialog/-/vex-dialog-1.1.0.tgz", - "integrity": "sha512-P+kf+LMI+jF1QtmH0iaqZLi5sVpL94d29SENBmI3x/2i8a1X6skN6Q2463m1triK8u4bpEWj1FndWqgpueA5cA==", - "dependencies": { - "deep-clone-simple": "1.1.1", - "domify": "1.4.0", - "form-serialize": "0.7.1" - } - }, - "node_modules/vex-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vex-js/-/vex-js-4.1.0.tgz", - "integrity": "sha512-n/dgknlD5CVl+Bn87kARnjwSknxZvSMflwzrBdgV0LLdLXpgmF6AKQ//bEEBBwK2JLXR0wG2R+il1zjcBtOi6Q==", - "dependencies": { - "classlist-polyfill": "1.2.0", - "domify": "1.4.0", - "es6-object-assign": "1.1.0", - "vex-dialog": "1.1.0" - } - }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { @@ -12293,11 +12246,6 @@ "dev": true, "optional": true }, - "classlist-polyfill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", - "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==" - }, "classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -12599,11 +12547,6 @@ } } }, - "deep-clone-simple": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-clone-simple/-/deep-clone-simple-1.1.1.tgz", - "integrity": "sha512-d/Knd6Cmq0hQTdTf5uggLD0ua6zpwGb1Yqorz3Y8NAmcAMKsG/UmV8+iaNqgT92WPFD7EDymwGIwwVy888T6fQ==" - }, "defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -12682,11 +12625,6 @@ "path-type": "^4.0.0" } }, - "domify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/domify/-/domify-1.4.0.tgz", - "integrity": "sha512-CBzequt8IJiKV8fav4tn0qA9KSiZ76w3C2EkyfC8+df6UGNcZJRfvfBcRzLW39eGeETcunkOvvRBj/gKXIdULw==" - }, "dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -13183,11 +13121,6 @@ "dev": true, "optional": true }, - "es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" - }, "esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -13531,11 +13464,6 @@ } } }, - "form-serialize": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/form-serialize/-/form-serialize-0.7.1.tgz", - "integrity": "sha512-WGThlbRC1ELTwfR4HGLFnUBU3yzaOmiZ1ZbPgic3XsS3nGTzZdrxOLwsP4kz6QBdy1KAwvoLwN9TJk08mfqxxA==" - }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -16187,31 +16115,10 @@ "spdx-expression-parse": "^3.0.0" } }, - "vex-dialog": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vex-dialog/-/vex-dialog-1.1.0.tgz", - "integrity": "sha512-P+kf+LMI+jF1QtmH0iaqZLi5sVpL94d29SENBmI3x/2i8a1X6skN6Q2463m1triK8u4bpEWj1FndWqgpueA5cA==", - "requires": { - "deep-clone-simple": "1.1.1", - "domify": "1.4.0", - "form-serialize": "0.7.1" - } - }, - "vex-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vex-js/-/vex-js-4.1.0.tgz", - "integrity": "sha512-n/dgknlD5CVl+Bn87kARnjwSknxZvSMflwzrBdgV0LLdLXpgmF6AKQ//bEEBBwK2JLXR0wG2R+il1zjcBtOi6Q==", - "requires": { - "classlist-polyfill": "1.2.0", - "domify": "1.4.0", - "es6-object-assign": "1.1.0", - "vex-dialog": "1.1.0" - } - }, "vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "requires": { "esbuild": "^0.25.0", diff --git a/package.json b/package.json index 24a41067..22a45cc2 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,7 @@ "react-dom": "^18.1.0", "react-tabs": "^6.0.2", "react-tooltip": "^5.15.0", - "uuid": "^9.0.0", - "vex-js": "^4.1.0" + "uuid": "^9.0.0" }, "devDependencies": { "@electron-forge/cli": "6.0.4", From 2f330007e59be189a874ec6357d3509c56bfec18 Mon Sep 17 00:00:00 2001 From: Santeri Hiitola Date: Wed, 29 Oct 2025 13:16:46 +0200 Subject: [PATCH 60/68] General fixes to file access --- src/main/typing.d.ts | 12 ----- src/renderer/components/App.tsx | 5 +- .../HelmetProject/HelmetProject.jsx | 21 +++++++-- src/renderer/components/Modal/Modal.jsx | 47 +++++++++++++++---- src/renderer/{main.jsx => main.tsx} | 8 ++-- src/renderer/search_emme_pythonpath.js | 9 ++-- tsconfig.json | 3 +- 7 files changed, 65 insertions(+), 40 deletions(-) rename src/renderer/{main.jsx => main.tsx} (89%) diff --git a/src/main/typing.d.ts b/src/main/typing.d.ts index edf3e0d0..3056148a 100644 --- a/src/main/typing.d.ts +++ b/src/main/typing.d.ts @@ -1,15 +1,3 @@ -interface VexDialogOptions { - message?: string; - input?: string; - callback?: (data?: any) => void; -} - -declare const vex: { - dialog: { - open: (opts: VexDialogOptions) => void; - prompt?: (opts: any) => void; - }; -}; type GithubTag = { name: string; diff --git a/src/renderer/components/App.tsx b/src/renderer/components/App.tsx index 44865880..802a7603 100644 --- a/src/renderer/components/App.tsx +++ b/src/renderer/components/App.tsx @@ -11,12 +11,11 @@ const homedir = os.homedir(); interface AppProps { helmetUIVersion: string; - searchEMMEPython: () => [boolean, string]; - listEMMEPythonPaths: () => string[]; + searchEMMEPython: () => [boolean, string | null]; + listEMMEPythonPaths: () => Promise<[boolean, string[] | null]>; } -// vex-js imported globally in index.html, since we cannot access webpack config in electron-forge const App: React.FC = ({helmetUIVersion, searchEMMEPython, listEMMEPythonPaths}) => { const { helmetModelSystemVersion, setHelmetModelSystemVersion } = useHelmetModelContext(); diff --git a/src/renderer/components/HelmetProject/HelmetProject.jsx b/src/renderer/components/HelmetProject/HelmetProject.jsx index 4f008d05..fc3a8670 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.jsx +++ b/src/renderer/components/HelmetProject/HelmetProject.jsx @@ -111,13 +111,14 @@ const HelmetProject = ({ "iterations" in obj ) { // Initialize scenario-specific namespace in the shared store - const namespace = `${configPath}/${fileName.slice(0, -5)}`; + const namespace = `${configPath}/${fileName.replace('.json', '')}`; if (!configStores.current.has(namespace)) { configStores.current.set(namespace, window.electronAPI.StoreAPI.getScenarioStore(namespace)); } return obj; // include valid scenario } else { + console.log(`Scenario structure:`, obj); console.warn(`Invalid scenario structure in file: ${fileName}`); } } catch (err) { @@ -226,10 +227,10 @@ const HelmetProject = ({ }; // Create the new scenario in "scenarios" array first setScenarios(scenarios.concat(newScenario)); - const namespace = `${ projectPath }/${ newScenario.id }`; + const namespace = `${ projectPath }/${ newScenario.name }`; const store = window.electronAPI.StoreAPI.getScenarioStore(namespace); configStores.current.set(namespace, store); - store.set('scenario', newScenario); + store.set(newScenario); // Then set scenario as open by id setOpenScenarioID(newId); }; @@ -257,10 +258,19 @@ const HelmetProject = ({ if (confirm(`Oletko varma skenaarion ${scenario.name} poistosta?`)) { setOpenScenarioID(null); setScenarios(scenarios.filter((s) => s.id !== scenario.id)); - await fs.unlink(path.join(projectPath, `${scenario.name}.json`)); + const filePath = path.join(projectPath, `${scenario.name}.json`); + try { + await fs.unlink(filePath); + } catch (err) { + if (err.code === 'ENOENT') { + console.warn(`File already deleted: ${filePath}`); + } else { + console.error(`Error deleting scenario file:`, err); + } + } ipcRenderer.send('focus-fix'); } else { - ipcRenderer.send('focus-fix'); + ipcRenderer.send('focus-fix'); } }; @@ -537,6 +547,7 @@ const HelmetProject = ({ value={newScenarioName} onChange={(e) => setNewScenarioName(e.target.value)} placeholder="Uuden skenaarion nimi" + autoFocus /> {modalError &&

{modalError}

} diff --git a/src/renderer/components/Modal/Modal.jsx b/src/renderer/components/Modal/Modal.jsx index bb71b382..5b83d4ab 100644 --- a/src/renderer/components/Modal/Modal.jsx +++ b/src/renderer/components/Modal/Modal.jsx @@ -1,7 +1,29 @@ -import React from 'react'; +import React, { useEffect, useCallback } from 'react'; import './Modal.css'; const Modal = ({ isOpen, onClose, onSubmit, title, children }) => { + // Pressing Enter should trigger "OK" + const handleKeyDown = useCallback( + (e) => { + if (e.key === 'Enter') { + e.preventDefault(); // prevent accidental form submits or newlines + onSubmit(); + } else if (e.key === 'Escape') { + onClose(); // optional: allow closing with ESC too + } + }, + [onSubmit, onClose] + ); + + useEffect(() => { + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + } + }, [isOpen, handleKeyDown]); + if (!isOpen) return null; return ( @@ -13,15 +35,20 @@ const Modal = ({ isOpen, onClose, onSubmit, title, children }) => { ×
-
{children}
-
- - -
+
e.preventDefault()}> +
{children}
+
+ + +
+
); diff --git a/src/renderer/main.jsx b/src/renderer/main.tsx similarity index 89% rename from src/renderer/main.jsx rename to src/renderer/main.tsx index a6561de0..b2fd3080 100644 --- a/src/renderer/main.jsx +++ b/src/renderer/main.tsx @@ -1,9 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './components/App.jsx'; +import App from './components/App.js'; import { searchEMMEPython, listEMMEPythonPaths } from './search_emme_pythonpath.js'; import { version } from '../../package.json'; // your package.json version -import { HelmetModelProvider } from './context/HelmetModelContext'; +import { HelmetModelProvider } from './context/HelmetModelContext.js'; // Import CSS here instead of in HTML import './components/App.css'; @@ -15,10 +15,8 @@ import './components/HelmetProject/Runtime/RunStatus/RunStatus.css'; import './components/HelmetProject/RunLog/RunLog.css'; import './components/HelmetProject/CostBenefitAnalysis/CostBenefitAnalysis.css'; import 'react-tabs/style/react-tabs.css'; -import 'vex-js/dist/css/vex.css'; -import 'vex-js/dist/css/vex-theme-os.css'; -const root = ReactDOM.createRoot(document.getElementById('root')); +const root = ReactDOM.createRoot(document.getElementById('root')!); root.render( diff --git a/src/renderer/search_emme_pythonpath.js b/src/renderer/search_emme_pythonpath.js index 78489244..72a678d5 100644 --- a/src/renderer/search_emme_pythonpath.js +++ b/src/renderer/search_emme_pythonpath.js @@ -7,6 +7,7 @@ const fs = window.electronAPI.fs; /** * Check and try to set EMME's Python location on Windows, searching from common known paths. + * @returns {[boolean, string | null]} */ export function searchEMMEPython(){ @@ -58,6 +59,9 @@ const checkEmmePythonEnv = () => { } } +/** +* @returns {Promise<[boolean, string[] | null]>} +*/ export async function listEMMEPythonPaths() { const pythonVersion = getVersion(versions.emme_python); @@ -158,7 +162,4 @@ async function hasPythonEnv(basePath) { return exePaths; } -export default { - searchEMMEPython: searchEMMEPython(), - listEMMEPythonPaths: listEMMEPythonPaths(), -}; +export default { searchEMMEPython, listEMMEPythonPaths }; diff --git a/tsconfig.json b/tsconfig.json index 80d35507..11ad9f63 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,8 @@ "isolatedModules": true, "noEmit": true, "allowJs": true, - "checkJs": false + "checkJs": false, + "resolveJsonModule": true }, "include": [ "src/**/*" From 8d88a398ad7f48f02a8c291f33aeb9fbdabb0dcb Mon Sep 17 00:00:00 2001 From: Santeri Hiitola Date: Thu, 30 Oct 2025 14:11:16 +0200 Subject: [PATCH 61/68] Fixes to running scenarios and logging their progress --- src/background/cba_script_python_shell.js | 5 +- src/background/cba_script_worker.html | 3 +- .../helmet_entrypoint_python_shell.js | 2 +- src/background/helmet_entrypoint_worker.html | 7 +- src/main/index.js | 64 +++++++++++++++---- src/main/preload.ts | 5 +- src/renderer/components/App.css | 4 ++ .../HelmetProject/HelmetProject.jsx | 24 ++++++- .../HelmetProject/Runtime/Runtime.jsx | 2 +- 9 files changed, 90 insertions(+), 26 deletions(-) diff --git a/src/background/cba_script_python_shell.js b/src/background/cba_script_python_shell.js index 8185ef2d..f2e97091 100644 --- a/src/background/cba_script_python_shell.js +++ b/src/background/cba_script_python_shell.js @@ -1,5 +1,6 @@ -const {ipcRenderer, path, ps} = window.electronAPI; -const PythonShell = ps.PythonShell; +const { ipcRenderer } = require('electron'); +const { PythonShell } = require('python-shell'); +const path = require('path'); module.exports = { diff --git a/src/background/cba_script_worker.html b/src/background/cba_script_worker.html index 06269e43..a174c09a 100644 --- a/src/background/cba_script_worker.html +++ b/src/background/cba_script_worker.html @@ -4,7 +4,8 @@ Hidden process-running window