diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5be35cde6..a9c90689a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@tanstack/react-query-devtools": "^5.4.2", "@webviz/subsurface-viewer": "^0.3.1", "@webviz/well-completions-plot": "^0.0.1-alpha.1", + "ajv": "^8.12.0", "animate.css": "^4.1.1", "axios": "^1.6.5", "culori": "^3.2.0", @@ -26,6 +27,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@playwright/experimental-ct-react": "^1.39.0", "@playwright/test": "^1.39.0", "@trivago/prettier-plugin-sort-imports": "^4.0.0", @@ -54,7 +56,7 @@ "prettier": "2.8.3", "sass": "^1.62.0", "tailwindcss": "^3.2.4", - "typescript": "^4.9.3", + "typescript": "^5.3.3", "vite": "^5.0.12", "vite-plugin-checker": "^0.6.0", "vitest": "^1.1.3" @@ -391,6 +393,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", @@ -5614,6 +5631,21 @@ "react-dom": "^17 || ^18" } }, + "node_modules/@webviz/subsurface-viewer/node_modules/ajv": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.2.4.tgz", + "integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@webviz/well-completions-plot": { "version": "0.0.1-alpha.1", "resolved": "https://registry.npmjs.org/@webviz/well-completions-plot/-/well-completions-plot-0.0.1-alpha.1.tgz", @@ -5631,6 +5663,21 @@ "ajv": "^7.2.1" } }, + "node_modules/@webviz/wsc-common/node_modules/ajv": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.2.4.tgz", + "integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/abs-svg-path": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", @@ -5686,9 +5733,9 @@ } }, "node_modules/ajv": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.2.4.tgz", - "integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -7439,22 +7486,6 @@ "node": "^16.14||>=18" } }, - "node_modules/dependency-cruiser/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/dependency-cruiser/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -14330,16 +14361,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ufo": { diff --git a/frontend/package.json b/frontend/package.json index d1ba36a4a..df35a4d4a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@tanstack/react-query-devtools": "^5.4.2", "@webviz/subsurface-viewer": "^0.3.1", "@webviz/well-completions-plot": "^0.0.1-alpha.1", + "ajv": "^8.12.0", "animate.css": "^4.1.1", "axios": "^1.6.5", "culori": "^3.2.0", @@ -37,6 +38,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@playwright/experimental-ct-react": "^1.39.0", "@playwright/test": "^1.39.0", "@trivago/prettier-plugin-sort-imports": "^4.0.0", @@ -65,7 +67,7 @@ "prettier": "2.8.3", "sass": "^1.62.0", "tailwindcss": "^3.2.4", - "typescript": "^4.9.3", + "typescript": "^5.3.3", "vite": "^5.0.12", "vite-plugin-checker": "^0.6.0", "vitest": "^1.1.3" diff --git a/frontend/src/assets/field-configs/DROGON.json b/frontend/src/assets/field-configs/DROGON.json new file mode 100644 index 000000000..8aedb6eda --- /dev/null +++ b/frontend/src/assets/field-configs/DROGON.json @@ -0,0 +1,4 @@ +{ + "fieldIdentifier": "DROGON", + "range": [0, 25] +} diff --git a/frontend/src/framework/FieldConfigs.ts b/frontend/src/framework/FieldConfigs.ts new file mode 100644 index 000000000..ae86a8786 --- /dev/null +++ b/frontend/src/framework/FieldConfigs.ts @@ -0,0 +1,18 @@ +export interface FieldConfig { + fieldIdentifier: string; + range: [number, number]; +} + +export class FieldConfigSet { + private _configMap: Map = new Map(); + + constructor(configArray: FieldConfig[]) { + for (const config of configArray) { + this._configMap.set(config.fieldIdentifier, config); + } + } + + getConfig(fieldIdentifier: string): FieldConfig | null { + return this._configMap.get(fieldIdentifier) || null; + } +} diff --git a/frontend/src/framework/Workbench.ts b/frontend/src/framework/Workbench.ts index c4179cc46..cfd1603ab 100644 --- a/frontend/src/framework/Workbench.ts +++ b/frontend/src/framework/Workbench.ts @@ -10,6 +10,7 @@ import { Template } from "./TemplateRegistry"; import { WorkbenchServices } from "./WorkbenchServices"; import { WorkbenchSession } from "./WorkbenchSession"; import { loadEnsembleSetMetadataFromBackend } from "./internal/EnsembleSetLoader"; +import { loadFieldConfigs } from "./internal/FieldConfigSetLoader"; import { PrivateWorkbenchServices } from "./internal/PrivateWorkbenchServices"; import { PrivateWorkbenchSettings } from "./internal/PrivateWorkbenchSettings"; import { WorkbenchSessionPrivate } from "./internal/WorkbenchSessionPrivate"; @@ -204,7 +205,7 @@ export class Workbench { queryClient: QueryClient, specifiedEnsembleIdents: EnsembleIdent[] ): Promise { - this.storeEnsembleSetInLocalStorage(specifiedEnsembleIdents); + this.storeSpecifiedEnsemblesInLocalStorage(specifiedEnsembleIdents); const ensembleIdentsToLoad: EnsembleIdent[] = []; for (const ensSpec of specifiedEnsembleIdents) { @@ -217,14 +218,32 @@ export class Workbench { console.debug("loadAndSetupEnsembleSetInSession - loading done"); console.debug("loadAndSetupEnsembleSetInSession - publishing"); this._workbenchSession.setEnsembleSetLoadingState(false); + return this._workbenchSession.setEnsembleSet(newEnsembleSet); } - private storeEnsembleSetInLocalStorage(specifiedEnsembleIdents: EnsembleIdent[]): void { + async loadAndSetupFieldConfigSetInSession(fieldsToLoadConfigFor: string[]): Promise { + this.storeFieldsToLoadConfigForInLocalStorage(fieldsToLoadConfigFor); + + console.debug("loadAndSetupFieldConfigSetInSession - starting load"); + this._workbenchSession.setFieldConfigSetLoadingState(true); + const newFieldConfigSet = await loadFieldConfigs(fieldsToLoadConfigFor); + console.debug("loadAndSetupFieldConfigSetInSession - loading done"); + console.debug("loadAndSetupFieldConfigSetInSession - publishing"); + this._workbenchSession.setFieldConfigSetLoadingState(false); + + return this._workbenchSession.setFieldConfigSet(newFieldConfigSet); + } + + private storeSpecifiedEnsemblesInLocalStorage(specifiedEnsembleIdents: EnsembleIdent[]): void { const ensembleIdentsToStore = specifiedEnsembleIdents.map((el) => el.toString()); localStorage.setItem("ensembleIdents", JSON.stringify(ensembleIdentsToStore)); } + private storeFieldsToLoadConfigForInLocalStorage(fieldsToLoadConfigFor: string[]): void { + localStorage.setItem("fieldsToLoadConfigFor", JSON.stringify(fieldsToLoadConfigFor)); + } + maybeLoadEnsembleSetFromLocalStorage(): EnsembleIdent[] | null { const ensembleIdentsString = localStorage.getItem("ensembleIdents"); if (!ensembleIdentsString) return null; diff --git a/frontend/src/framework/WorkbenchSession.ts b/frontend/src/framework/WorkbenchSession.ts index 4b0c7fc4b..0dbb5f335 100644 --- a/frontend/src/framework/WorkbenchSession.ts +++ b/frontend/src/framework/WorkbenchSession.ts @@ -2,26 +2,37 @@ import React from "react"; import { Ensemble } from "./Ensemble"; import { EnsembleSet } from "./EnsembleSet"; +import { FieldConfigSet } from "./FieldConfigs"; export enum WorkbenchSessionEvent { EnsembleSetChanged = "EnsembleSetChanged", EnsembleSetLoadingStateChanged = "EnsembleSetLoadingStateChanged", + FieldConfigSetChanged = "FieldConfigSetChanged", + FieldConfigSetLoadingStateChanged = "FieldConfigSetLoadingStateChanged", } export type WorkbenchSessionPayloads = { [WorkbenchSessionEvent.EnsembleSetLoadingStateChanged]: { isLoading: boolean; }; + [WorkbenchSessionEvent.FieldConfigSetLoadingStateChanged]: { + isLoading: boolean; + }; }; export class WorkbenchSession { private _subscribersMap: Map void>> = new Map(); protected _ensembleSet: EnsembleSet = new EnsembleSet([]); + protected _fieldConfigSet: FieldConfigSet = new FieldConfigSet([]); getEnsembleSet(): EnsembleSet { return this._ensembleSet; } + getFieldConfigSet(): FieldConfigSet { + return this._fieldConfigSet; + } + subscribe>( event: T, cb: () => void @@ -108,3 +119,49 @@ export function useIsEnsembleSetLoading(workbenchSession: WorkbenchSession): boo return isLoading; } + +export function useFieldConfigSet(workbenchSession: WorkbenchSession): FieldConfigSet { + const [storedFieldConfigSet, setStoredFieldConfigSet] = React.useState( + workbenchSession.getFieldConfigSet() + ); + + React.useEffect( + function subscribeToFieldConfigSetChanges() { + function handleFieldConfigSetChanged() { + setStoredFieldConfigSet(workbenchSession.getFieldConfigSet()); + } + + const unsubFunc = workbenchSession.subscribe( + WorkbenchSessionEvent.FieldConfigSetChanged, + handleFieldConfigSetChanged + ); + return unsubFunc; + }, + [workbenchSession] + ); + + return storedFieldConfigSet; +} + +export function useIsFieldConfigSetLoading(workbenchSession: WorkbenchSession): boolean { + const [isLoading, setIsLoading] = React.useState(false); + + React.useEffect( + function subscribeToFieldConfigSetLoadingStateChanges() { + function handleFieldConfigSetLoadingStateChanged( + payload: WorkbenchSessionPayloads[WorkbenchSessionEvent.FieldConfigSetLoadingStateChanged] + ) { + setIsLoading(payload.isLoading); + } + + const unsubFunc = workbenchSession.subscribe( + WorkbenchSessionEvent.FieldConfigSetLoadingStateChanged, + handleFieldConfigSetLoadingStateChanged + ); + return unsubFunc; + }, + [workbenchSession] + ); + + return isLoading; +} diff --git a/frontend/src/framework/components/VectorSelector/private-utils/VectorSelection.ts b/frontend/src/framework/components/VectorSelector/private-utils/VectorSelection.ts index ed9ee5b69..4c2a21ab2 100644 --- a/frontend/src/framework/components/VectorSelector/private-utils/VectorSelection.ts +++ b/frontend/src/framework/components/VectorSelector/private-utils/VectorSelection.ts @@ -106,12 +106,12 @@ export class VectorSelection extends TreeNodeSelection { } containsWildcard(): boolean { - const reg = RegExp(`^(([^${super.delimiter}\\|]+\\|)+([^${super.delimiter}\\|]+){1})$`); + const reg = RegExp(`^(([^${this.delimiter}\\|]+\\|)+([^${this.delimiter}\\|]+){1})$`); let level = 0; for (const el of this.getNodePath()) { if ( - (el.includes("?") || el.includes("*") || (super.allowOrOperator && reg.test(el))) && - level != super.getNumMetaNodes() - 1 + (el.includes("?") || el.includes("*") || (this.allowOrOperator && reg.test(el))) && + level !== super.getNumMetaNodes() - 1 ) { return true; } @@ -123,7 +123,7 @@ export class VectorSelection extends TreeNodeSelection { getCompleteNodePathAsString(): string { const result: string[] = []; for (let i = 0; i < this.countLevel(); i++) { - if (i != super.getNumMetaNodes() - 1) { + if (i !== super.getNumMetaNodes() - 1) { result.push(this.getNodeName(i) as string); } } @@ -149,8 +149,8 @@ export class VectorSelection extends TreeNodeSelection { delimiter: super.getDelimiter(), numMetaNodes: super.getNumMetaNodes(), treeData: this._myTreeData, - caseInsensitiveMatching: super.caseInsensitiveMatching, - allowOrOperator: super.allowOrOperator, + caseInsensitiveMatching: this.caseInsensitiveMatching, + allowOrOperator: this.allowOrOperator, }); } } diff --git a/frontend/src/framework/internal/FieldConfigSetLoader.ts b/frontend/src/framework/internal/FieldConfigSetLoader.ts new file mode 100644 index 000000000..325f7ea35 --- /dev/null +++ b/frontend/src/framework/internal/FieldConfigSetLoader.ts @@ -0,0 +1,100 @@ +import { FieldConfig, FieldConfigSet } from "@framework/FieldConfigs"; + +import Ajv, { ErrorObject, JSONSchemaType } from "ajv"; + +const CONFIG_JSON_SCHEMA: JSONSchemaType = { + type: "object", + properties: { + fieldIdentifier: { type: "string" }, + range: { + type: "array", + items: [{ type: "number" }, { type: "number" }], + minItems: 2, + maxItems: 2, + }, + }, + required: ["fieldIdentifier"], + additionalProperties: false, +}; + +const validateConfig = new Ajv().compile(CONFIG_JSON_SCHEMA); + +function stringifyConfigValidationErrors(errors: ErrorObject, unknown>[]): string { + return errors.map((error) => JSON.stringify(error)).join("\n"); +} + +type LoadConfigFromFileResult = { + config: FieldConfig | null; + error: Error | null; +}; + +async function loadConfigFromFile(fieldIdentifier: string): Promise { + const result: LoadConfigFromFileResult = { + config: null, + error: null, + }; + + try { + // With https://github.com/tc39/proposal-json-modules in place, we can make use of `{ with: {type: "json"} }` in order to make sure that the imported module is expected to be a JSON file. + // const content = await import(`/src/assets/field-configs/${fieldIdentifier}.json?init`, { with: {type: "json"} }); + // In addition, Vite will warn about not being able to analyze the dynamic import, so we are ignoring that warning. + const content = await import(/* @vite-ignore */ `/src/assets/field-configs/${fieldIdentifier}.json?init`); + if (!validateConfig(content.default)) { + result.error = new Error( + `Invalid format in config file '${fieldIdentifier}.json':\n${stringifyConfigValidationErrors( + validateConfig.errors ?? [] + )}` + ); + return result; + } + + if (content.default.fieldIdentifier !== fieldIdentifier) { + result.error = new Error(`Field identifier mismatch in config file '${fieldIdentifier}.json'`); + return result; + } + + result.config = content.default; + } catch (error: any) { + console.error(`Failed to load config for field '${fieldIdentifier}': ${error.message}`); + result.error = new Error(`Failed to load config for field '${fieldIdentifier}': ${error.message}`); + } + + return result; +} + +export async function loadFieldConfigs(fieldsToLoadConfigFor: string[]): Promise { + const promiseArray: Promise[] = []; + + for (const field of fieldsToLoadConfigFor) { + promiseArray.push(loadConfigFromFile(field)); + } + + const results = await Promise.allSettled(promiseArray); + + const configArray: FieldConfig[] = []; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const fieldIdentifier = fieldsToLoadConfigFor[i]; + + if (result.status === "rejected") { + console.error(`Error loading config for field '${fieldIdentifier}', dropping config`, result.reason); + continue; + } + + const config = result.value?.config; + + if (!config) { + console.error(`No config found for field '${fieldIdentifier}', dropping config`); + continue; + } + + if (config.fieldIdentifier !== fieldIdentifier) { + console.error(`Field identifier mismatch in config for field '${fieldIdentifier}', dropping config`); + continue; + } + + configArray.push(config); + } + + return new FieldConfigSet(configArray); +} diff --git a/frontend/src/framework/internal/WorkbenchSessionPrivate.ts b/frontend/src/framework/internal/WorkbenchSessionPrivate.ts index 5b8e01f8d..d623667b1 100644 --- a/frontend/src/framework/internal/WorkbenchSessionPrivate.ts +++ b/frontend/src/framework/internal/WorkbenchSessionPrivate.ts @@ -1,3 +1,5 @@ +import { FieldConfigSet } from "@framework/FieldConfigs"; + import { EnsembleSet } from "../EnsembleSet"; import { WorkbenchSession, WorkbenchSessionEvent } from "../WorkbenchSession"; @@ -10,8 +12,17 @@ export class WorkbenchSessionPrivate extends WorkbenchSession { this.notifySubscribers(WorkbenchSessionEvent.EnsembleSetLoadingStateChanged, { isLoading }); } + setFieldConfigSetLoadingState(isLoading: boolean): void { + this.notifySubscribers(WorkbenchSessionEvent.FieldConfigSetLoadingStateChanged, { isLoading }); + } + setEnsembleSet(newEnsembleSet: EnsembleSet): void { this._ensembleSet = newEnsembleSet; this.notifySubscribers(WorkbenchSessionEvent.EnsembleSetChanged); } + + setFieldConfigSet(newFieldConfigSet: FieldConfigSet): void { + this._fieldConfigSet = newFieldConfigSet; + this.notifySubscribers(WorkbenchSessionEvent.FieldConfigSetChanged); + } } diff --git a/frontend/src/framework/internal/components/NavBar/navBar.tsx b/frontend/src/framework/internal/components/NavBar/navBar.tsx index bdcccfec2..1b3158ade 100644 --- a/frontend/src/framework/internal/components/NavBar/navBar.tsx +++ b/frontend/src/framework/internal/components/NavBar/navBar.tsx @@ -115,12 +115,20 @@ export const NavBar: React.FC = (props) => { ensembleName: ens.getEnsembleName(), })); - function loadAndSetupEnsembles(selectedEnsembles: EnsembleItem[]): Promise { + function loadAndSetupEnsemblesAndFieldConfigs( + selectedEnsembles: EnsembleItem[], + selectedFieldIdentifiers: string[] + ): Promise { setNewSelectedEnsembles(selectedEnsembles); const selectedEnsembleIdents = selectedEnsembles.map( (ens) => new EnsembleIdent(ens.caseUuid, ens.ensembleName) ); - return props.workbench.loadAndSetupEnsembleSetInSession(queryClient, selectedEnsembleIdents); + const promises: Promise[] = []; + + promises.push(props.workbench.loadAndSetupEnsembleSetInSession(queryClient, selectedEnsembleIdents)); + promises.push(props.workbench.loadAndSetupFieldConfigSetInSession(selectedFieldIdentifiers)); + + return Promise.all(promises); } let fixedSelectedEnsembles = selectedEnsembles; @@ -256,7 +264,7 @@ export const NavBar: React.FC = (props) => { {ensembleDialogOpen && ( diff --git a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx index 4bfa2ac2d..fb27b12c2 100644 --- a/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx +++ b/frontend/src/framework/internal/components/SelectEnsemblesDialog/selectEnsemblesDialog.tsx @@ -29,7 +29,10 @@ export type EnsembleItem = { }; export type SelectEnsemblesDialogProps = { - loadAndSetupEnsembles: (selectedEnsembles: EnsembleItem[]) => Promise; + loadAndSetupEnsemblesAndFieldConfigs: ( + selectedEnsembles: EnsembleItem[], + selectedFieldIdentifiers: string[] + ) => Promise; onClose: () => void; selectedEnsembles: EnsembleItem[]; }; @@ -47,6 +50,7 @@ export const SelectEnsemblesDialog: React.FC = (prop const [isLoadingEnsembles, setIsLoadingEnsembles] = React.useState(false); const [confirmCancel, setConfirmCancel] = React.useState(false); const [newlySelectedEnsembles, setNewlySelectedEnsembles] = React.useState([]); + const [newlySelectedFieldIdentifiers, setNewlySelectedFieldIdentifiers] = React.useState>(new Set([])); const [casesFilteringOptions, setCasesFilteringOptions] = React.useState({ keep: true, onlyMyCases: false, @@ -137,9 +141,13 @@ export const SelectEnsemblesDialog: React.FC = (prop function handleAddEnsemble() { if (!checkIfEnsembleAlreadySelected()) { + const fieldIdentifier = selectedField; const caseName = casesQuery.data?.find((c) => c.uuid === selectedCaseId)?.name ?? "UNKNOWN"; - const ensArr = [{ caseUuid: selectedCaseId, caseName: caseName, ensembleName: selectedEnsembleName }]; + const ensArr = [ + { fieldIdentifier, caseUuid: selectedCaseId, caseName: caseName, ensembleName: selectedEnsembleName }, + ]; setNewlySelectedEnsembles((prev) => [...prev, ...ensArr]); + setNewlySelectedFieldIdentifiers((prev) => new Set([...prev, fieldIdentifier])); } } @@ -165,7 +173,7 @@ export const SelectEnsemblesDialog: React.FC = (prop function handleApplyEnsembleSelection() { setIsLoadingEnsembles(true); props - .loadAndSetupEnsembles(newlySelectedEnsembles) + .loadAndSetupEnsemblesAndFieldConfigs(newlySelectedEnsembles, Array.from(newlySelectedFieldIdentifiers)) .then(() => { handleClose(); }) diff --git a/frontend/src/lib/components/SmartNodeSelector/private-utils/treeData.ts b/frontend/src/lib/components/SmartNodeSelector/private-utils/treeData.ts index 47771a77d..5313d119a 100644 --- a/frontend/src/lib/components/SmartNodeSelector/private-utils/treeData.ts +++ b/frontend/src/lib/components/SmartNodeSelector/private-utils/treeData.ts @@ -76,7 +76,7 @@ export class TreeData { } this._stringifiedData = stringifiedData; this._nodeData = nodeData; - if (nodeData.length != indexCount) { + if (nodeData.length !== indexCount) { throw "implementation error"; } } diff --git a/frontend/src/modules/MyModule/loadModule.tsx b/frontend/src/modules/MyModule/loadModule.tsx index 7e0a51ed1..8388f6f14 100644 --- a/frontend/src/modules/MyModule/loadModule.tsx +++ b/frontend/src/modules/MyModule/loadModule.tsx @@ -1,7 +1,7 @@ import { ModuleRegistry } from "@framework/ModuleRegistry"; import { ColorScaleGradientType, ColorScaleType } from "@lib/utils/ColorScale"; -import { settings } from "./settings"; +import { Settings } from "./settings"; import { State } from "./state"; import { View } from "./view"; @@ -16,4 +16,4 @@ const defaultState: State = { const module = ModuleRegistry.initModule("MyModule", defaultState); module.viewFC = View; -module.settingsFC = settings; +module.settingsFC = Settings; diff --git a/frontend/src/modules/MyModule/settings.tsx b/frontend/src/modules/MyModule/settings.tsx index 252504fdb..e6520af83 100644 --- a/frontend/src/modules/MyModule/settings.tsx +++ b/frontend/src/modules/MyModule/settings.tsx @@ -1,6 +1,9 @@ import React from "react"; -import { ModuleFCProps } from "@framework/Module"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { ModuleFC, ModuleFCProps } from "@framework/Module"; +import { useEnsembleSet, useFieldConfigSet } from "@framework/WorkbenchSession"; +import { SingleEnsembleSelect } from "@framework/components/SingleEnsembleSelect"; import { ColorGradient } from "@lib/components/ColorGradient/colorGradient"; import { Input } from "@lib/components/Input"; import { Label } from "@lib/components/Label"; @@ -9,13 +12,28 @@ import { ColorScaleGradientType, ColorScaleType } from "@lib/utils/ColorScale"; import { State } from "./state"; -export const settings = (props: ModuleFCProps) => { +export const Settings: ModuleFC = (props: ModuleFCProps) => { const [type, setType] = props.moduleContext.useStoreState("type"); const [gradientType, setGradientType] = props.moduleContext.useStoreState("gradientType"); const [min, setMin] = props.moduleContext.useStoreState("min"); const [max, setMax] = props.moduleContext.useStoreState("max"); const [divMidPoint, setDivMidPoint] = props.moduleContext.useStoreState("divMidPoint"); + const [selectedEnsembeIdent, setSelectedEnsembleIdent] = React.useState(null); + const [fieldConfigApplied, setFieldConfigApplied] = React.useState(false); + + const ensembleSet = useEnsembleSet(props.workbenchSession); + const fieldConfigSet = useFieldConfigSet(props.workbenchSession); + const fieldConfig = fieldConfigSet.getConfig("DROGON"); + + React.useEffect(() => { + if (!fieldConfigApplied && fieldConfig) { + setFieldConfigApplied(true); + setMin(fieldConfig.range[0]); + setMax(fieldConfig.range[1]); + } + }, [fieldConfigApplied, fieldConfig, setMax, setMin]); + function handleTypeChange(e: React.ChangeEvent) { setType(e.target.value as ColorScaleType); } @@ -24,6 +42,10 @@ export const settings = (props: ModuleFCProps) => { setGradientType(e.target.value as ColorScaleGradientType); } + function handleEnsembleChange(ensembleIdent: EnsembleIdent | null) { + setSelectedEnsembleIdent(ensembleIdent); + } + const colorScale = type === ColorScaleType.Continuous ? props.workbenchSettings.useContinuousColorScale({ @@ -35,6 +57,13 @@ export const settings = (props: ModuleFCProps) => { return (
+