From f3a0410b8c2678ba20501007687c958e2ede9cf7 Mon Sep 17 00:00:00 2001 From: jorgenherje Date: Wed, 13 Nov 2024 09:42:33 +0100 Subject: [PATCH 01/33] WIP --- frontend/src/App.tsx | 11 +- frontend/src/framework/DeltaEnsemble.ts | 115 +++++ frontend/src/framework/EnsembleSet.ts | 30 +- frontend/src/framework/Workbench.ts | 16 +- .../EnsembleSelect/ensembleSelect.tsx | 34 +- .../framework/internal/EnsembleSetLoader.ts | 126 ++++- .../internal/components/NavBar/leftNavBar.tsx | 24 +- .../selectEnsemblesDialog.tsx | 463 ++++++++++++++++-- .../src/framework/utils/ensembleUiHelpers.ts | 30 ++ .../SimulationTimeSeries/interfaces.ts | 12 +- .../settings/atoms/derivedAtoms.ts | 40 +- .../settings/atoms/queryAtoms.ts | 2 +- .../useMakeSettingsStatusWriterMessages.ts | 6 +- .../settings/settings.tsx | 3 +- .../view/atoms/baseAtoms.ts | 2 + .../view/atoms/interfaceEffects.ts | 5 + 16 files changed, 834 insertions(+), 85 deletions(-) create mode 100644 frontend/src/framework/DeltaEnsemble.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1d03a1c5b..44e415543 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import React from "react"; import WebvizLogo from "@assets/webviz.svg"; import { GuiState, LeftDrawerContent } from "@framework/GuiMessageBroker"; -import { LayoutElement, Workbench } from "@framework/Workbench"; +import { LayoutElement, UserDeltaEnsembleSetting, Workbench } from "@framework/Workbench"; import { LeftNavBar, RightNavBar } from "@framework/internal/components/NavBar"; import { SettingsContentPanels } from "@framework/internal/components/SettingsContentPanels"; import { ToggleDevToolsButton } from "@framework/internal/components/ToggleDevToolsButton"; @@ -85,11 +85,14 @@ function App() { setIsMounted(true); const storedEnsembleIdents = workbench.maybeLoadEnsembleSettingsFromLocalStorage(); + const storedDeltaEnsembles: UserDeltaEnsembleSetting[] = []; // TODO: Store list of delta ensembles in local storage? if (storedEnsembleIdents) { setInitAppState(InitAppState.LoadingEnsembles); - workbench.loadAndSetupEnsembleSetInSession(queryClient, storedEnsembleIdents).finally(() => { - initApp(); - }); + workbench + .loadAndSetupEnsembleSetInSession(queryClient, storedEnsembleIdents, storedDeltaEnsembles) + .finally(() => { + initApp(); + }); } else { initApp(); } diff --git a/frontend/src/framework/DeltaEnsemble.ts b/frontend/src/framework/DeltaEnsemble.ts new file mode 100644 index 000000000..c5b9abafa --- /dev/null +++ b/frontend/src/framework/DeltaEnsemble.ts @@ -0,0 +1,115 @@ +import { v4 } from "uuid"; + +import { Ensemble } from "./Ensemble"; +import { EnsembleIdent } from "./EnsembleIdent"; +import { EnsembleParameters } from "./EnsembleParameters"; +import { EnsembleSensitivities } from "./EnsembleSensitivities"; + +export enum DeltaEnsembleElement { + FIRST = "first", + SECOND = "second", +} + +export class DeltaEnsemble { + private _deltaEnsembleIdent: EnsembleIdent; + private _firstEnsemble: Ensemble; + private _secondEnsemble: Ensemble; + private _color: string; + private _customName: string | null; + + private _realizationsArr: readonly number[]; + private _parameters: EnsembleParameters; + private _sensitivities: EnsembleSensitivities | null; + + constructor(firstEnsemble: Ensemble, secondEnsemble: Ensemble, color: string, customName: string | null = null) { + // NOTE: Delta ensembles are created using two ensembles, thus adding v4() to ensure uniqueness + const _deltaEnsembleCaseUuid = + firstEnsemble.getIdent().getCaseUuid() + secondEnsemble.getIdent().getCaseUuid() + v4(); + const _deltaEnsembleName = + `${firstEnsemble.getIdent().getEnsembleName()} - ${secondEnsemble.getIdent().getEnsembleName()}` + v4(); + this._deltaEnsembleIdent = new EnsembleIdent(_deltaEnsembleCaseUuid, _deltaEnsembleName); + + this._firstEnsemble = firstEnsemble; + this._secondEnsemble = secondEnsemble; + this._color = color; + this._customName = customName; + + // Intersection of realizations + const realizationIntersection = this._firstEnsemble + .getRealizations() + .filter((realization) => this._secondEnsemble.getRealizations().includes(realization)); + this._realizationsArr = Array.from(realizationIntersection).sort((a, b) => a - b); + + // TODO: + // - How to handle parameters and sensitivities? + // - Intersection or union? How to handle parameter values? + this._parameters = new EnsembleParameters([]); + this._sensitivities = null; + } + + getIdent(): EnsembleIdent { + return this._deltaEnsembleIdent; + } + + getEnsembleIdentByElement(element: DeltaEnsembleElement): EnsembleIdent { + if (element === DeltaEnsembleElement.FIRST) { + return this._firstEnsemble.getIdent(); + } + if (element === DeltaEnsembleElement.SECOND) { + return this._secondEnsemble.getIdent(); + } + throw new Error("Unhandled element type"); + } + + getCaseUuidByElement(element: DeltaEnsembleElement): string { + if (element === DeltaEnsembleElement.FIRST) { + return this._firstEnsemble.getCaseUuid(); + } + if (element === DeltaEnsembleElement.SECOND) { + return this._secondEnsemble.getCaseUuid(); + } + throw new Error("Unhandled element type"); + } + + getCaseNameByElement(element: DeltaEnsembleElement): string { + if (element === DeltaEnsembleElement.FIRST) { + return this._firstEnsemble.getCaseName(); + } + if (element === DeltaEnsembleElement.SECOND) { + return this._secondEnsemble.getCaseName(); + } + throw new Error("Unhandled element type"); + } + + getDisplayName(): string { + if (this._customName) { + return this._customName; + } + + return `${this._firstEnsemble.getDisplayName()} - ${this._secondEnsemble.getDisplayName()}`; + } + + getRealizations(): readonly number[] { + return this._realizationsArr; + } + + getRealizationsByElement(element: DeltaEnsembleElement): readonly number[] { + if (element === DeltaEnsembleElement.FIRST) { + return this._firstEnsemble.getRealizations(); + } else { + return this._secondEnsemble.getRealizations(); + } + } + + getRealizationsCount(): number { + return this._realizationsArr.length; + } + + getColor(): string { + return this._color; + } + + getCustomName(): string | null { + return this._customName; + } +} diff --git a/frontend/src/framework/EnsembleSet.ts b/frontend/src/framework/EnsembleSet.ts index f1198869d..78309a71b 100644 --- a/frontend/src/framework/EnsembleSet.ts +++ b/frontend/src/framework/EnsembleSet.ts @@ -1,11 +1,14 @@ +import { DeltaEnsemble } from "./DeltaEnsemble"; import { Ensemble } from "./Ensemble"; import { EnsembleIdent } from "./EnsembleIdent"; export class EnsembleSet { private _ensembleArr: Ensemble[]; + private _deltaEnsembleArr: DeltaEnsemble[]; - constructor(ensembles: Ensemble[]) { + constructor(ensembles: Ensemble[], deltaEnsembles: DeltaEnsemble[] = []) { this._ensembleArr = ensembles; + this._deltaEnsembleArr = deltaEnsembles; } /** @@ -15,14 +18,26 @@ export class EnsembleSet { return this._ensembleArr.length > 0; } + hasAnyDeltaEnsembles(): boolean { + return this._deltaEnsembleArr.length > 0; + } + hasEnsemble(ensembleIdent: EnsembleIdent): boolean { return this.findEnsemble(ensembleIdent) !== null; } + hasDeltaEnsemble(ensembleIdent: EnsembleIdent): boolean { + return this.findDeltaEnsemble(ensembleIdent) !== null; + } + findEnsemble(ensembleIdent: EnsembleIdent): Ensemble | null { return this._ensembleArr.find((ens) => ens.getIdent().equals(ensembleIdent)) ?? null; } + findDeltaEnsemble(ensembleIdent: EnsembleIdent): DeltaEnsemble | null { + return this._deltaEnsembleArr.find((ens) => ens.getIdent().equals(ensembleIdent)) ?? null; + } + findEnsembleByIdentString(ensembleIdentString: string): Ensemble | null { try { const ensembleIdent = EnsembleIdent.fromString(ensembleIdentString); @@ -32,10 +47,23 @@ export class EnsembleSet { } } + findDeltaEnsembleByIdentString(ensembleIdentString: string): DeltaEnsemble | null { + try { + const ensembleIdent = EnsembleIdent.fromString(ensembleIdentString); + return this.findDeltaEnsemble(ensembleIdent); + } catch { + return null; + } + } + getEnsembleArr(): readonly Ensemble[] { return this._ensembleArr; } + getDeltaEnsembleArr(): readonly DeltaEnsemble[] { + return this._deltaEnsembleArr; + } + // Temporary helper method findCaseName(ensembleIdent: EnsembleIdent): string { const foundEnsemble = this.findEnsemble(ensembleIdent); diff --git a/frontend/src/framework/Workbench.ts b/frontend/src/framework/Workbench.ts index 424813de4..4018762ce 100644 --- a/frontend/src/framework/Workbench.ts +++ b/frontend/src/framework/Workbench.ts @@ -27,6 +27,13 @@ export type LayoutElement = { relWidth: number; }; +export type UserDeltaEnsembleSetting = { + firstEnsembleIdent: EnsembleIdent; + secondEnsembleIdent: EnsembleIdent; + customName: string | null; + color: string; +}; + export type UserEnsembleSetting = { ensembleIdent: EnsembleIdent; customName: string | null; @@ -228,13 +235,18 @@ export class Workbench { async loadAndSetupEnsembleSetInSession( queryClient: QueryClient, - userEnsembleSettings: UserEnsembleSetting[] + userEnsembleSettings: UserEnsembleSetting[], + userDeltaEnsembleSettings: UserDeltaEnsembleSetting[] ): Promise { this.storeEnsembleSetInLocalStorage(userEnsembleSettings); console.debug("loadAndSetupEnsembleSetInSession - starting load"); this._workbenchSession.setEnsembleSetLoadingState(true); - const newEnsembleSet = await loadEnsembleSetMetadataFromBackend(queryClient, userEnsembleSettings); + const newEnsembleSet = await loadEnsembleSetMetadataFromBackend( + queryClient, + userEnsembleSettings, + userDeltaEnsembleSettings + ); console.debug("loadAndSetupEnsembleSetInSession - loading done"); console.debug("loadAndSetupEnsembleSetInSession - publishing"); this._workbenchSession.setEnsembleSetLoadingState(false); diff --git a/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx b/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx index 51a9601f6..cb6300759 100644 --- a/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx +++ b/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx @@ -1,3 +1,5 @@ +import { DeltaEnsemble } from "@framework/DeltaEnsemble"; +import { Ensemble } from "@framework/Ensemble"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { EnsembleSet } from "@framework/EnsembleSet"; import { ColorTile } from "@lib/components/ColorTile"; @@ -6,11 +8,12 @@ import { Select, SelectOption, SelectProps } from "@lib/components/Select"; type EnsembleSelectProps = { ensembleSet: EnsembleSet; value: EnsembleIdent[]; + allowDeltaEnsembles?: boolean; onChange: (ensembleIdentArr: EnsembleIdent[]) => void; } & Omit, "options" | "value" | "onChange">; export function EnsembleSelect(props: EnsembleSelectProps): JSX.Element { - const { ensembleSet, value, onChange, multiple, ...rest } = props; + const { ensembleSet, value, allowDeltaEnsembles, onChange, multiple, ...rest } = props; function handleSelectionChanged(selectedEnsembleIdentStrArr: string[]) { const identArr: EnsembleIdent[] = []; @@ -25,16 +28,9 @@ export function EnsembleSelect(props: EnsembleSelectProps): JSX.Element { } const optionsArr: SelectOption[] = []; - for (const ens of ensembleSet.getEnsembleArr()) { - optionsArr.push({ - value: ens.getIdent().toString(), - label: ens.getDisplayName(), - adornment: ( - - - - ), - }); + optionsArr.push(...createEnsembleSelectOptions(ensembleSet.getEnsembleArr())); + if (allowDeltaEnsembles) { + optionsArr.push(...createEnsembleSelectOptions(ensembleSet.getDeltaEnsembleArr())); } const selectedArr: string[] = []; @@ -54,3 +50,19 @@ export function EnsembleSelect(props: EnsembleSelectProps): JSX.Element { /> ); } + +function createEnsembleSelectOptions(ensembleArr: readonly Ensemble[] | readonly DeltaEnsemble[]): SelectOption[] { + const optionsArr: SelectOption[] = []; + for (const ens of ensembleArr) { + optionsArr.push({ + value: ens.getIdent().toString(), + label: ens.getDisplayName(), + adornment: ( + + + + ), + }); + } + return optionsArr; +} diff --git a/frontend/src/framework/internal/EnsembleSetLoader.ts b/frontend/src/framework/internal/EnsembleSetLoader.ts index d028d652a..b6f1a2a3a 100644 --- a/frontend/src/framework/internal/EnsembleSetLoader.ts +++ b/frontend/src/framework/internal/EnsembleSetLoader.ts @@ -1,6 +1,7 @@ import { EnsembleDetails_api, EnsembleParameter_api, EnsembleSensitivity_api } from "@api"; import { apiService } from "@framework/ApiService"; -import { UserEnsembleSetting } from "@framework/Workbench"; +import { DeltaEnsemble } from "@framework/DeltaEnsemble"; +import { UserDeltaEnsembleSetting, UserEnsembleSetting } from "@framework/Workbench"; import { QueryClient } from "@tanstack/react-query"; import { Ensemble } from "../Ensemble"; @@ -11,7 +12,8 @@ import { EnsembleSet } from "../EnsembleSet"; export async function loadEnsembleSetMetadataFromBackend( queryClient: QueryClient, - userEnsembleSettings: UserEnsembleSetting[] + userEnsembleSettings: UserEnsembleSetting[], + userDeltaEnsembleSettings: UserDeltaEnsembleSetting[] ): Promise { const ensembleIdentsToLoad: EnsembleIdent[] = userEnsembleSettings.map((setting) => setting.ensembleIdent); @@ -106,7 +108,125 @@ export async function loadEnsembleSetMetadataFromBackend( ); } - return new EnsembleSet(outEnsembleArr); + // TEMPORARY: Load delta ensembles after loading ensembles + const ensembleIdentsToLoadForDeltaEnsembles: EnsembleIdent[] = []; + for (const elm of userDeltaEnsembleSettings) { + if (!ensembleIdentsToLoadForDeltaEnsembles.includes(elm.firstEnsembleIdent)) { + ensembleIdentsToLoadForDeltaEnsembles.push(elm.firstEnsembleIdent); + } + if (!ensembleIdentsToLoadForDeltaEnsembles.includes(elm.secondEnsembleIdent)) { + ensembleIdentsToLoadForDeltaEnsembles.push(elm.secondEnsembleIdent); + } + } + + console.debug("loadDeltaEnsembleMetadataFromBackend", ensembleIdentsToLoad); + + const deltaEnsembleDetailsPromiseArr: Promise[] = []; + for (const ensembleIdent of ensembleIdentsToLoadForDeltaEnsembles) { + const caseUuid = ensembleIdent.getCaseUuid(); + const ensembleName = ensembleIdent.getEnsembleName(); + + const ensembleDetailsPromise = queryClient.fetchQuery({ + queryKey: ["getEnsembleDetails", caseUuid, ensembleName], + queryFn: () => apiService.explore.getEnsembleDetails(caseUuid, ensembleName), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }); + + deltaEnsembleDetailsPromiseArr.push(ensembleDetailsPromise); + } + console.debug(`Issued ${deltaEnsembleDetailsPromiseArr.length} promise(s)`); + + const outDeltaEnsembleArr: DeltaEnsemble[] = []; + + const deltaEnsembleDetailsOutcomeArr = await Promise.allSettled(deltaEnsembleDetailsPromiseArr); + for (const userDeltaEnsembleSetting of userDeltaEnsembleSettings) { + const firstEnsembleIdent = userDeltaEnsembleSetting.firstEnsembleIdent; + const secondEnsembleIdent = userDeltaEnsembleSetting.secondEnsembleIdent; + const color = userDeltaEnsembleSetting.color; + const customName = userDeltaEnsembleSetting.customName; + + let firstEnsembleDetails: EnsembleDetails_api | null = null; + let secondEnsembleDetails: EnsembleDetails_api | null = null; + for (const elm of deltaEnsembleDetailsOutcomeArr) { + if (elm.status === "rejected") { + continue; + } + const ensembleDetails: EnsembleDetails_api = elm.value; + if ( + firstEnsembleDetails === null && + ensembleDetails.case_uuid === firstEnsembleIdent.getCaseUuid() && + ensembleDetails.name === firstEnsembleIdent.getEnsembleName() + ) { + firstEnsembleDetails = elm.value; + } + + if ( + secondEnsembleDetails === null && + ensembleDetails.case_uuid === secondEnsembleIdent.getCaseUuid() && + ensembleDetails.name === secondEnsembleIdent.getEnsembleName() + ) { + secondEnsembleDetails = elm.value; + } + + if (firstEnsembleDetails !== null && secondEnsembleDetails !== null) { + break; + } + } + + if (!firstEnsembleDetails || !secondEnsembleDetails) { + const errorText = + !firstEnsembleDetails && !secondEnsembleDetails + ? "first and second" + : !firstEnsembleDetails + ? "first" + : "second"; + + console.error( + `Error fetching ${errorText} ensemble details, dropping delta ensemble: `, + userDeltaEnsembleSetting.customName ?? + `${userDeltaEnsembleSetting.firstEnsembleIdent.toString()} - ${userDeltaEnsembleSetting.secondEnsembleIdent.toString()}` + ); + continue; + } + + const firstEnsemble = new Ensemble( + firstEnsembleDetails.field_identifier, + firstEnsembleDetails.case_uuid, + firstEnsembleDetails.case_name, + firstEnsembleDetails.name, + firstEnsembleDetails.realizations, + [], + null, + "", // TODO: Add color support? Perhaps not necessary per ensemble for delta ensembles + null // TODO: Add custom name support + ); + + const secondEnsemble = new Ensemble( + secondEnsembleDetails.field_identifier, + secondEnsembleDetails.case_uuid, + secondEnsembleDetails.case_name, + secondEnsembleDetails.name, + secondEnsembleDetails.realizations, + [], + null, + "", + null + ); + + // Only add delta ensemble from already loaded ensembles? + // NOTE: not necessary, should probably be the ensemble dialogs responsibility + // const firstEnsemble = outEnsembleArr.find((ens) => ens.getIdent().equals(firstEnsembleIdent)); + // const secondEnsemble = outEnsembleArr.find((ens) => ens.getIdent().equals(secondEnsembleIdent)); + + // if (firstEnsemble && secondEnsemble) { + // outDeltaEnsembleArr.push(new DeltaEnsemble(firstEnsemble, secondEnsemble, color, customName)); + // } + + outDeltaEnsembleArr.push(new DeltaEnsemble(firstEnsemble, secondEnsemble, color, customName)); + } + + return new EnsembleSet(outEnsembleArr, outDeltaEnsembleArr); } function buildSensitivityArrFromApiResponse(apiSensitivityArr: EnsembleSensitivity_api[]): Sensitivity[] { diff --git a/frontend/src/framework/internal/components/NavBar/leftNavBar.tsx b/frontend/src/framework/internal/components/NavBar/leftNavBar.tsx index 05eb6bcd8..7069c5e7d 100644 --- a/frontend/src/framework/internal/components/NavBar/leftNavBar.tsx +++ b/frontend/src/framework/internal/components/NavBar/leftNavBar.tsx @@ -7,7 +7,10 @@ import { UserEnsembleSetting, Workbench, WorkbenchEvents } from "@framework/Work import { useEnsembleSet, useIsEnsembleSetLoading } from "@framework/WorkbenchSession"; import { LoginButton } from "@framework/internal/components/LoginButton"; import { SelectEnsemblesDialog } from "@framework/internal/components/SelectEnsemblesDialog"; -import { EnsembleItem } from "@framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog"; +import { + DeltaEnsembleItem, + EnsembleItem, +} from "@framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog"; import { Badge } from "@lib/components/Badge"; import { Button } from "@lib/components/Button"; import { CircularProgress } from "@lib/components/CircularProgress"; @@ -30,6 +33,7 @@ export const LeftNavBar: React.FC = (props) => { const ensembleSet = useEnsembleSet(props.workbench.getWorkbenchSession()); const [ensembleDialogOpen, setEnsembleDialogOpen] = React.useState(false); const [newSelectedEnsembles, setNewSelectedEnsembles] = React.useState([]); + const [newCreatedDeltaEnsembles, setNewCreatedDeltaEnsembles] = React.useState([]); const [layoutEmpty, setLayoutEmpty] = React.useState(props.workbench.getLayout().length === 0); const [collapsed, setCollapsed] = React.useState(localStorage.getItem("navBarCollapsed") === "true"); const [prevIsAppInitialized, setPrevIsAppInitialized] = React.useState(false); @@ -125,14 +129,27 @@ export const LeftNavBar: React.FC = (props) => { customName: ens.getCustomName(), })); - function loadAndSetupEnsembles(ensembleItems: EnsembleItem[]): Promise { + function loadAndSetupEnsembles( + ensembleItems: EnsembleItem[], + createdDeltaEnsembles: DeltaEnsembleItem[] + ): Promise { setNewSelectedEnsembles(ensembleItems); + setNewCreatedDeltaEnsembles(createdDeltaEnsembles); const ensembleSettings: UserEnsembleSetting[] = ensembleItems.map((ens) => ({ ensembleIdent: new EnsembleIdent(ens.caseUuid, ens.ensembleName), customName: ens.customName, color: ens.color, })); - return props.workbench.loadAndSetupEnsembleSetInSession(queryClient, ensembleSettings); + const deltaEnsembleSettings = createdDeltaEnsembles.map((deltaEns) => ({ + firstEnsembleIdent: new EnsembleIdent(deltaEns.firstEnsemble.caseUuid, deltaEns.firstEnsemble.ensembleName), + secondEnsembleIdent: new EnsembleIdent( + deltaEns.secondEnsemble.caseUuid, + deltaEns.secondEnsemble.ensembleName + ), + customName: deltaEns.customName, + color: deltaEns.color, + })); + return props.workbench.loadAndSetupEnsembleSetInSession(queryClient, ensembleSettings, deltaEnsembleSettings); } let fixedSelectedEnsembles = selectedEnsembles; @@ -270,6 +287,7 @@ export const LeftNavBar: React.FC = (props) => { diff --git a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx index 2abc7a2f0..3f1e31a20 100644 --- a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx +++ b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx @@ -17,15 +17,35 @@ import { Switch } from "@lib/components/Switch"; import { TableSelect, TableSelectOption } from "@lib/components/TableSelect"; import { useValidState } from "@lib/hooks/useValidState"; import { ColorSet } from "@lib/utils/ColorSet"; +import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { Add, Check, Remove } from "@mui/icons-material"; import { useQuery } from "@tanstack/react-query"; import { isEqual } from "lodash"; +import { v4 } from "uuid"; import { UserAvatar } from "./private-components/userAvatar"; import { LoadingOverlay } from "../LoadingOverlay"; +const CASE_UUID_ENSEMBLE_NAME_SEPARATOR = "~&&~"; + +type DeltaEnsembleInternalItem = { + firstEnsemble: EnsembleItem | null; + secondEnsemble: EnsembleItem | null; + uuid: string; // Not a real caseUuid, but a unique identifier for the delta ensemble + color: string; + customName: string | null; +}; + +export type DeltaEnsembleItem = { + firstEnsemble: EnsembleItem; + secondEnsemble: EnsembleItem; + uuid: string; + color: string; + customName: string | null; +}; + export type EnsembleItem = { caseUuid: string; caseName: string; @@ -35,9 +55,13 @@ export type EnsembleItem = { }; export type SelectEnsemblesDialogProps = { - loadAndSetupEnsembles: (selectedEnsembles: EnsembleItem[]) => Promise; + loadAndSetupEnsembles: ( + selectedEnsembles: EnsembleItem[], + createdDeltaEnsembles: DeltaEnsembleItem[] + ) => Promise; onClose: () => void; selectedEnsembles: EnsembleItem[]; + createdDeltaEnsembles: DeltaEnsembleItem[]; colorSet: ColorSet; }; @@ -66,17 +90,35 @@ export const SelectEnsemblesDialog: React.FC = (prop const [isLoadingEnsembles, setIsLoadingEnsembles] = React.useState(false); const [confirmCancel, setConfirmCancel] = React.useState(false); const [newlySelectedEnsembles, setNewlySelectedEnsembles] = React.useState([]); + const [newlyCreatedDeltaEnsembles, setNewlyCreatedDeltaEnsembles] = React.useState([]); const [casesFilteringOptions, setCasesFilteringOptions] = React.useState({ keep: !(readInitialStateFromLocalStorage("showKeepCases") === "false"), onlyMyCases: readInitialStateFromLocalStorage("showOnlyMyCases") === "true", users: [], }); + const [deltaEnsembles, setDeltaEnsembles] = React.useState([]); + const { userInfo } = useAuthProvider(); React.useLayoutEffect(() => { setNewlySelectedEnsembles(props.selectedEnsembles); - }, [props.selectedEnsembles]); + + // TODO: Verify that firstEnsemble and secondEnsemble are among props.selectedEnsembles? + setNewlyCreatedDeltaEnsembles(props.createdDeltaEnsembles); + }, [props.selectedEnsembles, props.createdDeltaEnsembles]); + + React.useLayoutEffect(() => { + setDeltaEnsembles( + props.createdDeltaEnsembles.map((elm) => ({ + firstEnsemble: elm.firstEnsemble, + secondEnsemble: elm.firstEnsemble, + uuid: elm.uuid, + color: elm.color, + customName: elm.customName, + })) + ); + }, [props.createdDeltaEnsembles]); const fieldsQuery = useQuery({ queryKey: ["getFields"], @@ -143,28 +185,130 @@ export const SelectEnsemblesDialog: React.FC = (prop } function checkIfEnsembleAlreadySelected(): boolean { - if (selectedCaseId && selectedEnsembleName) { - if ( - newlySelectedEnsembles.some( - (e) => e.caseUuid === selectedCaseId && e.ensembleName === selectedEnsembleName - ) - ) { - return true; - } + if ( + !selectedCaseId || + !selectedEnsembleName || + !newlySelectedEnsembles.some( + (e) => e.caseUuid === selectedCaseId && e.ensembleName === selectedEnsembleName + ) + ) { + return false; } - return false; + return true; } function tryToFindUnusedColor(): string { - const usedColors = newlySelectedEnsembles.map((e) => e.color); + const usedColors = [...newlySelectedEnsembles.map((e) => e.color), ...deltaEnsembles.map((e) => e.color)]; for (let i = 0; i < props.colorSet.getColorArray().length; i++) { - if (!usedColors.includes(props.colorSet.getColor(i))) { - return props.colorSet.getColor(i); + const candidateColor = props.colorSet.getColor(i); + if (!usedColors.includes(candidateColor)) { + return candidateColor; } } return props.colorSet.getColor(newlySelectedEnsembles.length); } + function handleDeltaEnsembleFirstEnsembleChange( + deltaEnsembleUuid: string, + newCaseUuidAndEnsembleNameString: string + ) { + const { caseUuid: newFirstEnsembleCaseUuid, ensembleName: newFirstEnsembleEnsembleName } = + createCaseUuidAndEnsembleNameFromString(newCaseUuidAndEnsembleNameString); + + const firstEnsemble = newlySelectedEnsembles.find( + (elm) => elm.caseUuid === newFirstEnsembleCaseUuid && elm.ensembleName === newFirstEnsembleEnsembleName + ); + if (!firstEnsemble) { + return; + } + + setDeltaEnsembles((prev) => + prev.map((elm) => { + if (elm.uuid === deltaEnsembleUuid) { + return { + ...elm, + firstEnsemble: { + caseUuid: firstEnsemble.caseUuid, + caseName: firstEnsemble.caseName, + ensembleName: firstEnsemble.ensembleName, + customName: firstEnsemble.customName, + color: firstEnsemble.color, + }, + // ensembleName: createDeltaEnsembleName(firstEnsemble, e.secondEnsemble), + }; + } + return elm; + }) + ); + } + + function handleDeltaEnsembleSecondEnsembleChange( + deltaEnsembleUuid: string, + newCaseUuidAndEnsembleNameString: string + ) { + const { caseUuid: newSecondEnsembleCaseUuid, ensembleName: newSecondEnsembleEnsembleName } = + createCaseUuidAndEnsembleNameFromString(newCaseUuidAndEnsembleNameString); + + const secondEnsemble = newlySelectedEnsembles.find( + (e) => e.caseUuid === newSecondEnsembleCaseUuid && e.ensembleName === newSecondEnsembleEnsembleName + ); + if (!secondEnsemble) { + return; + } + + setDeltaEnsembles((prev) => + prev.map((elm) => { + if (elm.uuid === deltaEnsembleUuid) { + return { + ...elm, + secondEnsemble: { + caseUuid: secondEnsemble.caseUuid, + caseName: secondEnsemble.caseName, + ensembleName: secondEnsemble.ensembleName, + customName: secondEnsemble.customName, + color: secondEnsemble.color, + }, + // ensembleName: createDeltaEnsembleName(secondEnsemble, e.item.secondEnsemble), + }; + } + return elm; + }) + ); + } + + function handleAddDeltaEnsemble() { + if (newlySelectedEnsembles.length === 0) { + return; + } + + const firstEnsemble = newlySelectedEnsembles[0]; + const secondEnsemble = + newlySelectedEnsembles.length === 1 ? newlySelectedEnsembles[0] : newlySelectedEnsembles[1]; + + const newDeltaEnsemble: DeltaEnsembleInternalItem = { + firstEnsemble: { + caseUuid: firstEnsemble.caseUuid, + caseName: firstEnsemble.caseName, + ensembleName: firstEnsemble.ensembleName, + customName: firstEnsemble.customName, + color: firstEnsemble.color, + }, + secondEnsemble: { + caseUuid: secondEnsemble.caseUuid, + caseName: secondEnsemble.caseName, + ensembleName: secondEnsemble.ensembleName, + customName: secondEnsemble.customName, + color: secondEnsemble.color, + }, + uuid: v4(), + // ensembleName: createDeltaEnsembleName(firstEnsemble, secondEnsemble), + color: tryToFindUnusedColor(), + customName: null, + }; + + setDeltaEnsembles((prev) => [...prev, newDeltaEnsemble]); + } + function handleAddEnsemble() { if (!checkIfEnsembleAlreadySelected()) { const caseName = casesQuery.data?.find((c) => c.uuid === selectedCaseId)?.name ?? "UNKNOWN"; @@ -181,10 +325,34 @@ export const SelectEnsemblesDialog: React.FC = (prop } } + function handleRemoveDeltaEnsemble(uuid: string) { + setDeltaEnsembles((prev) => [...prev.filter((e) => e.uuid !== uuid)]); + } + function handleRemoveEnsemble(caseUuid: string, ensembleName: string) { setNewlySelectedEnsembles((prev) => [ ...prev.filter((e) => e.caseUuid !== caseUuid || e.ensembleName !== ensembleName), ]); + + // Validate delta ensembles + const newDeltaEnsembles = [...deltaEnsembles]; + for (const elm of deltaEnsembles) { + if ( + elm.firstEnsemble && + elm.firstEnsemble.caseUuid === caseUuid && + elm.firstEnsemble.ensembleName === ensembleName + ) { + elm.firstEnsemble = null; + } + if ( + elm.secondEnsemble && + elm.secondEnsemble.caseUuid === caseUuid && + elm.secondEnsemble.ensembleName === ensembleName + ) { + elm.secondEnsemble = null; + } + } + setDeltaEnsembles(newDeltaEnsembles); } function handleClose() { @@ -201,9 +369,27 @@ export const SelectEnsemblesDialog: React.FC = (prop } function handleApplyEnsembleSelection() { + if (deltaEnsembles.some((elm) => !elm.firstEnsemble || !elm.secondEnsemble)) { + return; + } + + const validDeltaEnsembles: DeltaEnsembleItem[] = []; + for (const deltaEnsemble of deltaEnsembles) { + if (!deltaEnsemble.firstEnsemble || !deltaEnsemble.secondEnsemble) { + continue; + } + validDeltaEnsembles.push({ + firstEnsemble: deltaEnsemble.firstEnsemble, + secondEnsemble: deltaEnsemble.secondEnsemble, + uuid: deltaEnsemble.uuid, + color: deltaEnsemble.color, + customName: deltaEnsemble.customName, + }); + } + setIsLoadingEnsembles(true); props - .loadAndSetupEnsembles(newlySelectedEnsembles) + .loadAndSetupEnsembles(newlySelectedEnsembles, validDeltaEnsembles) .then(() => { handleClose(); }) @@ -212,6 +398,10 @@ export const SelectEnsemblesDialog: React.FC = (prop }); } + function checkHasInvalidDeltaEnsembles(): boolean { + return deltaEnsembles.some((elm) => !elm.firstEnsemble || !elm.secondEnsemble); + } + function checkIfAnyChanges(): boolean { return !isEqual(props.selectedEnsembles, newlySelectedEnsembles); } @@ -244,13 +434,35 @@ export const SelectEnsemblesDialog: React.FC = (prop return filteredCases; } + function handleDeltaEnsembleColorChange(uuid: string, color: string) { + setDeltaEnsembles((prev) => + prev.map((elm) => { + if (elm.uuid === uuid) { + return { ...elm, color: color }; + } + return elm; + }) + ); + } + function handleColorChange(caseUuid: string, ensembleName: string, color: string) { setNewlySelectedEnsembles((prev) => - prev.map((e) => { - if (e.caseUuid === caseUuid && e.ensembleName === ensembleName) { - return { ...e, color: color }; + prev.map((elm) => { + if (elm.caseUuid === caseUuid && elm.ensembleName === ensembleName) { + return { ...elm, color: color }; } - return e; + return elm; + }) + ); + } + + function handleDeltaEnsembleCustomNameChange(uuid: string, customName: string) { + setDeltaEnsembles((prev) => + prev.map((elm) => { + if (elm.uuid === uuid) { + return { ...elm, customName: customName === "" ? null : customName }; + } + return elm; }) ); } @@ -297,18 +509,20 @@ export const SelectEnsemblesDialog: React.FC = (prop modal width={"75%"} minWidth={800} + // minHeight={600} + height={"75"} actions={
-
-
{isLoadingEnsembles && } - { - setConfirmCancel(false)} - title="Unsaved changes" - modal - actions={ -
- - -
- } - > - You have unsaved changes which will be lost. Are you sure you want to cancel? -
- } + setConfirmCancel(false)} + title="Unsaved changes" + modal + actions={ +
+ + +
+ } + > + You have unsaved changes which will be lost. Are you sure you want to cancel? +
); }; + +function createCaseUuidAndEnsembleNameString(ensembleItem: EnsembleItem): string { + return `${ensembleItem.caseUuid}${CASE_UUID_ENSEMBLE_NAME_SEPARATOR}${ensembleItem.ensembleName}`; +} + +function createCaseUuidAndEnsembleNameFromString(caseUuidAndEnsembleNameString: string): { + caseUuid: string; + ensembleName: string; +} { + const [caseUuid, ensembleName] = caseUuidAndEnsembleNameString.split(CASE_UUID_ENSEMBLE_NAME_SEPARATOR); + if (!caseUuid || !ensembleName) { + throw new Error("Invalid caseUuidAndEnsembleNameString"); + } + + return { caseUuid, ensembleName }; +} + +function createDeltaEnsembleName(firstEnsemble: EnsembleItem | null, secondEnsemble: EnsembleItem | null): string { + if (firstEnsemble !== null && secondEnsemble !== null) { + return `${firstEnsemble.ensembleName} (${firstEnsemble.caseName}) - ${secondEnsemble.ensembleName} (${secondEnsemble.caseName})`; + } + + if (firstEnsemble !== null) { + return `${firstEnsemble.ensembleName} (${firstEnsemble.caseName}) - UNKNOWN`; + } + + if (secondEnsemble !== null) { + return `UNKNOWN - ${secondEnsemble.ensembleName} (${secondEnsemble.caseName})`; + } + + return "UNKNOWN - UNKNOWN"; +} diff --git a/frontend/src/framework/utils/ensembleUiHelpers.ts b/frontend/src/framework/utils/ensembleUiHelpers.ts index 850e2d477..d63f1e78b 100644 --- a/frontend/src/framework/utils/ensembleUiHelpers.ts +++ b/frontend/src/framework/utils/ensembleUiHelpers.ts @@ -43,6 +43,22 @@ export function fixupEnsembleIdent( return ensembleSet.getEnsembleArr()[0].getIdent(); } +export function fixupDeltaEnsembleIdent( + currIdent: EnsembleIdent | null, + ensembleSet: EnsembleSet | null +): EnsembleIdent | null { + if (!ensembleSet?.hasAnyDeltaEnsembles()) { + return null; + } + + if (currIdent) { + if (ensembleSet.hasDeltaEnsemble(currIdent)) { + return currIdent; + } + } + + return ensembleSet.getDeltaEnsembleArr()[0].getIdent(); +} /** * Validates the the EnsembleIdents specified in currIdents against the contents of the @@ -68,3 +84,17 @@ export function fixupEnsembleIdents( return currIdents.filter((currIdent) => ensembleSet.hasEnsemble(currIdent)); } +export function fixupDeltaEnsembleIdents( + currIdents: EnsembleIdent[] | null, + ensembleSet: EnsembleSet | null +): EnsembleIdent[] | null { + if (!ensembleSet?.hasAnyDeltaEnsembles()) { + return null; + } + + if (currIdents === null || currIdents.length === 0) { + return [ensembleSet.getDeltaEnsembleArr()[0].getIdent()]; + } + + return currIdents.filter((currIdent) => ensembleSet.hasDeltaEnsemble(currIdent)); +} diff --git a/frontend/src/modules/SimulationTimeSeries/interfaces.ts b/frontend/src/modules/SimulationTimeSeries/interfaces.ts index b1c2d1652..c0dbefd53 100644 --- a/frontend/src/modules/SimulationTimeSeries/interfaces.ts +++ b/frontend/src/modules/SimulationTimeSeries/interfaces.ts @@ -1,4 +1,5 @@ import { Frequency_api } from "@api"; +import { DeltaEnsemble } from "@framework/DeltaEnsemble"; import { Ensemble } from "@framework/Ensemble"; import { ParameterIdent } from "@framework/EnsembleParameters"; import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; @@ -12,7 +13,12 @@ import { statisticsSelectionAtom, visualizationModeAtom, } from "./settings/atoms/baseAtoms"; -import { parameterIdentAtom, selectedEnsemblesAtom, vectorSpecificationsAtom } from "./settings/atoms/derivedAtoms"; +import { + parameterIdentAtom, + selectedDeltaEnsemblesAtom, + selectedEnsemblesAtom, + vectorSpecificationsAtom, +} from "./settings/atoms/derivedAtoms"; import { GroupBy, StatisticsSelection, VectorSpec, VisualizationMode } from "./typesAndEnums"; export type SettingsToViewInterface = { @@ -25,6 +31,7 @@ export type SettingsToViewInterface = { colorByParameter: boolean; parameterIdent: ParameterIdent | null; selectedEnsembles: Ensemble[]; + selectedDeltaEnsembles: DeltaEnsemble[]; resampleFrequency: Frequency_api | null; }; @@ -60,6 +67,9 @@ export const settingsToViewInterfaceInitialization: InterfaceInitialization { return get(selectedEnsemblesAtom); }, + selectedDeltaEnsembles: (get) => { + return get(selectedDeltaEnsemblesAtom); + }, resampleFrequency: (get) => { return get(resampleFrequencyAtom); }, diff --git a/frontend/src/modules/SimulationTimeSeries/settings/atoms/derivedAtoms.ts b/frontend/src/modules/SimulationTimeSeries/settings/atoms/derivedAtoms.ts index 7614ba905..8219b1e18 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/SimulationTimeSeries/settings/atoms/derivedAtoms.ts @@ -1,8 +1,9 @@ +import { DeltaEnsemble } from "@framework/DeltaEnsemble"; import { Ensemble } from "@framework/Ensemble"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { Parameter, ParameterIdent, ParameterType } from "@framework/EnsembleParameters"; import { EnsembleSetAtom } from "@framework/GlobalAtoms"; -import { fixupEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; +import { fixupDeltaEnsembleIdents, fixupEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; import { createVectorSelectorDataFromVectors } from "@modules/_shared/components/VectorSelector"; import { atom } from "jotai"; @@ -29,20 +30,27 @@ export const statisticsTypeAtom = atom((get) => { return StatisticsType.INDIVIDUAL; }); -export const selectedEnsembleIdentsAtom = atom((get) => { +export const selectedEnsembleIdentsAtom = atom<{ + ensembleIdents: EnsembleIdent[]; + deltaEnsembleIdents: EnsembleIdent[]; +}>((get) => { const ensembleSet = get(EnsembleSetAtom); const selectedEnsembleIdents = get(userSelectedEnsembleIdentsAtom); const newSelectedEnsembleIdents = selectedEnsembleIdents.filter((ensemble) => ensembleSet.hasEnsemble(ensemble)); + const newSelectedDeltaEnsembleIdents = selectedEnsembleIdents.filter((ensemble) => + ensembleSet.hasDeltaEnsemble(ensemble) + ); const validatedEnsembleIdents = fixupEnsembleIdents(newSelectedEnsembleIdents, ensembleSet); + const validatedDeltaEnsembleIdents = fixupDeltaEnsembleIdents(newSelectedDeltaEnsembleIdents, ensembleSet); - return validatedEnsembleIdents ?? []; + return { ensembleIdents: validatedEnsembleIdents ?? [], deltaEnsembleIdents: validatedDeltaEnsembleIdents ?? [] }; }); -export const selectedEnsemblesAtom = atom((get) => { +export const selectedEnsemblesAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; const selectedEnsembles: Ensemble[] = []; @@ -56,9 +64,23 @@ export const selectedEnsemblesAtom = atom((get) => { return selectedEnsembles; }); +export const selectedDeltaEnsemblesAtom = atom((get) => { + const ensembleSet = get(EnsembleSetAtom); + const selectedDeltaEnsembleIdents = get(selectedEnsembleIdentsAtom).deltaEnsembleIdents; + + const selectedDeltaEnsembles: DeltaEnsemble[] = []; + for (const ensembleIdent of selectedDeltaEnsembleIdents) { + const ensemble = ensembleSet.findDeltaEnsemble(ensembleIdent); + if (ensemble) { + selectedDeltaEnsembles.push(ensemble); + } + } + return selectedDeltaEnsembles; +}); + export const continuousAndNonConstantParametersUnionAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; const continuousAndNonConstantParametersUnion: Parameter[] = []; @@ -114,9 +136,9 @@ export const vectorSelectorDataAtom = atom((get) => { return createVectorSelectorDataFromVectors(availableVectorNames); }); -export const ensembleVectorListsHelperAtom = atom((get) => { +export const ensembleVectorListsHelperAtom = atom((get) => { const vectorListQueries = get(vectorListQueriesAtom); - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; return new EnsembleVectorListsHelper(selectedEnsembleIdents, vectorListQueries); }); @@ -124,7 +146,7 @@ export const ensembleVectorListsHelperAtom = atom((get) => { export const vectorSpecificationsAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); const ensembleVectorListsHelper = get(ensembleVectorListsHelperAtom); - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; const selectedVectorNames = get(selectedVectorNamesAtom); const vectorSpecifications: VectorSpec[] = []; diff --git a/frontend/src/modules/SimulationTimeSeries/settings/atoms/queryAtoms.ts b/frontend/src/modules/SimulationTimeSeries/settings/atoms/queryAtoms.ts index 459b34cfe..15ea71ce1 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/atoms/queryAtoms.ts +++ b/frontend/src/modules/SimulationTimeSeries/settings/atoms/queryAtoms.ts @@ -7,7 +7,7 @@ const STALE_TIME = 60 * 1000; const CACHE_TIME = 60 * 1000; export const vectorListQueriesAtom = atomWithQueries((get) => { - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; const queries = selectedEnsembleIdents.map((ensembleIdent) => { return () => ({ diff --git a/frontend/src/modules/SimulationTimeSeries/settings/hooks/useMakeSettingsStatusWriterMessages.ts b/frontend/src/modules/SimulationTimeSeries/settings/hooks/useMakeSettingsStatusWriterMessages.ts index a11d5ed89..53e12f1a8 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/hooks/useMakeSettingsStatusWriterMessages.ts +++ b/frontend/src/modules/SimulationTimeSeries/settings/hooks/useMakeSettingsStatusWriterMessages.ts @@ -44,13 +44,13 @@ export function useMakeSettingsStatusWriterMessages(statusWriter: SettingsStatus } // Note: selectedVectorNames is not updated until vectorSelectorData is updated and VectorSelector triggers onChange - if (selectedEnsembleIdents.length === 1) { + if (selectedEnsembleIdents.ensembleIdents.length === 1) { // If single ensemble is selected and no vectors exist, selectedVectorNames is empty as no vectors are valid // in the VectorSelector. Then utilizing selectedVectorTags for status message const vectorNames = selectedVectorNames.length > 0 ? selectedVectorNames : selectedVectorTags; - validateVectorNamesInEnsemble(vectorNames, selectedEnsembleIdents[0]); + validateVectorNamesInEnsemble(vectorNames, selectedEnsembleIdents.ensembleIdents[0]); } - for (const ensembleIdent of selectedEnsembleIdents) { + for (const ensembleIdent of selectedEnsembleIdents.ensembleIdents) { validateVectorNamesInEnsemble(selectedVectorNames, ensembleIdent); } } diff --git a/frontend/src/modules/SimulationTimeSeries/settings/settings.tsx b/frontend/src/modules/SimulationTimeSeries/settings/settings.tsx index 36e86dc97..e6baed9e7 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/settings.tsx +++ b/frontend/src/modules/SimulationTimeSeries/settings/settings.tsx @@ -250,7 +250,8 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr diff --git a/frontend/src/modules/SimulationTimeSeries/view/atoms/baseAtoms.ts b/frontend/src/modules/SimulationTimeSeries/view/atoms/baseAtoms.ts index 8c0d836d5..7b2e12bbb 100644 --- a/frontend/src/modules/SimulationTimeSeries/view/atoms/baseAtoms.ts +++ b/frontend/src/modules/SimulationTimeSeries/view/atoms/baseAtoms.ts @@ -1,4 +1,5 @@ import { Frequency_api } from "@api"; +import { DeltaEnsemble } from "@framework/DeltaEnsemble"; import { Ensemble } from "@framework/Ensemble"; import { ParameterIdent } from "@framework/EnsembleParameters"; import { VectorSpec, VisualizationMode } from "@modules/SimulationTimeSeries/typesAndEnums"; @@ -13,3 +14,4 @@ export const showObservationsAtom = atom(true); export const interfaceColorByParameterAtom = atom(false); export const parameterIdentAtom = atom(null); export const selectedEnsemblesAtom = atom([]); +export const selectedDeltaEnsemblesAtom = atom([]); diff --git a/frontend/src/modules/SimulationTimeSeries/view/atoms/interfaceEffects.ts b/frontend/src/modules/SimulationTimeSeries/view/atoms/interfaceEffects.ts index da265b642..dbfad83fa 100644 --- a/frontend/src/modules/SimulationTimeSeries/view/atoms/interfaceEffects.ts +++ b/frontend/src/modules/SimulationTimeSeries/view/atoms/interfaceEffects.ts @@ -5,6 +5,7 @@ import { interfaceColorByParameterAtom, parameterIdentAtom, resampleFrequencyAtom, + selectedDeltaEnsemblesAtom, selectedEnsemblesAtom, showObservationsAtom, vectorSpecificationsAtom, @@ -40,4 +41,8 @@ export const settingsToViewInterfaceEffects: InterfaceEffects { + const selectedDeltaEnsembles = getInterfaceValue("selectedDeltaEnsembles"); + setAtomValue(selectedDeltaEnsemblesAtom, selectedDeltaEnsembles); + }, ]; From cbae31c4029df334093562aa4bc5488d3ac959cf Mon Sep 17 00:00:00 2001 From: jorgenherje Date: Thu, 14 Nov 2024 16:05:13 +0100 Subject: [PATCH 02/33] WIP Creating interfaces and generics --- frontend/src/framework/DeltaEnsemble.ts | 84 ++++++++------ frontend/src/framework/DeltaEnsembleIdent.ts | 60 ++++++++++ frontend/src/framework/Ensemble.ts | 3 +- frontend/src/framework/EnsembleIdent.ts | 17 ++- .../src/framework/EnsembleIdentInterface.ts | 6 + frontend/src/framework/EnsembleInterface.ts | 14 +++ frontend/src/framework/EnsembleSet.ts | 108 ++++++++++-------- frontend/src/framework/EnsembleTypeSet.ts | 26 +++++ .../EnsembleDropdown/ensembleDropdown.tsx | 43 ++++++- .../EnsembleSelect/ensembleSelect.tsx | 67 ++++++----- .../selectEnsemblesDialog.tsx | 15 +-- .../src/framework/utils/ensembleUiHelpers.ts | 87 +++++++------- .../view/utils/tableComponentUtils.ts | 3 +- .../settings/atoms/baseAtoms.ts | 3 +- .../settings/atoms/derivedAtoms.ts | 47 ++++---- .../settings/atoms/queryAtoms.ts | 8 +- .../useMakeSettingsStatusWriterMessages.ts | 12 +- .../settings/settings.tsx | 5 +- .../utils/ensemblesVectorListHelper.ts | 20 ++-- 19 files changed, 420 insertions(+), 208 deletions(-) create mode 100644 frontend/src/framework/DeltaEnsembleIdent.ts create mode 100644 frontend/src/framework/EnsembleIdentInterface.ts create mode 100644 frontend/src/framework/EnsembleInterface.ts create mode 100644 frontend/src/framework/EnsembleTypeSet.ts diff --git a/frontend/src/framework/DeltaEnsemble.ts b/frontend/src/framework/DeltaEnsemble.ts index c5b9abafa..0e5e28df2 100644 --- a/frontend/src/framework/DeltaEnsemble.ts +++ b/frontend/src/framework/DeltaEnsemble.ts @@ -1,7 +1,9 @@ import { v4 } from "uuid"; +import { DeltaEnsembleIdent } from "./DeltaEnsembleIdent"; import { Ensemble } from "./Ensemble"; import { EnsembleIdent } from "./EnsembleIdent"; +import { EnsembleInterface } from "./EnsembleInterface"; import { EnsembleParameters } from "./EnsembleParameters"; import { EnsembleSensitivities } from "./EnsembleSensitivities"; @@ -10,8 +12,12 @@ export enum DeltaEnsembleElement { SECOND = "second", } -export class DeltaEnsemble { - private _deltaEnsembleIdent: EnsembleIdent; +function createDeltaEnsembleName(firstEnsembleName: string, secondEnsembleName: string): string { + return `(${firstEnsembleName}) - (${secondEnsembleName})`; +} + +export class DeltaEnsemble implements EnsembleInterface { + private _deltaEnsembleIdent: DeltaEnsembleIdent; private _firstEnsemble: Ensemble; private _secondEnsemble: Ensemble; private _color: string; @@ -22,12 +28,12 @@ export class DeltaEnsemble { private _sensitivities: EnsembleSensitivities | null; constructor(firstEnsemble: Ensemble, secondEnsemble: Ensemble, color: string, customName: string | null = null) { - // NOTE: Delta ensembles are created using two ensembles, thus adding v4() to ensure uniqueness - const _deltaEnsembleCaseUuid = - firstEnsemble.getIdent().getCaseUuid() + secondEnsemble.getIdent().getCaseUuid() + v4(); - const _deltaEnsembleName = - `${firstEnsemble.getIdent().getEnsembleName()} - ${secondEnsemble.getIdent().getEnsembleName()}` + v4(); - this._deltaEnsembleIdent = new EnsembleIdent(_deltaEnsembleCaseUuid, _deltaEnsembleName); + const deltaEnsembleCaseUuid = v4(); + const deltaEnsembleName = createDeltaEnsembleName( + firstEnsemble.getIdent().getEnsembleName(), + secondEnsemble.getIdent().getEnsembleName() + ); + this._deltaEnsembleIdent = new DeltaEnsembleIdent(deltaEnsembleCaseUuid, deltaEnsembleName); this._firstEnsemble = firstEnsemble; this._secondEnsemble = secondEnsemble; @@ -47,10 +53,46 @@ export class DeltaEnsemble { this._sensitivities = null; } - getIdent(): EnsembleIdent { + // *** Interface methods *** + + getIdent(): DeltaEnsembleIdent { return this._deltaEnsembleIdent; } + getDisplayName(): string { + if (this._customName) { + return this._customName; + } + + return `${this._firstEnsemble.getDisplayName()} - ${this._secondEnsemble.getDisplayName()}`; + } + + getEnsembleName(): string { + return this._deltaEnsembleIdent.getEnsembleName(); + } + + getRealizations(): readonly number[] { + return this._realizationsArr; + } + + getRealizationCount(): number { + return this._realizationsArr.length; + } + + getMaxRealizationNumber(): number | undefined { + return this._realizationsArr[this._realizationsArr.length - 1]; + } + + getColor(): string { + return this._color; + } + + getCustomName(): string | null { + return this._customName; + } + + // *** Custom methods *** + getEnsembleIdentByElement(element: DeltaEnsembleElement): EnsembleIdent { if (element === DeltaEnsembleElement.FIRST) { return this._firstEnsemble.getIdent(); @@ -81,18 +123,6 @@ export class DeltaEnsemble { throw new Error("Unhandled element type"); } - getDisplayName(): string { - if (this._customName) { - return this._customName; - } - - return `${this._firstEnsemble.getDisplayName()} - ${this._secondEnsemble.getDisplayName()}`; - } - - getRealizations(): readonly number[] { - return this._realizationsArr; - } - getRealizationsByElement(element: DeltaEnsembleElement): readonly number[] { if (element === DeltaEnsembleElement.FIRST) { return this._firstEnsemble.getRealizations(); @@ -100,16 +130,4 @@ export class DeltaEnsemble { return this._secondEnsemble.getRealizations(); } } - - getRealizationsCount(): number { - return this._realizationsArr.length; - } - - getColor(): string { - return this._color; - } - - getCustomName(): string | null { - return this._customName; - } } diff --git a/frontend/src/framework/DeltaEnsembleIdent.ts b/frontend/src/framework/DeltaEnsembleIdent.ts new file mode 100644 index 000000000..19bb2dc2b --- /dev/null +++ b/frontend/src/framework/DeltaEnsembleIdent.ts @@ -0,0 +1,60 @@ +import { EnsembleIdentInterface } from "./EnsembleIdentInterface"; + +export class DeltaEnsembleIdent implements EnsembleIdentInterface { + private _uuid: string; + private _ensembleName: string; + + constructor(uuid: string, ensembleName: string) { + this._uuid = uuid; + this._ensembleName = ensembleName; + } + + static fromUuidAndName(uuid: string, ensembleName: string): DeltaEnsembleIdent { + return new DeltaEnsembleIdent(uuid, ensembleName); + } + + static uuidAndEnsembleNameToString(uuid: string, ensembleName: string): string { + return `${uuid}~@@~${ensembleName}`; + } + + static isValidDeltaEnsembleIdentString(deltaEnsembleIdentString: string): boolean { + const regex = DeltaEnsembleIdent.getDeltaEnsembleIdentRegex(); + const result = regex.exec(deltaEnsembleIdentString); + return !!result && !!result.groups && !!result.groups.uuid && !!result.groups.name; + } + + static fromString(deltaEnsembleIdentString: string): DeltaEnsembleIdent { + const regex = DeltaEnsembleIdent.getDeltaEnsembleIdentRegex(); + const result = regex.exec(deltaEnsembleIdentString); + if (!result || !result.groups || !result.groups.uuid || !result.groups.name) { + throw new Error(`Invalid ensemble ident: ${deltaEnsembleIdentString}`); + } + + const { uuid, name } = result.groups; + + return new DeltaEnsembleIdent(uuid, name); + } + + private static getDeltaEnsembleIdentRegex(): RegExp { + return /^(?)~@@~(?.*)$/; + } + + getEnsembleName(): string { + return this._ensembleName; + } + + toString(): string { + return DeltaEnsembleIdent.uuidAndEnsembleNameToString(this._uuid, this._ensembleName); + } + + equals(otherIdent: DeltaEnsembleIdent | null): boolean { + if (!otherIdent) { + return false; + } + if (otherIdent === this) { + return true; + } + + return this._uuid === otherIdent._uuid && this._ensembleName === otherIdent._ensembleName; + } +} diff --git a/frontend/src/framework/Ensemble.ts b/frontend/src/framework/Ensemble.ts index 7c982a315..3810bdfdf 100644 --- a/frontend/src/framework/Ensemble.ts +++ b/frontend/src/framework/Ensemble.ts @@ -1,8 +1,9 @@ import { EnsembleIdent } from "./EnsembleIdent"; +import { EnsembleInterface } from "./EnsembleInterface"; import { EnsembleParameters, Parameter } from "./EnsembleParameters"; import { EnsembleSensitivities, Sensitivity } from "./EnsembleSensitivities"; -export class Ensemble { +export class Ensemble implements EnsembleInterface { private _ensembleIdent: EnsembleIdent; private _fieldIdentifier: string; private _caseName: string; diff --git a/frontend/src/framework/EnsembleIdent.ts b/frontend/src/framework/EnsembleIdent.ts index 28ec10983..b5354467a 100644 --- a/frontend/src/framework/EnsembleIdent.ts +++ b/frontend/src/framework/EnsembleIdent.ts @@ -1,4 +1,6 @@ -export class EnsembleIdent { +import { EnsembleIdentInterface } from "./EnsembleIdentInterface"; + +export class EnsembleIdent implements EnsembleIdentInterface { private _caseUuid: string; private _ensembleName: string; @@ -15,9 +17,14 @@ export class EnsembleIdent { return `${caseUuid}::${ensembleName}`; } + static isValidEnsembleIdentString(ensembleIdentString: string): boolean { + const regex = EnsembleIdent.getEnsembleIdentRegex(); + const result = regex.exec(ensembleIdentString); + return !!result && !!result.groups && !!result.groups.caseUuid && !!result.groups.ensembleName; + } + static fromString(ensembleIdentString: string): EnsembleIdent { - const regex = - /^(?[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})::(?.*)$/; + const regex = EnsembleIdent.getEnsembleIdentRegex(); const result = regex.exec(ensembleIdentString); if (!result || !result.groups || !result.groups.caseUuid || !result.groups.ensembleName) { throw new Error(`Invalid ensemble ident: ${ensembleIdentString}`); @@ -28,6 +35,10 @@ export class EnsembleIdent { return new EnsembleIdent(caseUuid, ensembleName); } + private static getEnsembleIdentRegex(): RegExp { + return /^(?[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})::(?.*)$/; + } + getCaseUuid(): string { return this._caseUuid; } diff --git a/frontend/src/framework/EnsembleIdentInterface.ts b/frontend/src/framework/EnsembleIdentInterface.ts new file mode 100644 index 000000000..948720509 --- /dev/null +++ b/frontend/src/framework/EnsembleIdentInterface.ts @@ -0,0 +1,6 @@ +export interface EnsembleIdentInterface { + // createFromString(ensembleIdentString: string): TImplementation; + getEnsembleName(): string; + toString(): string; + equals(otherIdent: TImplementation | null): boolean; +} diff --git a/frontend/src/framework/EnsembleInterface.ts b/frontend/src/framework/EnsembleInterface.ts new file mode 100644 index 000000000..4968cb6f2 --- /dev/null +++ b/frontend/src/framework/EnsembleInterface.ts @@ -0,0 +1,14 @@ +import { EnsembleIdentInterface } from "./EnsembleIdentInterface"; + +export interface EnsembleInterface { + getIdent(): EnsembleIdentInterface; + getDisplayName(): string; + getEnsembleName(): string; + getRealizations(): readonly number[]; + getRealizationCount(): number; + getMaxRealizationNumber(): number | undefined; + // getParemters(): EnsembleParameters; + // getSensitivities(): EnsembleSensitivities | null; + getColor(): string; + getCustomName(): string | null; +} diff --git a/frontend/src/framework/EnsembleSet.ts b/frontend/src/framework/EnsembleSet.ts index 78309a71b..bb2d720be 100644 --- a/frontend/src/framework/EnsembleSet.ts +++ b/frontend/src/framework/EnsembleSet.ts @@ -1,72 +1,84 @@ import { DeltaEnsemble } from "./DeltaEnsemble"; +import { DeltaEnsembleIdent } from "./DeltaEnsembleIdent"; import { Ensemble } from "./Ensemble"; import { EnsembleIdent } from "./EnsembleIdent"; +import { EnsembleTypeSet } from "./EnsembleTypeSet"; +export enum EnsembleType { + ALL = "all", + REGULAR = "regular", + DELTA = "delta", +} export class EnsembleSet { - private _ensembleArr: Ensemble[]; - private _deltaEnsembleArr: DeltaEnsemble[]; + private _regularEnsembleSet: EnsembleTypeSet; + private _deltaEnsembleSet: EnsembleTypeSet; constructor(ensembles: Ensemble[], deltaEnsembles: DeltaEnsemble[] = []) { - this._ensembleArr = ensembles; - this._deltaEnsembleArr = deltaEnsembles; + this._regularEnsembleSet = new EnsembleTypeSet(ensembles); + this._deltaEnsembleSet = new EnsembleTypeSet(deltaEnsembles); } - /** - * Returns true if there is at least one ensemble in the set. - */ - hasAnyEnsembles(): boolean { - return this._ensembleArr.length > 0; - } + hasAnyEnsembles(type?: EnsembleType): boolean { + if (type === EnsembleType.ALL) { + return this._regularEnsembleSet.hasAnyEnsembles() || this._deltaEnsembleSet.hasAnyEnsembles(); + } + if (type === EnsembleType.DELTA) { + return this._deltaEnsembleSet.hasAnyEnsembles(); + } - hasAnyDeltaEnsembles(): boolean { - return this._deltaEnsembleArr.length > 0; + // Regular or undefined + return this._regularEnsembleSet.hasAnyEnsembles(); } - hasEnsemble(ensembleIdent: EnsembleIdent): boolean { - return this.findEnsemble(ensembleIdent) !== null; + hasEnsemble(ensembleIdent: EnsembleIdent | DeltaEnsembleIdent): boolean { + if (ensembleIdent instanceof EnsembleIdent) { + return this._regularEnsembleSet.findEnsemble(ensembleIdent) !== null; + } + if (ensembleIdent instanceof DeltaEnsembleIdent) { + return this._deltaEnsembleSet.findEnsemble(ensembleIdent) !== null; + } + return false; } - hasDeltaEnsemble(ensembleIdent: EnsembleIdent): boolean { - return this.findDeltaEnsemble(ensembleIdent) !== null; + findEnsemble(ensembleIdent: EnsembleIdent): Ensemble | null; + findEnsemble(ensembleIdent: DeltaEnsembleIdent): DeltaEnsemble | null; + findEnsemble(ensembleIdent: EnsembleIdent | DeltaEnsembleIdent): Ensemble | DeltaEnsemble | null; + findEnsemble(ensembleIdent: EnsembleIdent | DeltaEnsembleIdent): Ensemble | DeltaEnsemble | null { + if (ensembleIdent instanceof EnsembleIdent) { + return this._regularEnsembleSet.findEnsemble(ensembleIdent); + } + if (ensembleIdent instanceof DeltaEnsembleIdent) { + return this._deltaEnsembleSet.findEnsemble(ensembleIdent); + } + return null; } - findEnsemble(ensembleIdent: EnsembleIdent): Ensemble | null { - return this._ensembleArr.find((ens) => ens.getIdent().equals(ensembleIdent)) ?? null; - } + getEnsembleArr(type?: EnsembleType.REGULAR): readonly Ensemble[]; + getEnsembleArr(type: EnsembleType.DELTA): readonly DeltaEnsemble[]; + getEnsembleArr(type: EnsembleType.ALL): readonly (Ensemble | DeltaEnsemble)[]; + getEnsembleArr( + type: EnsembleType | undefined = undefined + ): readonly Ensemble[] | readonly DeltaEnsemble[] | readonly (Ensemble | DeltaEnsemble)[] { + if (type === EnsembleType.ALL) { + return [...this._regularEnsembleSet.getEnsembleArr(), ...this._deltaEnsembleSet.getEnsembleArr()]; + } + if (type === EnsembleType.DELTA) { + return this._deltaEnsembleSet.getEnsembleArr(); + } - findDeltaEnsemble(ensembleIdent: EnsembleIdent): DeltaEnsemble | null { - return this._deltaEnsembleArr.find((ens) => ens.getIdent().equals(ensembleIdent)) ?? null; + // Regular or undefined + return this._regularEnsembleSet.getEnsembleArr(); } - findEnsembleByIdentString(ensembleIdentString: string): Ensemble | null { - try { + findEnsembleByIdentString(ensembleIdentString: string): Ensemble | DeltaEnsemble | null { + if (EnsembleIdent.isValidEnsembleIdentString(ensembleIdentString)) { const ensembleIdent = EnsembleIdent.fromString(ensembleIdentString); - return this.findEnsemble(ensembleIdent); - } catch { - return null; + return this._regularEnsembleSet.findEnsemble(ensembleIdent); } - } - - findDeltaEnsembleByIdentString(ensembleIdentString: string): DeltaEnsemble | null { - try { - const ensembleIdent = EnsembleIdent.fromString(ensembleIdentString); - return this.findDeltaEnsemble(ensembleIdent); - } catch { - return null; + if (DeltaEnsembleIdent.isValidDeltaEnsembleIdentString(ensembleIdentString)) { + const deltaEnsembleIdent = DeltaEnsembleIdent.fromString(ensembleIdentString); + return this._deltaEnsembleSet.findEnsemble(deltaEnsembleIdent); } - } - - getEnsembleArr(): readonly Ensemble[] { - return this._ensembleArr; - } - - getDeltaEnsembleArr(): readonly DeltaEnsemble[] { - return this._deltaEnsembleArr; - } - - // Temporary helper method - findCaseName(ensembleIdent: EnsembleIdent): string { - const foundEnsemble = this.findEnsemble(ensembleIdent); - return foundEnsemble?.getCaseName() ?? ""; + return null; } } diff --git a/frontend/src/framework/EnsembleTypeSet.ts b/frontend/src/framework/EnsembleTypeSet.ts new file mode 100644 index 000000000..cf1c0b54d --- /dev/null +++ b/frontend/src/framework/EnsembleTypeSet.ts @@ -0,0 +1,26 @@ +import { EnsembleIdentInterface } from "./EnsembleIdentInterface"; +import { EnsembleInterface } from "./EnsembleInterface"; + +export class EnsembleTypeSet, TEnsemble extends EnsembleInterface> { + private _ensembleTypeArr: TEnsemble[]; + + constructor(ensembles: TEnsemble[]) { + this._ensembleTypeArr = ensembles; + } + + hasAnyEnsembles(): boolean { + return this._ensembleTypeArr.length > 0; + } + + hasEnsemble(ensembleIdent: TEnsembleIdent): boolean { + return this.findEnsemble(ensembleIdent) !== null; + } + + findEnsemble(ensembleIdent: TEnsembleIdent): TEnsemble | null { + return this._ensembleTypeArr.find((ens) => ens.getIdent().equals(ensembleIdent)) ?? null; + } + + getEnsembleArr(): readonly TEnsemble[] { + return this._ensembleTypeArr; + } +} diff --git a/frontend/src/framework/components/EnsembleDropdown/ensembleDropdown.tsx b/frontend/src/framework/components/EnsembleDropdown/ensembleDropdown.tsx index b292f249a..6f8fbcf6d 100644 --- a/frontend/src/framework/components/EnsembleDropdown/ensembleDropdown.tsx +++ b/frontend/src/framework/components/EnsembleDropdown/ensembleDropdown.tsx @@ -1,24 +1,55 @@ +import { DeltaEnsemble } from "@framework/DeltaEnsemble"; +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; import { EnsembleIdent } from "@framework/EnsembleIdent"; -import { EnsembleSet } from "@framework/EnsembleSet"; +import { EnsembleSet, EnsembleType } from "@framework/EnsembleSet"; import { ColorTile } from "@lib/components/ColorTile"; import { Dropdown, DropdownOption, DropdownProps } from "@lib/components/Dropdown"; -type EnsembleDropdownProps = { +// Overload for EnsembleDropdown with DeltaEnsembleIdent +export type EnsembleDropdownWithDeltaEnsemblesProps = { ensembleSet: EnsembleSet; + allowDeltaEnsembles: true; + value: EnsembleIdent | DeltaEnsembleIdent | null; + onChange: (ensembleIdent: EnsembleIdent | DeltaEnsembleIdent | null) => void; +} & Omit, "options" | "value" | "onChange">; + +// Overload for EnsembleDropdown without DeltaEnsembleIdent +export type EnsembleDropdownWithoutDeltaEnsemblesProps = { + ensembleSet: EnsembleSet; + allowDeltaEnsembles?: false | undefined; value: EnsembleIdent | null; onChange: (ensembleIdent: EnsembleIdent | null) => void; } & Omit, "options" | "value" | "onChange">; -export function EnsembleDropdown(props: EnsembleDropdownProps): JSX.Element { - const { ensembleSet, value, onChange, ...rest } = props; +export function EnsembleDropdown(props: EnsembleDropdownWithDeltaEnsemblesProps): JSX.Element; +export function EnsembleDropdown(props: EnsembleDropdownWithoutDeltaEnsemblesProps): JSX.Element; +export function EnsembleDropdown( + props: EnsembleDropdownWithDeltaEnsemblesProps | EnsembleDropdownWithoutDeltaEnsemblesProps +): JSX.Element { + const { ensembleSet, allowDeltaEnsembles, value, onChange, ...rest } = props; function handleSelectionChanged(selectedEnsembleIdentStr: string) { const foundEnsemble = ensembleSet.findEnsembleByIdentString(selectedEnsembleIdentStr); - onChange(foundEnsemble ? foundEnsemble.getIdent() : null); + if (foundEnsemble === null) { + onChange(null); + return; + } + if (allowDeltaEnsembles) { + onChange(foundEnsemble.getIdent()); + return; + } + if (foundEnsemble instanceof DeltaEnsemble) { + onChange(null); + return; + } + onChange(foundEnsemble.getIdent()); } const optionsArr: DropdownOption[] = []; - for (const ens of ensembleSet.getEnsembleArr()) { + const ensembleArr = allowDeltaEnsembles + ? ensembleSet.getEnsembleArr(EnsembleType.ALL) + : ensembleSet.getEnsembleArr(EnsembleType.REGULAR); + for (const ens of ensembleArr) { optionsArr.push({ value: ens.getIdent().toString(), label: ens.getDisplayName(), diff --git a/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx b/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx index cb6300759..b54748722 100644 --- a/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx +++ b/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx @@ -1,36 +1,67 @@ -import { DeltaEnsemble } from "@framework/DeltaEnsemble"; +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; import { Ensemble } from "@framework/Ensemble"; import { EnsembleIdent } from "@framework/EnsembleIdent"; -import { EnsembleSet } from "@framework/EnsembleSet"; +import { EnsembleSet, EnsembleType } from "@framework/EnsembleSet"; import { ColorTile } from "@lib/components/ColorTile"; import { Select, SelectOption, SelectProps } from "@lib/components/Select"; -type EnsembleSelectProps = { +// Overload for EnsembleSelect with DeltaEnsembleIdent +export type EnsembleSelectWithDeltaEnsemblesProps = { ensembleSet: EnsembleSet; + multiple?: boolean; + allowDeltaEnsembles: true; + value: (EnsembleIdent | DeltaEnsembleIdent)[]; + onChange: (ensembleIdentArr: (EnsembleIdent | DeltaEnsembleIdent)[]) => void; +} & Omit, "options" | "value" | "onChange">; + +// Overload for EnsembleSelect without DeltaEnsembleIdent +export type EnsembleSelectWithoutDeltaEnsemblesProps = { + ensembleSet: EnsembleSet; + multiple?: boolean; + allowDeltaEnsembles?: false | undefined; value: EnsembleIdent[]; - allowDeltaEnsembles?: boolean; onChange: (ensembleIdentArr: EnsembleIdent[]) => void; } & Omit, "options" | "value" | "onChange">; -export function EnsembleSelect(props: EnsembleSelectProps): JSX.Element { +export function EnsembleSelect(props: EnsembleSelectWithDeltaEnsemblesProps): JSX.Element; +export function EnsembleSelect(props: EnsembleSelectWithoutDeltaEnsemblesProps): JSX.Element; +export function EnsembleSelect( + props: EnsembleSelectWithDeltaEnsemblesProps | EnsembleSelectWithoutDeltaEnsemblesProps +): JSX.Element { const { ensembleSet, value, allowDeltaEnsembles, onChange, multiple, ...rest } = props; function handleSelectionChanged(selectedEnsembleIdentStrArr: string[]) { - const identArr: EnsembleIdent[] = []; + const identArr: (EnsembleIdent | DeltaEnsembleIdent)[] = []; for (const identStr of selectedEnsembleIdentStrArr) { const foundEnsemble = ensembleSet.findEnsembleByIdentString(identStr); - if (foundEnsemble) { + if (foundEnsemble !== null && (allowDeltaEnsembles || foundEnsemble instanceof Ensemble)) { identArr.push(foundEnsemble.getIdent()); } } + // Filter to match the correct return type before calling onChange + if (!allowDeltaEnsembles) { + const validIdentArr = identArr.filter((ident) => ident instanceof EnsembleIdent) as EnsembleIdent[]; + onChange(validIdentArr); + return; + } onChange(identArr); } const optionsArr: SelectOption[] = []; - optionsArr.push(...createEnsembleSelectOptions(ensembleSet.getEnsembleArr())); - if (allowDeltaEnsembles) { - optionsArr.push(...createEnsembleSelectOptions(ensembleSet.getDeltaEnsembleArr())); + const ensembleArr = allowDeltaEnsembles + ? ensembleSet.getEnsembleArr(EnsembleType.ALL) + : ensembleSet.getEnsembleArr(EnsembleType.REGULAR); + for (const ens of ensembleArr) { + optionsArr.push({ + value: ens.getIdent().toString(), + label: ens.getDisplayName(), + adornment: ( + + + + ), + }); } const selectedArr: string[] = []; @@ -50,19 +81,3 @@ export function EnsembleSelect(props: EnsembleSelectProps): JSX.Element { /> ); } - -function createEnsembleSelectOptions(ensembleArr: readonly Ensemble[] | readonly DeltaEnsemble[]): SelectOption[] { - const optionsArr: SelectOption[] = []; - for (const ens of ensembleArr) { - optionsArr.push({ - value: ens.getIdent().toString(), - label: ens.getDisplayName(), - adornment: ( - - - - ), - }); - } - return optionsArr; -} diff --git a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx index 3f1e31a20..3a6b44bb3 100644 --- a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx +++ b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx @@ -30,10 +30,11 @@ import { LoadingOverlay } from "../LoadingOverlay"; const CASE_UUID_ENSEMBLE_NAME_SEPARATOR = "~&&~"; -type DeltaEnsembleInternalItem = { - firstEnsemble: EnsembleItem | null; - secondEnsemble: EnsembleItem | null; - uuid: string; // Not a real caseUuid, but a unique identifier for the delta ensemble +// Internal type before applying created delta ensemble externally +type InternalDeltaEnsembleItem = { + firstEnsemble: EnsembleItem | null; // Allows null + secondEnsemble: EnsembleItem | null; // Allows null + uuid: string; color: string; customName: string | null; }; @@ -90,14 +91,14 @@ export const SelectEnsemblesDialog: React.FC = (prop const [isLoadingEnsembles, setIsLoadingEnsembles] = React.useState(false); const [confirmCancel, setConfirmCancel] = React.useState(false); const [newlySelectedEnsembles, setNewlySelectedEnsembles] = React.useState([]); - const [newlyCreatedDeltaEnsembles, setNewlyCreatedDeltaEnsembles] = React.useState([]); + const [newlyCreatedDeltaEnsembles, setNewlyCreatedDeltaEnsembles] = React.useState([]); const [casesFilteringOptions, setCasesFilteringOptions] = React.useState({ keep: !(readInitialStateFromLocalStorage("showKeepCases") === "false"), onlyMyCases: readInitialStateFromLocalStorage("showOnlyMyCases") === "true", users: [], }); - const [deltaEnsembles, setDeltaEnsembles] = React.useState([]); + const [deltaEnsembles, setDeltaEnsembles] = React.useState([]); const { userInfo } = useAuthProvider(); @@ -285,7 +286,7 @@ export const SelectEnsemblesDialog: React.FC = (prop const secondEnsemble = newlySelectedEnsembles.length === 1 ? newlySelectedEnsembles[0] : newlySelectedEnsembles[1]; - const newDeltaEnsemble: DeltaEnsembleInternalItem = { + const newDeltaEnsemble: InternalDeltaEnsembleItem = { firstEnsemble: { caseUuid: firstEnsemble.caseUuid, caseName: firstEnsemble.caseName, diff --git a/frontend/src/framework/utils/ensembleUiHelpers.ts b/frontend/src/framework/utils/ensembleUiHelpers.ts index d63f1e78b..a660d52aa 100644 --- a/frontend/src/framework/utils/ensembleUiHelpers.ts +++ b/frontend/src/framework/utils/ensembleUiHelpers.ts @@ -1,5 +1,6 @@ +import { DeltaEnsembleIdent } from "../DeltaEnsembleIdent"; import { EnsembleIdent } from "../EnsembleIdent"; -import { EnsembleSet } from "../EnsembleSet"; +import { EnsembleSet, EnsembleType } from "../EnsembleSet"; export function maybeAssignFirstSyncedEnsemble( currIdent: EnsembleIdent | null, @@ -18,19 +19,31 @@ export function maybeAssignFirstSyncedEnsemble( } /** - * Validates the the EnsembleIdent specified in currIdent against the contents of the - * EnsembleSet and fixes the value if it isn't valid. + * Validates the the EnsembleIdent or DeltaEnsembleIdent specified in currIdent against the + * contents of the EnsembleSet and fixes the value if it isn't valid. * * Returns null if an empty EnsembleSet is specified. * - * Note that if the specified EnsembleIdent is valid, this function will always return - * a reference to the exact same object that was passed in currIdent. This means that - * you can compare the references (fixedIdent !== currIdent) to detect any changes. + * Note that if the specified EnsembleIdents and DeltaEnsembleIdents are valid, this function + * will always return a reference to the exact same object that was passed in currIdent. This + * means that you can compare the references (fixedIdent !== currIdent) to detect any changes. */ export function fixupEnsembleIdent( currIdent: EnsembleIdent | null, ensembleSet: EnsembleSet | null -): EnsembleIdent | null { +): EnsembleIdent | null; +export function fixupEnsembleIdent( + currIdent: DeltaEnsembleIdent | null, + ensembleSet: EnsembleSet | null +): DeltaEnsembleIdent | null; +export function fixupEnsembleIdent( + currIdent: EnsembleIdent | DeltaEnsembleIdent | null, + ensembleSet: EnsembleSet | null +): (EnsembleIdent | DeltaEnsembleIdent) | null; +export function fixupEnsembleIdent( + currIdent: EnsembleIdent | DeltaEnsembleIdent | null, + ensembleSet: EnsembleSet | null +): (EnsembleIdent | DeltaEnsembleIdent) | null { if (!ensembleSet?.hasAnyEnsembles()) { return null; } @@ -41,39 +54,39 @@ export function fixupEnsembleIdent( } } - return ensembleSet.getEnsembleArr()[0].getIdent(); -} -export function fixupDeltaEnsembleIdent( - currIdent: EnsembleIdent | null, - ensembleSet: EnsembleSet | null -): EnsembleIdent | null { - if (!ensembleSet?.hasAnyDeltaEnsembles()) { - return null; - } - - if (currIdent) { - if (ensembleSet.hasDeltaEnsemble(currIdent)) { - return currIdent; - } + if (currIdent instanceof DeltaEnsembleIdent) { + return ensembleSet.getEnsembleArr(EnsembleType.DELTA)[0].getIdent(); } - return ensembleSet.getDeltaEnsembleArr()[0].getIdent(); + return ensembleSet.getEnsembleArr()[0].getIdent(); } /** - * Validates the the EnsembleIdents specified in currIdents against the contents of the - * EnsembleSet and fixes the value if it isn't valid. + * Validates the the EnsembleIdents or DeltaEnsembleIdents specified in currIdents against the + * contents of the EnsembleSet and fixes the value if it isn't valid. * * Returns null if an empty EnsembleSet is specified. * - * Note that if the specified EnsembleIdents are valid, this function will always return - * a reference to the exact same object that was passed in currIdent. This means that - * you can compare the references (fixedIdent !== currIdent) to detect any changes. + * Note that if the specified EnsembleIdents and DeltaEnsembleIdents are valid, this function + * will always return a reference to the exact same object that was passed in currIdent. This + * means that you can compare the references (fixedIdent !== currIdent) to detect any changes. */ export function fixupEnsembleIdents( currIdents: EnsembleIdent[] | null, ensembleSet: EnsembleSet | null -): EnsembleIdent[] | null { +): EnsembleIdent[] | null; +export function fixupEnsembleIdents( + currIdents: DeltaEnsembleIdent[] | null, + ensembleSet: EnsembleSet | null +): DeltaEnsembleIdent[] | null; +export function fixupEnsembleIdents( + currIdents: (EnsembleIdent | DeltaEnsembleIdent)[] | null, + ensembleSet: EnsembleSet | null +): (EnsembleIdent | DeltaEnsembleIdent)[] | null; +export function fixupEnsembleIdents( + currIdents: (EnsembleIdent | DeltaEnsembleIdent)[] | null, + ensembleSet: EnsembleSet | null +): (EnsembleIdent | DeltaEnsembleIdent)[] | null { if (!ensembleSet?.hasAnyEnsembles()) { return null; } @@ -82,19 +95,7 @@ export function fixupEnsembleIdents( return [ensembleSet.getEnsembleArr()[0].getIdent()]; } - return currIdents.filter((currIdent) => ensembleSet.hasEnsemble(currIdent)); -} -export function fixupDeltaEnsembleIdents( - currIdents: EnsembleIdent[] | null, - ensembleSet: EnsembleSet | null -): EnsembleIdent[] | null { - if (!ensembleSet?.hasAnyDeltaEnsembles()) { - return null; - } - - if (currIdents === null || currIdents.length === 0) { - return [ensembleSet.getDeltaEnsembleArr()[0].getIdent()]; - } - - return currIdents.filter((currIdent) => ensembleSet.hasDeltaEnsemble(currIdent)); + return currIdents.filter((currIdent) => { + return ensembleSet.hasEnsemble(currIdent); + }); } diff --git a/frontend/src/modules/InplaceVolumetricsTable/view/utils/tableComponentUtils.ts b/frontend/src/modules/InplaceVolumetricsTable/view/utils/tableComponentUtils.ts index 583afeb51..b34c52099 100644 --- a/frontend/src/modules/InplaceVolumetricsTable/view/utils/tableComponentUtils.ts +++ b/frontend/src/modules/InplaceVolumetricsTable/view/utils/tableComponentUtils.ts @@ -1,4 +1,5 @@ import { FluidZone_api, InplaceVolumetricStatistic_api } from "@api"; +import { Ensemble } from "@framework/Ensemble"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { EnsembleSet } from "@framework/EnsembleSet"; import { TableHeading, TableRow } from "@lib/components/Table/table"; @@ -190,7 +191,7 @@ function formatEnsembleIdent(value: string | number | null, ensembleSet: Ensembl return "-"; } const ensemble = ensembleSet.findEnsembleByIdentString(value.toString()); - if (ensemble) { + if (ensemble && ensemble instanceof Ensemble) { return makeDistinguishableEnsembleDisplayName( EnsembleIdent.fromString(value.toString()), ensembleSet.getEnsembleArr() diff --git a/frontend/src/modules/SimulationTimeSeries/settings/atoms/baseAtoms.ts b/frontend/src/modules/SimulationTimeSeries/settings/atoms/baseAtoms.ts index 2aee400f0..6fc77473d 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/atoms/baseAtoms.ts +++ b/frontend/src/modules/SimulationTimeSeries/settings/atoms/baseAtoms.ts @@ -1,4 +1,5 @@ import { Frequency_api, StatisticFunction_api } from "@api"; +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ParameterIdent } from "@framework/EnsembleParameters"; import { atomWithCompare } from "@framework/utils/atomUtils"; @@ -25,7 +26,7 @@ export const statisticsSelectionAtom = atom({ FanchartStatisticsSelection: Object.values(FanchartStatisticOption), }); -export const userSelectedEnsembleIdentsAtom = atomWithCompare([], isEqual); +export const userSelectedEnsembleIdentsAtom = atomWithCompare<(EnsembleIdent | DeltaEnsembleIdent)[]>([], isEqual); export const selectedVectorNamesAtom = atomWithCompare([], isEqual); diff --git a/frontend/src/modules/SimulationTimeSeries/settings/atoms/derivedAtoms.ts b/frontend/src/modules/SimulationTimeSeries/settings/atoms/derivedAtoms.ts index 8219b1e18..6e1c084f5 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/SimulationTimeSeries/settings/atoms/derivedAtoms.ts @@ -1,9 +1,10 @@ import { DeltaEnsemble } from "@framework/DeltaEnsemble"; +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; import { Ensemble } from "@framework/Ensemble"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { Parameter, ParameterIdent, ParameterType } from "@framework/EnsembleParameters"; import { EnsembleSetAtom } from "@framework/GlobalAtoms"; -import { fixupDeltaEnsembleIdents, fixupEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; +import { fixupEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; import { createVectorSelectorDataFromVectors } from "@modules/_shared/components/VectorSelector"; import { atom } from "jotai"; @@ -30,33 +31,26 @@ export const statisticsTypeAtom = atom((get) => { return StatisticsType.INDIVIDUAL; }); -export const selectedEnsembleIdentsAtom = atom<{ - ensembleIdents: EnsembleIdent[]; - deltaEnsembleIdents: EnsembleIdent[]; -}>((get) => { +export const selectedEnsembleIdentsAtom = atom<(EnsembleIdent | DeltaEnsembleIdent)[]>((get) => { const ensembleSet = get(EnsembleSetAtom); const selectedEnsembleIdents = get(userSelectedEnsembleIdentsAtom); const newSelectedEnsembleIdents = selectedEnsembleIdents.filter((ensemble) => ensembleSet.hasEnsemble(ensemble)); - const newSelectedDeltaEnsembleIdents = selectedEnsembleIdents.filter((ensemble) => - ensembleSet.hasDeltaEnsemble(ensemble) - ); - const validatedEnsembleIdents = fixupEnsembleIdents(newSelectedEnsembleIdents, ensembleSet); - const validatedDeltaEnsembleIdents = fixupDeltaEnsembleIdents(newSelectedDeltaEnsembleIdents, ensembleSet); - return { ensembleIdents: validatedEnsembleIdents ?? [], deltaEnsembleIdents: validatedDeltaEnsembleIdents ?? [] }; + return validatedEnsembleIdents ?? []; }); export const selectedEnsemblesAtom = atom((get) => { + // NOTE: Used for view and color by parameter, i.e. not for delta ensembles yet! const ensembleSet = get(EnsembleSetAtom); - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); const selectedEnsembles: Ensemble[] = []; for (const ensembleIdent of selectedEnsembleIdents) { const ensemble = ensembleSet.findEnsemble(ensembleIdent); - if (ensemble) { + if (ensemble && ensemble instanceof Ensemble) { selectedEnsembles.push(ensemble); } } @@ -66,11 +60,15 @@ export const selectedEnsemblesAtom = atom((get) => { export const selectedDeltaEnsemblesAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); - const selectedDeltaEnsembleIdents = get(selectedEnsembleIdentsAtom).deltaEnsembleIdents; + const selectedDeltaEnsembleIdents = get(selectedEnsembleIdentsAtom); const selectedDeltaEnsembles: DeltaEnsemble[] = []; for (const ensembleIdent of selectedDeltaEnsembleIdents) { - const ensemble = ensembleSet.findDeltaEnsemble(ensembleIdent); + if (ensembleIdent instanceof EnsembleIdent) { + continue; + } + + const ensemble = ensembleSet.findEnsemble(ensembleIdent); if (ensemble) { selectedDeltaEnsembles.push(ensemble); } @@ -80,14 +78,14 @@ export const selectedDeltaEnsemblesAtom = atom((get) => { export const continuousAndNonConstantParametersUnionAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); const continuousAndNonConstantParametersUnion: Parameter[] = []; for (const ensembleIdent of selectedEnsembleIdents) { const ensemble = ensembleSet.findEnsemble(ensembleIdent); - if (!ensemble) { + if (!ensemble || !(ensemble instanceof Ensemble)) { continue; } @@ -138,20 +136,27 @@ export const vectorSelectorDataAtom = atom((get) => { export const ensembleVectorListsHelperAtom = atom((get) => { const vectorListQueries = get(vectorListQueriesAtom); - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); - return new EnsembleVectorListsHelper(selectedEnsembleIdents, vectorListQueries); + const regularEnsembleIdents = selectedEnsembleIdents.filter( + (ensembleIdent) => ensembleIdent instanceof EnsembleIdent + ) as EnsembleIdent[]; + + return new EnsembleVectorListsHelper(regularEnsembleIdents, vectorListQueries); }); export const vectorSpecificationsAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); const ensembleVectorListsHelper = get(ensembleVectorListsHelperAtom); - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); const selectedVectorNames = get(selectedVectorNamesAtom); const vectorSpecifications: VectorSpec[] = []; - for (const ensembleIdent of selectedEnsembleIdents) { + const regularEnsembleIdents = selectedEnsembleIdents.filter( + (ensembleIdent) => ensembleIdent instanceof EnsembleIdent + ) as EnsembleIdent[]; + for (const ensembleIdent of regularEnsembleIdents) { for (const vectorName of selectedVectorNames) { if (!ensembleVectorListsHelper.isVectorInEnsemble(ensembleIdent, vectorName)) { continue; diff --git a/frontend/src/modules/SimulationTimeSeries/settings/atoms/queryAtoms.ts b/frontend/src/modules/SimulationTimeSeries/settings/atoms/queryAtoms.ts index 15ea71ce1..d1583261d 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/atoms/queryAtoms.ts +++ b/frontend/src/modules/SimulationTimeSeries/settings/atoms/queryAtoms.ts @@ -1,4 +1,5 @@ import { apiService } from "@framework/ApiService"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; import { atomWithQueries } from "@framework/utils/atomUtils"; import { selectedEnsembleIdentsAtom } from "./derivedAtoms"; @@ -7,9 +8,12 @@ const STALE_TIME = 60 * 1000; const CACHE_TIME = 60 * 1000; export const vectorListQueriesAtom = atomWithQueries((get) => { - const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom).ensembleIdents; + const selectedEnsembleIdents = get(selectedEnsembleIdentsAtom); + const regularEnsembleIdents = selectedEnsembleIdents.filter( + (ensembleIdent) => ensembleIdent instanceof EnsembleIdent + ) as EnsembleIdent[]; - const queries = selectedEnsembleIdents.map((ensembleIdent) => { + const queries = regularEnsembleIdents.map((ensembleIdent) => { return () => ({ queryKey: ["ensembles", ensembleIdent.toString()], queryFn: () => diff --git a/frontend/src/modules/SimulationTimeSeries/settings/hooks/useMakeSettingsStatusWriterMessages.ts b/frontend/src/modules/SimulationTimeSeries/settings/hooks/useMakeSettingsStatusWriterMessages.ts index 53e12f1a8..9314a6f33 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/hooks/useMakeSettingsStatusWriterMessages.ts +++ b/frontend/src/modules/SimulationTimeSeries/settings/hooks/useMakeSettingsStatusWriterMessages.ts @@ -1,3 +1,4 @@ +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { SettingsStatusWriter } from "@framework/StatusWriter"; @@ -29,7 +30,10 @@ export function useMakeSettingsStatusWriterMessages(statusWriter: SettingsStatus } // Set warning for vector names not existing in a selected ensemble - function validateVectorNamesInEnsemble(vectorNames: string[], ensembleIdent: EnsembleIdent) { + function validateVectorNamesInEnsemble(vectorNames: string[], ensembleIdent: EnsembleIdent | DeltaEnsembleIdent) { + if (ensembleIdent instanceof DeltaEnsembleIdent) { + return; + } const existingVectors = vectorNames.filter((vector) => ensembleVectorListsHelper.isVectorInEnsemble(ensembleIdent, vector) ); @@ -44,13 +48,13 @@ export function useMakeSettingsStatusWriterMessages(statusWriter: SettingsStatus } // Note: selectedVectorNames is not updated until vectorSelectorData is updated and VectorSelector triggers onChange - if (selectedEnsembleIdents.ensembleIdents.length === 1) { + if (selectedEnsembleIdents.length === 1) { // If single ensemble is selected and no vectors exist, selectedVectorNames is empty as no vectors are valid // in the VectorSelector. Then utilizing selectedVectorTags for status message const vectorNames = selectedVectorNames.length > 0 ? selectedVectorNames : selectedVectorTags; - validateVectorNamesInEnsemble(vectorNames, selectedEnsembleIdents.ensembleIdents[0]); + validateVectorNamesInEnsemble(vectorNames, selectedEnsembleIdents[0]); } - for (const ensembleIdent of selectedEnsembleIdents.ensembleIdents) { + for (const ensembleIdent of selectedEnsembleIdents) { validateVectorNamesInEnsemble(selectedVectorNames, ensembleIdent); } } diff --git a/frontend/src/modules/SimulationTimeSeries/settings/settings.tsx b/frontend/src/modules/SimulationTimeSeries/settings/settings.tsx index e6baed9e7..d11ab3f83 100644 --- a/frontend/src/modules/SimulationTimeSeries/settings/settings.tsx +++ b/frontend/src/modules/SimulationTimeSeries/settings/settings.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Frequency_api, StatisticFunction_api } from "@api"; +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { Parameter, ParameterIdent } from "@framework/EnsembleParameters"; import { ModuleSettingsProps } from "@framework/Module"; @@ -104,7 +105,7 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr setUserSelectedParameterIdentStr(null); } - function handleEnsembleSelectChange(ensembleIdentArr: EnsembleIdent[]) { + function handleEnsembleSelectChange(ensembleIdentArr: (EnsembleIdent | DeltaEnsembleIdent)[]) { setUserSelectedEnsembleIdents(ensembleIdentArr); } @@ -250,7 +251,7 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr []; - constructor(ensembles: EnsembleIdent[], vectorListQueryResults: UseQueryResult[]) { - if (ensembles.length !== vectorListQueryResults.length) { + constructor(ensembleIdents: EnsembleIdent[], vectorListQueryResults: UseQueryResult[]) { + if (ensembleIdents.length !== vectorListQueryResults.length) { throw new Error("Number of ensembles and vector list query results must be equal"); } - this._ensembleIdents = ensembles; + this._ensembleIdents = ensembleIdents; this._queries = vectorListQueryResults; } @@ -45,12 +45,12 @@ export class EnsembleVectorListsHelper { /** * - * @param ensemble - EnsembleIdent to check + * @param ensembleIdent - EnsembleIdent to check * @param vector - Vector name to look for * @returns */ - isVectorInEnsemble(ensemble: EnsembleIdent, vector: string): boolean { - const index = this._ensembleIdents.indexOf(ensemble); + isVectorInEnsemble(ensembleIdent: EnsembleIdent, vector: string): boolean { + const index = this._ensembleIdents.findIndex((ident) => ident.equals(ensembleIdent)); if (index === -1 || !this._queries[index].data) return false; @@ -59,14 +59,14 @@ export class EnsembleVectorListsHelper { /** * - * @param ensemble - EnsembleIdent to check + * @param ensembleIdent - EnsembleIdent to check * @param vector - Vector name to look for * @returns */ - hasHistoricalVector(ensemble: EnsembleIdent, vector: string): boolean { - if (!this.isVectorInEnsemble(ensemble, vector)) return false; + hasHistoricalVector(ensembleIdent: EnsembleIdent, vector: string): boolean { + if (!this.isVectorInEnsemble(ensembleIdent, vector)) return false; - const index = this._ensembleIdents.indexOf(ensemble); + const index = this._ensembleIdents.findIndex((ident) => ident.equals(ensembleIdent)); if (index === -1 || !this._queries[index].data) return false; return this._queries[index].data?.some((vec) => vec.name === vector && vec.has_historical) ?? false; From fea2f5e78b2f46a75ad4b0e1a0a6d8dba9d84f26 Mon Sep 17 00:00:00 2001 From: jorgenherje Date: Fri, 15 Nov 2024 15:36:28 +0100 Subject: [PATCH 03/33] WIP - Improve interaction/checks in dialog - Add local store of created delta ensembles - Add utils for array of various EnsembleIdentInterface implementaitons - Improve functionalityof ensemble select dialog --- frontend/src/App.tsx | 12 +- frontend/src/framework/DeltaEnsemble.ts | 20 --- frontend/src/framework/DeltaEnsembleIdent.ts | 15 +- frontend/src/framework/Workbench.ts | 41 +++++- .../internal/components/NavBar/leftNavBar.tsx | 45 ++++-- .../selectEnsemblesDialog.tsx | 128 ++++++++++-------- .../src/framework/utils/ensembleIdentUtils.ts | 17 +++ .../settings/atoms/derivedAtoms.ts | 5 +- 8 files changed, 180 insertions(+), 103 deletions(-) create mode 100644 frontend/src/framework/utils/ensembleIdentUtils.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 44e415543..99930f0c7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import React from "react"; import WebvizLogo from "@assets/webviz.svg"; import { GuiState, LeftDrawerContent } from "@framework/GuiMessageBroker"; -import { LayoutElement, UserDeltaEnsembleSetting, Workbench } from "@framework/Workbench"; +import { LayoutElement, Workbench } from "@framework/Workbench"; import { LeftNavBar, RightNavBar } from "@framework/internal/components/NavBar"; import { SettingsContentPanels } from "@framework/internal/components/SettingsContentPanels"; import { ToggleDevToolsButton } from "@framework/internal/components/ToggleDevToolsButton"; @@ -85,11 +85,15 @@ function App() { setIsMounted(true); const storedEnsembleIdents = workbench.maybeLoadEnsembleSettingsFromLocalStorage(); - const storedDeltaEnsembles: UserDeltaEnsembleSetting[] = []; // TODO: Store list of delta ensembles in local storage? - if (storedEnsembleIdents) { + const storedDeltaEnsembles = workbench.maybeLoadDeltaEnsembleSettingsFromLocalStorage(); + if (storedEnsembleIdents || storedDeltaEnsembles) { setInitAppState(InitAppState.LoadingEnsembles); workbench - .loadAndSetupEnsembleSetInSession(queryClient, storedEnsembleIdents, storedDeltaEnsembles) + .loadAndSetupEnsembleSetInSession( + queryClient, + storedEnsembleIdents ?? [], + storedDeltaEnsembles ?? [] + ) .finally(() => { initApp(); }); diff --git a/frontend/src/framework/DeltaEnsemble.ts b/frontend/src/framework/DeltaEnsemble.ts index 0e5e28df2..d9c5310b5 100644 --- a/frontend/src/framework/DeltaEnsemble.ts +++ b/frontend/src/framework/DeltaEnsemble.ts @@ -103,26 +103,6 @@ export class DeltaEnsemble implements EnsembleInterface { throw new Error("Unhandled element type"); } - getCaseUuidByElement(element: DeltaEnsembleElement): string { - if (element === DeltaEnsembleElement.FIRST) { - return this._firstEnsemble.getCaseUuid(); - } - if (element === DeltaEnsembleElement.SECOND) { - return this._secondEnsemble.getCaseUuid(); - } - throw new Error("Unhandled element type"); - } - - getCaseNameByElement(element: DeltaEnsembleElement): string { - if (element === DeltaEnsembleElement.FIRST) { - return this._firstEnsemble.getCaseName(); - } - if (element === DeltaEnsembleElement.SECOND) { - return this._secondEnsemble.getCaseName(); - } - throw new Error("Unhandled element type"); - } - getRealizationsByElement(element: DeltaEnsembleElement): readonly number[] { if (element === DeltaEnsembleElement.FIRST) { return this._firstEnsemble.getRealizations(); diff --git a/frontend/src/framework/DeltaEnsembleIdent.ts b/frontend/src/framework/DeltaEnsembleIdent.ts index 19bb2dc2b..d6a7eb45b 100644 --- a/frontend/src/framework/DeltaEnsembleIdent.ts +++ b/frontend/src/framework/DeltaEnsembleIdent.ts @@ -20,23 +20,28 @@ export class DeltaEnsembleIdent implements EnsembleIdentInterface)~@@~(?.*)$/; + return /^(?[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})~@@~(?.*)$/; + } + + getUuid(): string { + return this._uuid; } getEnsembleName(): string { diff --git a/frontend/src/framework/Workbench.ts b/frontend/src/framework/Workbench.ts index 4018762ce..4634c52a9 100644 --- a/frontend/src/framework/Workbench.ts +++ b/frontend/src/framework/Workbench.ts @@ -27,15 +27,15 @@ export type LayoutElement = { relWidth: number; }; -export type UserDeltaEnsembleSetting = { - firstEnsembleIdent: EnsembleIdent; - secondEnsembleIdent: EnsembleIdent; +export type UserEnsembleSetting = { + ensembleIdent: EnsembleIdent; customName: string | null; color: string; }; -export type UserEnsembleSetting = { - ensembleIdent: EnsembleIdent; +export type UserDeltaEnsembleSetting = { + firstEnsembleIdent: EnsembleIdent; + secondEnsembleIdent: EnsembleIdent; customName: string | null; color: string; }; @@ -46,6 +46,13 @@ export type StoredUserEnsembleSetting = { color: string; }; +export type StoredUserDeltaEnsembleSetting = { + firstEnsembleIdent: string; + secondEnsembleIdent: string; + customName: string; + color: string; +}; + export class Workbench { private _moduleInstances: ModuleInstance[]; private _workbenchSession: WorkbenchSessionPrivate; @@ -239,6 +246,7 @@ export class Workbench { userDeltaEnsembleSettings: UserDeltaEnsembleSetting[] ): Promise { this.storeEnsembleSetInLocalStorage(userEnsembleSettings); + this.storeDeltaEnsembleSetInLocalStorage(userDeltaEnsembleSettings); console.debug("loadAndSetupEnsembleSetInSession - starting load"); this._workbenchSession.setEnsembleSetLoadingState(true); @@ -261,6 +269,15 @@ export class Workbench { localStorage.setItem("userEnsembleSettings", JSON.stringify(ensembleIdentsToStore)); } + private storeDeltaEnsembleSetInLocalStorage(ensemblesToStore: UserDeltaEnsembleSetting[]): void { + const deltaEnsembleIdentsToStore = ensemblesToStore.map((el) => ({ + ...el, + firstEnsembleIdent: el.firstEnsembleIdent.toString(), + secondEnsembleIdent: el.secondEnsembleIdent.toString(), + })); + localStorage.setItem("userDeltaEnsembleSettings", JSON.stringify(deltaEnsembleIdentsToStore)); + } + maybeLoadEnsembleSettingsFromLocalStorage(): UserEnsembleSetting[] | null { const ensembleSettingsString = localStorage.getItem("userEnsembleSettings"); if (!ensembleSettingsString) return null; @@ -274,6 +291,20 @@ export class Workbench { return ensembleIdentsParsed; } + maybeLoadDeltaEnsembleSettingsFromLocalStorage(): UserDeltaEnsembleSetting[] | null { + const deltaEnsembleSettingsString = localStorage.getItem("userDeltaEnsembleSettings"); + if (!deltaEnsembleSettingsString) return null; + + const deltaEnsembleIdents = JSON.parse(deltaEnsembleSettingsString) as StoredUserDeltaEnsembleSetting[]; + const deltaEnsembleIdentsParsed = deltaEnsembleIdents.map((el) => ({ + ...el, + firstEnsembleIdent: EnsembleIdent.fromString(el.firstEnsembleIdent), + secondEnsembleIdent: EnsembleIdent.fromString(el.secondEnsembleIdent), + })); + + return deltaEnsembleIdentsParsed; + } + applyTemplate(template: Template): void { this.clearLayout(); diff --git a/frontend/src/framework/internal/components/NavBar/leftNavBar.tsx b/frontend/src/framework/internal/components/NavBar/leftNavBar.tsx index 7069c5e7d..4bb19e079 100644 --- a/frontend/src/framework/internal/components/NavBar/leftNavBar.tsx +++ b/frontend/src/framework/internal/components/NavBar/leftNavBar.tsx @@ -1,13 +1,16 @@ import React from "react"; import WebvizLogo from "@assets/webviz.svg"; +import { DeltaEnsembleElement } from "@framework/DeltaEnsemble"; import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { EnsembleType } from "@framework/EnsembleSet"; import { GuiState, LeftDrawerContent, useGuiState, useGuiValue } from "@framework/GuiMessageBroker"; import { UserEnsembleSetting, Workbench, WorkbenchEvents } from "@framework/Workbench"; import { useEnsembleSet, useIsEnsembleSetLoading } from "@framework/WorkbenchSession"; import { LoginButton } from "@framework/internal/components/LoginButton"; import { SelectEnsemblesDialog } from "@framework/internal/components/SelectEnsemblesDialog"; import { + DeltaEnsembleBaseItem, DeltaEnsembleItem, EnsembleItem, } from "@framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog"; @@ -121,14 +124,6 @@ export const LeftNavBar: React.FC = (props) => { localStorage.setItem("navBarCollapsed", (!collapsed).toString()); } - const selectedEnsembles: EnsembleItem[] = ensembleSet.getEnsembleArr().map((ens) => ({ - caseUuid: ens.getCaseUuid(), - caseName: ens.getCaseName(), - ensembleName: ens.getEnsembleName(), - color: ens.getColor(), - customName: ens.getCustomName(), - })); - function loadAndSetupEnsembles( ensembleItems: EnsembleItem[], createdDeltaEnsembles: DeltaEnsembleItem[] @@ -152,11 +147,43 @@ export const LeftNavBar: React.FC = (props) => { return props.workbench.loadAndSetupEnsembleSetInSession(queryClient, ensembleSettings, deltaEnsembleSettings); } + const selectedEnsembles: EnsembleItem[] = ensembleSet.getEnsembleArr(EnsembleType.REGULAR).map((ens) => ({ + caseUuid: ens.getCaseUuid(), + caseName: ens.getCaseName(), + ensembleName: ens.getEnsembleName(), + color: ens.getColor(), + customName: ens.getCustomName(), + })); + let fixedSelectedEnsembles = selectedEnsembles; if (loadingEnsembleSet) { fixedSelectedEnsembles = newSelectedEnsembles; } + const createdDeltaEnsembles: DeltaEnsembleItem[] = ensembleSet.getEnsembleArr(EnsembleType.DELTA).map((ens) => { + const firstEnsemble: DeltaEnsembleBaseItem = { + caseUuid: ens.getEnsembleIdentByElement(DeltaEnsembleElement.FIRST).getCaseUuid(), + ensembleName: ens.getEnsembleIdentByElement(DeltaEnsembleElement.FIRST).getEnsembleName(), + }; + const secondEnsemble: DeltaEnsembleBaseItem = { + caseUuid: ens.getEnsembleIdentByElement(DeltaEnsembleElement.SECOND).getCaseUuid(), + ensembleName: ens.getEnsembleIdentByElement(DeltaEnsembleElement.SECOND).getEnsembleName(), + }; + + const deltaEnsembleItem: DeltaEnsembleItem = { + firstEnsemble: firstEnsemble, + secondEnsemble: secondEnsemble, + uuid: ens.getIdent().getUuid(), + color: ens.getColor(), + customName: ens.getCustomName(), + }; + return deltaEnsembleItem; + }); + let fixedCreatedDeltaEnsembles = createdDeltaEnsembles; + if (loadingEnsembleSet) { + fixedCreatedDeltaEnsembles = newCreatedDeltaEnsembles; + } + return (
= (props) => { diff --git a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx index 3a6b44bb3..f1964d448 100644 --- a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx +++ b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx @@ -30,18 +30,23 @@ import { LoadingOverlay } from "../LoadingOverlay"; const CASE_UUID_ENSEMBLE_NAME_SEPARATOR = "~&&~"; +export type DeltaEnsembleBaseItem = { + caseUuid: string; + ensembleName: string; +}; + // Internal type before applying created delta ensemble externally type InternalDeltaEnsembleItem = { - firstEnsemble: EnsembleItem | null; // Allows null - secondEnsemble: EnsembleItem | null; // Allows null + firstEnsemble: DeltaEnsembleBaseItem | null; // Allows null + secondEnsemble: DeltaEnsembleBaseItem | null; // Allows null uuid: string; color: string; customName: string | null; }; export type DeltaEnsembleItem = { - firstEnsemble: EnsembleItem; - secondEnsemble: EnsembleItem; + firstEnsemble: DeltaEnsembleBaseItem; + secondEnsemble: DeltaEnsembleBaseItem; uuid: string; color: string; customName: string | null; @@ -91,7 +96,6 @@ export const SelectEnsemblesDialog: React.FC = (prop const [isLoadingEnsembles, setIsLoadingEnsembles] = React.useState(false); const [confirmCancel, setConfirmCancel] = React.useState(false); const [newlySelectedEnsembles, setNewlySelectedEnsembles] = React.useState([]); - const [newlyCreatedDeltaEnsembles, setNewlyCreatedDeltaEnsembles] = React.useState([]); const [casesFilteringOptions, setCasesFilteringOptions] = React.useState({ keep: !(readInitialStateFromLocalStorage("showKeepCases") === "false"), onlyMyCases: readInitialStateFromLocalStorage("showOnlyMyCases") === "true", @@ -99,6 +103,9 @@ export const SelectEnsemblesDialog: React.FC = (prop }); const [deltaEnsembles, setDeltaEnsembles] = React.useState([]); + // const [newlyCreatedDeltaEnsembles, setNewlyCreatedDeltaEnsembles] = React.useState( + // props.createdDeltaEnsembles + // ); const { userInfo } = useAuthProvider(); @@ -106,14 +113,14 @@ export const SelectEnsemblesDialog: React.FC = (prop setNewlySelectedEnsembles(props.selectedEnsembles); // TODO: Verify that firstEnsemble and secondEnsemble are among props.selectedEnsembles? - setNewlyCreatedDeltaEnsembles(props.createdDeltaEnsembles); - }, [props.selectedEnsembles, props.createdDeltaEnsembles]); + // setNewlyCreatedDeltaEnsembles(props.createdDeltaEnsembles); + }, [props.selectedEnsembles]); React.useLayoutEffect(() => { setDeltaEnsembles( props.createdDeltaEnsembles.map((elm) => ({ firstEnsemble: elm.firstEnsemble, - secondEnsemble: elm.firstEnsemble, + secondEnsemble: elm.secondEnsemble, uuid: elm.uuid, color: elm.color, customName: elm.customName, @@ -226,16 +233,13 @@ export const SelectEnsemblesDialog: React.FC = (prop setDeltaEnsembles((prev) => prev.map((elm) => { if (elm.uuid === deltaEnsembleUuid) { + const newFirstEnsemble: DeltaEnsembleBaseItem = { + caseUuid: firstEnsemble.caseUuid, + ensembleName: firstEnsemble.ensembleName, + }; return { ...elm, - firstEnsemble: { - caseUuid: firstEnsemble.caseUuid, - caseName: firstEnsemble.caseName, - ensembleName: firstEnsemble.ensembleName, - customName: firstEnsemble.customName, - color: firstEnsemble.color, - }, - // ensembleName: createDeltaEnsembleName(firstEnsemble, e.secondEnsemble), + firstEnsemble: newFirstEnsemble, }; } return elm; @@ -260,16 +264,13 @@ export const SelectEnsemblesDialog: React.FC = (prop setDeltaEnsembles((prev) => prev.map((elm) => { if (elm.uuid === deltaEnsembleUuid) { + const newSecondEnsemble: DeltaEnsembleBaseItem = { + caseUuid: secondEnsemble.caseUuid, + ensembleName: secondEnsemble.ensembleName, + }; return { ...elm, - secondEnsemble: { - caseUuid: secondEnsemble.caseUuid, - caseName: secondEnsemble.caseName, - ensembleName: secondEnsemble.ensembleName, - customName: secondEnsemble.customName, - color: secondEnsemble.color, - }, - // ensembleName: createDeltaEnsembleName(secondEnsemble, e.item.secondEnsemble), + secondEnsemble: newSecondEnsemble, }; } return elm; @@ -289,20 +290,13 @@ export const SelectEnsemblesDialog: React.FC = (prop const newDeltaEnsemble: InternalDeltaEnsembleItem = { firstEnsemble: { caseUuid: firstEnsemble.caseUuid, - caseName: firstEnsemble.caseName, ensembleName: firstEnsemble.ensembleName, - customName: firstEnsemble.customName, - color: firstEnsemble.color, }, secondEnsemble: { caseUuid: secondEnsemble.caseUuid, - caseName: secondEnsemble.caseName, ensembleName: secondEnsemble.ensembleName, - customName: secondEnsemble.customName, - color: secondEnsemble.color, }, uuid: v4(), - // ensembleName: createDeltaEnsembleName(firstEnsemble, secondEnsemble), color: tryToFindUnusedColor(), customName: null, }; @@ -399,11 +393,29 @@ export const SelectEnsemblesDialog: React.FC = (prop }); } - function checkHasInvalidDeltaEnsembles(): boolean { + function hasAnyDeltaEnsemblesChanged(): boolean { + if (props.createdDeltaEnsembles.length !== deltaEnsembles.length) { + return true; + } + + const isContentEqual = props.createdDeltaEnsembles.every((elm, idx) => { + const internalDeltaEnsemble = deltaEnsembles[idx]; + return ( + elm.uuid === internalDeltaEnsemble.uuid && + elm.color === internalDeltaEnsemble.color && + elm.customName === internalDeltaEnsemble.customName && + isEqual(elm.firstEnsemble, internalDeltaEnsemble.firstEnsemble) && + isEqual(elm.secondEnsemble, internalDeltaEnsemble.secondEnsemble) + ); + }); + return !isContentEqual; + } + + function areAnyDeltaEnsemblesInvalid(): boolean { return deltaEnsembles.some((elm) => !elm.firstEnsemble || !elm.secondEnsemble); } - function checkIfAnyChanges(): boolean { + function hasAnyEnsembleChanged(): boolean { return !isEqual(props.selectedEnsembles, newlySelectedEnsembles); } @@ -517,13 +529,21 @@ export const SelectEnsemblesDialog: React.FC = (prop -
+
- {/* */}
Selected Ensembles
@@ -732,9 +731,8 @@ export const SelectEnsemblesDialog: React.FC = (prop
@@ -781,10 +779,10 @@ export const SelectEnsemblesDialog: React.FC = (prop { "hover:bg-red-50 odd:bg-red-200 even:bg-red-300": !isDeltaEnsembleValid, - "hover:bg-slate-100 odd:bg-slate-50": isDeltaEnsembleValid, - "hover:bg-blue-50 odd:bg-blue-200 even:bg-blue-300": isDeltaEnsembleValid && isDuplicateDeltaEnsemble, + "hover:bg-slate-100 odd:bg-slate-50": + isDeltaEnsembleValid && !isDuplicateDeltaEnsemble, } )} > From 9c6d84137c79fc007d091fef41000e14e8d39dc4 Mon Sep 17 00:00:00 2001 From: jorgenherje Date: Mon, 16 Dec 2024 14:12:22 +0100 Subject: [PATCH 31/33] Fix incorrect placement of message text for no selected/created ensemble --- .../selectEnsemblesDialog.tsx | 391 +++++++++--------- 1 file changed, 202 insertions(+), 189 deletions(-) diff --git a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx index d9dec6203..dd43e7b33 100644 --- a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx +++ b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx @@ -657,227 +657,240 @@ export const SelectEnsemblesDialog: React.FC = (prop
-
Selected Ensembles
-
-
- - - - - - - - - - - {newlySelectedEnsembles.map((item) => ( - - - - - - +
+
Selected Ensembles
+
+
ColorCustom nameCaseEnsembleActions
- - handleColorChange(item.caseUuid, item.ensembleName, value) - } - /> - - ) => - handleEnsembleCustomNameChange( - item.caseUuid, - item.ensembleName, - e.target.value - ) - } - /> - -
- {item.caseName} -
-
-
- {item.ensembleName} -
-
- - handleRemoveEnsemble(item.caseUuid, item.ensembleName) - } - color="danger" - title="Remove ensemble from selection" - > - - {" "} -
+ + + + + + + - ))} - -
ColorCustom nameCaseEnsembleActions
-
-
- -
Delta Ensembles
- - - -
-
- - - - - - - - - - - - {deltaEnsembles.map((elm) => { - const isDeltaEnsembleValid = - elm.compareEnsemble !== null && elm.referenceEnsemble !== null; - const isDuplicateDeltaEnsemble = - deltaEnsembles.filter( - (e) => - e.compareEnsemble?.caseUuid === elm.compareEnsemble?.caseUuid && - e.compareEnsemble?.ensembleName === - elm.compareEnsemble?.ensembleName && - e.referenceEnsemble?.caseUuid === elm.referenceEnsemble?.caseUuid && - e.referenceEnsemble?.ensembleName === - elm.referenceEnsemble?.ensembleName - ).length > 1; - return ( + + + {newlySelectedEnsembles.map((item) => ( - ); - })} - -
ColorCustom nameCompare EnsembleReference EnsembleActions
- handleDeltaEnsembleColorChange(elm.uuid, value) + handleColorChange(item.caseUuid, item.ensembleName, value) } /> ) => - handleDeltaEnsembleCustomNameChange( - elm.uuid, + handleEnsembleCustomNameChange( + item.caseUuid, + item.ensembleName, e.target.value ) } /> - { - return { - value: createCaseUuidAndEnsembleNameString( - elm.caseUuid, - elm.ensembleName - ), - label: - elm.customName ?? - `${elm.ensembleName} (${elm.caseName})`, - }; - })} - value={ - elm.compareEnsemble - ? createCaseUuidAndEnsembleNameString( - elm.compareEnsemble.caseUuid, - elm.compareEnsemble.ensembleName - ) - : undefined - } - onChange={(newCaseUuidAndEnsembleNameString) => { - handleDeltaEnsembleCompareEnsembleChange( - elm.uuid, - newCaseUuidAndEnsembleNameString - ); - }} - /> +
+ {item.caseName} +
- { - return { - value: createCaseUuidAndEnsembleNameString( - elm.caseUuid, - elm.ensembleName - ), - label: - elm.customName ?? - `${elm.ensembleName} (${elm.caseName})`, - }; - })} - value={ - elm.referenceEnsemble - ? createCaseUuidAndEnsembleNameString( - elm.referenceEnsemble.caseUuid, - elm.referenceEnsemble.ensembleName - ) - : undefined - } - onChange={(value) => { - handleDeltaEnsembleReferenceEnsembleChange(elm.uuid, value); - }} - /> +
+ {item.ensembleName} +
handleRemoveDeltaEnsemble(elm.uuid)} + onClick={() => + handleRemoveEnsemble(item.caseUuid, item.ensembleName) + } color="danger" - title="Remove delta ensemble from selection" + title="Remove ensemble from selection" > {" "}
+ ))} + + +
+ {newlySelectedEnsembles.length === 0 && ( +
No ensembles selected.
+ )} +
+
+
+ +
Delta Ensembles
+ + + +
+
+ + + + + + + + + + + + {deltaEnsembles.map((elm) => { + const isDeltaEnsembleValid = + elm.compareEnsemble !== null && elm.referenceEnsemble !== null; + const isDuplicateDeltaEnsemble = + deltaEnsembles.filter( + (e) => + e.compareEnsemble?.caseUuid === elm.compareEnsemble?.caseUuid && + e.compareEnsemble?.ensembleName === + elm.compareEnsemble?.ensembleName && + e.referenceEnsemble?.caseUuid === + elm.referenceEnsemble?.caseUuid && + e.referenceEnsemble?.ensembleName === + elm.referenceEnsemble?.ensembleName + ).length > 1; + return ( + + + + + + + + ); + })} + +
ColorCustom nameCompare EnsembleReference EnsembleActions
+ + handleDeltaEnsembleColorChange(elm.uuid, value) + } + /> + + ) => + handleDeltaEnsembleCustomNameChange( + elm.uuid, + e.target.value + ) + } + /> + + { + return { + value: createCaseUuidAndEnsembleNameString( + elm.caseUuid, + elm.ensembleName + ), + label: + elm.customName ?? + `${elm.ensembleName} (${elm.caseName})`, + }; + })} + value={ + elm.compareEnsemble + ? createCaseUuidAndEnsembleNameString( + elm.compareEnsemble.caseUuid, + elm.compareEnsemble.ensembleName + ) + : undefined + } + onChange={(newCaseUuidAndEnsembleNameString) => { + handleDeltaEnsembleCompareEnsembleChange( + elm.uuid, + newCaseUuidAndEnsembleNameString + ); + }} + /> + + { + return { + value: createCaseUuidAndEnsembleNameString( + elm.caseUuid, + elm.ensembleName + ), + label: + elm.customName ?? + `${elm.ensembleName} (${elm.caseName})`, + }; + })} + value={ + elm.referenceEnsemble + ? createCaseUuidAndEnsembleNameString( + elm.referenceEnsemble.caseUuid, + elm.referenceEnsemble.ensembleName + ) + : undefined + } + onChange={(value) => { + handleDeltaEnsembleReferenceEnsembleChange( + elm.uuid, + value + ); + }} + /> + + handleRemoveDeltaEnsemble(elm.uuid)} + color="danger" + title="Remove delta ensemble from selection" + > + + {" "} +
+
+ {deltaEnsembles.length === 0 && ( +
No delta ensembles created.
+ )}
- {newlySelectedEnsembles.length === 0 &&
No ensembles selected.
} {isLoadingEnsembles && } From 96e5d89bcfb3ec871d761c2097f8d02a7df3c29f Mon Sep 17 00:00:00 2001 From: jorgenherje Date: Mon, 16 Dec 2024 14:54:18 +0100 Subject: [PATCH 32/33] Fix errors after merge --- frontend/src/framework/WorkbenchSession.ts | 2 +- .../framework/LayerManager/LayerManager.ts | 8 +++---- .../DrilledWellTrajectoriesLayer/types.ts | 4 ++-- .../DrilledWellborePicksLayer/types.ts | 4 ++-- .../ObservedSurfaceSettingsContext.ts | 2 +- .../ObservedSurfaceLayer/types.ts | 4 ++-- .../RealizationGridLayer/types.ts | 4 ++-- .../RealizationPolygonsLayer/types.ts | 4 ++-- .../RealizationSurfaceLayer/types.ts | 4 ++-- .../StatisticalSurfaceLayer/types.ts | 4 ++-- .../implementations/EnsembleSetting.tsx | 22 +++++++++---------- .../2DViewer/settings/atoms/derivedAtoms.ts | 4 ++-- 12 files changed, 33 insertions(+), 33 deletions(-) diff --git a/frontend/src/framework/WorkbenchSession.ts b/frontend/src/framework/WorkbenchSession.ts index 181325c24..b1774c595 100644 --- a/frontend/src/framework/WorkbenchSession.ts +++ b/frontend/src/framework/WorkbenchSession.ts @@ -79,7 +79,7 @@ export class WorkbenchSession { } } -function createEnsembleRealizationFilterFuncForWorkbenchSession(workbenchSession: WorkbenchSession) { +export function createEnsembleRealizationFilterFuncForWorkbenchSession(workbenchSession: WorkbenchSession) { return function ensembleRealizationFilterFunc( ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent ): readonly number[] { diff --git a/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts b/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts index 98208330a..e4aaed2a9 100644 --- a/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts +++ b/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts @@ -1,4 +1,4 @@ -import { Ensemble } from "@framework/Ensemble"; +import { RegularEnsemble } from "@framework/RegularEnsemble"; import { EnsembleRealizationFilterFunction, WorkbenchSession, @@ -36,7 +36,7 @@ export type LayerManagerTopicPayload = { export type GlobalSettings = { fieldId: string | null; - ensembles: readonly Ensemble[]; + ensembles: readonly RegularEnsemble[]; realizationFilterFunction: EnsembleRealizationFilterFunction; }; @@ -194,7 +194,7 @@ export class LayerManager implements Group, PublishSubscribe ensemble.getFieldIdentifier() === fieldIdentifier) .map((ensemble) => ensemble.getIdent()); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts index a4bc063f3..d89905be3 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts @@ -1,8 +1,8 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; export type ObservedSurfaceSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.SURFACE_ATTRIBUTE]: string | null; [SettingType.SURFACE_NAME]: string | null; [SettingType.TIME_OR_INTERVAL]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts index 2ac9df1c2..a746e527c 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts @@ -1,8 +1,8 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; export type RealizationGridSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.REALIZATION]: number | null; [SettingType.GRID_ATTRIBUTE]: string | null; [SettingType.GRID_NAME]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts index 1b79a705b..5535d6772 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts @@ -1,9 +1,9 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "../../../settings/settingsTypes"; export type RealizationPolygonsSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.REALIZATION]: number | null; [SettingType.POLYGONS_ATTRIBUTE]: string | null; [SettingType.POLYGONS_NAME]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts index 6477cdcc6..d1d1bfed8 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts @@ -1,9 +1,9 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "../../../settings/settingsTypes"; export type RealizationSurfaceSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.REALIZATION]: number | null; [SettingType.SURFACE_ATTRIBUTE]: string | null; [SettingType.SURFACE_NAME]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts index 639fc8253..adb0a2aa7 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts @@ -1,11 +1,11 @@ import { SurfaceStatisticFunction_api } from "@api"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; import { SensitivityNameCasePair } from "../../../settings/implementations/SensitivitySetting"; export type StatisticalSurfaceSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.STATISTIC_FUNCTION]: SurfaceStatisticFunction_api; [SettingType.SENSITIVITY]: SensitivityNameCasePair | null; [SettingType.SURFACE_ATTRIBUTE]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/settings/implementations/EnsembleSetting.tsx b/frontend/src/modules/2DViewer/layers/settings/implementations/EnsembleSetting.tsx index de7eeb630..49172a50a 100644 --- a/frontend/src/modules/2DViewer/layers/settings/implementations/EnsembleSetting.tsx +++ b/frontend/src/modules/2DViewer/layers/settings/implementations/EnsembleSetting.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; import { SettingDelegate } from "../../delegates/SettingDelegate"; @@ -8,11 +8,11 @@ import { Setting, SettingComponentProps, ValueToStringArgs } from "../../interfa import { SettingRegistry } from "../SettingRegistry"; import { SettingType } from "../settingsTypes"; -export class EnsembleSetting implements Setting { - private _delegate: SettingDelegate; +export class EnsembleSetting implements Setting { + private _delegate: SettingDelegate; constructor() { - this._delegate = new SettingDelegate(null, this); + this._delegate = new SettingDelegate(null, this); } getType(): SettingType { @@ -23,20 +23,20 @@ export class EnsembleSetting implements Setting { return "Ensemble"; } - getDelegate(): SettingDelegate { + getDelegate(): SettingDelegate { return this._delegate; } - serializeValue(value: EnsembleIdent | null): string { + serializeValue(value: RegularEnsembleIdent | null): string { return value?.toString() ?? ""; } - deserializeValue(serializedValue: string): EnsembleIdent | null { - return serializedValue !== "" ? EnsembleIdent.fromString(serializedValue) : null; + deserializeValue(serializedValue: string): RegularEnsembleIdent | null { + return serializedValue !== "" ? RegularEnsembleIdent.fromString(serializedValue) : null; } - makeComponent(): (props: SettingComponentProps) => React.ReactNode { - return function Ensemble(props: SettingComponentProps) { + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { const ensembles = props.globalSettings.ensembles.filter((ensemble) => props.availableValues.includes(ensemble.getIdent()) ); @@ -53,7 +53,7 @@ export class EnsembleSetting implements Setting { }; } - valueToString(args: ValueToStringArgs): string { + valueToString(args: ValueToStringArgs): string { const { value, workbenchSession } = args; if (value === null) { return "-"; diff --git a/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts b/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts index 5fc4447f4..ae64f8ba7 100644 --- a/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts @@ -10,9 +10,9 @@ export const selectedFieldIdentifierAtom = atom((get) => { if ( !userSelectedField || - !ensembleSet.getEnsembleArr().some((ens) => ens.getFieldIdentifier() === userSelectedField) + !ensembleSet.getRegularEnsembleArray().some((ens) => ens.getFieldIdentifier() === userSelectedField) ) { - return ensembleSet.getEnsembleArr().at(0)?.getFieldIdentifier() ?? null; + return ensembleSet.getRegularEnsembleArray().at(0)?.getFieldIdentifier() ?? null; } return userSelectedField; From 2c125e677ffa9c8da3b6b05d216eb154078a436c Mon Sep 17 00:00:00 2001 From: jorgenherje Date: Tue, 17 Dec 2024 13:42:30 +0100 Subject: [PATCH 33/33] Refactor fixupEnsembleIdent(s) overloaded functions into separate functions Based on discussion: Remove overload, and explicitly implement fixup based on argument types. As the overloads had a bug, where currIdent could be of a type, and if the type does not exist in ensembleSet, an incorrect ident would be returned for a module not supporting it. E.g. request RegularEnsembleIdent, ensemble set removes last RegularEnsembleIdent, then DeltaEnsembleIdent is returned, and the module might get error scenario. --- .../src/framework/utils/ensembleUiHelpers.ts | 109 ++++++++-------- .../settings/atoms/derivedAtoms.ts | 4 +- .../settings/atoms/derivedAtoms.ts | 4 +- .../settings/atoms/derivedAtoms.ts | 4 +- .../src/modules/Map/settings/settings.tsx | 4 +- .../Rft/settings/atoms/derivedAtoms.ts | 4 +- .../settings/settings.tsx | 4 +- .../SubsurfaceMap/settings/settings.tsx | 4 +- .../Vfp/settings/atoms/derivedAtoms.ts | 4 +- frontend/tests/unit/ensembleUiHelpers.test.ts | 119 ++++++++++++++++-- 10 files changed, 182 insertions(+), 78 deletions(-) diff --git a/frontend/src/framework/utils/ensembleUiHelpers.ts b/frontend/src/framework/utils/ensembleUiHelpers.ts index 9aedc1d22..3fe6247b4 100644 --- a/frontend/src/framework/utils/ensembleUiHelpers.ts +++ b/frontend/src/framework/utils/ensembleUiHelpers.ts @@ -1,5 +1,3 @@ -import { isEnsembleIdentOfType } from "./ensembleIdentUtils"; - import { DeltaEnsembleIdent } from "../DeltaEnsembleIdent"; import { EnsembleSet } from "../EnsembleSet"; import { RegularEnsembleIdent } from "../RegularEnsembleIdent"; @@ -30,22 +28,10 @@ export function maybeAssignFirstSyncedEnsemble( * will always return a reference to the exact same object that was passed in currIdent. This * means that you can compare the references (fixedIdent !== currIdent) to detect any changes. */ -export function fixupEnsembleIdent( - currIdent: RegularEnsembleIdent | null, - ensembleSet: EnsembleSet | null -): RegularEnsembleIdent | null; -export function fixupEnsembleIdent( - currIdent: DeltaEnsembleIdent | null, - ensembleSet: EnsembleSet | null -): DeltaEnsembleIdent | null; export function fixupEnsembleIdent( currIdent: RegularEnsembleIdent | DeltaEnsembleIdent | null, ensembleSet: EnsembleSet | null -): (RegularEnsembleIdent | DeltaEnsembleIdent) | null; -export function fixupEnsembleIdent( - currIdent: RegularEnsembleIdent | DeltaEnsembleIdent | null, - ensembleSet: EnsembleSet | null -): (RegularEnsembleIdent | DeltaEnsembleIdent) | null { +): RegularEnsembleIdent | DeltaEnsembleIdent | null { if (!ensembleSet?.hasAnyEnsembles()) { return null; } @@ -54,46 +40,45 @@ export function fixupEnsembleIdent( return currIdent; } - const regularEnsembles = ensembleSet.getRegularEnsembleArray(); - const deltaEnsembles = ensembleSet.getDeltaEnsembleArray(); + return ensembleSet.getEnsembleArray()[0].getIdent(); +} - if (currIdent && isEnsembleIdentOfType(currIdent, RegularEnsembleIdent) && regularEnsembles.length > 0) { - return regularEnsembles[0].getIdent(); +/** + * Validates the the RegularEnsembleIdent specified in currIdent against the contents of the + * EnsembleSet and fixes the value if it isn't valid. + * + * Returns null if specified EnsembleSet does not contain any regular ensembles. + * + * Note that if the specified RegularEnsembleIdents is valid, this function will always return + * a reference to the exact same object that was passed in currIdent. This means that you can + * compare the references (fixedIdent !== currIdent) to detect any changes. + */ +export function fixupRegularEnsembleIdent( + currIdent: RegularEnsembleIdent | null, + ensembleSet: EnsembleSet | null +): RegularEnsembleIdent | null { + if (!ensembleSet?.hasAnyRegularEnsembles()) { + return null; } - if (currIdent && isEnsembleIdentOfType(currIdent, DeltaEnsembleIdent) && deltaEnsembles.length > 0) { - return deltaEnsembles[0].getIdent(); + if (currIdent && ensembleSet.hasEnsemble(currIdent)) { + return currIdent; } - return regularEnsembles.length > 0 - ? regularEnsembles[0].getIdent() - : deltaEnsembles.length > 0 - ? deltaEnsembles[0].getIdent() - : null; + return ensembleSet.getRegularEnsembleArray()[0].getIdent(); } /** - * Validates the the EnsembleIdents or DeltaEnsembleIdents specified in currIdents against the - * contents of the EnsembleSet and fixes the value if it isn't valid. + * Validates the the RegularEnsembleIdents or DeltaEnsembleIdents specified in currIdents + * against the contents of the EnsembleSet and fixes the value if it isn't valid. * * Returns null if an empty EnsembleSet is specified. * - * Note that if the specified EnsembleIdents and DeltaEnsembleIdents are valid, this function - * will always return a reference to the exact same object that was passed in currIdent. This - * means that you can compare the references (fixedIdent !== currIdent) to detect any changes. + * Note that if the specified RegularEnsembleIdents and DeltaEnsembleIdents are valid, this + * function will always return a reference to the exact same object that was passed in + * currIdent. This means that you can compare the references (fixedIdent !== currIdent) to + * detect any changes. */ -export function fixupEnsembleIdents( - currIdents: RegularEnsembleIdent[] | null, - ensembleSet: EnsembleSet | null -): RegularEnsembleIdent[] | null; -export function fixupEnsembleIdents( - currIdents: DeltaEnsembleIdent[] | null, - ensembleSet: EnsembleSet | null -): DeltaEnsembleIdent[] | null; -export function fixupEnsembleIdents( - currIdents: (RegularEnsembleIdent | DeltaEnsembleIdent)[] | null, - ensembleSet: EnsembleSet | null -): (RegularEnsembleIdent | DeltaEnsembleIdent)[] | null; export function fixupEnsembleIdents( currIdents: (RegularEnsembleIdent | DeltaEnsembleIdent)[] | null, ensembleSet: EnsembleSet | null @@ -103,17 +88,33 @@ export function fixupEnsembleIdents( } if (currIdents === null || currIdents.length === 0) { - // Provide first regular ensemble ident by default - if (ensembleSet.hasAnyRegularEnsembles()) { - return [ensembleSet.getRegularEnsembleArray()[0].getIdent()]; - } - if (ensembleSet.hasAnyDeltaEnsembles()) { - return [ensembleSet.getDeltaEnsembleArray()[0].getIdent()]; - } - return []; + return [ensembleSet.getEnsembleArray()[0].getIdent()]; + } + + return currIdents.filter((currIdent) => ensembleSet.hasEnsemble(currIdent)); +} + +/** + * Validates the the RegularEnsembleIdents specified in currIdents against the contents of the + * EnsembleSet and fixes the value if it isn't valid. + * + * Returns null if an empty EnsembleSet is specified. + * + * Note that if the specified RegularEnsembleIdents are valid, this function will always return + * a reference to the exact same object that was passed in currIdent. This means that you can + * compare the references (fixedIdent !== currIdent) to detect any changes. + */ +export function fixupRegularEnsembleIdents( + currIdents: RegularEnsembleIdent[] | null, + ensembleSet: EnsembleSet | null +): RegularEnsembleIdent[] | null { + if (!ensembleSet?.hasAnyRegularEnsembles()) { + return null; + } + + if (currIdents === null || currIdents.length === 0) { + return [ensembleSet.getRegularEnsembleArray()[0].getIdent()]; } - return currIdents.filter((currIdent) => { - return ensembleSet.hasEnsemble(currIdent); - }); + return currIdents.filter((currIdent) => ensembleSet.hasEnsemble(currIdent)); } diff --git a/frontend/src/modules/FlowNetwork/settings/atoms/derivedAtoms.ts b/frontend/src/modules/FlowNetwork/settings/atoms/derivedAtoms.ts index 0949b5e78..9d86dca9a 100644 --- a/frontend/src/modules/FlowNetwork/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/FlowNetwork/settings/atoms/derivedAtoms.ts @@ -1,7 +1,7 @@ import { DatedFlowNetwork_api, FlowNetworkMetadata_api } from "@api"; import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; -import { fixupEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; +import { fixupRegularEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; import { atom } from "jotai"; @@ -25,7 +25,7 @@ export const selectedEnsembleIdentAtom = atom((get) const ensembleSet = get(EnsembleSetAtom); const userSelectedEnsembleIdent = get(userSelectedEnsembleIdentAtom); - const validEnsembleIdent = fixupEnsembleIdent(userSelectedEnsembleIdent, ensembleSet); + const validEnsembleIdent = fixupRegularEnsembleIdent(userSelectedEnsembleIdent, ensembleSet); return validEnsembleIdent; }); diff --git a/frontend/src/modules/InplaceVolumetricsPlot/settings/atoms/derivedAtoms.ts b/frontend/src/modules/InplaceVolumetricsPlot/settings/atoms/derivedAtoms.ts index fd9eb2457..e7cc889be 100644 --- a/frontend/src/modules/InplaceVolumetricsPlot/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/InplaceVolumetricsPlot/settings/atoms/derivedAtoms.ts @@ -1,6 +1,6 @@ import { FluidZone_api, InplaceVolumetricResultName_api, InplaceVolumetricsIdentifierWithValues_api } from "@api"; import { EnsembleSetAtom } from "@framework/GlobalAtoms"; -import { fixupEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; +import { fixupRegularEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; import { fixupUserSelection } from "@lib/utils/fixupUserSelection"; import { fixupUserSelectedIdentifierValues } from "@modules/_shared/InplaceVolumetrics/fixupUserSelectedIdentifierValues"; import { RealSelector, SelectorColumn, SourceAndTableIdentifierUnion } from "@modules/_shared/InplaceVolumetrics/types"; @@ -41,7 +41,7 @@ export const selectedEnsembleIdentsAtom = atom((get) => { ensembleSet.hasEnsemble(ensemble) ); - const validatedEnsembleIdents = fixupEnsembleIdents(newSelectedEnsembleIdents, ensembleSet); + const validatedEnsembleIdents = fixupRegularEnsembleIdents(newSelectedEnsembleIdents, ensembleSet); return validatedEnsembleIdents ?? []; }); diff --git a/frontend/src/modules/InplaceVolumetricsTable/settings/atoms/derivedAtoms.ts b/frontend/src/modules/InplaceVolumetricsTable/settings/atoms/derivedAtoms.ts index aa262e145..04cfb02ca 100644 --- a/frontend/src/modules/InplaceVolumetricsTable/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/InplaceVolumetricsTable/settings/atoms/derivedAtoms.ts @@ -1,6 +1,6 @@ import { FluidZone_api, InplaceVolumetricResultName_api, InplaceVolumetricsIdentifierWithValues_api } from "@api"; import { EnsembleSetAtom } from "@framework/GlobalAtoms"; -import { fixupEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; +import { fixupRegularEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; import { fixupUserSelection } from "@lib/utils/fixupUserSelection"; import { fixupUserSelectedIdentifierValues } from "@modules/_shared/InplaceVolumetrics/fixupUserSelectedIdentifierValues"; import { SourceAndTableIdentifierUnion, SourceIdentifier } from "@modules/_shared/InplaceVolumetrics/types"; @@ -36,7 +36,7 @@ export const selectedEnsembleIdentsAtom = atom((get) => { ensembleSet.hasEnsemble(ensemble) ); - const validatedEnsembleIdents = fixupEnsembleIdents(newSelectedEnsembleIdents, ensembleSet); + const validatedEnsembleIdents = fixupRegularEnsembleIdents(newSelectedEnsembleIdents, ensembleSet); return validatedEnsembleIdents ?? []; }); diff --git a/frontend/src/modules/Map/settings/settings.tsx b/frontend/src/modules/Map/settings/settings.tsx index b34cf9776..9d3875b58 100644 --- a/frontend/src/modules/Map/settings/settings.tsx +++ b/frontend/src/modules/Map/settings/settings.tsx @@ -7,7 +7,7 @@ import { useSettingsStatusWriter } from "@framework/StatusWriter"; import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; import { useEnsembleSet } from "@framework/WorkbenchSession"; import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; -import { fixupEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/utils/ensembleUiHelpers"; +import { fixupRegularEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/utils/ensembleUiHelpers"; import { Checkbox } from "@lib/components/Checkbox"; import { CircularProgress } from "@lib/components/CircularProgress"; import { Input } from "@lib/components/Input"; @@ -53,7 +53,7 @@ export function MapSettings(props: ModuleSettingsProps) { const syncedValueDate = syncHelper.useValue(SyncSettingKey.DATE, "global.syncValue.date"); const candidateEnsembleIdent = maybeAssignFirstSyncedEnsemble(selectedEnsembleIdent, syncedValueEnsembles); - const computedEnsembleIdent = fixupEnsembleIdent(candidateEnsembleIdent, ensembleSet); + const computedEnsembleIdent = fixupRegularEnsembleIdent(candidateEnsembleIdent, ensembleSet); const realizationSurfacesMetaQuery = useRealizationSurfacesMetadataQuery( computedEnsembleIdent?.getCaseUuid(), computedEnsembleIdent?.getEnsembleName() diff --git a/frontend/src/modules/Rft/settings/atoms/derivedAtoms.ts b/frontend/src/modules/Rft/settings/atoms/derivedAtoms.ts index ebc3672b3..f4994bc36 100644 --- a/frontend/src/modules/Rft/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/Rft/settings/atoms/derivedAtoms.ts @@ -1,6 +1,6 @@ import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; -import { fixupEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; +import { fixupRegularEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; import { atom } from "jotai"; @@ -30,7 +30,7 @@ export const selectedEnsembleIdentAtom = atom((get) const ensembleSet = get(EnsembleSetAtom); const userSelectedEnsembleIdent = get(userSelectedEnsembleIdentAtom); - const validEnsembleIdent = fixupEnsembleIdent(userSelectedEnsembleIdent, ensembleSet); + const validEnsembleIdent = fixupRegularEnsembleIdent(userSelectedEnsembleIdent, ensembleSet); return validEnsembleIdent; }); diff --git a/frontend/src/modules/SimulationTimeSeriesSensitivity/settings/settings.tsx b/frontend/src/modules/SimulationTimeSeriesSensitivity/settings/settings.tsx index ecafa23c0..69d315d7c 100644 --- a/frontend/src/modules/SimulationTimeSeriesSensitivity/settings/settings.tsx +++ b/frontend/src/modules/SimulationTimeSeriesSensitivity/settings/settings.tsx @@ -6,7 +6,7 @@ import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; import { useEnsembleSet } from "@framework/WorkbenchSession"; import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; -import { fixupEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/utils/ensembleUiHelpers"; +import { fixupRegularEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/utils/ensembleUiHelpers"; import { Checkbox } from "@lib/components/Checkbox"; import { CircularProgress } from "@lib/components/CircularProgress"; import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; @@ -55,7 +55,7 @@ export function Settings({ settingsContext, workbenchSession, workbenchServices const syncedValueSummaryVector = syncHelper.useValue(SyncSettingKey.TIME_SERIES, "global.syncValue.timeSeries"); const candidateEnsembleIdent = maybeAssignFirstSyncedEnsemble(selectedEnsembleIdent, syncedValueEnsembles); - const computedEnsembleIdent = fixupEnsembleIdent(candidateEnsembleIdent, ensembleSet); + const computedEnsembleIdent = fixupRegularEnsembleIdent(candidateEnsembleIdent, ensembleSet); const vectorsListQuery = useVectorListQuery( computedEnsembleIdent?.getCaseUuid(), diff --git a/frontend/src/modules/SubsurfaceMap/settings/settings.tsx b/frontend/src/modules/SubsurfaceMap/settings/settings.tsx index 08b17493c..d7f4a5b9b 100644 --- a/frontend/src/modules/SubsurfaceMap/settings/settings.tsx +++ b/frontend/src/modules/SubsurfaceMap/settings/settings.tsx @@ -6,7 +6,7 @@ import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; import { useEnsembleSet } from "@framework/WorkbenchSession"; import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; -import { fixupEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/utils/ensembleUiHelpers"; +import { fixupRegularEnsembleIdent, maybeAssignFirstSyncedEnsemble } from "@framework/utils/ensembleUiHelpers"; import { Button } from "@lib/components/Button"; import { Checkbox } from "@lib/components/Checkbox"; import { CircularProgress } from "@lib/components/CircularProgress"; @@ -99,7 +99,7 @@ export function Settings({ settingsContext, workbenchSession, workbenchServices const syncedValueEnsembles = syncHelper.useValue(SyncSettingKey.ENSEMBLE, "global.syncValue.ensembles"); const syncedValueSurface = syncHelper.useValue(SyncSettingKey.SURFACE, "global.syncValue.surface"); const candidateEnsembleIdent = maybeAssignFirstSyncedEnsemble(selectedEnsembleIdent, syncedValueEnsembles); - const computedEnsembleIdent = fixupEnsembleIdent(candidateEnsembleIdent, ensembleSet); + const computedEnsembleIdent = fixupRegularEnsembleIdent(candidateEnsembleIdent, ensembleSet); if (computedEnsembleIdent && !computedEnsembleIdent.equals(selectedEnsembleIdent)) { setSelectedEnsembleIdent(computedEnsembleIdent); } diff --git a/frontend/src/modules/Vfp/settings/atoms/derivedAtoms.ts b/frontend/src/modules/Vfp/settings/atoms/derivedAtoms.ts index ca7eefd2b..49f69798f 100644 --- a/frontend/src/modules/Vfp/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/Vfp/settings/atoms/derivedAtoms.ts @@ -1,6 +1,6 @@ import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; -import { fixupEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; +import { fixupRegularEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; import { isProdTable } from "@modules/Vfp/utils/vfpTableClassifier"; import { atom } from "jotai"; @@ -34,7 +34,7 @@ export const selectedEnsembleIdentAtom = atom((get) const ensembleSet = get(EnsembleSetAtom); const userSelectedEnsembleIdent = get(userSelectedEnsembleIdentAtom); - const validEnsembleIdent = fixupEnsembleIdent(userSelectedEnsembleIdent, ensembleSet); + const validEnsembleIdent = fixupRegularEnsembleIdent(userSelectedEnsembleIdent, ensembleSet); return validEnsembleIdent; }); diff --git a/frontend/tests/unit/ensembleUiHelpers.test.ts b/frontend/tests/unit/ensembleUiHelpers.test.ts index 0afa88c08..b5d859124 100644 --- a/frontend/tests/unit/ensembleUiHelpers.test.ts +++ b/frontend/tests/unit/ensembleUiHelpers.test.ts @@ -3,7 +3,12 @@ import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; import { EnsembleSet } from "@framework/EnsembleSet"; import { RegularEnsemble } from "@framework/RegularEnsemble"; import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; -import { fixupEnsembleIdent, fixupEnsembleIdents } from "@framework/utils/ensembleUiHelpers"; +import { + fixupEnsembleIdent, + fixupEnsembleIdents, + fixupRegularEnsembleIdent, + fixupRegularEnsembleIdents, +} from "@framework/utils/ensembleUiHelpers"; import { describe, expect, test } from "vitest"; @@ -37,6 +42,31 @@ describe("fixupEnsembleIdent", () => { expect(fixupEnsembleIdent(deltaEnsembleIdent, ENSEMBLE_SET)).toBe(deltaEnsembleIdent); }); + test("should return first regular ensemble ident if currIdent is invalid regular ensemble ident", () => { + const nonExistingRegularEnsembleIdent = new RegularEnsembleIdent( + "55555555-aaaa-4444-aaaa-aaaaaaaaaaaa", + "ens4" + ); + // Fixup non-existing regular ensemble ident + expect(fixupEnsembleIdent(nonExistingRegularEnsembleIdent, ENSEMBLE_SET)).toBe(ensembleArray[0].getIdent()); + }); + + test("should return first regular ensemble ident if currIdent is invalid delta ensemble ident", () => { + const nonExistingDeltaEnsembleIdent = new DeltaEnsembleIdent( + ensembleArray[2].getIdent(), + ensembleArray[1].getIdent() + ); + + // Fixup non-existing delta ensemble ident + expect(fixupEnsembleIdent(nonExistingDeltaEnsembleIdent, ENSEMBLE_SET)).toBe(ensembleArray[0].getIdent()); + }); + + test("should return the first regular ensemble ident if currIdent is null and regular ensembles are available", () => { + const regularEnsembleIdent = ensembleArray[0].getIdent(); + + expect(fixupEnsembleIdent(null, ENSEMBLE_SET)).toBe(regularEnsembleIdent); + }); + test("should return the first delta ensemble ident if currIdent is a non-existing DeltaEnsembleIdent or no regular ensembles are available", () => { const ensembleSetWithoutRegularEnsembles = new EnsembleSet([], deltaEnsembleArray); @@ -61,12 +91,6 @@ describe("fixupEnsembleIdent", () => { ); }); - test("should return the first regular ensemble ident if currIdent is null and regular ensembles are available", () => { - const regularEnsembleIdent = ensembleArray[0].getIdent(); - - expect(fixupEnsembleIdent(null, ENSEMBLE_SET)).toBe(regularEnsembleIdent); - }); - test("should return the first regular ensemble ident if currIdent is null or a DeltaEnsembleIdent and no delta ensembles are available", () => { const ensembleSetWithoutDeltaEnsembles = new EnsembleSet(ensembleArray); @@ -78,7 +102,6 @@ describe("fixupEnsembleIdent", () => { // Fixup null expect(fixupEnsembleIdent(null, ensembleSetWithoutDeltaEnsembles)).toBe(ensembleArray[0].getIdent()); - // console.log(ensembleSetWithoutDeltaEnsembles.getRegularEnsembleArray()); // Fixup DeltaEnsembleIdent expect(fixupEnsembleIdent(deltaEnsembleArray[0].getIdent(), ensembleSetWithoutDeltaEnsembles)).toBe( ensembleArray[0].getIdent() @@ -86,6 +109,41 @@ describe("fixupEnsembleIdent", () => { }); }); +describe("fixupRegularEnsembleIdent", () => { + test("should return null if ensembleSet is null or has no ensembles", () => { + const currIdent = ensembleArray[0].getIdent(); + const emptyEnsembleSet = new EnsembleSet([]); + + expect(fixupRegularEnsembleIdent(currIdent, null)).toBeNull(); + expect(fixupRegularEnsembleIdent(currIdent, emptyEnsembleSet)).toBeNull(); + }); + + test("should return currIdent if it is valid in the ensembleSet", () => { + const regularEnsembleIdent = ensembleArray[1].getIdent(); + + expect(fixupRegularEnsembleIdent(regularEnsembleIdent, ENSEMBLE_SET)).toBe(regularEnsembleIdent); + }); + + test("should return currIdent if it exist", () => { + const regularEnsembleIdent = ensembleArray[1].getIdent(); + + expect(fixupRegularEnsembleIdent(regularEnsembleIdent, ENSEMBLE_SET)).toBe(regularEnsembleIdent); + }); + + test("should return null if currIdent is a RegularEnsembleIdent and no regular ensembles are available", () => { + const ensembleSetWithoutRegularEnsembles = new EnsembleSet([], deltaEnsembleArray); + + // Non-existing delta ensemble ident + expect(fixupRegularEnsembleIdent(ensembleArray[0].getIdent(), ensembleSetWithoutRegularEnsembles)).toBeNull(); + }); + + test("should return the first regular ensemble ident if currIdent is null and regular ensembles are available", () => { + const regularEnsembleIdent = ensembleArray[0].getIdent(); + + expect(fixupRegularEnsembleIdent(null, ENSEMBLE_SET)).toBe(regularEnsembleIdent); + }); +}); + describe("fixupEnsembleIdents", () => { test("should return null if ensembleSet is null or has no ensembles", () => { const currIdents = [ensembleArray[0].getIdent()]; @@ -147,3 +205,48 @@ describe("fixupEnsembleIdents", () => { ).toEqual([validDeltaEnsembleIdent, validRegularEnsembleIdent]); }); }); + +describe("fixupRegularEnsembleIdents", () => { + test("should return null if ensembleSet is null or has no ensembles", () => { + const currIdents = [ensembleArray[0].getIdent()]; + const emptyEnsembleSet = new EnsembleSet([]); + + expect(fixupRegularEnsembleIdents(currIdents, null)).toBeNull(); + expect(fixupRegularEnsembleIdents(currIdents, emptyEnsembleSet)).toBeNull(); + }); + + test("should return currIdents if they are valid in the ensembleSet", () => { + const regularEnsembleIdents = [ensembleArray[2].getIdent(), ensembleArray[1].getIdent()]; + + expect(fixupRegularEnsembleIdents(regularEnsembleIdents, ENSEMBLE_SET)).toEqual(regularEnsembleIdents); + }); + + test("should return the first regular ensemble ident if currIdents is null or empty and regular ensembles are available", () => { + const regularEnsembleIdent = ensembleArray[0].getIdent(); + + expect(fixupRegularEnsembleIdents(null, ENSEMBLE_SET)).toEqual([regularEnsembleIdent]); + expect(fixupRegularEnsembleIdents([], ENSEMBLE_SET)).toEqual([regularEnsembleIdent]); + }); + + test("should return null if currIdents is null or empty and no regular ensembles are available", () => { + const regularEnsembleIdents = [ensembleArray[2].getIdent(), ensembleArray[1].getIdent()]; + const ensembleSetWithoutRegularEnsembles = new EnsembleSet([], deltaEnsembleArray); + + expect(fixupRegularEnsembleIdents(regularEnsembleIdents, ensembleSetWithoutRegularEnsembles)).toBeNull(); + expect(fixupRegularEnsembleIdents(null, ensembleSetWithoutRegularEnsembles)).toBeNull(); + expect(fixupRegularEnsembleIdents([], ensembleSetWithoutRegularEnsembles)).toBeNull(); + }); + + test("should filter out non-existing idents from currIdents", () => { + const validRegularEnsembleIdent = ensembleArray[0].getIdent(); + const nonExistingRegularEnsembleIdent = new RegularEnsembleIdent( + "55555555-aaaa-4444-aaaa-aaaaaaaaaaaa", + "ens4" + ); + + expect(fixupRegularEnsembleIdents([nonExistingRegularEnsembleIdent], ENSEMBLE_SET)).toEqual([]); + expect( + fixupRegularEnsembleIdents([validRegularEnsembleIdent, nonExistingRegularEnsembleIdent], ENSEMBLE_SET) + ).toEqual([validRegularEnsembleIdent]); + }); +});