diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d54827ab..235b41c6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,14 +20,12 @@ jobs: name: E2E testing (${{ matrix.os }}) runs-on: ${{ matrix.os-version }} - strategy: fail-fast: false matrix: os: - linux - win64 - # - macos include: - os: linux os-version: ubuntu-latest @@ -42,6 +40,11 @@ jobs: activate-environment: watertap-ui-env miniforge-version: latest + - name: Set up debug logging + run: | + echo "ACTIONS_RUNNER_DEBUG=true" >> $GITHUB_ENV + echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV + - name: Add theme to .env file working-directory: ./electron/ui run: | @@ -127,6 +130,13 @@ jobs: electron/ui/cypress/screenshots/ electron/ui/cypress/videos/ + ## post-run conda often fails for weird reasons. this is a potential solution + ## see https://github.com/conda-incubator/setup-miniconda/issues/277#issuecomment-1431458277 + - name: Rename conda package cache + if: runner.os == 'Windows' + shell: bash + run: mv "${CONDA_PKGS_DIR}" "${CONDA_PKGS_DIR}_do_not_cache" + pytest: name: pytest (${{ matrix.os }}) runs-on: ${{ matrix.os-version }} diff --git a/electron/ui/src/App.js b/electron/ui/src/App.js index 74251b88..fb4d376b 100644 --- a/electron/ui/src/App.js +++ b/electron/ui/src/App.js @@ -11,6 +11,8 @@ import {getProjectName} from './services/projectName.service'; import MainContent from "./components/MainContent/MainContent"; import WaitForProject from "./components/WaitForProject/WaitForProject"; import {themes} from './theme'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; + function App() { let navigate = useNavigate(); @@ -21,7 +23,14 @@ function App() { const [checkAgain, setCheckAgain] = useState(1) const WAIT_TIME = 2 - console.log("App hasTheme = ",hasTheme); + // use Material UI theme for styles to be consistent throughout app + const mui_theme = createTheme({ + palette: { + primary: { + main: theme?.button.background, + }, + }, + }); useEffect(() => { if (hasTheme && checkAgain !== 0) @@ -29,7 +38,7 @@ function App() { // Get list of flowsheets getFlowsheetsList() .then((data) => { - console.log("got flowsheets list") + // console.log("got flowsheets list") setHasFlowsheetsList(true); setCheckAgain(0) }).catch((e) => { @@ -45,12 +54,15 @@ function App() { const subProcState = {value: numberOfSubprocesses, setValue: setNumberOfSubprocesses} return ( -
- - - -
+ + +
+ + + +
+
) } diff --git a/electron/ui/src/components/Graph/Graph.js b/electron/ui/src/components/Graph/Graph.js index 424e63fe..31d04a97 100644 --- a/electron/ui/src/components/Graph/Graph.js +++ b/electron/ui/src/components/Graph/Graph.js @@ -19,7 +19,6 @@ export default function Graph() { setGraphImage(URL.createObjectURL(data)) } else { - console.log("data.size is 0") if (tryAgain) setTryAgain(false) } }).catch((err)=>{ diff --git a/electron/ui/src/components/SingleOutput/SingleOutput.js b/electron/ui/src/components/SingleOutput/SingleOutput.js index e8b233e1..496a06f8 100644 --- a/electron/ui/src/components/SingleOutput/SingleOutput.js +++ b/electron/ui/src/components/SingleOutput/SingleOutput.js @@ -1,19 +1,24 @@ -import React, {useState} from "react"; +import React, {useState, useEffect, Fragment} from "react"; import {useParams} from "react-router-dom"; // MUI imports -import Accordion from "@mui/material/Accordion"; -import AccordionDetails from "@mui/material/AccordionDetails"; -import AccordionSummary from "@mui/material/AccordionSummary"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import DownloadIcon from '@mui/icons-material/Download'; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import Grid from "@mui/material/Grid"; import Modal from "@mui/material/Modal"; import SaveIcon from '@mui/icons-material/Save'; import Stack from "@mui/material/Stack"; import TextField from "@mui/material/TextField"; import Toolbar from "@mui/material/Toolbar"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material' +import Paper from '@mui/material/Paper'; export default function SingleOutput(props) { let params = useParams(); @@ -21,10 +26,37 @@ export default function SingleOutput(props) { const [configName, setConfigName] = useState(outputData.name) const [openSaveConfig, setOpenSaveConfig] = React.useState(false); const [saved, setSaved] = React.useState(false); + const [outputTableData, setOutputTableData] = useState({}) const handleOpenSaveConfig = () => setOpenSaveConfig(true); const handleCloseSaveConfig = () => setOpenSaveConfig(false); + /** + * organize output data into a list of dictionaries formatted for the output table + */ + useEffect(()=> { + let export_variables = {...outputData.outputData.exports} + let rows = {} + for (let key of Object.keys(export_variables)) { + let export_variable = export_variables[key] + let category = export_variable.output_category + if (!category) category = export_variable.input_category + let category_rows + if (Object.keys(rows).includes(category)) category_rows = rows[category] + else { + category_rows = [] + rows[category] = category_rows + } + category_rows.push({ + key: key, + name: export_variable.name, + value: export_variable.value, + units: export_variable.display_units, + rounding: export_variable.rounding || 2 + }) + } + setOutputTableData(rows) + }, [outputData]) const modalStyle = { position: 'absolute', @@ -87,101 +119,49 @@ export default function SingleOutput(props) { }); } - const renderOutputAccordions = () => { - let var_sections = organizeVariables(outputData.outputData.exports) - // console.log("var_sections",var_sections) - return Object.entries(var_sections).map(([key, value]) => { - let gridSize = 4; - let _key = key + Math.floor(Math.random() * 100001); - if (Object.keys(value.output_variables).length > 0) { - return ( - - }> - {value.display_name} - - - :not(style)': {m: 1}, - }} - autoComplete="off" + /** + * generate html for table + * @returns table body component containing table rows + */ + const renderRows = () => { + try { + return ( + + {Object.entries(outputTableData).map(([category, rows]) => ( + + + - { - renderFields(value.output_variables) - } - - - - ) - } - else { - return null; - } - }) - }; - - // renders the data in output accordions - const renderFields = (fieldData) => { - // console.log("field data", fieldData) - return Object.keys(fieldData).map((key) => { - let _key = key + Math.floor(Math.random() * 100001); - - // handle rounding - let roundedValue - if (fieldData[key].rounding != null) { - if (fieldData[key].rounding > 0) { - roundedValue = parseFloat((fieldData[key].value).toFixed(fieldData[key].rounding)) - } else if (fieldData[key].rounding === 0) { - roundedValue = Math.round(fieldData[key].value) - } else // if rounding is negative - { - let factor = 1 - let tempRounding = fieldData[key].rounding - console.log('rounding is negative : ', fieldData[key].rounding) - while (tempRounding < 0) { - factor *= 10 - tempRounding += 1 - } - roundedValue = Math.round((fieldData[key].value / factor)) * factor - console.log("old value is: ", fieldData[key].value) - console.log('new value is: ', roundedValue) - } - } else // if rounding is not provided, just use given value - { - roundedValue = fieldData[key].value - } - return (
- {fieldData[key].name + " "} - {roundedValue} - {" " + fieldData[key].display_units} -
) - }) - }; - - const organizeVariables = (bvars) => { - let var_sections = {} - for (const [key, v] of Object.entries(bvars)) { - let catg = v.output_category - let is_input = v.is_input - let is_output = v.is_output - if (catg === null) { - catg = "" - } - if (!Object.hasOwn(var_sections, catg)) { - var_sections[catg] = { - display_name: catg, - variables: {}, - input_variables: {}, - output_variables: {} - } - } - var_sections[catg]["variables"][key] = v - if (is_input) var_sections[catg]["input_variables"][key] = v; - if (is_output) var_sections[catg]["output_variables"][key] = v + {category} + + + {rows.map((row, idx) => ( + + + + {row.name} + + + {row.value.toLocaleString('en-US', {maximumFractionDigits:row.rounding})} + + + {row.units} + + + ))} + + + ))} + + + ) + } catch(e) { + console.log("unable to render rows: ") + console.log(e) } - return var_sections + } return ( @@ -228,7 +208,21 @@ export default function SingleOutput(props) { - {renderOutputAccordions()} + + + + + + Category + Variable + Value + Units + + + {renderRows()} +
+
+
); } \ No newline at end of file diff --git a/electron/ui/src/components/SolveDialog/SolveDialog.js b/electron/ui/src/components/SolveDialog/SolveDialog.js index 7d13d5e3..4f0d545f 100644 --- a/electron/ui/src/components/SolveDialog/SolveDialog.js +++ b/electron/ui/src/components/SolveDialog/SolveDialog.js @@ -10,11 +10,9 @@ export default function SolveDialog(props) { const { open, handleSolved, handleError, flowsheetData, id, isSweep } = props; useEffect(()=>{ - console.log("solve dialog use effect") try { if(open) { - console.log("open solve dialog is true") if(isSweep) { sweep(id, flowsheetData.inputData) .then(r => r.json().then(data => ({status: r.status, body: data}))) diff --git a/electron/ui/src/theme.js b/electron/ui/src/theme.js index ae2eacd7..287312e7 100644 --- a/electron/ui/src/theme.js +++ b/electron/ui/src/theme.js @@ -34,7 +34,7 @@ export const themes = { color: '#FFFFFF', background: '#67C3E4', logoBackground: '#F2F7F8' }, button: { - background: '#1976d2' + background: '#1976d2', }, tabs: { background: '#F1F3F3', color: '#727272' @@ -62,7 +62,7 @@ export const themes = { color: '#000000', background: '#F6F4F4', logoBackground: '#F8F6F6' }, button: { - background: '#1669B6' + background: '#1669B6', }, tabs: { background: '#F6F4F4', color: '#727272' @@ -87,7 +87,7 @@ export const themes = { color: '#FFFFFF', background: '#000000', logoBackground: '#333333' // FIXME? }, button: { - background: '#1669B6' + background: '#333333', }, tabs: { background: '#F1F3F3', color: '#727272' diff --git a/electron/ui/src/views/FlowsheetConfig/ConfigInput/ConfigInput.js b/electron/ui/src/views/FlowsheetConfig/ConfigInput/ConfigInput.js index 74b35a1b..32671306 100644 --- a/electron/ui/src/views/FlowsheetConfig/ConfigInput/ConfigInput.js +++ b/electron/ui/src/views/FlowsheetConfig/ConfigInput/ConfigInput.js @@ -179,7 +179,7 @@ export default function ConfigInput(props) { /** * Organize variables into sections by their 'category' attribute. * - * @returns Object {: [list, of, variable, objects]} + * @returns [Object(left) {: [list, of, variable, objects]}, Object(right) {: [list, of, variable, objects]}] */ const organizeVariables = (bvars) => { let var_sections = {} @@ -234,41 +234,110 @@ export default function ConfigInput(props) { } - //sorting the keys of var_sections by amount of variables into object "items" - let items = Object.keys(var_sections).map(function (key) { - return [key, var_sections[key]['num_variables']]; - }); - items.sort(function (first, second) { - return second[1] - first[1]; - }); - // console.log(items) - return var_sections + /** + * sort the keys of var_sections into two groups that have as close as possible to even amount of total variables + * we want the two columns to be roughly the same length if possible + **/ + let var_sections_left = {} + let var_sections_right = {} + let total_variables_left = 0 + let total_variables_right = 0 + let next_section = "left" + try { + for (let category of Object.keys(var_sections)) { + let section = var_sections[category] + let input_data = section.input_variables + + // calculate variable amount - fixed variables take up about 45% as much space as free + // if variable is fixed, count it as 1 + // if variable is free, count it as 2 + let variable_amount = 0 + for (let input_variable_key of Object.keys(input_data)) { + let input_variable = input_data[input_variable_key] + if (input_variable.fixed) { + variable_amount += 1 + } + else { + variable_amount += 2 + } + } + + if (next_section === "left") { + total_variables_left+=variable_amount + var_sections_left[category] = section + if (total_variables_left > total_variables_right) next_section = "right" + } + else // if (next_section === "right") + { + total_variables_right+=variable_amount + var_sections_right[category] = section + if (total_variables_right > total_variables_left) next_section = "left" + } + } + } catch(e) { + console.log("error sorting: ") + console.log(e) + } + + return [var_sections_left, var_sections_right] } const renderInputAccordions = () => { try { if (Object.keys(displayData).length > 0) { let var_sections = organizeVariables(displayData.exports) - return Object.entries(var_sections).map(([key, value]) => { - let _key; - if (key === undefined || key === null) { - _key = key + Math.floor(Math.random() * 100001); - } else { - _key = key + value.display_name + value.output_variables; - } - if (Object.keys(value.input_variables).length > 0) { - return ( - - ) - } - }) + let var_sections_left = var_sections[0] + let var_sections_right = var_sections[1] + + return + + {Object.entries(var_sections_left).map(([key, value]) => { + let _key; + if (key === undefined || key === null) { + _key = key + Math.floor(Math.random() * 100001); + } else { + _key = key + value.display_name + value.output_variables; + } + if (Object.keys(value.input_variables).length > 0) { + return ( + ) + } + })} + + + + + {Object.entries(var_sections_right).map(([key, value]) => { + let _key; + if (key === undefined || key === null) { + _key = key + Math.floor(Math.random() * 100001); + } else { + _key = key + value.display_name + value.output_variables; + } + if (Object.keys(value.input_variables).length > 0) { + return ( + ) + } + })} + + + } } catch (e) { // version of data is likely wrong @@ -431,13 +500,11 @@ const RunButton = forwardRef(({...props}, ref) => { checkDisableRun })); - return (
diff --git a/electron/ui/src/views/FlowsheetConfig/FlowsheetConfig.js b/electron/ui/src/views/FlowsheetConfig/FlowsheetConfig.js index f4481775..2e2a5ec0 100644 --- a/electron/ui/src/views/FlowsheetConfig/FlowsheetConfig.js +++ b/electron/ui/src/views/FlowsheetConfig/FlowsheetConfig.js @@ -87,12 +87,10 @@ export default function FlowsheetConfig(props) { const [isBuilt, setIsBuilt] = useState(false) const [showBuildOptions, setShowBuildOptions] = useState(false) const theme = props.theme; - console.log("flowsheet config theme=", theme); const [inputsChanged, setInputsChanged] = useState(false); useEffect(() => { - console.log("params.id", params.id); if (!params.hasOwnProperty("id") || !params.id) return; // gotta find a way to figure out whether to build or not @@ -125,10 +123,8 @@ export default function FlowsheetConfig(props) { }, [params.id]); useEffect(() => { - console.info("Check/set whether flowsheet is built"); const inputs = getInputs(flowsheetData); if (!emptyOrNullObj(inputs)) { - console.log('flowsheet is indeed built'); setIsBuilt(true); } }, [flowsheetData]) @@ -307,8 +303,6 @@ export default function FlowsheetConfig(props) { } } - console.log("Returning container for FlowsheetConfig. build_options=", flowsheetData.inputData.build_options, "isBuilt=", isBuilt, - "loadingFlowsheetData=", loadingFlowsheetData); return ( {(loadingFlowsheetData) ? @@ -380,10 +374,7 @@ export default function FlowsheetConfig(props) { -
+