Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,12 @@ server {
index index.html;
try_files $uri $uri/ /index.html;
}

# the page itself is checked on every load, so a deploy reaches browsers at once; the scripts it loads have
# hashed names and may stay cached. add_header here replaces the server's ones, hence HSTS again
location = /index.html {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache" always;
add_header Strict-Transport-Security "max-age=63072000" always;
}
}
1 change: 1 addition & 0 deletions src/JsRoot/GraphData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type Estimator = {
name: string;
metadata?: unknown;
pages: Page[];
outputUuid?: string;
scoringOutputJsonRef?: ScoringOutputJSON;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useTheme } from '@mui/material';
import { SyntheticEvent, useCallback, useEffect, useState } from 'react';
import { SyntheticEvent, useCallback, useEffect, useRef, useState } from 'react';

import { useRemoteWorkerSimulation } from '../../../../services/RemoteWorkerSimulationContextProvider';
import { ResultsSimulationDataWithSource } from '../../../../services/StoreService';
import { StyledTab, StyledTabs } from '../../../../shared/components/Tabs/StyledTabs';
import { SpecificEstimator } from '../../../../types/RequestTypes';
import { EstimatorPagesByDimensions } from '../../../../types/ResponseTypes';
import { FullSimulationData } from '../../../../types/SimulationService';
import { partialRevision } from '../../../../util/partialResults';

interface EstimatorsTabProps {
tabsValue: number;
Expand All @@ -31,12 +32,23 @@ const EstimatorsSelect = ({
const [controller] = useState(new AbortController());

const { getEstimatorsPages } = useRemoteWorkerSimulation();
const revision = partialRevision(simulation);
// the partial results revision each tab was loaded at, so a newer partial or the final results load it again;
// the estimators there at mount were loaded at the revision then (the component is keyed by simulation)
const loadedRevisions = useRef(new Map<number, number | undefined>());
const mountRevision = useRef(revision);

const handleEstimatorTabChange = useCallback(
async (id: number) => {
async (id: number, isStale: () => boolean) => {
const currentEstimatorData = simulation.estimators;
const loadedRevision = loadedRevisions.current.has(id)
? loadedRevisions.current.get(id)
: mountRevision.current;

const estimatorExists =
currentEstimatorData[id] && currentEstimatorData[id].name !== '';
currentEstimatorData[id] &&
currentEstimatorData[id].name !== '' &&
loadedRevision === revision;

if (estimatorsPagesData) {
const estimatorsPagesByDimensions = estimatorsPagesData[id].pagesByDimensions;
Expand All @@ -50,7 +62,8 @@ const EstimatorsSelect = ({
{
jobId: simulationJobId,
estimatorName: estimatorsTabData[id],
pageNumbers: pageDimension.pageNums
pageNumbers: pageDimension.pageNums,
revision
},
controller.signal,
true
Expand All @@ -61,7 +74,8 @@ const EstimatorsSelect = ({
}
}

if (allResults.length === 0) return;
// a newer simulation or tab is being loaded meanwhile
if (allResults.length === 0 || isStale()) return;

const aggregationResults = allResults.reduce(
(acc, result) => {
Expand Down Expand Up @@ -97,7 +111,9 @@ const EstimatorsSelect = ({
...simulation,
estimators: newEstimators
};
setResultsSimulationData({ source: 'onSelect', data: newSimulation });
loadedRevisions.current.set(id, revision);
// not onSelect: a new partial loaded while another screen is open must not switch to Results
setResultsSimulationData({ source: 'onLoad', data: newSimulation });
}
}
},
Expand All @@ -106,14 +122,21 @@ const EstimatorsSelect = ({
estimatorsPagesData,
estimatorsTabData,
getEstimatorsPages,
revision,
setResultsSimulationData,
simulation
]
);

// the tab on screen, whenever it or the simulation changes
useEffect(() => {
handleEstimatorTabChange(0);
}, [handleEstimatorTabChange]);
let stale = false;
handleEstimatorTabChange(tabsValue, () => stale);

return () => {
stale = true;
};
}, [handleEstimatorTabChange, tabsValue]);

const handleChange = (_event: SyntheticEvent, newValue: number) => {
setTabsValue(newValue);
Expand All @@ -131,7 +154,6 @@ const EstimatorsSelect = ({
key={`tab_${estimatorName}`}
label={estimatorName}
value={id}
onClick={() => handleEstimatorTabChange(id)}
/>
);
})}
Expand Down
31 changes: 31 additions & 0 deletions src/WrapperApp/components/Results/ResultsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import { useStore } from '../../../services/StoreService';
import { InfoTooltip } from '../../../shared/components/tooltip/InfoTooltip';
import { titleToKebabCase } from '../../../ThreeEditor/components/Dialog/CustomDialog';
import { FullSimulationData } from '../../../types/SimulationService';
import { partialResultsLabel } from '../../../util/partialResults';
import EstimatorsSelect from './EstimatorsSelect/EstimatorsSelect';
import EstimatorTab from './EstimatorTab/EstimatorTab';
import { useFollowPartialResults } from './useFollowPartialResults';

export interface EstimatorResults extends Estimator {
tablePages: Page0D[];
Expand All @@ -38,8 +40,29 @@ function ResultsPanel(props: { simulation: FullSimulationData | undefined }) {
const { simulation } = props;

const [tabsValue, setTabsValue] = useState(0);
// another simulation starts at its first estimator; reset while rendering, before EstimatorsSelect loads a tab
const [shownJobId, setShownJobId] = useState(simulation?.jobId);

if (simulation?.jobId !== shownJobId) {
setShownJobId(simulation?.jobId);
setTabsValue(0);
}

const [estimatorsResults, setEstimatorsResults] = useState<EstimatorResults[]>([]);
const [groupQuantities, setGroupQuantities] = useState(false);
// re-rendered now and then, so the age of partial results stays right between updates
const [, setNow] = useState(Date.now());
const partialLabel = simulation && partialResultsLabel(simulation);
const isPartial = partialLabel !== undefined;

useFollowPartialResults(simulation);

useEffect(() => {
if (!isPartial) return;
const timer = setInterval(() => setNow(Date.now()), 30000);

return () => clearInterval(timer);
}, [isPartial]);

const userTabInputFilesEstimatorNames = simulation?.input.estimatorsItems?.map(
estimator => estimator.name
Expand Down Expand Up @@ -121,6 +144,13 @@ function ResultsPanel(props: { simulation: FullSimulationData | undefined }) {
lineHeight={2}>
{title}
</Typography>
{partialLabel && (
<Typography
variant='body2'
color='warning.main'>
Partial results: {partialLabel}
</Typography>
)}
<Divider />
</Box>
<Box
Expand Down Expand Up @@ -171,6 +201,7 @@ function ResultsPanel(props: { simulation: FullSimulationData | undefined }) {
{estimatorsResults.length > 0 && simulation && (
<>
<EstimatorsSelect
key={simulation.jobId}
tabsValue={tabsValue}
setTabsValue={setTabsValue}
simulation={simulation}
Expand Down
36 changes: 36 additions & 0 deletions src/WrapperApp/components/Results/useFollowPartialResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useRef } from 'react';

import { useRemoteWorkerSimulation } from '../../../services/RemoteWorkerSimulationContextProvider';
import { useStore } from '../../../services/StoreService';
import { withStatus } from '../../../services/useSimulationStatusStream';
import { FullSimulationData } from '../../../types/SimulationService';
import { partialRevision } from '../../../util/partialResults';

/**
* Keeps the results on screen up to date with the status the server pushes (useSimulationStatusStream): a new
* partial, the final results, or a failed or canceled run replaces the shown status, and EstimatorsSelect loads the
* estimator on screen again when the partial results changed.
*/
export function useFollowPartialResults(simulation: FullSimulationData | undefined) {
const { statusStream } = useRemoteWorkerSimulation();
const { setResultsSimulationData } = useStore();
// read when a status comes, so updating the results does not apply the same status again
const latest = useRef(simulation);
latest.current = simulation;

const jobId = simulation?.jobId;
const status = jobId === undefined ? undefined : statusStream?.statuses[jobId];

useEffect(() => {
const shown = latest.current;

if (!status || shown?.jobId !== status.jobId) return;
const updated = withStatus(shown, status);

if (
updated.jobState !== shown.jobState ||
partialRevision(updated) !== partialRevision(shown)
)
setResultsSimulationData({ source: 'onLoad', data: updated });
}, [status, setResultsSimulationData]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SimulationInputFiles,
StatusState
} from '../../../../types/ResponseTypes';
import { partialRevision } from '../../../../util/partialResults';

interface SimulationCardActionsProps {
simulationStatus: Omit<JobUnknownStatus & SimulationInfo, never>;
Expand Down Expand Up @@ -96,11 +97,24 @@ export const SimulationCardActions = ({
}
}

// results stored while the simulation ran, kept when it was cancelled or failed
const partialAction = partialRevision(simulationStatus) !== undefined && (
<Button
sx={{ fontSize: '.8em' }}
size='small'
color='secondary'
disabled={!Boolean(loadResults)}
onClick={onClickLoadResults}>
Partial Results
</Button>
);

return (
<CardActions sx={{ p: 1 }}>
<ButtonGroup
fullWidth
aria-label='full width outlined button group'>
{partialAction}
{statusAction}
<Button
sx={{ fontSize: '.8em' }}
Expand Down
Loading
Loading